web-apps/vendor/touch/sencha-touch-all-debug.js
Maxim Kadushkin 741b10515d webapps added
2016-03-10 21:48:53 -03:00

109175 lines
3.3 MiB

/*
This file is part of Sencha Touch 2.1
Copyright (c) 2011-2012 Sencha Inc
Contact: http://www.sencha.com/contact
GNU General Public License Usage
This file may be used under the terms of the GNU General Public License version 3.0 as
published by the Free Software Foundation and appearing in the file LICENSE included in the
packaging of this file.
Please review the following information to ensure the GNU General Public License version 3.0
requirements will be met: http://www.gnu.org/copyleft/gpl.html.
If you are unsure which license is appropriate for your use, please contact the sales department
at http://www.sencha.com/contact.
Build date: 2012-11-05 22:31:29 (08c91901ae8449841ff23e5d3fb404d6128d3b0b)
*/
//@tag foundation,core
//@define Ext
/**
* @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, {
/**
* @property {Function}
* 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.
*/
iterate: function(object, fn, scope) {
if (Ext.isEmpty(object)) {
return;
}
if (scope === undefined) {
scope = object;
}
if (Ext.isIterable(object)) {
Ext.Array.each.call(Ext.Array, object, fn, scope);
}
else {
Ext.Object.each.call(Ext.Object, object, fn, scope);
}
}
});
Ext.apply(Ext, {
/**
* This method deprecated. Use {@link Ext#define Ext.define} instead.
* @method
* @param {Function} superclass
* @param {Object} overrides
* @return {Function} The subclass constructor from the `overrides` parameter, or a generated one if not provided.
* @deprecated 4.0.0 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);
};
}
//<debug>
if (!superclass) {
Ext.Error.raise({
sourceClass: 'Ext',
sourceMethod: 'extend',
msg: 'Attempting to extend from a class which has not been loaded on the page.'
});
}
//</debug>
// 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
* @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=false] (optional) `true` to allow zero length strings to qualify as non-empty.
* @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}
*/
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';
}
//<debug error>
Ext.Error.raise({
sourceClass: 'Ext',
sourceMethod: 'typeOf',
msg: 'Failed to determine the type of the specified value "' + value + '". This is most likely a bug.'
});
//</debug>
},
/**
* 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=false] (optional) `true` to allow empty strings.
* @return {Boolean}
*/
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);
})();
}
//<feature logger>
/**
* @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');
}
}
//</feature>
});
/**
* Old alias to {@link Ext#typeOf}.
* @deprecated 4.0.0 Please use {@link Ext#typeOf} instead.
* @method
* @alias Ext#typeOf
*/
Ext.type = Ext.typeOf;
})();
//@tag foundation,core
//@define Ext.Version
//@require Ext
/**
* @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
*/
(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;
},
/**
* @param value
* @return {Number}
*/
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:
*
* var version = new Ext.Version('1.0.2beta');
* console.log(version.match(1)); // true
* console.log(version.match(1.0)); // true
* console.log(version.match('1.0.2')); // true
* console.log(version.match('1.0.2RC')); // false
*
* @param {String/Number} target The version to compare with.
* @return {Boolean} `true` if this version matches the target, `false` otherwise.
*/
match: function(target) {
target = String(target);
return this.version.substr(0, target.length) === target;
},
/**
* Returns this format: [major, minor, patch, build, release]. Useful for comparison.
* @return {Number[]}
*/
toArray: function() {
return [this.getMajor(), this.getMinor(), this.getPatch(), this.getBuild(), this.getRelease()];
},
/**
* Returns shortVersion version without dots and release.
* @return {String}
*/
getShortVersion: function() {
return this.shortVersion;
},
/**
* 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 (`this`) if the closure
*/
deprecate: function(packageName, since, closure, scope) {
if (Version.compare(Ext.getVersion(packageName), since) < 1) {
closure.call(scope);
}
}
}); // End Versioning
Ext.setVersion('core', version);
})();
//@tag foundation,core
//@define Ext.String
//@require Ext.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.
*
* // alternate sort directions
* sort = Ext.String.toggle(sort, 'ASC', 'DESC');
*
* // instead of conditional logic:
* sort = (sort == 'ASC' ? 'DESC' : 'ASC');
*
* @param {String} string The current string.
* @param {String} value The value to compare to the current string.
* @param {String} other The new value to use if the string already equals the first value passed in.
* @return {String} The new value.
*/
toggle: function(string, value, other) {
return string === value ? other : value;
},
/**
* Pads the left side of a string with a specified character. This is especially useful
* for normalizing number and date strings. Example usage:
*
* var s = Ext.String.leftPad('123', 5, '0');
* alert(s); // '00123'
*
* @param {String} string The original string.
* @param {Number} size The total length of the output string.
* @param {String} [character= ] (optional) The character with which to pad the original string (defaults to empty string " ").
* @return {String} The padded string.
*/
leftPad: function(string, size, character) {
var result = String(string);
character = character || " ";
while (result.length < size) {
result = character + result;
}
return result;
},
/**
* Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens. Each
* token must be unique, and must increment in the format {0}, {1}, etc. Example usage:
*
* var cls = 'my-class',
* text = 'Some text';
* var s = Ext.String.format('<div class="{0}">{1}</div>', cls, text);
* alert(s); // '<div class="my-class">Some text</div>'
*
* @param {String} string The tokenized string to be formatted.
* @param {String} value1 The value to replace token {0}.
* @param {String} value2 Etc...
* @return {String} The formatted string.
*/
format: function(format) {
var args = Ext.Array.toArray(arguments, 1);
return format.replace(Ext.String.formatRe, function(m, i) {
return args[i];
});
},
/**
* Returns a string with a specified number of repetitions 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;
//@tag foundation,core
//@define Ext.Array
//@require Ext.String
/**
* @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=false] (Optional) Reverse the iteration order (loop from the end to the beginning).
* @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=0] (Optional) a zero-based index that specifies the start of extraction.
* @param {Number} [end=-1] (Optional) a zero-based index that specifies the end of extraction.
* @return {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) {
//<debug>
if (!fn) {
Ext.Error.raise('Ext.Array.every must have a callback function passed as second argument.');
}
//</debug>
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) {
//<debug>
if (!fn) {
Ext.Error.raise('Ext.Array.some must have a callback function passed as second argument.');
}
//</debug>
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=false] (Optional) `true` to clone the given array and return a new reference if necessary.
* @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 comparison 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 comparison 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;
},
//<debug>
_replaceSim: replaceSim, // for unit testing
_spliceSim: spliceSim,
//</debug>
/**
* 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);
};
})();
//@tag foundation,core
//@define Ext.Number
//@require Ext.Array
/**
* @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);
};
//@tag foundation,core
//@define Ext.Object
//@require Ext.Number
/**
* @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: ('create' in Object) ? function(object){
return Object.create(object);
} : 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:
*
* Non-recursive:
*
* var objects = Ext.Object.toQueryObjects('hobbies', ['reading', 'cooking', 'swimming']);
*
* // objects then equals:
* [
* { name: 'hobbies', value: 'reading' },
* { name: 'hobbies', value: 'cooking' },
* { name: 'hobbies', value: 'swimming' }
* ]
*
* Recursive:
*
* var objects = Ext.Object.toQueryObjects('dateOfBirth', {
* day: 3,
* month: 8,
* year: 1987,
* extra: {
* hour: 4,
* minute: 30
* }
* }, true);
*
* // 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=false] `true` to recursively encode any sub-objects.
* @return {Object[]} Array of objects with `name` and `value` fields.
*/
toQueryObjects: function(name, value, recursive) {
var self = ExtObject.toQueryObjects,
objects = [],
i, ln;
if (Ext.isArray(value)) {
for (i = 0, ln = value.length; i < ln; i++) {
if (recursive) {
objects = objects.concat(self(name + '[' + i + ']', value[i], true));
}
else {
objects.push({
name: name,
value: value[i]
});
}
}
}
else if (Ext.isObject(value)) {
for (i in value) {
if (value.hasOwnProperty(i)) {
if (recursive) {
objects = objects.concat(self(name + '[' + i + ']', value[i], true));
}
else {
objects.push({
name: name,
value: value[i]
});
}
}
}
}
else {
objects.push({
name: name,
value: value
});
}
return objects;
},
/**
* Takes an object and converts it to an encoded query string.
*
* Non-recursive:
*
* Ext.Object.toQueryString({foo: 1, bar: 2}); // returns "foo=1&bar=2"
* Ext.Object.toQueryString({foo: null, bar: 2}); // returns "foo=&bar=2"
* Ext.Object.toQueryString({'some price': '$300'}); // returns "some%20price=%24300"
* Ext.Object.toQueryString({date: new Date(2011, 0, 1)}); // returns "date=%222011-01-01T00%3A00%3A00%22"
* Ext.Object.toQueryString({colors: ['red', 'green', 'blue']}); // returns "colors=red&colors=green&colors=blue"
*
* Recursive:
*
* Ext.Object.toQueryString({
* username: 'Jacky',
* dateOfBirth: {
* day: 1,
* month: 2,
* year: 1911
* },
* hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
* }, true);
*
* // returns the following string (broken down and url-decoded for ease of reading purpose):
* // username=Jacky
* // &dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911
* // &hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff
*
* @param {Object} object The object to encode.
* @param {Boolean} [recursive=false] Whether or not to interpret the object in recursive format.
* (PHP / Ruby on Rails servers and similar).
* @return {String} queryString
*/
toQueryString: function(object, recursive) {
var paramObjects = [],
params = [],
i, j, ln, paramObject, value;
for (i in object) {
if (object.hasOwnProperty(i)) {
paramObjects = paramObjects.concat(ExtObject.toQueryObjects(i, object[i], recursive));
}
}
for (j = 0, ln = paramObjects.length; j < ln; j++) {
paramObject = paramObjects[j];
value = paramObject.value;
if (Ext.isEmpty(value)) {
value = '';
}
else if (Ext.isDate(value)) {
value = Ext.Date.toString(value);
}
params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value)));
}
return params.join('&');
},
/**
* Converts a query string back into an object.
*
* Non-recursive:
*
* Ext.Object.fromQueryString("foo=1&bar=2"); // returns {foo: 1, bar: 2}
* Ext.Object.fromQueryString("foo=&bar=2"); // returns {foo: null, bar: 2}
* Ext.Object.fromQueryString("some%20price=%24300"); // returns {'some price': '$300'}
* Ext.Object.fromQueryString("colors=red&colors=green&colors=blue"); // returns {colors: ['red', 'green', 'blue']}
*
* Recursive:
*
* Ext.Object.fromQueryString("username=Jacky&dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911&hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff", true);
*
* // returns
* {
* username: 'Jacky',
* dateOfBirth: {
* day: '1',
* month: '2',
* year: '1911'
* },
* hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
* }
*
* @param {String} queryString The query string to decode.
* @param {Boolean} [recursive=false] Whether or not to recursively decode the string. This format is supported by
* PHP / Ruby on Rails servers and similar.
* @return {Object}
*/
fromQueryString: function(queryString, recursive) {
var parts = queryString.replace(/^\?/, '').split('&'),
object = {},
temp, components, name, value, i, ln,
part, j, subLn, matchedKeys, matchedName,
keys, key, nextKey;
for (i = 0, ln = parts.length; i < ln; i++) {
part = parts[i];
if (part.length > 0) {
components = part.split('=');
name = decodeURIComponent(components[0]);
value = (components[1] !== undefined) ? decodeURIComponent(components[1]) : '';
if (!recursive) {
if (object.hasOwnProperty(name)) {
if (!Ext.isArray(object[name])) {
object[name] = [object[name]];
}
object[name].push(value);
}
else {
object[name] = value;
}
}
else {
matchedKeys = name.match(/(\[):?([^\]]*)\]/g);
matchedName = name.match(/^([^\[]+)/);
//<debug error>
if (!matchedName) {
throw new Error('[Ext.Object.fromQueryString] Malformed query string given, failed parsing name from "' + part + '"');
}
//</debug>
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.
* @param {String} fn.key
* @param {Mixed} fn.value
* @param {Object} fn.object The object itself
* @param {Object} [scope] The execution scope (`this`) of the callback function
*/
each: function(object, fn, scope) {
for (var property in object) {
if (object.hasOwnProperty(property)) {
if (fn.call(scope || object, property, object[property], object) === false) {
return;
}
}
}
},
/**
* Merges any number of objects recursively without referencing them or their children.
*
* var extjs = {
* companyName: 'Ext JS',
* products: ['Ext JS', 'Ext GWT', 'Ext Designer'],
* isSuperCool: true,
* office: {
* size: 2000,
* location: 'Palo Alto',
* isFun: true
* }
* };
*
* var newStuff = {
* companyName: 'Sencha Inc.',
* products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
* office: {
* size: 40000,
* location: 'Redwood City'
* }
* };
*
* var sencha = Ext.Object.merge({}, extjs, newStuff);
*
* // sencha then equals to
* {
* companyName: 'Sencha Inc.',
* products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
* isSuperCool: true
* office: {
* size: 40000,
* location: 'Redwood City'
* isFun: true
* }
* }
*
* @param {Object} source The first object into which to merge the others.
* @param {Object...} objs One or more objects to be merged into the first.
* @return {Object} The object that is created as a result of merging all the objects passed in.
*/
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
*/
getKey: function(object, value) {
for (var property in object) {
if (object.hasOwnProperty(property) && object[property] === value) {
return property;
}
}
return null;
},
/**
* Gets all values of the given object as an array.
*
* var values = Ext.Object.getValues({
* name: 'Jacky',
* loves: 'food'
* }); // ['Jacky', 'food']
*
* @param {Object} object
* @return {Array} An array of values from the object.
*/
getValues: function(object) {
var values = [],
property;
for (property in object) {
if (object.hasOwnProperty(property)) {
values.push(object[property]);
}
}
return values;
},
/**
* Gets all keys of the given object as an array.
*
* var values = Ext.Object.getKeys({
* name: 'Jacky',
* loves: 'food'
* }); // ['name', 'loves']
*
* @param {Object} object
* @return {String[]} An array of keys from the object.
* @method
*/
getKeys: ('keys' in Object) ? Object.keys : function(object) {
var keys = [],
property;
for (property in object) {
if (object.hasOwnProperty(property)) {
keys.push(property);
}
}
return keys;
},
/**
* Gets the total number of this object's own properties.
*
* var size = Ext.Object.getSize({
* name: 'Jacky',
* loves: 'food'
* }); // size equals 2
*
* @param {Object} object
* @return {Number} size
*/
getSize: function(object) {
var size = 0,
property;
for (property in object) {
if (object.hasOwnProperty(property)) {
size++;
}
}
return size;
},
/**
* @private
*/
classify: function(object) {
var objectProperties = [],
arrayProperties = [],
propertyClassesMap = {},
objectClass = function() {
var i = 0,
ln = objectProperties.length,
property;
for (; i < ln; i++) {
property = objectProperties[i];
this[property] = new propertyClassesMap[property];
}
ln = arrayProperties.length;
for (i = 0; i < ln; i++) {
property = arrayProperties[i];
this[property] = object[property].slice();
}
},
key, value, constructor;
for (key in object) {
if (object.hasOwnProperty(key)) {
value = object[key];
if (value) {
constructor = value.constructor;
if (constructor === Object) {
objectProperties.push(key);
propertyClassesMap[key] = ExtObject.classify(value);
}
else if (constructor === Array) {
arrayProperties.push(key);
}
}
}
}
objectClass.prototype = object;
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);
};
})();
//@tag foundation,core
//@define Ext.Function
//@require Ext.Object
/**
* @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
* @return {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=null] (optional) The value to return if the passed function return `false`.
* @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 milliseconds 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() {
var callArgs = args || Array.prototype.slice.call(arguments, 0),
me = scope || this;
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(function(){
fn.apply(me, callArgs);
}, 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.
* @return {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');
//@tag foundation,core
//@define Ext.JSON
//@require Ext.Function
/**
* @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](http://www.json.org/js.html)
* @singleton
*/
Ext.JSON = new(function() {
var useHasOwn = !! {}.hasOwnProperty,
isNative = function() {
var useNative = null;
return function() {
if (useNative === null) {
useNative = Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]';
}
return useNative;
};
}(),
pad = function(n) {
return n < 10 ? "0" + n : n;
},
doDecode = function(json) {
return eval("(" + json + ')');
},
doEncode = function(o) {
if (!Ext.isDefined(o) || o === null) {
return "null";
} else if (Ext.isArray(o)) {
return encodeArray(o);
} else if (Ext.isDate(o)) {
return Ext.JSON.encodeDate(o);
} else if (Ext.isString(o)) {
return encodeString(o);
} else if (typeof o == "number") {
//don't use isNumber here, since finite checks happen inside isNumber
return isFinite(o) ? String(o) : "null";
} else if (Ext.isBoolean(o)) {
return String(o);
} else if (Ext.isObject(o)) {
return encodeObject(o);
} else if (typeof o === "function") {
return "null";
}
return 'undefined';
},
m = {
"\b": '\\b',
"\t": '\\t',
"\n": '\\n',
"\f": '\\f',
"\r": '\\r',
'"': '\\"',
"\\": '\\\\',
'\x0b': '\\u000b' //ie doesn't handle \v
},
charToReplace = /[\\\"\x00-\x1f\x7f-\uffff]/g,
encodeString = function(s) {
return '"' + s.replace(charToReplace, function(a) {
var c = m[a];
return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"';
},
encodeArray = function(o) {
var a = ["[", ""],
// Note empty string in case there are no serializable members.
len = o.length,
i;
for (i = 0; i < len; i += 1) {
a.push(doEncode(o[i]), ',');
}
// Overwrite trailing comma (or empty string)
a[a.length - 1] = ']';
return a.join("");
},
encodeObject = function(o) {
var a = ["{", ""],
// Note empty string in case there are no serializable members.
i;
for (i in o) {
if (!useHasOwn || o.hasOwnProperty(i)) {
a.push(doEncode(i), ":", doEncode(o[i]), ',');
}
}
// Overwrite trailing comma (or empty string)
a[a.length - 1] = '}';
return a.join("");
};
/**
* Encodes a Date. This returns the actual string which is inserted into the JSON string as the literal expression.
* __The returned value includes enclosing double quotation marks.__
*
* The default return format is "yyyy-mm-ddThh:mm:ss".
*
* To override this:
*
* Ext.JSON.encodeDate = function(d) {
* return Ext.Date.format(d, '"Y-m-d"');
* };
*
* @param {Date} d The Date to encode.
* @return {String} The string literal to use in a JSON string.
*/
this.encodeDate = function(o) {
return '"' + o.getFullYear() + "-"
+ pad(o.getMonth() + 1) + "-"
+ pad(o.getDate()) + "T"
+ pad(o.getHours()) + ":"
+ pad(o.getMinutes()) + ":"
+ pad(o.getSeconds()) + '"';
};
/**
* Encodes an Object, Array or other value.
* @param {Object} o The variable to encode.
* @return {String} The JSON string.
* @method
*/
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 Error 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/null} The resulting object.
* @method
*/
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;
//@tag foundation,core
//@define Ext.Error
//@require Ext.JSON
Ext.Error = {
raise: function(object) {
throw new Error(object.msg);
}
};
//@tag foundation,core
//@define Ext.Date
//@require Ext.Error
/**
*
*/
Ext.Date = {
/** @ignore */
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');
}
};
//@tag foundation,core
//@define Ext.Base
//@require Ext.Date
/**
* @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,
prototype, i, ln, name, statics;
prototype = this.prototype = Ext.Object.chain(parentPrototype);
prototype.self = this;
this.superclass = prototype.superclass = parentPrototype;
if (!parent.$isClass) {
Ext.apply(prototype, Ext.Base.prototype);
prototype.constructor = function() {
parentPrototype.constructor.apply(this, arguments);
};
}
//<feature classSystem.inheritableStatics>
// 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];
}
}
}
//</feature>
if (parent.$onExtended) {
this.$onExtended = parent.$onExtended.slice();
}
//<feature classSystem.config>
prototype.config = prototype.defaultConfig = new prototype.configClass;
prototype.initConfigList = prototype.initConfigList.slice();
prototype.initConfigMap = Ext.Object.chain(prototype.initConfigMap);
//</feature>
},
/**
* @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', {
* // this.se
* });
*
* 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;
//<debug>
var className = Ext.getClassName(this);
//</debug>
for (name in members) {
if (members.hasOwnProperty(name)) {
member = members[name];
//<debug>
if (typeof member == 'function') {
member.displayName = className + '.' + name;
}
//</debug>
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 = {};
}
//<debug>
var className = Ext.getClassName(this);
//</debug>
for (name in members) {
if (members.hasOwnProperty(name)) {
member = members[name];
//<debug>
if (typeof member == 'function') {
member.displayName = className + '.' + name;
}
//</debug>
this[name] = member;
if (!hasInheritableStatics[name]) {
hasInheritableStatics[name] = true;
inheritableStatics.push(name);
}
}
}
return this;
},
/**
* Add methods / properties to the prototype of this class.
*
* @example
* Ext.define('My.awesome.Cat', {
* constructor: function() {
* // ...
* }
* });
*
* My.awesome.Cat.addMembers({
* meow: function() {
* alert('Meowww...');
* }
* });
*
* var kitty = new My.awesome.Cat();
* kitty.meow();
*
* @param {Object} members
* @static
* @inheritable
*/
addMembers: function(members) {
var prototype = this.prototype,
names = [],
name, member;
//<debug>
var className = this.$className || '';
//</debug>
for (name in members) {
if (members.hasOwnProperty(name)) {
member = members[name];
if (typeof member == 'function' && !member.$isClass && member !== Ext.emptyFn) {
member.$owner = this;
member.$name = name;
//<debug>
member.displayName = className + '#' + name;
//</debug>
}
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;
//<debug>
member.displayName = (this.$className || '') + '#' + name;
//</debug>
}
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,
//<debug>
className = Ext.getClassName(this),
//</debug>
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);
};
//<debug>
if (className) {
fn.displayName = className + '#' + name;
}
//</debug>
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 2.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 2.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);
}
//<debug>
var className = me.$className;
if (className) {
member.displayName = className + '#' + name;
}
//</debug>
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;
},
/**
* @protected
* @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);
},
//<feature classSystem.mixins>
/**
* 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];
}
}
//<feature classSystem.config>
if ('config' in mixin) {
this.addConfig(mixin.config, false);
}
//</feature>
prototype.mixins[name] = mixin;
},
//</feature>
/**
* Get the current class' name in string format.
*
* Ext.define('My.cool.Class', {
* constructor: function() {
* alert(this.self.getName()); // alerts 'My.cool.Class'
* }
* });
*
* My.cool.Class.getName(); // 'My.cool.Class'
*
* @return {String} className
* @static
* @inheritable
*/
getName: function() {
return Ext.getClassName(this);
},
/**
* Create aliases for existing prototype methods. Example:
*
* Ext.define('My.cool.Class', {
* method1: function() { },
* method2: function() { }
* });
*
* var test = new My.cool.Class();
*
* My.cool.Class.createAlias({
* method3: 'method1',
* method4: 'method2'
* });
*
* test.method3(); // test.method1()
*
* My.cool.Class.createAlias('method5', 'method3');
*
* test.method5(); // test.method3() -> test.method1()
*
* @param {String/Object} alias The new method name, or an object to set multiple aliases. See
* {@link Ext.Function#flexSetter flexSetter}
* @param {String/Object} origin The original method name
* @static
* @inheritable
* @method
*/
createAlias: flexSetter(function(alias, origin) {
this.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
*
* To override a method and replace it and also call the superclass method, use
* {@link #callSuper}. This is often done to patch a method to fix a bug.
*
* @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]));
//<debug error>
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') + ")");
}
}
//</debug>
return superMethod.apply(this, args || noArgs);
},
/**
* This method is used by an override to call the superclass method but bypass any
* overridden method. This is often done to "patch" a method that contains a bug
* but for whatever reason cannot be fixed directly.
*
* Consider:
*
* Ext.define('Ext.some.Class', {
* method: function () {
* console.log('Good');
* }
* });
*
* Ext.define('Ext.some.DerivedClass', {
* method: function () {
* console.log('Bad');
*
* // ... logic but with a bug ...
*
* this.callParent();
* }
* });
*
* To patch the bug in `DerivedClass.method`, the typical solution is to create an
* override:
*
* Ext.define('App.paches.DerivedClass', {
* override: 'Ext.some.DerivedClass',
*
* method: function () {
* console.log('Fixed');
*
* // ... logic but with bug fixed ...
*
* this.callSuper();
* }
* });
*
* The patch method cannot use `callParent` to call the superclass `method` since
* that would call the overridden method containing the bug. In other words, the
* above patch would only produce "Fixed" then "Good" in the console log, whereas,
* using `callParent` would produce "Fixed" then "Bad" then "Good".
*
* @protected
* @param {Array/Arguments} args The arguments, either an array or the `arguments` object
* from the current method, for example: `this.callSuper(arguments)`
* @return {Object} Returns the result of calling the superclass method
*/
callSuper: function(args) {
var method,
superMethod = (method = this.callSuper.caller) && ((method = method.$owner ? method : method.caller) &&
method.$owner.superclass[method.$name]);
//<debug error>
if (!superMethod) {
method = this.callSuper.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.callSuper() was called but there's no such method (" + methodName +
") found in the parent class (" + (Ext.getClassName(parentClass) || 'Object') + ")");
}
}
//</debug>
return superMethod.apply(this, args || noArgs);
},
/**
* Call the original method that was previously overridden with {@link Ext.Base#override},
*
* This method is deprecated as {@link #callParent} does the same thing.
*
* 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
* @deprecated Use callParent instead
*/
callOverridden: function(args) {
var method;
return (method = this.callOverridden.caller) && method.$previous.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); // dependent 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;
},
//<feature classSystem.config>
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) {
//<debug>
// if (instanceConfig && instanceConfig.breakOnInitConfig) {
// debugger;
// }
//</debug>
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;
},
set: function(name, value) {
return this[Ext.Class.configNameCache[name].set].call(this, value);
},
get: function(name) {
return this[Ext.Class.configNameCache[name].get].call(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,
//<debug>
className = self.$className,
//</debug>
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;
//<debug>
newUpdater.displayName = className + '#' + updaterName;
//</debug>
this[updaterName] = newUpdater;
}
},
//</feature>
/**
* @private
* @param name
* @param value
* @return {Mixed}
*/
link: function(name, value) {
this.$links = {};
this.link = this.doLink;
return this.link.apply(this, arguments);
},
doLink: function(name, value) {
this.$links[name] = true;
this[name] = value;
return value;
},
/**
* @private
*/
unlink: function() {
var i, ln, link, value;
for (i = 0, ln = arguments.length; i < ln; i++) {
link = arguments[i];
if (this.hasOwnProperty(link)) {
value = this[link];
if (value) {
if (value.isInstance && !value.isDestroyed) {
value.destroy();
}
else if (value.parentNode && 'nodeType' in value) {
value.parentNode.removeChild(value);
}
}
delete this[link];
}
}
return this;
},
/**
* @protected
*/
destroy: function() {
this.destroy = Ext.emptyFn;
this.isDestroyed = true;
if (this.hasOwnProperty('$links')) {
this.unlink.apply(this, Ext.Object.getKeys(this.$links));
delete this.$links;
}
}
});
Ext.Base = Base;
})(Ext.Function.flexSetter);
//@tag foundation,core
//@define Ext.Class
//@require Ext.Base
/**
* @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 dependency 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);
* }
* });
*
* @param {Function} fn.cls The created class.
* @param {Object} fn.data The set of properties passed in {@link Ext.Class} constructor.
* @param {Function} fn.fn The callback function that __must__ to be executed when this pre-processor finishes,
* regardless of whether the processing is synchronous or asynchronous.
*
* @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:
*
* @example
* Ext.define('Person', {
* say: function(text) {
* alert(text);
* }
* });
*
* Ext.define('Developer', {
* extend: 'Person',
* say: function(text) {
* this.callParent(["print " + text]);
* }
* });
*
* var person1 = Ext.create("Person");
* person1.say("Bill");
*
* var developer1 = Ext.create("Developer");
* developer1.say("Ted");
*/
ExtClass.registerPreprocessor('extend', function(Class, data) {
var Base = Ext.Base,
extend = data.extend,
Parent;
delete data.extend;
if (extend && extend !== Object) {
Parent = extend;
}
else {
Parent = Base;
}
Class.extend(Parent);
Class.triggerExtended.apply(Class, arguments);
if (data.onClassExtended) {
Class.onExtended(data.onClassExtended, Class);
delete data.onClassExtended;
}
}, true);
//<feature classSystem.statics>
/**
* @cfg {Object} statics
* List of static methods for this class. For example:
*
* Ext.define('Computer', {
* statics: {
* factory: function(brand) {
* // 'this' in static methods refer to the class itself
* return new this(brand);
* }
* },
*
* constructor: function() {
* // ...
* }
* });
*
* var dellComputer = Computer.factory('Dell');
*/
ExtClass.registerPreprocessor('statics', function(Class, data) {
Class.addStatics(data.statics);
delete data.statics;
});
//</feature>
//<feature classSystem.inheritableStatics>
/**
* @cfg {Object} inheritableStatics
* List of inheritable static methods for this class.
* Otherwise just like {@link #statics} but subclasses inherit these methods.
*/
ExtClass.registerPreprocessor('inheritableStatics', function(Class, data) {
Class.addInheritableStatics(data.inheritableStatics);
delete data.inheritableStatics;
});
//</feature>
//<feature classSystem.config>
/**
* @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, throughout 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: an "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 < 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 necessary. 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 hierarchy
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);
});
//</feature>
//<feature classSystem.mixins>
/**
* @cfg {Object} mixins
* List of classes to mix into this class. For example:
*
* Ext.define('CanSing', {
* sing: function() {
* alert("I'm on the highway to hell...");
* }
* });
*
* Ext.define('Musician', {
* extend: 'Person',
*
* mixins: {
* canSing: 'CanSing'
* }
* });
*/
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]);
}
}
}
});
});
//</feature>
//<feature classSystem.backwardsCompatible>
// 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'
//<feature classSystem.statics>
,'statics'
//</feature>
//<feature classSystem.inheritableStatics>
,'inheritableStatics'
//</feature>
//<feature classSystem.mixins>
,'mixins'
//</feature>
//<feature classSystem.config>
,'config'
//</feature>
];
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;
};
//</feature>
})();
//@tag foundation,core
//@define Ext.ClassManager
//@require Ext.Class
/**
* @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.ClassManager#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.
*
* @example
* 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;
//<debug error>
if (typeof className != 'string' || className.length < 1) {
throw new Error("[Ext.ClassManager] Invalid classname, must be a string and must not be empty");
}
//</debug>
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) {
//<debug error>
if (typeof namespace != 'string') {
throw new Error("[Ext.ClassManager] Invalid namespace, must be a string");
}
//</debug>
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) {
//<debug info>
if (aliasToNameMap[alias]) {
Ext.Logger.info("[Ext.ClassManager] Overriding existing alias: '" + alias + "' " +
"of: '" + aliasToNameMap[alias] + "' with: '" + className + "'. Be sure it's intentional.");
}
//</debug>
aliasToNameMap[alias] = className;
}
if (!nameToAliasesMap[className]) {
nameToAliasesMap[className] = [];
}
if (alias) {
Ext.Array.include(nameToAliasesMap[className], alias);
}
return this;
},
/**
* Adds a batch of class name to alias mappings
* @param {Object} aliases The set of mappings of the form
* className : [values...]
*/
addNameAliasMappings: function(aliases){
var aliasToNameMap = this.maps.aliasToName,
nameToAliasesMap = this.maps.nameToAliases,
className, aliasList, alias, i;
for (className in aliases) {
aliasList = nameToAliasesMap[className] ||
(nameToAliasesMap[className] = []);
for (i = 0; i < aliases[className].length; i++) {
alias = aliases[className][i];
if (!aliasToNameMap[alias]) {
aliasToNameMap[alias] = className;
aliasList.push(alias);
}
}
}
return this;
},
/**
*
* @param {Object} alternates The set of mappings of the form
* className : [values...]
*/
addNameAlternateMappings: function(alternates) {
var alternateToName = this.maps.alternateToName,
nameToAlternates = this.maps.nameToAlternates,
className, aliasList, alternate, i;
for (className in alternates) {
aliasList = nameToAlternates[className] ||
(nameToAlternates[className] = []);
for (i = 0; i < alternates[className].length; i++) {
alternate = alternates[className];
if (!alternateToName[alternate]) {
alternateToName[alternate] = className;
aliasList.push(alternate);
}
}
}
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
*/
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) {
//<debug error>
if (typeof className != 'string') {
throw new Error("[Ext.define] Invalid class name '" + className + "' specified, must be a non-empty string");
}
//</debug>
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,
requires = Ext.Array.from(data.requires);
delete data.override;
delete data.requires;
this.existCache[className] = true;
Ext.require(requires, function() {
// 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);
}, this);
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];
//<debug error>
if (!className) {
throw new Error("[Ext.createByAlias] Cannot create an instance of unrecognized alias: " + alias);
}
//</debug>
//<debug warn>
Ext.Logger.warn("[Ext.Loader] Synchronously loading '" + className + "'; consider adding " +
"Ext.require('" + alias + "') above Ext.onReady");
//</debug>
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.ClassManager#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 formPanel = Ext.create('widget.formpanel', { width: 600, height: 800 });
*
* // alternate name
* var formPanel = Ext.create('Ext.form.FormPanel', { width: 600, height: 800 });
*
* // full class name
* var formPanel = Ext.create('Ext.form.Panel', { 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') {
//<debug error>
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");
}
//</debug>
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) {
//<debug warn>
Ext.Logger.warn("[Ext.Loader] Synchronously loading '" + name + "'; consider adding '" +
((possibleName) ? alias : name) + "' explicitly as a require of the corresponding class");
//</debug>
Ext.syncRequire(name);
cls = this.get(name);
}
//<debug error>
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");
}
//</debug>
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(',') + ')');
//<debug>
instantiator.displayName = "Ext.ClassManager.instantiate" + length;
//</debug>
}
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
*/
getNamesByExpression: function(expression) {
var nameToAliasesMap = this.maps.nameToAliases,
names = [],
name, alias, aliases, possibleName, regex, i, ln;
//<debug error>
if (typeof expression != 'string' || expression.length < 1) {
throw new Error("[Ext.ClassManager.getNamesByExpression] Expression " + expression + " is invalid, must be a non-empty string");
}
//</debug>
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;
}
};
//<feature classSystem.alias>
/**
* @cfg {String[]} alias
* @member Ext.Class
* List of short aliases for class names. Most useful for defining xtypes for widgets:
*
* Ext.define('MyApp.CoolPanel', {
* extend: 'Ext.panel.Panel',
* alias: ['widget.coolpanel'],
* title: 'Yeah!'
* });
*
* // Using Ext.create
* Ext.create('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']);
//</feature>
//<feature classSystem.singleton>
/**
* @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;
});
//</feature>
//<feature classSystem.alternateClassName>
/**
* @cfg {String/String[]} alternateClassName
* @member Ext.Class
* Defines alternate names for this class. For example:
*
* @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];
//<debug error>
if (typeof alternate != 'string') {
throw new Error("[Ext.define] Invalid alternate of: '" + alternate + "' for class: '" + name + "'; must be a valid string");
}
//</debug>
this.set(alternate, cls);
}
});
//</feature>
Ext.apply(Ext, {
/**
* Instantiate a class by either full name, alias or alternate name.
*
* If {@link Ext.Loader} is {@link Ext.Loader#setConfig enabled} and the class has not been defined yet, it will
* attempt to load the class via synchronous loading.
*
* For example, all these three lines return the same result:
*
* // alias
* var formPanel = Ext.create('widget.formpanel', { width: 600, height: 800 });
*
* // alternate name
* var formPanel = Ext.create('Ext.form.FormPanel', { width: 600, height: 800 });
*
* // full class name
* var formPanel = Ext.create('Ext.form.Panel', { 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
* @member Ext
*/
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.
* @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;
//<debug>
cls.displayName = cls.$className;
//</debug>
}
}, 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];
//<debug error>
if (typeof alias != 'string' || alias.length < 1) {
throw new Error("[Ext.define] Invalid alias of: '" + alias + "' for class: '" + name + "'; must be a valid string");
}
//</debug>
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];
//<debug error>
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");
}
//</debug>
Ext.Array.include(aliases, widgetPrefix + xtype);
}
data.alias = aliases;
}, ['xtype', 'alias']);
})(Ext.Class, Ext.Function.alias, Array.prototype.slice, Ext.Array.from, Ext.global);
//@tag foundation,core
//@define Ext.Loader
//@require Ext.ClassManager
/**
* @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(function () {
* // ...
* });
*
* 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,
setPathCount = 0;;
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 every time 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.
* @cfg {Boolean} enabled
*/
enabled: true,
/**
* @cfg {Boolean} disableCaching
* Appends current timestamp to script files to prevent caching.
*/
disableCaching: true,
/**
* @cfg {String} disableCachingParam
* The get parameter name for the cache buster's timestamp.
*/
disableCachingParam: '_dc',
/**
* @cfg {Object} paths
* The mapping from namespaces to file paths.
*
* {
* 'Ext': '.', // This is set by default, Ext.layout.container.Container will be
* // loaded from ./layout/Container.js
*
* 'My': './src/my_own_folder' // My.layout.Container will be loaded from
* // ./src/my_own_folder/layout/Container.js
* }
*
* Note that all relative paths are relative to the current HTML document.
* If not being specified, for example, `Other.awesome.Class`
* will simply be loaded from `./Other/awesome/Class.js`.
*/
paths: {
'Ext': '.'
}
},
/**
* Set the configuration for the loader. This should be called right after ext-(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
*/
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;
}
setPathCount += 1;
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;
setPathCount += 1;
return this;
}),
/**
* Sets a batch of path entries
*
* @param {Object } paths a set of className: path mappings
* @return {Ext.Loader} this
*/
addClassPathMappings: function(paths) {
var name;
if(setPathCount == 0){
Loader.config.paths = paths;
} else {
for(name in paths){
Loader.config.paths[name] = paths[name];
}
}
setPathCount++;
return Loader;
},
/**
* 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.
*/
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
*/
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.
*/
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 (`this`) of the callback function.
* @param {Boolean} withDomReady Whether or not to wait for document DOM ready as well.
*/
onReady: function(fn, scope, withDomReady, options) {
var oldFn;
if (withDomReady !== false && Ext.onDocumentReady) {
oldFn = fn;
fn = function() {
Ext.onDocumentReady(oldFn, scope, options);
};
}
fn.call(scope);
}
};
//<feature classSystem.loader>
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()) : ''),
xhr, status, content, onScriptError;
if (isFileLoaded[url]) {
return this;
}
scope = scope || this;
this.isLoading = true;
if (!synchronous) {
onScriptError = function() {
//<debug error>
onError.call(scope, "Failed loading '" + url + "', please verify that the file exists", synchronous);
//</debug>
};
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', noCacheUrl, false);
xhr.send(null);
}
catch (e) {
//<debug error>
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);
//</debug>
}
status = (xhr.status == 1223) ? 204 : xhr.status;
content = xhr.responseText;
if ((status >= 200 && status < 300) || status == 304 || (status == 0 && content.length > 0)) {
// 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(content + "\n//@ sourceURL=" + url);
onLoad.call(scope);
}
else {
//<debug>
onError.call(this, "Failed loading synchronously via XHR: '" + url + "'; please " +
"verify that the file exists. " +
"XHR status code: " + status, synchronous);
//</debug>
}
// 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 asynchronously 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();
}
//<debug>
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("', '"));
}
//</debug>
},
/**
* @private
*/
onFileLoadError: function(className, filePath, errorMessage, isSynchronous) {
this.numPendingFiles--;
this.hasFileLoadError = true;
//<debug error>
throw new Error("[Ext.Loader] " + errorMessage);
//</debug>
},
/**
* @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;
}
});
//</feature>
/**
* 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 and all dependencies are loaded.
*
* @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;
}
//<feature classSystem.loader>
//<debug error>
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;
//<debug>
if (!Loader.requiredByMap) Loader.requiredByMap = {};
Ext.Array.each(dependencies, function(dependency){
if (!Loader.requiredByMap[dependency]) Loader.requiredByMap[dependency] = [];
Loader.requiredByMap[dependency].push(className);
});
//</debug>
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);
}
//</debug>
//</feature>
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');
//<feature classSystem.loader>
/**
* @cfg {String[]} uses
* @member Ext.Class
* List of optional classes to load together with this class. These aren't necessarily 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);
//</feature>
})(Ext.ClassManager, Ext.Class, Ext.Function.flexSetter, Ext.Function.alias,
Ext.Function.pass, Ext.Array.from, Ext.Array.erase, Ext.Array.include);
// initalize the default path of the framework
// trimmed down version of sench-touch-debug-suffix.js
// with alias / alternates removed, as those are handled separately by
// compiler-generated metadata
(function() {
var scripts = document.getElementsByTagName('script'),
currentScript = scripts[scripts.length - 1],
src = currentScript.src,
path = src.substring(0, src.lastIndexOf('/') + 1),
Loader = Ext.Loader;
//<debug>
// if we're running in dev mode out of the repo src tree, then this
// file will potentially be loaded from the touch/src/core/class folder
// so we'll need to adjust for that
if(src.indexOf("src/core/class/") != -1) {
path = path + "../../../";
}
//</debug>
Loader.setConfig({
enabled: true,
disableCaching: !/[?&](cache|breakpoint)/i.test(location.search),
paths: {
'Ext' : path + 'src'
}
});
})();
//@tag dom,core
//@define Ext.EventManager
//@define Ext.core.EventManager
//@require Ext.Loader
/**
* @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
*/
//@tag dom,core
//@define Ext-more
//@require Ext.EventManager
/**
* @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.1.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=ext-gen] (optional) The `id` prefix.
* @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, `undefined` if not found, or `null` if a
* Class was found.
*/
getCmp: function(id) {
return Ext.ComponentMgr.get(id);
},
/**
* Copies a set of named properties from 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=false] (optional) 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:
*
* // gets dom node based on id
* var elDom = Ext.getDom('elId');
*
* // gets dom node based on the dom node
* var elDom1 = Ext.getDom(elDom);
*
* // If we don't know if we are working with an
* // Ext.Element or a dom node use Ext.getDom
* function(el){
* var dom = Ext.getDom(el);
* // do something with the dom node
* }
*
* __Note:__ the dom node to be found actually needs to exist (be rendered, etc)
* when this method is called to be successful.
* @param {Mixed} el
* @return {HTMLElement}
*/
getDom: function(el) {
if (!el || !document) {
return null;
}
return el.dom ? el.dom : (typeof el == 'string' ? document.getElementById(el) : el);
},
/**
* 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'
// },
elementPaint: {
xclass: 'Ext.event.publisher.ElementPaint'
},
elementSize: {
xclass: 'Ext.event.publisher.ElementSize'
}
//<feature charts>
,seriesItemEvents: {
xclass: 'Ext.chart.series.ItemPublisher'
}
//</feature>
},
//<feature logger>
logger: {
enabled: true,
xclass: 'Ext.log.Logger',
minPriority: 'deprecate',
writers: {
console: {
xclass: 'Ext.log.writer.Console',
throwOnErrors: true,
formatter: {
xclass: 'Ext.log.formatter.Default'
}
}
}
},
//</feature>
animator: {
xclass: 'Ext.fx.Runner'
},
viewport: {
xclass: 'Ext.viewport.Viewport'
}
},
/**
* @private
*/
isSetup: false,
/**
* This indicate the start timestamp of current cycle.
* It is only reliable during dom-event-initiated cycles and
* {@link Ext.draw.Animator} initiated cycles.
*/
frameStartTime: +new Date(),
/**
* @private
*/
setupListeners: [],
/**
* @private
*/
onSetup: function(fn, scope) {
if (Ext.isSetup) {
fn.call(scope);
}
else {
Ext.setupListeners.push({
fn: fn,
scope: scope
});
}
},
/**
* Ext.setup() is the entry-point to initialize a Sencha Touch application. Note that if your application makes
* use of MVC architecture, use {@link Ext#application} instead.
*
* This method accepts one single argument in object format. The most basic use of Ext.setup() is as follows:
*
* Ext.setup({
* onReady: function() {
* // ...
* }
* });
*
* This sets up the viewport, initializes the event system, instantiates a default animation runner, and a default
* logger (during development). When all of that is ready, it invokes the callback function given to the `onReady` key.
*
* The default scope (`this`) of `onReady` is the main viewport. By default the viewport instance is stored in
* {@link Ext.Viewport}. For example, this snippet adds a 'Hello World' button that is centered on the screen:
*
* Ext.setup({
* onReady: function() {
* this.add({
* xtype: 'button',
* centered: true,
* text: 'Hello world!'
* }); // Equivalent to Ext.Viewport.add(...)
* }
* });
*
* @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.
*
* @param {Object} config.viewport
* A custom config 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: {
* width: 500,
* height: 500
* },
* onReady: function() {
* // ...
* }
* });
*
* @param {String/Object} config.icon
* Specifies a set of URLs to the application icon for different device form factors. This icon is displayed
* when the application is added to the device's Home Screen.
*
* Ext.setup({
* icon: {
* 57: 'resources/icons/Icon.png',
* 72: 'resources/icons/Icon~ipad.png',
* 114: 'resources/icons/Icon@2x.png',
* 144: 'resources/icons/Icon~ipad@2x.png'
* },
* onReady: function() {
* // ...
* }
* });
*
* Each key represents the dimension of the icon as a square shape. For example: '57' is the key for a 57 x 57
* icon image. Here is the breakdown of each dimension and its device target:
*
* - 57: Non-retina iPhone, iPod touch, and all Android devices
* - 72: Retina iPhone and iPod touch
* - 114: Non-retina iPad (first and second generation)
* - 144: Retina iPad (third generation)
*
* Note that the dimensions of the icon images must be exactly 57x57, 72x72, 114x114 and 144x144 respectively.
*
* It is highly recommended that you provide all these different sizes to accommodate a full range of
* devices currently available. However if you only have one icon in one size, make it 57x57 in size and
* specify it as a string value. This same icon will be used on all supported devices.
*
* Ext.setup({
* icon: 'resources/icons/Icon.png',
* onReady: function() {
* // ...
* }
* });
*
* @param {Object} config.startupImage
* Specifies a set of URLs to the application startup images for different device form factors. This image is
* displayed when the application is being launched from the Home Screen icon. Note that this currently only applies
* to iOS devices.
*
* Ext.setup({
* startupImage: {
* '320x460': 'resources/startup/320x460.jpg',
* '640x920': 'resources/startup/640x920.png',
* '640x1096': 'resources/startup/640x1096.png',
* '768x1004': 'resources/startup/768x1004.png',
* '748x1024': 'resources/startup/748x1024.png',
* '1536x2008': 'resources/startup/1536x2008.png',
* '1496x2048': 'resources/startup/1496x2048.png'
* },
* onReady: function() {
* // ...
* }
* });
*
* Each key represents the dimension of the image. For example: '320x460' is the key for a 320px x 460px image.
* Here is the breakdown of each dimension and its device target:
*
* - 320x460: Non-retina iPhone, iPod touch, and all Android devices
* - 640x920: Retina iPhone and iPod touch
* - 640x1096: iPhone 5 and iPod touch (fifth generation)
* - 768x1004: Non-retina iPad (first and second generation) in portrait orientation
* - 748x1024: Non-retina iPad (first and second generation) in landscape orientation
* - 1536x2008: Retina iPad (third generation) in portrait orientation
* - 1496x2048: Retina iPad (third generation) in landscape orientation
*
* Please note that there's no automatic fallback mechanism for the startup images. In other words, if you don't specify
* a valid image for a certain device, nothing will be displayed while the application is being launched on that device.
*
* @param {Boolean} isIconPrecomposed
* True to not having a glossy effect added to the icon by the OS, which will preserve its exact look. This currently
* only applies to iOS devices.
*
* @param {String} statusBarStyle
* The style of status bar to be shown on applications added to the iOS home screen. Valid options are:
*
* * `default`
* * `black`
* * `black-translucent`
*
* @param {String[]} config.requires
* An array of required classes for your application which will be automatically loaded before `onReady` is invoked.
* 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 recognize events fired
* in your application. The list of default recognizers can be found in the documentation for
* {@link Ext.event.recognizer.Recognizer}.
*
* To change the default recognizers, you can use the following syntax:
*
* Ext.setup({
* eventPublishers: {
* touchGesture: {
* recognizers: {
* swipe: {
* // this will include both vertical and horizontal swipe recognizers
* 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,
head = Ext.getHead(),
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']);
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);
head.append(meta);
}
function addIcon(href, sizes, precomposed) {
var link = document.createElement('link');
link.setAttribute('rel', 'apple-touch-icon' + (precomposed ? '-precomposed' : ''));
link.setAttribute('href', href);
if (sizes) {
link.setAttribute('sizes', sizes);
}
head.append(link);
}
function addStartupImage(href, media) {
var link = document.createElement('link');
link.setAttribute('rel', 'apple-touch-startup-image');
link.setAttribute('href', href);
if (media) {
link.setAttribute('media', media);
}
head.append(link);
}
var icon = config.icon,
isIconPrecomposed = Boolean(config.isIconPrecomposed),
startupImage = config.startupImage || {},
statusBarStyle = config.statusBarStyle,
devicePixelRatio = window.devicePixelRatio || 1;
if (navigator.standalone) {
addMeta('viewport', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0');
}
else {
addMeta('viewport', 'initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0');
}
addMeta('apple-mobile-web-app-capable', 'yes');
addMeta('apple-touch-fullscreen', 'yes');
// status bar style
if (statusBarStyle) {
addMeta('apple-mobile-web-app-status-bar-style', statusBarStyle);
}
if (Ext.isString(icon)) {
icon = {
57: icon,
72: icon,
114: icon,
144: icon
};
}
else if (!icon) {
icon = {};
}
if (Ext.os.is.iPad) {
if (devicePixelRatio >= 2) {
// Retina iPad - Landscape
if ('1496x2048' in startupImage) {
addStartupImage(startupImage['1496x2048'], '(orientation: landscape)');
}
// Retina iPad - Portrait
if ('1536x2008' in startupImage) {
addStartupImage(startupImage['1536x2008'], '(orientation: portrait)');
}
// Retina iPad
if ('144' in icon) {
addIcon(icon['144'], '144x144', isIconPrecomposed);
}
}
else {
// Non-Retina iPad - Landscape
if ('748x1024' in startupImage) {
addStartupImage(startupImage['748x1024'], '(orientation: landscape)');
}
// Non-Retina iPad - Portrait
if ('768x1004' in startupImage) {
addStartupImage(startupImage['768x1004'], '(orientation: portrait)');
}
// Non-Retina iPad
if ('72' in icon) {
addIcon(icon['72'], '72x72', isIconPrecomposed);
}
}
}
else {
// Retina iPhone, iPod touch with iOS version >= 4.3
if (devicePixelRatio >= 2 && Ext.os.version.gtEq('4.3')) {
if (Ext.os.is.iPhone5) {
addStartupImage(startupImage['640x1096']);
} else {
addStartupImage(startupImage['640x920']);
}
// Retina iPhone and iPod touch
if ('114' in icon) {
addIcon(icon['114'], '114x114', isIconPrecomposed);
}
}
else {
addStartupImage(startupImage['320x460']);
// Non-Retina iPhone, iPod touch, and Android devices
if ('57' in icon) {
addIcon(icon['57'], null, isIconPrecomposed);
}
}
}
},
/**
* @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
* Specifies a set of URLs to the application icon for different device form factors. This icon is displayed
* when the application is added to the device's Home Screen.
*
* Ext.application({
* icon: {
* 57: 'resources/icons/Icon.png',
* 72: 'resources/icons/Icon~ipad.png',
* 114: 'resources/icons/Icon@2x.png',
* 144: 'resources/icons/Icon~ipad@2x.png'
* },
* launch: function() {
* // ...
* }
* });
*
* Each key represents the dimension of the icon as a square shape. For example: '57' is the key for a 57 x 57
* icon image. Here is the breakdown of each dimension and its device target:
*
* - 57: Non-retina iPhone, iPod touch, and all Android devices
* - 72: Retina iPhone and iPod touch
* - 114: Non-retina iPad (first and second generation)
* - 144: Retina iPad (third generation)
*
* Note that the dimensions of the icon images must be exactly 57x57, 72x72, 114x114 and 144x144 respectively.
*
* It is highly recommended that you provide all these different sizes to accommodate a full range of
* devices currently available. However if you only have one icon in one size, make it 57x57 in size and
* specify it as a string value. This same icon will be used on all supported devices.
*
* Ext.setup({
* icon: 'resources/icons/Icon.png',
* onReady: function() {
* // ...
* }
* });
*
* @param {Object} config.startupImage
* Specifies a set of URLs to the application startup images for different device form factors. This image is
* displayed when the application is being launched from the Home Screen icon. Note that this currently only applies
* to iOS devices.
*
* Ext.application({
* startupImage: {
* '320x460': 'resources/startup/320x460.jpg',
* '640x920': 'resources/startup/640x920.png',
* '640x1096': 'resources/startup/640x1096.png',
* '768x1004': 'resources/startup/768x1004.png',
* '748x1024': 'resources/startup/748x1024.png',
* '1536x2008': 'resources/startup/1536x2008.png',
* '1496x2048': 'resources/startup/1496x2048.png'
* },
* launch: function() {
* // ...
* }
* });
*
* Each key represents the dimension of the image. For example: '320x460' is the key for a 320px x 460px image.
* Here is the breakdown of each dimension and its device target:
*
* - 320x460: Non-retina iPhone, iPod touch, and all Android devices
* - 640x920: Retina iPhone and iPod touch
* - 640x1096: iPhone 5 and iPod touch (fifth generation)
* - 768x1004: Non-retina iPad (first and second generation) in portrait orientation
* - 748x1024: Non-retina iPad (first and second generation) in landscape orientation
* - 1536x2008: Retina iPad (third generation) in portrait orientation
* - 1496x2048: Retina iPad (third generation) in landscape orientation
*
* Please note that there's no automatic fallback mechanism for the startup images. In other words, if you don't specify
* a valid image for a certain device, nothing will be displayed while the application is being launched on that device.
*
* @param {Boolean} config.isIconPrecomposed
* True to not having a glossy effect added to the icon by the OS, which will preserve its exact look. This currently
* only applies to iOS devices.
*
* @param {String} config.statusBarStyle
* The style of status bar to be shown on applications added to the iOS home screen. 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 recognize events fired
* in your application. The list of default recognizers can be found in the documentation for {@link Ext.event.recognizer.Recognizer}.
*
* To change the default recognizers, you can use the following syntax:
*
* Ext.application({
* eventPublishers: {
* touchGesture: {
* recognizers: {
* swipe: {
* // this will include both vertical and horizontal swipe recognizers
* 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);
},
/**
* A global factory method to instantiate a class from a config object. For example, these two calls are equivalent:
*
* Ext.factory({ text: 'My Button' }, 'Ext.Button');
* Ext.create('Ext.Button', { text: 'My Button' });
*
* If an existing instance is also specified, it will be updated with the supplied config object. This is useful
* if you need to either create or update an object, depending on if an instance already exists. For example:
*
* var button;
* button = Ext.factory({ text: 'New Button' }, 'Ext.Button', button); // Button created
* button = Ext.factory({ text: 'Updated Button' }, 'Ext.Button', button); // Button updated
*
* @param {Object} config The config object to instantiate or update an instance with.
* @param {String} classReference The class to instantiate from.
* @param {Object} [instance] The instance to update.
* @param [aliasNamespace]
* @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 it 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);
}
}
if (config === true) {
return instance || manager.instantiate(classReference);
}
//<debug error>
if (!Ext.isObject(config)) {
Ext.Logger.error("Invalid config, must be a valid config object");
}
//</debug>
if ('xtype' in config) {
newInstance = manager.instantiateByAlias('widget.' + config.xtype, config);
}
else 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() {
//<debug warn>
Ext.Logger.deprecate(message, 1);
//</debug>
return this[newName];
},
set: function(value) {
//<debug warn>
Ext.Logger.deprecate(message, 1);
//</debug>
this[newName] = value;
},
configurable: true
});
}
},
/**
* @private
* @member Ext
*/
deprecatePropertyValue: function(object, name, value, message) {
Ext.Object.defineProperty(object, name, {
get: function() {
//<debug warn>
Ext.Logger.deprecate(message, 1);
//</debug>
return value;
},
configurable: true
});
},
/**
* @private
* @member Ext
*/
deprecateMethod: function(object, name, method, message) {
object[name] = function() {
//<debug warn>
Ext.Logger.deprecate(message, 2);
//</debug>
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() {
//<debug warn>
Ext.Logger.deprecate(message, this);
//</debug>
return this[method].apply(this, arguments);
};
}
else {
member = function() {
//<debug warn>
Ext.Logger.deprecate(message, this);
//</debug>
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);
},
//<debug>
/**
* 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);
},
//</debug>
/**
* True when the document is fully initialized and ready for action
* @type Boolean
* @member Ext
* @private
*/
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);
}
}
}
});
//<debug>
Ext.Object.defineProperty(Ext, 'Msg', {
get: function() {
Ext.Logger.error("Using Ext.Msg without requiring Ext.MessageBox");
return null;
},
set: function(value) {
Ext.Object.defineProperty(Ext, 'Msg', {
value: value
});
return value;
},
configurable: true
});
//</debug>
//@tag dom,core
//@require Ext-more
/**
* Provides information about browser.
*
* Should not be manually instantiated unless for unit-testing.
* Access the global instance stored in {@link Ext.browser} instead.
* @private
*/
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, for example:
*
* if (Ext.browser.is('IE')) {
* // ...
* }
*
* Or as an object with Boolean properties, for example:
*
* if (Ext.browser.is.IE) {
* // ...
* }
*
* Versions can be conveniently checked as well. For example:
*
* if (Ext.browser.is.IE6) {
* // Equivalent to (Ext.browser.is.IE && Ext.browser.version.equals(6))
* }
*
* __Note:__ 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
* - WebView
* - Other
*
* @param {String} value The OS name to check.
* @return {Boolean}
*/
is: Ext.emptyFn,
/**
* The full name of the current browser.
* Possible values are:
*
* - IE
* - Firefox
* - Safari
* - Chrome
* - Opera
* - Other
* @type String
* @readonly
*/
name: null,
/**
* Refer to {@link Ext.Version}.
* @type Ext.Version
* @readonly
*/
version: null,
/**
* The full name of the current browser's engine.
* Possible values are:
*
* - WebKit
* - Gecko
* - Presto
* - Trident
* - Other
* @type String
* @readonly
*/
engineName: null,
/**
* Refer to {@link Ext.Version}.
* @type Ext.Version
* @readonly
*/
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;
}
if (userAgent.match(/Android.*Chrome/g)) {
browserName = 'ChromeMobile';
}
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' || typeof window.Cordova != 'undefined' || typeof window.cordova != 'undefined') {
isWebView = true;
this.setFlag('PhoneGap');
}
else if (!!window.isNK) {
isWebView = true;
this.setFlag('Sencha');
}
// Check if running in UIWebView
if (/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)(?!.*FBAN)/i.test(userAgent)) {
isWebView = true;
}
// 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() {
/**
* @class Ext.browser
* @extends Ext.env.Browser
* @singleton
* Provides useful information about the current browser.
*
* Example:
*
* if (Ext.browser.is.IE) {
* // IE specific code here
* }
*
* if (Ext.browser.is.WebKit) {
* // WebKit specific code here
* }
*
* console.log("Version " + Ext.browser.version);
*
* For a full list of supported values, refer to {@link #is} property/method.
*
* @aside guide environment_package
*/
var browserEnv = Ext.browser = new this(Ext.global.navigator.userAgent);
});
//@tag dom,core
//@require Ext.env.Browser
/**
* Provides information about operating system environment.
*
* Should not be manually instantiated unless for unit-testing.
* Access the global instance stored in {@link Ext.os} instead.
* @private
*/
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
* - iPhone5 (also true for 4in iPods).
* - iPod
* - Android
* - WebOS
* - BlackBerry
* - Bada
* - MacOS
* - Windows
* - Linux
* - Other
* @param {String} value The OS name to check.
* @return {Boolean}
*/
is: Ext.emptyFn,
/**
* @property {String} [name=null]
* @readonly
* The full name of the current operating system. Possible values are:
*
* - iOS
* - Android
* - WebOS
* - BlackBerry,
* - MacOS
* - Windows
* - Linux
* - Other
*/
name: null,
/**
* @property {Ext.Version} [version=null]
* Refer to {@link Ext.Version}
* @readonly
*/
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.replace(/ simulator$/i, ''));
}
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));
}
}
}
// Detect if the device is the iPhone 5.
if (this.name == "iOS" && window.screen.height == 568) {
this.setFlag('iPhone5');
}
return this;
}
}, function() {
var navigation = Ext.global.navigator,
userAgent = navigation.userAgent,
osEnv, osName, deviceType;
/**
* @class Ext.os
* @extends Ext.env.OS
* @singleton
* Provides useful information about the current operating system environment.
*
* 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 the {@link #is} property/method.
*
* @aside guide environment_package
*/
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';
// always set it to false when you are on a desktop
Ext.browser.is.WebView = false;
}
else if (osEnv.is.iPad || osEnv.is.Android3 || (osEnv.is.Android4 && userAgent.search(/mobile/i) == -1)) {
deviceType = 'Tablet';
}
else {
deviceType = 'Phone';
}
}
/**
* @property {String} deviceType
* The generic type of the current device.
*
* Possible values:
*
* - Phone
* - Tablet
* - Desktop
*
* For testing purposes the deviceType can be overridden by adding
* a deviceType parameter to the URL of the page, like so:
*
* http://localhost/mypage.html?deviceType=Tablet
*
*/
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.browser}, {@link Ext.os} and {@link Ext.feature} classes instead.
*/
});
//@tag dom,core
/**
* Provides information about browser.
*
* Should not be manually instantiated unless for unit-testing.
* Access the global instance stored in {@link Ext.browser} instead.
* @private
*/
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() {
/**
* @class Ext.feature
* @extend Ext.env.Feature
* @singleton
*
* 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 {@link #has} property/method for details of the features that can be detected.
*
* @aside guide environment_package
*/
Ext.feature = new this;
var has = Ext.feature.has;
/**
* @method has
* @member Ext.feature
* Verifies if a browser feature exists or not on the current device.
*
* A "hybrid" property, can be either accessed as a method call, i.e:
*
* if (Ext.feature.has('Canvas')) {
* // ...
* }
*
* or as an object with boolean properties, i.e:
*
* if (Ext.feature.has.Canvas) {
* // ...
* }
*
* Possible properties/parameter values:
*
* - Canvas
* - Svg
* - Vml
* - Touch - supports touch events (`touchstart`).
* - Orientation - supports different orientations.
* - OrientationChange - supports the `orientationchange` event.
* - DeviceMotion - supports the `devicemotion` event.
* - Geolocation
* - SqlDatabase
* - WebSockets
* - Range - supports [DOM document fragments.][1]
* - CreateContextualFragment - supports HTML fragment parsing using [range.createContextualFragment()][2].
* - History - supports history management with [history.pushState()][3].
* - CssTransforms
* - Css3dTransforms
* - CssAnimations
* - CssTransitions
* - Audio - supports the `<audio>` tag.
* - Video - supports the `<video>` tag.
* - ClassList - supports the HTML5 classList API.
* - LocalStorage - LocalStorage is supported and can be written to.
*
* [1]: https://developer.mozilla.org/en/DOM/range
* [2]: https://developer.mozilla.org/en/DOM/range.createContextualFragment
* [3]: https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
*
* @param {String} value The feature name to check.
* @return {Boolean}
*/
Ext.feature.registerTest({
Canvas: function() {
var element = this.getTestElement('canvas');
return !!(element && element.getContext && element.getContext('2d'));
},
Svg: function() {
var doc = document;
return !!(doc.createElementNS && !!doc.createElementNS("http:/" + "/www.w3.org/2000/svg", "svg").createSVGRect);
},
Vml: function() {
var element = this.getTestElement(),
ret = false;
element.innerHTML = "<!--[if vml]><br><![endif]-->";
ret = (element.childNodes.length === 1);
element.innerHTML = "";
return ret;
},
Touch: function() {
return this.isEventSupported('touchstart') && !(Ext.os && Ext.os.name.match(/Windows|MacOS|Linux/) && !Ext.os.is.BlackBerry6);
},
Orientation: function() {
return ('orientation' in window) && this.isEventSupported('orientationchange');
},
OrientationChange: function() {
return this.isEventSupported('orientationchange');
},
DeviceMotion: function() {
return this.isEventSupported('devicemotion');
},
Geolocation: function() {
return 'geolocation' in window.navigator;
},
SqlDatabase: function() {
return 'openDatabase' in window;
},
WebSockets: function() {
return 'WebSocket' in window;
},
Range: function() {
return !!document.createRange;
},
CreateContextualFragment: function() {
var range = !!document.createRange ? document.createRange() : false;
return range && !!range.createContextualFragment;
},
History: function() {
return ('history' in window && 'pushState' in window.history);
},
CssTransforms: function() {
return this.isStyleSupported('transform');
},
Css3dTransforms: function() {
// See https://sencha.jira.com/browse/TOUCH-1544
return this.has('CssTransforms') && this.isStyleSupported('perspective') && !Ext.os.is.Android2;
},
CssAnimations: function() {
return this.isStyleSupported('animationName');
},
CssTransitions: function() {
return this.isStyleSupported('transitionProperty');
},
Audio: function() {
return !!this.getTestElement('audio').canPlayType;
},
Video: function() {
return !!this.getTestElement('video').canPlayType;
},
ClassList: function() {
return "classList" in this.getTestElement();
},
LocalStorage : function() {
var supported = false;
try {
if ('localStorage' in window && window['localStorage'] !== null) {
//this should throw an error in private browsing mode in iOS
localStorage.setItem('sencha-localstorage-test', 'test success');
//clean up if setItem worked
localStorage.removeItem('sencha-localstorage-test');
supported = true;
}
} catch ( e ) {}
return supported;
}
});
});
//@tag dom,core
//@define Ext.DomQuery
//@define Ext.core.DomQuery
//@require Ext.env.Feature
/**
* @class Ext.DomQuery
* @alternateClassName Ext.dom.Query
*
* Provides functionality to select elements on the page based on a CSS selector. Delegates to
* document.querySelectorAll. More information can be found at
* [http://www.w3.org/TR/css3-selectors/](http://www.w3.org/TR/css3-selectors/)
*
* 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 descendant 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:not(S) an E element that does not match simple selector S
* * E:has(S) an E element that has a descendant 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');
});
//@tag dom,core
//@define Ext.DomHelper
//@require Ext.dom.Query
/**
* @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;
});
//@tag dom,core
//@require Ext.dom.Helper
/**
* 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;
}
});
//@tag dom,core
//@define Ext.Element-all
//@define Ext.Element
/**
* 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 {@link 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 {@link 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';
}
if (attributes.namespace) {
element = document.createElementNS(attributes.namespace, tag);
} else {
element = document.createElement(tag);
}
elementStyle = element.style;
for (name in attributes) {
if (name != 'tag') {
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.Element Element}s.** This method retrieves Ext.dom.Element
* objects which encapsulate DOM elements. To retrieve a Element by its ID, use {@link Ext.ElementManager#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,
/**
* @event painted
* Fires whenever this Element actually becomes visible (painted) on the screen. This is useful when you need to
* perform 'read' operations on the DOM element, i.e: calculating natural sizes and positioning.
*
* __Note:__ This event is not available to be used with event delegation. Instead `painted` only fires if you explicitly
* add at least one listener to it, for performance reasons.
*
* @param {Ext.Element} this The component instance.
*/
/**
* @event resize
* Important note: For the best performance on mobile devices, use this only when you absolutely need to monitor
* a Element's size.
*
* __Note:__ This event is not available to be used with event delegation. Instead `resize` only fires if you explicitly
* add at least one listener to it, for performance reasons.
*
* @param {Ext.Element} this The component instance.
*/
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];
},
setSizeState: function(state) {
var classes = ['x-sized', 'x-unsized', 'x-stretched'],
states = [true, false, null],
index = states.indexOf(state),
addedClass;
if (index !== -1) {
addedClass = classes[index];
classes.splice(index, 1);
this.addCls(addedClass);
}
this.removeCls(classes);
return this;
},
/**
* 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 {@link 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');
});
//@tag dom,core
//@define Ext.Element-all
//@define Ext.Element-static
//@require Ext.Element
/**
* @class Ext.dom.Element
*/
Ext.dom.Element.addStatics({
numberRe: /\d+$/,
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 {Object} size The size to set.
* @param {String} units The units to append to a numeric size value.
* @return {String}
* @private
* @static
*/
addUnits: function(size, units) {
// 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 (Ext.isNumber(size) || this.numberRe.test(size)) {
return size + (units || this.defaultUnit || 'px');
}
else if (!this.unitRe.test(size)) {
//<debug>
Ext.Logger.warn("Warning, size detected (" + size + ") not a valid property value on Element.addUnits.");
//</debug>
return size || '';
}
return size;
},
/**
* @static
* @return {Boolean}
* @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 containing the unit
*/
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: parts[0] || 0,
right: parts[1] || 0,
bottom: parts[2] || 0,
left: 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 me = this;
box = me.parseBox(box);
return me.addUnits(box.top, units) + ' ' +
me.addUnits(box.right, units) + ' ' +
me.addUnits(box.bottom, units) + ' ' +
me.addUnits(box.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;
}
});
//@tag dom,core
//@define Ext.Element-all
//@define Ext.Element-alignment
//@require Ext.Element-static
/**
* @class Ext.dom.Element
*/
//@tag dom,core
//@define Ext.Element-all
//@define Ext.Element-insertion
//@require Ext.Element-alignment
/**
* @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=before] (optional) 'before' or 'after'.
* @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(element) {
element = Ext.getDom(element);
element.parentNode.replaceChild(this.dom, element);
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;
},
doReplaceWith: function(element) {
var dom = this.dom;
dom.parentNode.replaceChild(Ext.getDom(element), dom);
},
/**
* 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;
},
detach: function() {
var dom = this.dom;
if (dom && dom.parentNode && dom.tagName !== 'BODY') {
dom.parentNode.removeChild(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=false] (optional) `true` to return an Ext.dom.Element.
* @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;
}
});
//@tag dom,core
//@define Ext.Element-all
//@define Ext.Element-position
//@require Ext.Element-insertion
/**
* @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 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 {@link #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.
*
* 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}.
*
* @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} An object in the format
* @return {Number} return.x The element's X position.
* @return {Number} return.y The element's Y position.
* @return {Number} return.width The element's width.
* @return {Number} return.height The element's height.
* @return {Number} return.bottom The element's lower bound.
* @return {Number} return.right The element's rightmost bound.
*/
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 {@link Ext.util.Region} will be returned.
* @return {Object} box An object in the format:
*
* {
* x: <Element's X position>,
* y: <Element's Y position>,
* width: <Element's width>,
* height: <Element's height>,
* bottom: <Element's lower bound>,
* right: <Element's rightmost bound>
* }
*
* The returned object may also be addressed as an Array where index 0 contains the X position
* and index 1 contains the Y position. So the result may also be used for {@link #setXY}.
*/
getPageBox: function(getRegion) {
var me = this,
el = me.dom,
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
};
}
}
});
//@tag dom,core
//@define Ext.Element-all
//@define Ext.Element-style
//@require Ext.Element-position
/**
* @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, force){
if (typeof force !== 'boolean') {
force = !this.hasCls(className);
}
return (force) ? this.addCls(className) : this.removeCls(className);
},
/**
* @private
* @param firstClass
* @param secondClass
* @param flag
* @param prefix
* @return {Mixed}
*/
swapCls: function(firstClass, secondClass, flag, prefix) {
if (flag === undefined) {
flag = true;
}
var addedClass = flag ? firstClass : secondClass,
removedClass = flag ? secondClass : firstClass;
if (removedClass) {
this.removeCls(prefix ? prefix + '-' + removedClass : removedClass);
}
if (addedClass) {
this.addCls(prefix ? prefix + '-' + addedClass : addedClass);
}
return this;
},
/**
* 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 visibility 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() {
this.dom.style.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 the style. 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 this style. 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
*/
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;
},
getStyleValue: function(name) {
return this.dom.style.getPropertyValue(name);
},
/**
* 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: Ext.dom.Element.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 {@link #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);
}
}
});
//@tag dom,core
//@define Ext.Element-all
//@define Ext.Element-traversal
//@require Ext.Element-style
/**
* @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/null} 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/null} 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/null} 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=false] (optional) `true` to return the DOM node instead of Ext.dom.Element.
* @return {HTMLElement/Ext.dom.Element} The child Ext.dom.Element (or DOM node if `returnDom` is `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=false] (optional) `true` to return the DOM node instead of Ext.dom.Element.
* @return {HTMLElement/Ext.dom.Element} The child Ext.dom.Element (or DOM node if `returnDom` is `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/null} 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/null} 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/null} 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/null} 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/null} 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);
}
});
//@tag dom,core
//@require Ext.Element-all
/**
* 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);
*
* @mixins Ext.dom.Element
*/
Ext.define('Ext.dom.CompositeElementLite', {
alternateClassName: ['Ext.CompositeElementLite', 'Ext.CompositeElement'],
requires: ['Ext.dom.Element'],
// We use the @mixins tag above to document that CompositeElement has
// all the same methods as Element, but the @mixins tag also pulls in
// configs and properties which we don't want, so hide them explicitly:
/** @cfg bubbleEvents @hide */
/** @cfg listeners @hide */
/** @property DISPLAY @hide */
/** @property OFFSETS @hide */
/** @property VISIBILITY @hide */
/** @property defaultUnit @hide */
/** @property dom @hide */
/** @property id @hide */
// Also hide the static #get method that also gets inherited
/** @method get @static @hide */
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
* @readonly
* The Array of DOM elements which this CompositeElement encapsulates.
*
* 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.
* @param {HTMLElement/String} [root] The root element of the query or id of the root.
* @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) {
e.on(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 {
//<debug>
throw new Error("[Ext.select] Invalid selector specified: " + selector);
//</debug>
}
return new Ext.CompositeElementLite(elements);
};
/**
* @member Ext
* @method select
* @alias Ext.dom.Element#select
*/
Ext.select = function() {
return Element.select.apply(Element, arguments);
};
});
//@require Ext.Class
//@require Ext.ClassManager
//@require Ext.Loader
/**
* 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;
});
});
}
});
}
}
}
});
//@require @core
/**
* @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;
}
});
//@require @core
/**
* @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;
}
});
//@require @core
/**
* @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;
}
});
/**
* 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 handler 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 listener's `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();
//<debug error>
if (!id.match(this.validIdRegex)) {
Ext.Logger.error("Invalid unique id of '" + id + "' for this object", this);
}
//</debug>
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}).
*
* The first argument is the name of the event. Every other argument passed will be available when you listen for
* the event.
*
* ## Example
*
* Firstly, we set up a listener for our new event.
*
* this.on('myevent', function(arg1, arg2, arg3, arg4, options, e) {
* console.log(arg1); // true
* console.log(arg2); // 2
* console.log(arg3); // { test: 'foo' }
* console.log(arg4); // 14
* console.log(options); // the options added when adding the listener
* console.log(e); // the event object with information about the event
* });
*
* And then we can fire off the event.
*
* this.fireEvent('myevent', true, 2, { test: 'foo' }, 14);
*
* 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.
* @return {Object}
*/
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
* @return {Boolean}
*/
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
* @return {Object}
*/
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);
}
}
return this;
},
/**
* 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/String[]/Object} 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:
* @param {Object} [options.scope] The scope (`this` reference) in which the handler function is executed. If omitted, defaults to the object
* which fired the event.
* @param {Number} [options.delay] The number of milliseconds to delay the invocation of the handler after the event fires.
* @param {Boolean} [options.single] `true` to add a handler to handle just the next firing of the event, and then remove itself.
* @param {String} [options.order=current] 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`.
*
* @param {Number} [options.buffer] 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.
* @param {String} [options.element] Allows you to add a listener onto a element of this component using the elements reference.
*
* Ext.create('Ext.Component', {
* listeners: {
* element: 'element',
* tap: function() {
* alert('element tap!');
* }
* }
* });
*
* All components have the `element` reference, which is the outer most element of the component. {@link Ext.Container} also has the
* `innerElement` element which contains all children. In most cases `element` is adequate.
*
* @param {String} [options.delegate] 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);
},
toggleListener: function(toggle, eventName, fn, scope, options, order) {
return this.changeListener(toggle ? this.doAddListener : this.doRemoveListener, 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/String[]/Object} 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/String[]/Object} 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/String[]/Object} 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/String[]/Object} 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/String[]/Object} 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 `this`.
* @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 re-fires the event from this object as the passed event name.
* @param newName
* @return {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();
}
},
/**
* @ignore
*/
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;
}
}
// The old value might have been changed at this point
// (after the apply call chain) so it should be read again
oldValue = this[internalName];
if (value !== oldValue) {
if (initialized) {
this.fireAction(changeEventName, [this, value, oldValue], this.doSet, this, {
nameMap: nameMap
});
}
else {
this[internalName] = value;
if (this[doSetName]) {
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;
if (me[nameMap.doSet]) {
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 overridden.
*/
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 (!this.bodyElement) {
this.bodyElement = this.innerElement;
}
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;
}
});
/**
* Represents a collection of a set of key and value pairs. Each key in the HashMap must be unique, the same
* key cannot exist twice. Access to items is provided via the key only. Sample usage:
*
* var map = Ext.create('Ext.util.HashMap');
* map.add('key1', 1);
* map.add('key2', 2);
* map.add('key3', 3);
*
* map.each(function(key, value, length){
* console.log(key, value, length);
* });
*
* The HashMap is an unordered class, there is no guarantee when iterating over the items that they will be in
* any particular order. If this is required, then use a {@link Ext.util.MixedCollection}.
*/
Ext.define('Ext.util.HashMap', {
mixins: {
observable: 'Ext.mixin.Observable'
},
/**
* @cfg {Function} keyFn
* A function that is used to retrieve a default key for a passed object.
* A default is provided that returns the **id** property on the object.
* This function is only used if the add method is called with a single argument.
*/
/**
* Creates new HashMap.
* @param {Object} config The configuration options
*/
constructor: function(config) {
/**
* @event add
* Fires when a new item is added to the hash.
* @param {Ext.util.HashMap} this
* @param {String} key The key of the added item.
* @param {Object} value The value of the added item.
*/
/**
* @event clear
* Fires when the hash is cleared.
* @param {Ext.util.HashMap} this
*/
/**
* @event remove
* Fires when an item is removed from the hash.
* @param {Ext.util.HashMap} this
* @param {String} key The key of the removed item.
* @param {Object} value The value of the removed item.
*/
/**
* @event replace
* Fires when an item is replaced in the hash.
* @param {Ext.util.HashMap} this
* @param {String} key The key of the replaced item.
* @param {Object} value The new value for the item.
* @param {Object} old The old value for the item.
*/
this.callParent();
this.mixins.observable.constructor.call(this);
this.clear(true);
},
/**
* Gets the number of items in the hash.
* @return {Number} The number of items in the hash.
*/
getCount: function() {
return this.length;
},
/**
* Implementation for being able to extract the key from an object if only
* a single argument is passed.
* @private
* @param {String} key The key
* @param {Object} value The value
* @return {Array} [key, value]
*/
getData: function(key, value) {
// if we have no value, it means we need to get the key from the object
if (value === undefined) {
value = key;
key = this.getKey(value);
}
return [key, value];
},
/**
* Extracts the key from an object. This is a default implementation, it may be overridden.
* @private
* @param {Object} o The object to get the key from.
* @return {String} The key to use.
*/
getKey: function(o) {
return o.id;
},
/**
* Add a new item to the hash. An exception will be thrown if the key already exists.
* @param {String} key The key of the new item.
* @param {Object} value The value of the new item.
* @return {Object} The value of the new item added.
*/
add: function(key, value) {
var me = this,
data;
if (me.containsKey(key)) {
throw new Error('This key already exists in the HashMap');
}
data = this.getData(key, value);
key = data[0];
value = data[1];
me.map[key] = value;
++me.length;
me.fireEvent('add', me, key, value);
return value;
},
/**
* Replaces an item in the hash. If the key doesn't exist, the
* `{@link #method-add}` method will be used.
* @param {String} key The key of the item.
* @param {Object} value The new value for the item.
* @return {Object} The new value of the item.
*/
replace: function(key, value) {
var me = this,
map = me.map,
old;
if (!me.containsKey(key)) {
me.add(key, value);
}
old = map[key];
map[key] = value;
me.fireEvent('replace', me, key, value, old);
return value;
},
/**
* Remove an item from the hash.
* @param {Object} o The value of the item to remove.
* @return {Boolean} `true` if the item was successfully removed.
*/
remove: function(o) {
var key = this.findKey(o);
if (key !== undefined) {
return this.removeByKey(key);
}
return false;
},
/**
* Remove an item from the hash.
* @param {String} key The key to remove.
* @return {Boolean} `true` if the item was successfully removed.
*/
removeByKey: function(key) {
var me = this,
value;
if (me.containsKey(key)) {
value = me.map[key];
delete me.map[key];
--me.length;
me.fireEvent('remove', me, key, value);
return true;
}
return false;
},
/**
* Retrieves an item with a particular key.
* @param {String} key The key to lookup.
* @return {Object} The value at that key. If it doesn't exist, `undefined` is returned.
*/
get: function(key) {
return this.map[key];
},
/**
* Removes all items from the hash.
* @return {Ext.util.HashMap} this
*/
clear: function(/* private */ initial) {
var me = this;
me.map = {};
me.length = 0;
if (initial !== true) {
me.fireEvent('clear', me);
}
return me;
},
/**
* Checks whether a key exists in the hash.
* @param {String} key The key to check for.
* @return {Boolean} `true` if they key exists in the hash.
*/
containsKey: function(key) {
return this.map[key] !== undefined;
},
/**
* Checks whether a value exists in the hash.
* @param {Object} value The value to check for.
* @return {Boolean} `true` if the value exists in the dictionary.
*/
contains: function(value) {
return this.containsKey(this.findKey(value));
},
/**
* Return all of the keys in the hash.
* @return {Array} An array of keys.
*/
getKeys: function() {
return this.getArray(true);
},
/**
* Return all of the values in the hash.
* @return {Array} An array of values.
*/
getValues: function() {
return this.getArray(false);
},
/**
* Gets either the keys/values in an array from the hash.
* @private
* @param {Boolean} isKey `true` to extract the keys, otherwise, the value.
* @return {Array} An array of either keys/values from the hash.
*/
getArray: function(isKey) {
var arr = [],
key,
map = this.map;
for (key in map) {
if (map.hasOwnProperty(key)) {
arr.push(isKey ? key : map[key]);
}
}
return arr;
},
/**
* Executes the specified function once for each item in the hash.
*
* @param {Function} fn The function to execute.
* @param {String} fn.key The key of the item.
* @param {Number} fn.value The value of the item.
* @param {Number} fn.length The total number of items in the hash.
* @param {Boolean} fn.return Returning `false` from the function will cease the iteration.
* @param {Object} [scope=this] The scope to execute in.
* @return {Ext.util.HashMap} this
*/
each: function(fn, scope) {
// copy items so they may be removed during iteration.
var items = Ext.apply({}, this.map),
key,
length = this.length;
scope = scope || this;
for (key in items) {
if (items.hasOwnProperty(key)) {
if (fn.call(scope, key, items[key], length) === false) {
break;
}
}
}
return this;
},
/**
* Performs a shallow copy on this hash.
* @return {Ext.util.HashMap} The new hash object.
*/
clone: function() {
var hash = new Ext.util.HashMap(),
map = this.map,
key;
hash.suspendEvents();
for (key in map) {
if (map.hasOwnProperty(key)) {
hash.add(key, map[key]);
}
}
hash.resumeEvents();
return hash;
},
/**
* @private
* Find the key for a value.
* @param {Object} value The value to find.
* @return {Object} The value of the item. Returns `undefined` if not found.
*/
findKey: function(value) {
var key,
map = this.map;
for (key in map) {
if (map.hasOwnProperty(key) && map[key] === value) {
return key;
}
}
return undefined;
}
});
/**
* @private
*/
Ext.define('Ext.AbstractManager', {
/* Begin Definitions */
requires: ['Ext.util.HashMap'],
/* End Definitions */
typeName: 'type',
constructor: function(config) {
Ext.apply(this, config || {});
/**
* @property {Ext.util.HashMap} all
* Contains all of the items currently managed
*/
this.all = Ext.create('Ext.util.HashMap');
this.types = {};
},
/**
* Returns an item by id.
* For additional details see {@link Ext.util.HashMap#get}.
* @param {String} id The `id` of the item.
* @return {Object} The item, `undefined` if not found.
*/
get : function(id) {
return this.all.get(id);
},
/**
* Registers an item to be managed.
* @param {Object} item The item to register.
*/
register: function(item) {
this.all.add(item);
},
/**
* Unregisters an item by removing it from this manager.
* @param {Object} item The item to unregister.
*/
unregister: function(item) {
this.all.remove(item);
},
/**
* Registers a new item constructor, keyed by a type key.
* @param {String} type The mnemonic string by which the class may be looked up.
* @param {Function} cls The new instance class.
*/
registerType : function(type, cls) {
this.types[type] = cls;
cls[this.typeName] = type;
},
/**
* Checks if an item type is registered.
* @param {String} type The mnemonic string by which the class may be looked up.
* @return {Boolean} Whether the type is registered.
*/
isRegistered : function(type){
return this.types[type] !== undefined;
},
/**
* Creates and returns an instance of whatever this manager manages, based on the supplied type and
* config object.
* @param {Object} config The config object.
* @param {String} defaultType If no type is discovered in the config object, we fall back to this type.
* @return {Object} The instance of whatever this manager is managing.
*/
create: function(config, defaultType) {
var type = config[this.typeName] || config.type || defaultType,
Constructor = this.types[type];
//<debug>
if (Constructor == undefined) {
Ext.Error.raise("The '" + type + "' type has not been registered with this manager");
}
//</debug>
return new Constructor(config);
},
/**
* Registers a function that will be called when an item with the specified id is added to the manager.
* This will happen on instantiation.
* @param {String} id The item `id`.
* @param {Function} fn The callback function. Called with a single parameter, the item.
* @param {Object} scope The scope (`this` reference) in which the callback is executed.
* Defaults to the item.
*/
onAvailable : function(id, fn, scope){
var all = this.all,
item;
if (all.containsKey(id)) {
item = all.get(id);
fn.call(scope || item, item);
} else {
all.on('add', function(map, key, item){
if (key == id) {
fn.call(scope || item, item);
all.un('add', fn, scope);
}
});
}
},
/**
* Executes the specified function once for each item in the collection.
* @param {Function} fn The function to execute.
* @param {String} fn.key The key of the item
* @param {Number} fn.value The value of the item
* @param {Number} fn.length The total number of items in the collection
* @param {Boolean} fn.return False to cease iteration.
* @param {Object} [scope=this] The scope to execute in.
*/
each: function(fn, scope){
this.all.each(fn, scope || this);
},
/**
* Gets the number of items in the collection.
* @return {Number} The number of items in the collection.
*/
getCount: function(){
return this.all.getCount();
}
});
/**
* @private
*
* 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}).
*
* This object also provides a registry of available Component _classes_
* indexed by a mnemonic code known as the Component's `xtype`.
* The `xtype` provides a way to avoid instantiating child Components
* when creating a full, nested config object for a complete Ext page.
*
* A child Component may be specified simply as a _config object_
* as long as the correct `xtype` is specified so that if and when the Component
* needs rendering, the correct type can be looked up for lazy instantiation.
*
* For a list of all available `xtype`, see {@link Ext.Component}.
*/
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();
// <debug>
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()`.');
}
// </debug>
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, or `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 `xtype`. (Optional if the config contains an `xtype`).
* @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
});
//@define Ext.DateExtras
/**
* @class Ext.Date
* @mixins Ext.DateExtras
* A set of useful static methods to deal with date.
*
* __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}:
*
* @example
* Ext.setup({
* requires: 'Ext.DateExtras',
* onReady: function() {
* var date = new Date();
* alert(Ext.Date.format(date, 'n/j/Y'));
* }
* });
*
* The date parsing and formatting syntax contains a subset of
* [PHP's `date()` function](http://www.php.net/date), 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>
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>
*
* For more information on the ISO 8601 date/time format, see [http://www.w3.org/TR/NOTE-datetime](http://www.w3.org/TR/NOTE-datetime).
*
* Example usage (note that you must escape format specifiers with '\\' to render them as character literals):
*
* // Sample date:
* // 'Wed Jan 10 2007 15:05:01 GMT-0600 (Central Standard Time)'
*
* var dt = new Date('1/10/2007 03:05:01 PM GMT-0600');
* console.log(Ext.Date.format(dt, 'Y-m-d')); // 2007-01-10
* console.log(Ext.Date.format(dt, 'F j, Y, g:i a')); // January 10, 2007, 3:05 pm
* console.log(Ext.Date.format(dt, 'l, \\t\\he jS \\of F Y h:i:s A')); // Wednesday, the 10th of January 2007 03:05:01 PM
*
* Here are some standard date/time patterns that you might find helpful. They
* are not part of the source of Ext.Date, but to use them you can simply copy this
* block of code into any script that is included after Ext.Date and they will also become
* globally available on the Date object. Feel free to add or remove patterns as needed in your code.
*
* Ext.Date.patterns = {
* ISO8601Long: "Y-m-d H:i:s",
* ISO8601Short: "Y-m-d",
* ShortDate: "n/j/Y",
* LongDate: "l, F d, Y",
* FullDateTime: "l, F d, Y g:i:s A",
* MonthDay: "F d",
* ShortTime: "g:i A",
* LongTime: "g:i:s A",
* SortableDateTime: "Y-m-d\\TH:i:s",
* UniversalSortableDateTime: "Y-m-d H:i:sO",
* YearMonth: "F, Y"
* };
*
* Example usage:
*
* @example
* var dt = new Date();
* Ext.Date.patterns = {
* ShortDate: "n/j/Y"
* };
* alert(Ext.Date.format(dt, Ext.Date.patterns.ShortDate));
*
* Developer-written, custom formats may be used by supplying both a formatting and a parsing function
* which perform to specialized requirements. The functions are stored in {@link #parseFunctions} and {@link #formatFunctions}.
* @singleton
*/
/*
* Most of the date-formatting functions below are the excellent work of Baron Schwartz.
* see http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/)
* They generate precompiled functions from format patterns instead of parsing and
* processing each pattern every time a date is formatted. These functions are available
* on every Date object.
*/
(function() {
// create private copy of Ext's Ext.util.Format.format() method
// - to remove unnecessary dependency
// - to resolve namespace conflict with MS-Ajax's implementation
function xf(format) {
var args = Array.prototype.slice.call(arguments, 1);
return format.replace(/\{(\d+)\}/g, function(m, i) {
return args[i];
});
}
/**
* Extra methods to be mixed into Ext.Date.
*
* Require this class to get Ext.Date with all the methods listed below.
*
* Using Ext.setup:
*
* @example
* Ext.setup({
* requires: 'Ext.DateExtras',
* onReady: function() {
* var date = new Date();
* alert(Ext.Date.format(date, 'n/j/Y'));
* }
* });
*
* Using Ext.application:
*
* @example
* Ext.application({
* requires: 'Ext.DateExtras',
* launch: function() {
* var date = new Date();
* alert(Ext.Date.format(date, 'n/j/Y'));
* }
* });
*
* @singleton
*/
Ext.DateExtras = {
/**
* Returns the current timestamp.
* @return {Number} 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=new Date()] (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 behavior of JavaScript Date objects.
* (see {@link #parse} for more information)
* @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 unrecognized characters as literals
};
},
/**
* An object hash in which each property is a date parsing function. The property name is the
* format string which that function parses.
*
* This object is automatically populated with date parsing functions as
* date formats are requested for Ext standard formatting strings.
*
* Custom parsing functions may be inserted into this object, keyed by a name which from then on
* may be used as a format string to {@link #parse}.
*
* Example:
*
* Ext.Date.parseFunctions['x-date-format'] = myDateParser;
*
* A parsing function should return a Date object, and is passed the following parameters:
*
* - `date`: {@link String} - The date string to parse.
* - `strict`: {@link Boolean} - `true` to validate date strings while parsing
* (i.e. prevent JavaScript Date "rollover"). __The default must be `false`.__
* Invalid date strings should return `null` when parsed.
*
* To enable Dates to also be _formatted_ according to that format, a corresponding
* formatting function must be placed into the {@link #formatFunctions} property.
* @property parseFunctions
* @type Object
*/
parseFunctions: {
"MS": function(input, strict) {
// note: the timezone offset is ignored since the MS Ajax server sends
// a UTC milliseconds-since-Unix-epoch value (negative values are allowed)
var re = new RegExp('\\/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\/');
var r = (input || '').match(re);
return r? new Date(((r[1] || '') + r[2]) * 1) : null;
}
},
parseRegexes: [],
/**
* An object hash in which each property is a date formatting function. The property name is the
* format string which corresponds to the produced formatted date string.
*
* This object is automatically populated with date formatting functions as
* date formats are requested for Ext standard formatting strings.
*
* Custom formatting functions may be inserted into this object, keyed by a name which from then on
* may be used as a format string to {@link #format}.
*
* Example:
*
* Ext.Date.formatFunctions['x-date-format'] = myDateFormatter;
*
* A formatting function should return a string representation of the Date object which is the scope (this) of the function.
*
* To enable date strings to also be _parsed_ according to that format, a corresponding
* parsing function must be placed into the {@link #parseFunctions} property.
* @property formatFunctions
* @type Object
*/
formatFunctions: {
"MS": function() {
// UTC milliseconds since Unix epoch (MS-AJAX serialized date format (MRSF))
return '\\/Date(' + this.getTime() + ')\\/';
}
},
y2kYear : 50,
/**
* Date interval constant.
* @type String
* @readonly
*/
MILLI : "ms",
/**
* Date interval constant.
* @type String
* @readonly
*/
SECOND : "s",
/**
* Date interval constant.
* @type String
* @readonly
*/
MINUTE : "mi",
/**
* Date interval constant.
* @type String
* @readonly
*/
HOUR : "h",
/**
* Date interval constant.
* @type String
* @readonly
*/
DAY : "d",
/**
* Date interval constant.
* @type String
* @readonly
*/
MONTH : "mo",
/**
* Date interval constant.
* @type String
* @readonly
*/
YEAR : "y",
/**
* An object hash containing default date values used during date parsing.
*
* The following properties are available:
*
* - `y`: {@link Number} - The default year value. Defaults to `undefined`.
* - `m`: {@link Number} - The default 1-based month value. Defaults to `undefined`.
* - `d`: {@link Number} - The default day value. Defaults to `undefined`.
* - `h`: {@link Number} - The default hour value. Defaults to `undefined`.
* - `i`: {@link Number} - The default minute value. Defaults to `undefined`.
* - `s`: {@link Number} - The default second value. Defaults to `undefined`.
* - `ms`: {@link Number} - The default millisecond value. Defaults to `undefined`.
*
* Override these properties to customize the default date values used by the {@link #parse} method.
*
* __Note:__ In countries which experience Daylight Saving Time (i.e. DST), the `h`, `i`, `s`
* and `ms` properties may coincide with the exact time in which DST takes effect.
* It is the responsibility of the developer to account for this.
*
* Example Usage:
*
* @example
* // 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.
* alert(Ext.Date.parse('2009-02', 'Y-m')); // returns a Date object representing February 1st 2009.
*
* @property defaults
* @type Object
*/
defaults: {},
/**
* An array of textual day names.
* Override these values for international dates.
* Example:
*
* Ext.Date.dayNames = [
* 'SundayInYourLang',
* 'MondayInYourLang'
* // ...
* ];
*
* @type Array
*/
dayNames : [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
/**
* An array of textual month names.
* Override these values for international dates.
* Example:
*
* Ext.Date.monthNames = [
* 'JanInYourLang',
* 'FebInYourLang'
* // ...
* ];
*
* @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:
*
* Ext.Date.monthNumbers = {
* 'ShortJanNameInYourLang': 0,
* 'ShortFebNameInYourLang': 1
* // ...
* };
*
* @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
},
/**
* The date format string that the {@link Ext.util.Format#date} function uses.
* See {@link Ext.Date} for details.
*
* This defaults to `m/d/Y`, but may be overridden in a locale file.
* @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:
*
* @example
* Ext.Date.formatCodes.x = "Ext.util.Format.leftPad(this.getDate(), 2, '0')";
* alert(Ext.Date.format(new Date(), 'x')); // returns the current day of the month
*
* @type Object
*/
formatCodes : {
d: "Ext.String.leftPad(this.getDate(), 2, '0')",
D: "Ext.Date.getShortDayName(this.getDay())", // get localized 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 localized short month name
n: "(this.getMonth() + 1)",
t: "Ext.Date.getDaysInMonth(this)",
L: "(Ext.Date.isLeapYear(this) ? 1 : 0)",
o: "(this.getFullYear() + (Ext.Date.getWeekOfYear(this) == 1 && this.getMonth() > 0 ? +1 : (Ext.Date.getWeekOfYear(this) >= 52 && this.getMonth() < 11 ? -1 : 0)))",
Y: "Ext.String.leftPad(this.getFullYear(), 4, '0')",
y: "('' + this.getFullYear()).substring(2, 4)",
a: "(this.getHours() < 12 ? 'am' : 'pm')",
A: "(this.getHours() < 12 ? 'AM' : 'PM')",
g: "((this.getHours() % 12) ? this.getHours() % 12 : 12)",
G: "this.getHours()",
h: "Ext.String.leftPad((this.getHours() % 12) ? this.getHours() % 12 : 12, 2, '0')",
H: "Ext.String.leftPad(this.getHours(), 2, '0')",
i: "Ext.String.leftPad(this.getMinutes(), 2, '0')",
s: "Ext.String.leftPad(this.getSeconds(), 2, '0')",
u: "Ext.String.leftPad(this.getMilliseconds(), 3, '0')",
O: "Ext.Date.getGMTOffset(this)",
P: "Ext.Date.getGMTOffset(this, true)",
T: "Ext.Date.getTimezone(this)",
Z: "(this.getTimezoneOffset() * -60)",
c: function() { // ISO-8601 -- GMT format
for (var c = "Y-m-dTH:i:sP", code = [], i = 0, l = c.length; i < l; ++i) {
var e = c.charAt(i);
code.push(e == "T" ? "'T'" : utilDate.getFormatCode(e)); // treat T as a character literal
}
return code.join(" + ");
},
/*
c: function() { // ISO-8601 -- UTC format
return [
"this.getUTCFullYear()", "'-'",
"Ext.util.Format.leftPad(this.getUTCMonth() + 1, 2, '0')", "'-'",
"Ext.util.Format.leftPad(this.getUTCDate(), 2, '0')",
"'T'",
"Ext.util.Format.leftPad(this.getUTCHours(), 2, '0')", "':'",
"Ext.util.Format.leftPad(this.getUTCMinutes(), 2, '0')", "':'",
"Ext.util.Format.leftPad(this.getUTCSeconds(), 2, '0')",
"'Z'"
].join(" + ");
},
*/
U: "Math.round(this.getTime() / 1000)"
},
/**
* Checks if the passed Date parameters will cause a JavaScript Date "rollover".
* @param {Number} year 4-digit year.
* @param {Number} month 1-based month-of-year.
* @param {Number} day Day of month.
* @param {Number} hour (optional) Hour.
* @param {Number} minute (optional) Minute.
* @param {Number} second (optional) Second.
* @param {Number} millisecond (optional) Millisecond.
* @return {Boolean} `true` if the passed parameters do not cause a Date "rollover", `false` otherwise.
*/
isValid : function(y, m, d, h, i, s, ms) {
// setup defaults
h = h || 0;
i = i || 0;
s = s || 0;
ms = ms || 0;
// Special handling for year < 100
var dt = utilDate.add(new Date(y < 100 ? 100 : y, m - 1, d, h, i, s, ms), utilDate.YEAR, y < 100 ? y - 100 : 0);
return y == dt.getFullYear() &&
m == dt.getMonth() + 1 &&
d == dt.getDate() &&
h == dt.getHours() &&
i == dt.getMinutes() &&
s == dt.getSeconds() &&
ms == dt.getMilliseconds();
},
/**
* Parses the passed string using the specified date format.
* Note that this function expects normal calendar dates, meaning that months are 1-based (i.e. 1 = January).
* The {@link #defaults} hash will be used for any date value (i.e. year, month, day, hour, minute, second or millisecond)
* which cannot be found in the passed string. If a corresponding default date value has not been specified in the {@link #defaults} hash,
* the current date's year, month, day or DST-adjusted zero-hour time value will be used instead.
* Keep in mind that the input date string must precisely match the specified format string
* in order for the parse operation to be successful (failed parse operations return a `null` value).
*
* Example:
*
* // dt = Fri May 25 2007 (current date)
* var dt = new Date();
*
* // dt = Thu May 25 2006 (today's month/day in 2006)
* dt = Ext.Date.parse("2006", "Y");
*
* // dt = Sun Jan 15 2006 (all date parts specified)
* dt = Ext.Date.parse("2006-01-15", "Y-m-d");
*
* // dt = Sun Jan 15 2006 15:20:01
* dt = Ext.Date.parse("2006-01-15 3:20:01 PM", "Y-m-d g:i:s A");
*
* // attempt to parse Sun Feb 29 2006 03:20:01 in strict mode
* dt = Ext.Date.parse("2006-02-29 03:20:01", "Y-m-d H:i:s", true); // null
*
* @param {String} input The raw date string.
* @param {String} format The expected date string format.
* @param {Boolean} [strict=false] (optional) `true` to validate date strings while parsing (i.e. prevents JavaScript Date "rollover").
* Invalid date strings will return `null` when parsed.
* @return {Date/null} The parsed Date, or `null` if an invalid date string.
*/
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 if (ch == '\n') {
code.push(Ext.JSON.encode(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){",
// favor 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 zeros (01 - 31)
},
j: {
g:1,
c:"d = parseInt(results[{0}], 10);\n",
s:"(\\d{1,2})" // day of month without leading zeros (1 - 31)
},
D: function() {
for (var a = [], i = 0; i < 7; a.push(utilDate.getShortDayName(i)), ++i); // get localized 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 localized month number
s:"(" + utilDate.monthNames.join("|") + ")"
};
},
M: function() {
for (var a = [], i = 0; i < 12; a.push(utilDate.getShortMonthName(i)), ++i); // get localized 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 zeros (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 zeros (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.
*
* @example
* var dt = new Date('9/17/2011');
* alert(Ext.Date.getTimezone(dt));
*
* @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').
*
* @example
* var dt = new Date('9/17/2011');
* alert(Ext.Date.getGMTOffset(dt));
*
* @param {Date} date The date.
* @param {Boolean} [colon=false] (optional) `true` to separate the hours and minutes with a colon.
* @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.
*
* @example
* var dt = new Date('9/17/2011');
* alert(Ext.Date.getDayOfYear(dt)); // 259
*
* @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).
*
* @example
* var dt = new Date('9/17/2011');
* alert(Ext.Date.getWeekOfYear(dt)); // 37
*
* @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.
*
* @example
* var dt = new Date('1/10/2011');
* alert(Ext.Date.isLeapYear(dt)); // false
*
* @param {Date} date The date.
* @return {Boolean} `true` if the current date falls within a leap year, `false` otherwise.
*/
isLeapYear : function(date) {
var year = date.getFullYear();
return !!((year & 3) == 0 && (year % 100 || (year % 400 == 0 && year)));
},
/**
* Get the first day of the current month, adjusted for leap year. The returned value
* is the numeric day index within the week (0-6) which can be used in conjunction with
* the {@link #monthNames} array to retrieve the textual day name.
*
* @example
* var dt = new Date('1/10/2007'),
* firstDay = Ext.Date.getFirstDayOfMonth(dt);
* alert(Ext.Date.dayNames[firstDay]); // 'Monday'
*
* @param {Date} date The date
* @return {Number} The day number (0-6).
*/
getFirstDayOfMonth : function(date) {
var day = (date.getDay() - (date.getDate() - 1)) % 7;
return (day < 0) ? (day + 7) : day;
},
/**
* Get the last day of the current month, adjusted for leap year. The returned value
* is the numeric day index within the week (0-6) which can be used in conjunction with
* the {@link #monthNames} array to retrieve the textual day name.
*
* @example
* var dt = new Date('1/10/2007'),
* lastDay = Ext.Date.getLastDayOfMonth(dt);
* alert(Ext.Date.dayNames[lastDay]); // 'Wednesday'
*
* @param {Date} date The date.
* @return {Number} The day number (0-6).
*/
getLastDayOfMonth : function(date) {
return utilDate.getLastDateOfMonth(date).getDay();
},
/**
* Get the date of the first day of the month in which this date resides.
*
* @example
* var dt = new Date('1/10/2007'),
* lastDate = Ext.Date.getFirstDateOfMonth(dt);
* alert(lastDate); // Mon Jan 01 2007 00:00:00 GMT-0800 (PST)
*
* @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.
*
* @example
* var dt = new Date('1/10/2007'),
* lastDate = Ext.Date.getLastDateOfMonth(dt);
* alert(lastDate); // Wed Jan 31 2007 00:00:00 GMT-0800 (PST)
*
* @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.
*
* @example
* var dt = new Date('1/10/2007');
* alert(Ext.Date.getDaysInMonth(dt)); // 31
*
* @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').
*
* @example
* var dt = new Date('9/17/2011');
* alert(Ext.Date.getSuffix(dt)); // 'th'
*
* @param {Date} date The date.
* @return {String} 'st', 'nd', 'rd' or 'th'.
*/
getSuffix : function(date) {
switch (date.getDate()) {
case 1:
case 21:
case 31:
return "st";
case 2:
case 22:
return "nd";
case 3:
case 23:
return "rd";
default:
return "th";
}
},
/**
* Creates and returns a new Date instance with the exact same date value as the called instance.
* Dates are copied and passed by reference, so if a copied date variable is modified later, the original
* variable will also be changed. When the intention is to create a new variable that will not
* modify the original instance, you should create a clone.
*
* Example of correctly cloning a date:
*
* // wrong way:
* var orig = new Date('10/1/2006');
* var copy = orig;
* copy.setDate(5);
* console.log(orig); // returns 'Thu Oct 05 2006'!
*
* // correct way:
* var orig = new Date('10/1/2006'),
* copy = Ext.Date.clone(orig);
* copy.setDate(5);
* console.log(orig); // returns 'Thu Oct 01 2006'
*
* @param {Date} date The date.
* @return {Date} The new Date instance.
*/
clone : function(date) {
return new Date(date.getTime());
},
/**
* Checks if the current date is affected by Daylight Saving Time (DST).
*
* @example
* var dt = new Date('9/17/2011');
* alert(Ext.Date.isDST(dt));
*
* @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=false] `true` to create a clone of this date, clear the time and return it.
* @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.
*
* @example
* // Basic usage:
* var dt = Ext.Date.add(new Date('10/29/2006'), Ext.Date.DAY, 5);
* alert(dt); // 'Fri Nov 03 2006 00:00:00'
*
* You can also subtract date values by passing a negative value:
*
* @example
* // Negative values will be subtracted:
* var dt2 = Ext.Date.add(new Date('10/1/2006'), Ext.Date.DAY, -5);
* alert(dt2); // 'Tue Sep 26 2006 00:00:00'
*
* @param {Date} date The date to modify.
* @param {String} interval A valid date interval enum value.
* @param {Number} value The amount to add to the current date.
* @return {Date} The new Date instance.
*/
add : function(date, interval, value) {
var d = Ext.Date.clone(date);
if (!interval || value === 0) return d;
switch(interval.toLowerCase()) {
case Ext.Date.MILLI:
d= new Date(d.valueOf() + value);
break;
case Ext.Date.SECOND:
d= new Date(d.valueOf() + value * 1000);
break;
case Ext.Date.MINUTE:
d= new Date(d.valueOf() + value * 60000);
break;
case Ext.Date.HOUR:
d= new Date(d.valueOf() + value * 3600000);
break;
case Ext.Date.DAY:
d= new Date(d.valueOf() + value * 86400000);
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();
},
/**
* Calculate how many units are there between two time.
* @param {Date} min The first time.
* @param {Date} max The second time.
* @param {String} unit The unit. This unit is compatible with the date interval constants.
* @return {Number} The maximum number n of units that min + n * unit <= max.
*/
diff: function (min, max, unit) {
var ExtDate = Ext.Date, est, diff = +max - min;
switch (unit) {
case ExtDate.MILLI:
return diff;
case ExtDate.SECOND:
return Math.floor(diff / 1000);
case ExtDate.MINUTE:
return Math.floor(diff / 60000);
case ExtDate.HOUR:
return Math.floor(diff / 3600000);
case ExtDate.DAY:
return Math.floor(diff / 86400000);
case 'w':
return Math.floor(diff / 604800000);
case ExtDate.MONTH:
est = (max.getFullYear() * 12 + max.getMonth()) - (min.getFullYear() * 12 + min.getMonth());
if (Ext.Date.add(min, unit, est) > max) {
return est - 1;
} else {
return est;
}
case ExtDate.YEAR:
est = max.getFullYear() - min.getFullYear();
if (Ext.Date.add(min, unit, est) > max) {
return est - 1;
} else {
return est;
}
}
},
/**
* Align the date to `unit`.
* @param {Date} date The date to be aligned.
* @param {String} unit The unit. This unit is compatible with the date interval constants.
* @return {Date} The aligned date.
*/
align: function (date, unit, step) {
var num = new Date(+date);
switch (unit.toLowerCase()) {
case Ext.Date.MILLI:
return num;
break;
case Ext.Date.SECOND:
num.setUTCSeconds(num.getUTCSeconds() - num.getUTCSeconds() % step);
num.setUTCMilliseconds(0);
return num;
break;
case Ext.Date.MINUTE:
num.setUTCMinutes(num.getUTCMinutes() - num.getUTCMinutes() % step);
num.setUTCSeconds(0);
num.setUTCMilliseconds(0);
return num;
break;
case Ext.Date.HOUR:
num.setUTCHours(num.getUTCHours() - num.getUTCHours() % step);
num.setUTCMinutes(0);
num.setUTCSeconds(0);
num.setUTCMilliseconds(0);
return num;
break;
case Ext.Date.DAY:
if (step == 7 || step == 14){
num.setUTCDate(num.getUTCDate() - num.getUTCDay() + 1);
}
num.setUTCHours(0);
num.setUTCMinutes(0);
num.setUTCSeconds(0);
num.setUTCMilliseconds(0);
return num;
break;
case Ext.Date.MONTH:
num.setUTCMonth(num.getUTCMonth() - (num.getUTCMonth() - 1) % step,1);
num.setUTCHours(0);
num.setUTCMinutes(0);
num.setUTCSeconds(0);
num.setUTCMilliseconds(0);
return num;
break;
case Ext.Date.YEAR:
num.setUTCFullYear(num.getUTCFullYear() - num.getUTCFullYear() % step, 1, 1);
num.setUTCHours(0);
num.setUTCMinutes(0);
num.setUTCSeconds(0);
num.setUTCMilliseconds(0);
return date;
break;
}
}
};
var utilDate = Ext.DateExtras;
Ext.apply(Ext.Date, utilDate);
})();
/**
* Reusable data formatting functions
*/
Ext.define('Ext.util.Format', {
requires: [
'Ext.DateExtras'
],
singleton: true,
/**
* The global default date format.
*/
defaultDateFormat: 'm/d/Y',
escapeRe: /('|\\)/g,
trimRe: /^[\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,
formatRe: /\{(\d+)\}/g,
escapeRegexRe: /([-.*+?^${}()|[\]\/\\])/g,
dashesRe: /-/g,
iso8601TestRe: /\d\dT\d\d/,
iso8601SplitRe: /[- :T\.Z\+]/,
/**
* 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} str
* @return {String}
*/
escapeRegex: function(s) {
return s.replace(Ext.util.Format.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.util.Format.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:__ This method returns the new value but does not change the current string.
*
* // alternate sort directions
* sort = Ext.util.Format.toggle(sort, 'ASC', 'DESC');
*
* // instead of conditional logic:
* sort = (sort === 'ASC' ? 'DESC' : 'ASC');
*
* @param {String} string The current string
* @param {String} value The value to compare to the current string
* @param {String} other The new value to use if the string already equals the first value passed in
* @return {String} The new value
*/
toggle: function(string, value, other) {
return string == value ? other : value;
},
/**
* Trims whitespace from either end of a string, leaving spaces within the string intact. Example:
*
* var s = ' foo bar ';
* alert('-' + s + '-'); // alerts "- foo bar -"
* alert('-' + Ext.util.Format.trim(s) + '-'); // alerts "-foo bar-"
*
* @param {String} string The string to escape
* @return {String} The trimmed string
*/
trim: function(string) {
return string.replace(Ext.util.Format.trimRe, "");
},
/**
* Pads the left side of a string with a specified character. This is especially useful
* for normalizing number and date strings. Example usage:
*
* var s = Ext.util.Format.leftPad('123', 5, '0');
* // s now contains the string: '00123'
*
* @param {String} string The original string.
* @param {Number} size The total length of the output string.
* @param {String} [char=' '] (optional) The character with which to pad the original string.
* @return {String} The padded string.
*/
leftPad: function (val, size, ch) {
var result = String(val);
ch = ch || " ";
while (result.length < size) {
result = ch + result;
}
return result;
},
/**
* Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens. Each
* token must be unique, and must increment in the format {0}, {1}, etc. Example usage:
*
* var cls = 'my-class', text = 'Some text';
* var s = Ext.util.Format.format('<div class="{0}">{1}</div>', cls, text);
* // s now contains the string: '<div class="my-class">Some text</div>'
*
* @param {String} string The tokenized string to be formatted.
* @param {String...} values The values to replace token {0}, {1}, etc.
* @return {String} The formatted string.
*/
format: function (format) {
var args = Ext.toArray(arguments, 1);
return format.replace(Ext.util.Format.formatRe, function(m, i) {
return args[i];
});
},
/**
* Convert certain characters (&, <, >, and ') to their HTML character equivalents for literal display in web pages.
* @param {String} value The string to encode.
* @return {String} The encoded text.
*/
htmlEncode: function(value) {
return ! value ? value: String(value).replace(/&/g, "&amp;").replace(/>/g, "&gt;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
},
/**
* Convert certain characters (&, <, >, and ') from their HTML character equivalents.
* @param {String} value The string to decode.
* @return {String} The decoded text.
*/
htmlDecode: function(value) {
return ! value ? value: String(value).replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&quot;/g, '"').replace(/&amp;/g, "&");
},
/**
* Parse a value into a formatted date using the specified format pattern.
* @param {String/Date} value The value to format. Strings must conform to the format expected by the JavaScript
* Date object's [parse() method](http://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/parse).
* @param {String} [format='m/d/Y'] (optional) Any valid date format string.
* @return {String} The formatted date string.
*/
date: function(value, format) {
var date = value;
if (!value) {
return "";
}
if (!Ext.isDate(value)) {
date = new Date(Date.parse(value));
if (isNaN(date)) {
// Dates with ISO 8601 format are not well supported by mobile devices, this can work around the issue.
if (this.iso8601TestRe.test(value)) {
date = value.split(this.iso8601SplitRe);
date = new Date(date[0], date[1]-1, date[2], date[3], date[4], date[5]);
}
if (isNaN(date)) {
// Dates with the format "2012-01-20" fail, but "2012/01/20" work in some browsers. We'll try and
// get around that.
date = new Date(Date.parse(value.replace(this.dashesRe, "/")));
//<debug>
if (isNaN(date)) {
Ext.Logger.error("Cannot parse the passed value " + value + " into a valid date");
}
//</debug>
}
}
value = date;
}
return Ext.Date.format(value, format || Ext.util.Format.defaultDateFormat);
}
});
/**
* Represents an HTML fragment template. Templates may be {@link #compile precompiled} for greater performance.
*
* An instance of this class may be created by passing to the constructor either a single argument, or multiple
* arguments:
*
* # Single argument: String/Array
*
* The single argument may be either a String or an Array:
*
* - String:
*
* var t = new Ext.Template("<div>Hello {0}.</div>");
* t.{@link #append}('some-element', ['foo']);
*
* - Array: An Array will be combined with `join('')`.
*
* var t = new Ext.Template([
* '<div name="{id}">',
* '<span class="{cls}">{name:trim} {value:ellipsis(10)}</span>',
* '</div>'
* ]);
* t.{@link #compile}();
* t.{@link #append}('some-element', {id: 'myid', cls: 'myclass', name: 'foo', value: 'bar'});
*
* # Multiple arguments: String, Object, Array, ...
*
* Multiple arguments will be combined with `join('')`.
*
* var t = new Ext.Template(
* '<div name="{id}">',
* '<span class="{cls}">{name} {value}</span>',
* '</div>',
* // a configuration object:
* {
* compiled: true // {@link #compile} immediately
* }
* );
*
* # Notes
*
* - For a list of available format functions, see {@link Ext.util.Format}.
* - `disableFormats` reduces `{@link #apply}` time when no formatting is required.
*/
Ext.define('Ext.Template', {
/* Begin Definitions */
requires: ['Ext.dom.Helper', 'Ext.util.Format'],
inheritableStatics: {
/**
* Creates a template from the passed element's value (_display:none_ textarea, preferred) or `innerHTML`.
* @param {String/HTMLElement} el A DOM element or its `id`.
* @param {Object} config (optional) Config object.
* @return {Ext.Template} The created template.
* @static
* @inheritable
*/
from: function(el, config) {
el = Ext.getDom(el);
return new this(el.value || el.innerHTML, config || '');
}
},
/* End Definitions */
/**
* Creates new template.
*
* @param {String...} html List of strings to be concatenated into template.
* Alternatively an array of strings can be given, but then no config object may be passed.
* @param {Object} config (optional) Config object.
*/
constructor: function(html) {
var me = this,
args = arguments,
buffer = [],
i = 0,
length = args.length,
value;
me.initialConfig = {};
// Allow an array to be passed here so we can
// pass an array of strings and an object
// at the end
if (length === 1 && Ext.isArray(html)) {
args = html;
length = args.length;
}
if (length > 1) {
for (; i < length; i++) {
value = args[i];
if (typeof value == 'object') {
Ext.apply(me.initialConfig, value);
Ext.apply(me, value);
} else {
buffer.push(value);
}
}
} else {
buffer.push(html);
}
// @private
me.html = buffer.join('');
if (me.compiled) {
me.compile();
}
},
/**
* @property {Boolean} isTemplate
* `true` in this class to identify an object as an instantiated Template, or subclass thereof.
*/
isTemplate: true,
/**
* @cfg {Boolean} [compiled=false]
* `true` to immediately compile the template.
*/
/**
* @cfg {Boolean} [disableFormats=false]
* `true` to disable format functions in the template. If the template doesn't contain
* format functions, setting `disableFormats` to `true` will reduce apply time.
*/
disableFormats: false,
re: /\{([\w\-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g,
/**
* Returns an HTML fragment of this template with the specified values applied.
*
* @param {Object/Array} values The template values. Can be an array if your params are numeric:
*
* var tpl = new Ext.Template('Name: {0}, Age: {1}');
* tpl.apply(['John', 25]);
*
* or an object:
*
* var tpl = new Ext.Template('Name: {name}, Age: {age}');
* tpl.apply({name: 'John', age: 25});
*
* @return {String} The HTML fragment.
*/
apply: function(values) {
var me = this,
useFormat = me.disableFormats !== true,
fm = Ext.util.Format,
tpl = me,
ret;
if (me.compiled) {
return me.compiled(values).join('');
}
function fn(m, name, format, args) {
if (format && useFormat) {
if (args) {
args = [values[name]].concat(Ext.functionFactory('return ['+ args +'];')());
} else {
args = [values[name]];
}
if (format.substr(0, 5) == "this.") {
return tpl[format.substr(5)].apply(tpl, args);
}
else {
return fm[format].apply(fm, args);
}
}
else {
return values[name] !== undefined ? values[name] : "";
}
}
ret = me.html.replace(me.re, fn);
return ret;
},
/**
* Appends the result of this template to the provided output array.
* @param {Object/Array} values The template values. See {@link #apply}.
* @param {Array} out The array to which output is pushed.
* @return {Array} The given out array.
*/
applyOut: function(values, out) {
var me = this;
if (me.compiled) {
out.push.apply(out, me.compiled(values));
} else {
out.push(me.apply(values));
}
return out;
},
/**
* @method applyTemplate
* @member Ext.Template
* Alias for {@link #apply}.
* @inheritdoc Ext.Template#apply
*/
applyTemplate: function () {
return this.apply.apply(this, arguments);
},
/**
* Sets the HTML used as the template and optionally compiles it.
* @param {String} html
* @param {Boolean} compile (optional) `true` to compile the template.
* @return {Ext.Template} this
*/
set: function(html, compile) {
var me = this;
me.html = html;
me.compiled = null;
return compile ? me.compile() : me;
},
compileARe: /\\/g,
compileBRe: /(\r\n|\n)/g,
compileCRe: /'/g,
/**
* Compiles the template into an internal function, eliminating the RegEx overhead.
* @return {Ext.Template} this
*/
compile: function() {
var me = this,
fm = Ext.util.Format,
useFormat = me.disableFormats !== true,
body, bodyReturn;
function fn(m, name, format, args) {
if (format && useFormat) {
args = args ? ',' + args: "";
if (format.substr(0, 5) != "this.") {
format = "fm." + format + '(';
}
else {
format = 'this.' + format.substr(5) + '(';
}
}
else {
args = '';
format = "(values['" + name + "'] == undefined ? '' : ";
}
return "'," + format + "values['" + name + "']" + args + ") ,'";
}
bodyReturn = me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn);
body = "this.compiled = function(values){ return ['" + bodyReturn + "'];};";
eval(body);
return me;
},
/**
* Applies the supplied values to the template and inserts the new node(s) as the first child of el.
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) `true` to return a Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element.
*/
insertFirst: function(el, values, returnElement) {
return this.doInsert('afterBegin', el, values, returnElement);
},
/**
* Applies the supplied values to the template and inserts the new node(s) before el.
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) `true` to return an Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element
*/
insertBefore: function(el, values, returnElement) {
return this.doInsert('beforeBegin', el, values, returnElement);
},
/**
* Applies the supplied values to the template and inserts the new node(s) after el.
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) `true` to return a Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element.
*/
insertAfter: function(el, values, returnElement) {
return this.doInsert('afterEnd', el, values, returnElement);
},
/**
* Applies the supplied `values` to the template and appends the new node(s) to the specified `el`.
*
* For example usage see {@link Ext.Template Ext.Template class docs}.
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) true to return an Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element.
*/
append: function(el, values, returnElement) {
return this.doInsert('beforeEnd', el, values, returnElement);
},
doInsert: function(where, el, values, returnElement) {
var newNode = Ext.DomHelper.insertHtml(where, Ext.getDom(el), this.apply(values));
return returnElement ? Ext.get(newNode) : newNode;
},
/**
* Applies the supplied values to the template and overwrites the content of el with the new node(s).
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) true to return a Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element.
*/
overwrite: function(el, values, returnElement) {
var newNode = Ext.DomHelper.overwrite(Ext.getDom(el), this.apply(values));
return returnElement ? Ext.get(newNode) : newNode;
}
});
/**
* 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}`).
* @param {String} 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,
prop;
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 xxx> 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;
// Extract property name to use from indexed item
if (prop = me.propRe.exec(m[4])) {
actions.propName = prop[1] || prop[2];
}
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 if (m[0].length === 5) {
// if the length of m[0] is 5, assume that we're dealing with an opening tpl tag with no attributes (e.g. <tpl>...</tpl>)
// in this case no action is needed other than pushing it on to the stack
stack.push({ type: 'tpl' });
} 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,
propRe: /prop=(?:(?:"([^"]*)")|(?:'([^']*)'))/,
defaultRe: /^\s*default\s*$/,
elseRe: /^\s*else\s*$/
});
/**
* 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
* }
*
* @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,
// See [http://jsperf.com/nige-array-append](http://jsperf.com/nige-array-append) for quickest way to append to an array of unknown length
// (Due to arbitrary code execution inside a template, we cannot easily track the length in var)
// On IE6 and 7 `myArray[myArray.length]='foo'` is better. On other browsers `myArray.push('foo')` is better.
useIndex: Ext.isIE6 || Ext.isIE7,
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,
// note: Ext here is properly sandboxed
definitions = 'var fm=Ext.util.Format,ts=Object.prototype.toString;',
code;
// Track how many levels we use, so that we only "var" each level's variables once
me.maxLevel = 0;
me.body = [
'var c0=values, a0=' + me.createArrayTest(0) + ', p0=parent, n0=xcount, i0=xindex, v;\n'
];
if (me.definitions) {
if (typeof me.definitions === 'string') {
me.definitions = [me.definitions, definitions ];
} else {
me.definitions.push(definitions);
}
} else {
me.definitions = [ definitions ];
}
me.switches = [];
me.parse(tpl);
me.definitions.push(
(me.useEval ? '$=' : 'return') + ' function (' + me.fnArgs + ') {',
me.body.join(''),
'}'
);
code = me.definitions.join('\n');
// Free up the arrays.
me.definitions.length = me.body.length = me.switches.length = 0;
delete me.definitions;
delete me.body;
delete me.switches;
return code;
},
//-----------------------------------
// XTemplateParser callouts
//
doText: function (text) {
var me = this,
out = me.body;
text = text.replace(me.aposRe, "\\'").replace(me.newLineRe, '\\n');
if (me.useIndex) {
out.push('out[out.length]=\'', text, '\'\n');
} else {
out.push('out.push(\'', text, '\')\n');
}
},
doExpr: function (expr) {
var out = this.body;
out.push('if ((v=' + expr + ')!==undefined && (v=' + expr + ')!==null) out');
// Coerce value to string using concatenation of an empty string literal.
// See http://jsperf.com/tostringvscoercion/5
if (this.useIndex) {
out.push('[out.length]=v+\'\'\n');
} else {
out.push('.push(v+\'\')\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 (action === '.') {
me.body.push('if (values) {\n');
} else 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 (action === '.') {
me.body.push('else if (values) {\n');
} else 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 (action === '.') {
me.body.push('switch (values) {\n');
} else 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,
L = me.level,
up = L-1,
pL = 'p' + L,
parentAssignment;
// If it's just a propName, use it directly in the switch
if (action === '.') {
s = 'values';
} else if (me.propNameRe.test(action)) {
s = me.parseTag(action);
}
// Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
else {
s = me.addFn(action) + me.callFn;
}
/*
We are trying to produce a block of code that looks like below. We use the nesting
level to uniquely name the control variables.
// Omit "var " if we have already been through level 2
var i2 = 0,
n2 = 0,
c2 = values['propName'],
// c2 is the context object for the for loop
a2 = Array.isArray(c2);
p2 = c1,
// p2 is the parent context (of the outer for loop)
r2 = values
// r2 is the values object to
// If iterating over the current data, the parent is always set to c2
parent = c2;
// If iterating over a property in an object, set the parent to the object
parent = a1 ? c1[i1] : p2 // set parent
if (c2) {
if (a2) {
n2 = c2.length;
} else if (c2.isMixedCollection) {
c2 = c2.items;
n2 = c2.length;
} else if (c2.isStore) {
c2 = c2.data.items;
n2 = c2.length;
} else {
c2 = [ c2 ];
n2 = 1;
}
}
// i2 is the loop index and n2 is the number (xcount) of this for loop
for (xcount = n2; i2 < n2; ++i2) {
values = c2[i2] // 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).
*/
// Declare the vars for a particular level only if we have not already declared them.
if (me.maxLevel < L) {
me.maxLevel = L;
me.body.push('var ');
}
if (action == '.') {
parentAssignment = 'c' + L;
} else {
parentAssignment = 'a' + up + '?c' + up + '[i' + up + ']:p' + L;
}
me.body.push('i',L,'=0,n', L, '=0,c',L,'=',s,',a',L,'=', me.createArrayTest(L), ',p',L,'=c',up,',r',L,'=values;\n',
'parent=',parentAssignment,'\n',
'if (c',L,'){if(a',L,'){n', L,'=c', L, '.length;}else if (c', L, '.isMixedCollection){c',L,'=c',L,'.items;n',L,'=c',L,'.length;}else if(c',L,'.isStore){c',L,'=c',L,'.data.items;n',L,'=c',L,'.length;}else{c',L,'=[c',L,'];n',L,'=1;}}\n',
'for (xcount=n',L,';i',L,'<n'+L+';++i',L,'){\n',
'values=c',L,'[i',L,']');
if (actions.propName) {
me.body.push('.', actions.propName);
}
me.body.push('\n',
'xindex=i',L,'+1\n');
},
createArrayTest: ('isArray' in Array) ? function(L) {
return 'Array.isArray(c' + L + ')';
} : function(L) {
return 'ts.call(c' + L + ')==="[object Array]"';
},
doExec: function (action, actions) {
var me = this,
name = 'f' + me.definitions.length;
me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
' try { with(values) {',
' ' + action,
' }} catch(e) {',
//<debug>
'Ext.Logger.log("XTemplate Error: " + e.message);',
//</debug>
'}',
'}');
me.body.push(name + me.callFn + '\n');
},
//-----------------------------------
// Internal
//
addFn: function (body) {
var me = this,
name = 'f' + me.definitions.length;
if (body === '.') {
me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
' return values',
'}');
} else if (body === '..') {
me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
' return parent',
'}');
} else {
me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
' try { with(values) {',
' return(' + body + ')',
' }} catch(e) {',
//<debug>
'Ext.Logger.log("XTemplate Error: " + e.message);',
//</debug>
'}',
'}');
}
return name;
},
parseTag: function (tag) {
var me = this,
m = me.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
if (!me.validTypes) {
me.definitions.push('var validTypes={string:1,number:1,boolean:1};');
me.validTypes = true;
}
v = 'validTypes[typeof values] || ts.call(values) === "[object Date]" ? values : ""';
}
// name = "#" - Use the xindex
else if (name == '#') {
v = 'xindex';
}
else if (name.substr(0, 7) == "parent.") {
v = name;
}
// compound JavaScript property name (e.g., "foo.bar")
else if (isNaN(name) && name.indexOf('-') == -1 && name.indexOf('.') != -1) {
v = "values." + name;
}
// number or a '-' in it or a single word (maybe a keyword): use array notation
// (http://jsperf.com/string-property-access/4)
else {
v = "values['" + name + "']";
}
if (math) {
v = '(' + v + math + ')';
}
if (format && me.useFormat) {
args = args ? ',' + args : "";
if (format.substr(0, 5) != "this.") {
format = "fm." + format + '(';
} else {
format += '(';
}
} else {
return v;
}
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 + ')';
});
/**
* A template class that supports advanced functionality like:
*
* - Autofilling arrays using templates and sub-templates
* - Conditional processing with basic comparison operators
* - Basic math function support
* - Execute arbitrary inline code with special built-in template variables
* - Custom member functions
* - Many special tags and built-in operators that aren't defined as part of the API, but are supported in the templates that can be created
*
* XTemplate provides the templating mechanism built into {@link Ext.DataView}.
*
* The {@link Ext.Template} describes the acceptable parameters to pass to the constructor. The following examples
* demonstrate all of the supported features.
*
* # Sample Data
*
* This is the data object used for reference in each code example:
*
* var data = {
* name: 'Don Griffin',
* title: 'Senior Technomage',
* company: 'Sencha Inc.',
* drinks: ['Coffee', 'Water', 'More Coffee'],
* kids: [
* { name: 'Aubrey', age: 17 },
* { name: 'Joshua', age: 13 },
* { name: 'Cale', age: 10 },
* { name: 'Nikol', age: 5 },
* { name: 'Solomon', age: 0 }
* ]
* };
*
* # Auto filling of arrays
*
* The **tpl** tag and the **for** operator are used to process the provided data object:
*
* - If the value specified in for is an array, it will auto-fill, repeating the template block inside the tpl
* tag for each item in the array.
* - If for="." is specified, the data object provided is examined.
* - While processing an array, the special variable {#} will provide the current array index + 1 (starts at 1, not 0).
*
* Examples:
*
* <tpl for=".">...</tpl> // loop through array at root node
* <tpl for="foo">...</tpl> // loop through array at foo node
* <tpl for="foo.bar">...</tpl> // loop through array at foo.bar node
*
* Using the sample data above:
*
* var tpl = new Ext.XTemplate(
* '<p>Kids: ',
* '<tpl for=".">', // process the data.kids node
* '<p>{#}. {name}</p>', // use current array index to autonumber
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data.kids); // pass the kids property of the data object
*
* An example illustrating how the **for** property can be leveraged to access specified members of the provided data
* object to populate the template:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Title: {title}</p>',
* '<p>Company: {company}</p>',
* '<p>Kids: ',
* '<tpl for="kids">', // interrogate the kids property within the data
* '<p>{name}</p>',
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data); // pass the root node of the data object
*
* Flat arrays that contain values (and not objects) can be auto-rendered using the special **`{.}`** variable inside a
* loop. This variable will represent the value of the array at the current index:
*
* var tpl = new Ext.XTemplate(
* '<p>{name}\'s favorite beverages:</p>',
* '<tpl for="drinks">',
* '<div> - {.}</div>',
* '</tpl>'
* );
* tpl.overwrite(panel.body, data);
*
* When processing a sub-template, for example while looping through a child array, you can access the parent object's
* members via the **parent** object:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<tpl if="age &gt; 1">',
* '<p>{name}</p>',
* '<p>Dad: {parent.name}</p>',
* '</tpl>',
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data);
*
* # Conditional processing with basic comparison operators
*
* The **tpl** tag and the **if** operator are used to provide conditional checks for deciding whether or not to render
* specific parts of the template.
*
* Using the sample data above:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<tpl if="age &gt; 1">',
* '<p>{name}</p>',
* '</tpl>',
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data);
*
* More advanced conditionals are also supported:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<p>{name} is a ',
* '<tpl if="age &gt;= 13">',
* '<p>teenager</p>',
* '<tpl elseif="age &gt;= 2">',
* '<p>kid</p>',
* '<tpl else>',
* '<p>baby</p>',
* '</tpl>',
* '</tpl></p>'
* );
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<p>{name} is a ',
* '<tpl switch="name">',
* '<tpl case="Aubrey" case="Nikol">',
* '<p>girl</p>',
* '<tpl default">',
* '<p>boy</p>',
* '</tpl>',
* '</tpl></p>'
* );
*
* A `break` is implied between each case and default, however, multiple cases can be listed
* in a single &lt;tpl&gt; tag.
*
* # Using double quotes
*
* Examples:
*
* var tpl = new Ext.XTemplate(
* "<tpl if='age &gt; 1 && age &lt; 10'>Child</tpl>",
* "<tpl if='age &gt;= 10 && age &lt; 18'>Teenager</tpl>",
* "<tpl if='this.isGirl(name)'>...</tpl>",
* '<tpl if="id == \'download\'">...</tpl>',
* "<tpl if='needsIcon'><img src='{icon}' class='{iconCls}'/></tpl>",
* "<tpl if='name == \"Don\"'>Hello</tpl>"
* );
*
* # Basic math support
*
* The following basic math operators may be applied directly on numeric data values:
*
* + - * /
*
* For example:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<tpl if="age &gt; 1">', // <-- Note that the > is encoded
* '<p>{#}: {name}</p>', // <-- Auto-number each item
* '<p>In 5 Years: {age+5}</p>', // <-- Basic math
* '<p>Dad: {parent.name}</p>',
* '</tpl>',
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data);
*
* # Execute arbitrary inline code with special built-in template variables
*
* Anything between `{[ ... ]}` is considered code to be executed in the scope of the template.
* The expression is evaluated and the result is included in the generated result. There are
* some special variables available in that code:
*
* - **out**: The output array into which the template is being appended (using `push` to later
* `join`).
* - **values**: The values in the current scope. If you are using scope changing sub-templates,
* you can change what values is.
* - **parent**: The scope (values) of the ancestor template.
* - **xindex**: If you are in a looping template, the index of the loop you are in (1-based).
* - **xcount**: If you are in a looping template, the total length of the array you are looping.
*
* This example demonstrates basic row striping using an inline code block and the xindex variable:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<div class="{[xindex % 2 === 0 ? "even" : "odd"]}">',
* '{name}',
* '</div>',
* '</tpl></p>'
* );
*
* Any code contained in "verbatim" blocks (using "{% ... %}") will be inserted directly in
* the generated code for the template. These blocks are not included in the output. This
* can be used for simple things like break/continue in a loop, or control structures or
* method calls (when they don't produce output). The `this` references the template instance.
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '{% if (xindex % 2 === 0) continue; %}',
* '{name}',
* '{% if (xindex > 100) break; %}',
* '</div>',
* '</tpl></p>'
* );
*
* # Template member functions
*
* One or more member functions can be specified in a configuration object passed into the XTemplate constructor for
* more complex processing:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<tpl if="this.isGirl(name)">',
* '<p>Girl: {name} - {age}</p>',
* '<tpl else>',
* '<p>Boy: {name} - {age}</p>',
* '</tpl>',
* '<tpl if="this.isBaby(age)">',
* '<p>{name} is a baby!</p>',
* '</tpl>',
* '</tpl></p>',
* {
* // XTemplate configuration:
* disableFormats: true,
* // member functions:
* isGirl: function(name){
* return name == 'Sara Grace';
* },
* isBaby: function(age){
* return age < 1;
* }
* }
* );
* tpl.overwrite(panel.body, data);
*/
Ext.define('Ext.XTemplate', {
extend: 'Ext.Template',
requires: 'Ext.XTemplateCompiler',
/**
* @private
*/
emptyObj: {},
/**
* @cfg {Boolean} compiled
* Only applies to {@link Ext.Template}, XTemplates are compiled automatically on the
* first call to {@link #apply} or {@link #applyOut}.
* @hide
*/
apply: function(values) {
return this.applyOut(values, []).join('');
},
/**
* Appends the result of this template to the provided output array.
* @param {Object/Array} values The template values. See {@link #apply}.
* @param {Array} out The array to which output is pushed.
* @param {Object} parent
* @return {Array} The given out array.
*/
applyOut: function(values, out, parent) {
var me = this,
xindex = values.xindex,
xcount = values.xcount,
compiler;
if (!me.fn) {
compiler = new Ext.XTemplateCompiler({
useFormat : me.disableFormats !== true,
definitions : me.definitions
});
me.fn = compiler.compile(me.html);
}
try {
xindex = typeof xindex === 'number' ? xindex : 1;
xcount = typeof xcount === 'number' ? xcount : 1;
me.fn.call(me, out, values, parent || me.emptyObj, xindex, xcount);
} catch (e) {
//<debug>
Ext.Logger.log('Error: ' + e.message);
//</debug>
}
return out;
},
/**
* Does nothing. XTemplates are compiled automatically, so this function simply returns this.
* @return {Ext.XTemplate} this
*/
compile: function() {
return this;
},
statics: {
/**
* Gets an `XTemplate` from an object (an instance of an {@link Ext#define}'d class).
* Many times, templates are configured high in the class hierarchy and are to be
* shared by all classes that derive from that base. To further complicate matters,
* these templates are seldom actual instances but are rather configurations. For
* example:
*
* Ext.define('MyApp.Class', {
* someTpl: [
* 'tpl text here'
* ]
* });
*
* The goal being to share that template definition with all instances and even
* instances of derived classes, until `someTpl` is overridden. This method will
* "upgrade" these configurations to be real `XTemplate` instances *in place* (to
* avoid creating one instance per object).
*
* @param {Object} instance The object from which to get the `XTemplate` (must be
* an instance of an {@link Ext#define}'d class).
* @param {String} name The name of the property by which to get the `XTemplate`.
* @return {Ext.XTemplate} The `XTemplate` instance or null if not found.
* @protected
*/
getTpl: function (instance, name) {
var tpl = instance[name], // go for it! 99% of the time we will get it!
proto;
if (tpl && !tpl.isTemplate) { // tpl is just a configuration (not an instance)
// create the template instance from the configuration:
tpl = Ext.ClassManager.dynInstantiate('Ext.XTemplate', tpl);
// and replace the reference with the new instance:
if (instance.hasOwnProperty(name)) { // the tpl is on the instance
instance[name] = tpl;
} else { // must be somewhere in the prototype chain
for (proto = instance.self.prototype; proto; proto = proto.superclass) {
if (proto.hasOwnProperty(name)) {
proto[name] = tpl;
break;
}
}
}
}
// else !tpl (no such tpl) or the tpl is an instance already... either way, tpl
// is ready to return
return tpl || null;
}
}
});
/**
* @private
*/
Ext.define('Ext.behavior.Behavior', {
constructor: function(component) {
this.component = component;
component.on('destroy', 'onComponentDestroy', this);
},
onComponentDestroy: Ext.emptyFn
});
/**
* @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.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
*
* The abstract class. Sub-classes are expected, at the very least, to implement translation logics inside
* the 'translate' method
*/
Ext.define('Ext.util.translatable.Abstract', {
extend: 'Ext.Evented',
requires: ['Ext.fx.easing.Linear'],
config: {
easing: null,
easingX: null,
easingY: null,
fps: Ext.os.is.Android4 ? 50 : 60
},
/**
* @event animationstart
* Fires whenever the animation is started
* @param {Ext.util.translatable.Abstract} this
* @param {Number} x The current translation on the x axis
* @param {Number} y The current translation on the y axis
*/
/**
* @event animationframe
* Fires for each animation frame
* @param {Ext.util.translatable.Abstract} this
* @param {Number} x The new translation on the x axis
* @param {Number} y The new translation on the y axis
*/
/**
* @event animationend
* Fires whenever the animation is ended
* @param {Ext.util.translatable.Abstract} this
* @param {Number} x The current translation on the x axis
* @param {Number} y The current translation on the y axis
*/
x: 0,
y: 0,
activeEasingX: null,
activeEasingY: null,
isAnimating: false,
isTranslatable: true,
constructor: function(config) {
this.doAnimationFrame = Ext.Function.bind(this.doAnimationFrame, this);
this.initConfig(config);
},
factoryEasing: function(easing) {
return Ext.factory(easing, Ext.fx.easing.Linear, null, 'easing');
},
applyEasing: function(easing) {
if (!this.getEasingX()) {
this.setEasingX(this.factoryEasing(easing));
}
if (!this.getEasingY()) {
this.setEasingY(this.factoryEasing(easing));
}
},
applyEasingX: function(easing) {
return this.factoryEasing(easing);
},
applyEasingY: function(easing) {
return this.factoryEasing(easing);
},
updateFps: function(fps) {
this.animationInterval = 1000 / fps;
},
doTranslate: Ext.emptyFn,
translate: function(x, y, animation) {
if (animation) {
return this.translateAnimated(x, y, animation);
}
if (this.isAnimating) {
this.stopAnimation();
}
if (!isNaN(x) && typeof x == 'number') {
this.x = x;
}
if (!isNaN(y) && typeof y == 'number') {
this.y = y;
}
this.doTranslate(x, y);
},
translateAxis: function(axis, value, animation) {
var x, y;
if (axis == 'x') {
x = value;
}
else {
y = value;
}
return this.translate(x, y, animation);
},
animate: function(easingX, easingY) {
this.activeEasingX = easingX;
this.activeEasingY = easingY;
this.isAnimating = true;
this.lastX = null;
this.lastY = null;
this.animationFrameId = requestAnimationFrame(this.doAnimationFrame);
this.fireEvent('animationstart', this, this.x, this.y);
return this;
},
translateAnimated: function(x, y, animation) {
if (!Ext.isObject(animation)) {
animation = {};
}
if (this.isAnimating) {
this.stopAnimation();
}
var now = Ext.Date.now(),
easing = animation.easing,
easingX = (typeof x == 'number') ? (animation.easingX || easing || this.getEasingX() || true) : null,
easingY = (typeof y == 'number') ? (animation.easingY || easing || this.getEasingY() || true) : null;
if (easingX) {
easingX = this.factoryEasing(easingX);
easingX.setStartTime(now);
easingX.setStartValue(this.x);
easingX.setEndValue(x);
if ('duration' in animation) {
easingX.setDuration(animation.duration);
}
}
if (easingY) {
easingY = this.factoryEasing(easingY);
easingY.setStartTime(now);
easingY.setStartValue(this.y);
easingY.setEndValue(y);
if ('duration' in animation) {
easingY.setDuration(animation.duration);
}
}
return this.animate(easingX, easingY);
},
doAnimationFrame: function() {
var me = this,
easingX = me.activeEasingX,
easingY = me.activeEasingY,
now = Date.now(),
x, y;
this.animationFrameId = requestAnimationFrame(this.doAnimationFrame);
if (!me.isAnimating) {
return;
}
me.lastRun = now;
if (easingX === null && easingY === null) {
me.stopAnimation();
return;
}
if (easingX !== null) {
me.x = x = Math.round(easingX.getValue());
if (easingX.isEnded) {
me.activeEasingX = null;
me.fireEvent('axisanimationend', me, 'x', x);
}
}
else {
x = me.x;
}
if (easingY !== null) {
me.y = y = Math.round(easingY.getValue());
if (easingY.isEnded) {
me.activeEasingY = null;
me.fireEvent('axisanimationend', me, 'y', y);
}
}
else {
y = me.y;
}
if (me.lastX !== x || me.lastY !== y) {
me.doTranslate(x, y);
me.lastX = x;
me.lastY = y;
}
me.fireEvent('animationframe', me, x, y);
},
stopAnimation: function() {
if (!this.isAnimating) {
return;
}
this.activeEasingX = null;
this.activeEasingY = null;
this.isAnimating = false;
cancelAnimationFrame(this.animationFrameId);
this.fireEvent('animationend', this, this.x, this.y);
},
refresh: function() {
this.translate(this.x, this.y);
},
destroy: function() {
if (this.isAnimating) {
this.stopAnimation();
}
this.callParent(arguments);
}
});
/**
* @private
*/
Ext.define('Ext.util.translatable.Dom', {
extend: 'Ext.util.translatable.Abstract',
config: {
element: null
},
applyElement: function(element) {
if (!element) {
return;
}
return Ext.get(element);
},
updateElement: function() {
this.refresh();
}
});
/**
* @private
*
* CSS Transform implementation
*/
Ext.define('Ext.util.translatable.CssTransform', {
extend: 'Ext.util.translatable.Dom',
doTranslate: function() {
this.getElement().dom.style.webkitTransform = 'translate3d(' + this.x + 'px, ' + this.y + 'px, 0px)';
},
destroy: function() {
var element = this.getElement();
if (element && !element.isDestroyed) {
element.dom.style.webkitTransform = null;
}
this.callSuper();
}
});
/**
* @private
*
* Scroll position implementation
*/
Ext.define('Ext.util.translatable.ScrollPosition', {
extend: 'Ext.util.translatable.Dom',
wrapperWidth: 0,
wrapperHeight: 0,
config: {
useWrapper: true
},
getWrapper: function() {
var wrapper = this.wrapper,
element = this.getElement(),
container;
if (!wrapper) {
container = element.getParent();
if (!container) {
return null;
}
if (this.getUseWrapper()) {
wrapper = element.wrap();
}
else {
wrapper = container;
}
element.addCls('x-translatable');
wrapper.addCls('x-translatable-container');
this.wrapper = wrapper;
wrapper.on('resize', 'onWrapperResize', this);
wrapper.on('painted', 'refresh', this);
this.refresh();
}
return wrapper;
},
doTranslate: function(x, y) {
var wrapper = this.getWrapper(),
dom;
if (wrapper) {
dom = wrapper.dom;
if (typeof x == 'number') {
dom.scrollLeft = this.wrapperWidth - x;
}
if (typeof y == 'number') {
dom.scrollTop = this.wrapperHeight - y;
}
}
},
onWrapperResize: function(wrapper, info) {
this.wrapperWidth = info.width;
this.wrapperHeight = info.height;
this.refresh();
},
destroy: function() {
var element = this.getElement(),
wrapper = this.wrapper;
if (wrapper) {
if (!element.isDestroyed) {
if (this.getUseWrapper()) {
wrapper.doReplaceWith(element);
}
element.removeCls('x-translatable');
}
wrapper.removeCls('x-translatable-container');
wrapper.un('resize', 'onWrapperResize', this);
wrapper.un('painted', 'refresh', this);
delete this.wrapper;
delete this._element;
}
this.callSuper();
}
});
/**
* The utility class to abstract different implementations to have the best performance when applying 2D translation
* on any DOM element.
*
* @private
*/
Ext.define('Ext.util.Translatable', {
requires: [
'Ext.util.translatable.CssTransform',
'Ext.util.translatable.ScrollPosition'
],
constructor: function(config) {
var namespace = Ext.util.translatable,
CssTransform = namespace.CssTransform,
ScrollPosition = namespace.ScrollPosition,
classReference;
if (typeof config == 'object' && 'translationMethod' in config) {
if (config.translationMethod === 'scrollposition') {
classReference = ScrollPosition;
}
else if (config.translationMethod === 'csstransform') {
classReference = CssTransform;
}
}
if (!classReference) {
if (Ext.os.is.Android2 || Ext.browser.is.ChromeMobile) {
classReference = ScrollPosition;
}
else {
classReference = CssTransform;
}
}
return new classReference(config);
}
});
/**
* @private
*/
Ext.define('Ext.behavior.Translatable', {
extend: 'Ext.behavior.Behavior',
requires: [
'Ext.util.Translatable'
],
setConfig: function(config) {
var translatable = this.translatable,
component = this.component;
if (config) {
if (!translatable) {
this.translatable = translatable = new Ext.util.Translatable(config);
translatable.setElement(component.renderElement);
translatable.on('destroy', 'onTranslatableDestroy', this);
}
else if (Ext.isObject(config)) {
translatable.setConfig(config);
}
}
else if (translatable) {
translatable.destroy();
}
return this;
},
getTranslatable: function() {
return this.translatable;
},
onTranslatableDestroy: function() {
delete this.translatable;
},
onComponentDestroy: function() {
var translatable = this.translatable;
if (translatable) {
translatable.destroy();
}
}
});
/**
* A core util class to bring Draggable behavior to any DOM element
*/
Ext.define('Ext.util.Draggable', {
isDraggable: true,
mixins: [
'Ext.mixin.Observable'
],
requires: [
'Ext.util.Translatable'
],
/**
* @event dragstart
* @preventable initDragStart
* Fires whenever the component starts to be dragged
* @param {Ext.util.Draggable} this
* @param {Ext.event.Event} e the event object
* @param {Number} offsetX The current offset value on the x axis
* @param {Number} offsetY The current offset value on the y axis
*/
/**
* @event drag
* Fires whenever the component is dragged
* @param {Ext.util.Draggable} this
* @param {Ext.event.Event} e the event object
* @param {Number} offsetX The new offset value on the x axis
* @param {Number} offsetY The new offset value on the y axis
*/
/**
* @event dragend
* Fires whenever the component is dragged
* @param {Ext.util.Draggable} this
* @param {Ext.event.Event} e the event object
* @param {Number} offsetX The current offset value on the x axis
* @param {Number} offsetY The current offset value on the y axis
*/
config: {
cls: Ext.baseCSSPrefix + 'draggable',
draggingCls: Ext.baseCSSPrefix + 'dragging',
element: null,
constraint: 'container',
disabled: null,
/**
* @cfg {String} direction
* Possible values: 'vertical', 'horizontal', or 'both'
* @accessor
*/
direction: 'both',
/**
* @cfg {Object/Number} initialOffset
* The initial draggable offset. When specified as Number,
* both x and y will be set to that value.
*/
initialOffset: {
x: 0,
y: 0
},
translatable: {}
},
DIRECTION_BOTH: 'both',
DIRECTION_VERTICAL: 'vertical',
DIRECTION_HORIZONTAL: 'horizontal',
defaultConstraint: {
min: { x: -Infinity, y: -Infinity },
max: { x: Infinity, y: Infinity }
},
containerWidth: 0,
containerHeight: 0,
width: 0,
height: 0,
/**
* Creates new Draggable.
* @param {Object} config The configuration object for this Draggable.
*/
constructor: function(config) {
var element;
this.extraConstraint = {};
this.initialConfig = config;
this.offset = {
x: 0,
y: 0
};
this.listeners = {
dragstart: 'onDragStart',
drag : 'onDrag',
dragend : 'onDragEnd',
resize : 'onElementResize',
scope: this
};
if (config && config.element) {
element = config.element;
delete config.element;
this.setElement(element);
}
return this;
},
applyElement: function(element) {
if (!element) {
return;
}
return Ext.get(element);
},
updateElement: function(element) {
element.on(this.listeners);
this.initConfig(this.initialConfig);
},
updateInitialOffset: function(initialOffset) {
if (typeof initialOffset == 'number') {
initialOffset = {
x: initialOffset,
y: initialOffset
};
}
var offset = this.offset,
x, y;
offset.x = x = initialOffset.x;
offset.y = y = initialOffset.y;
this.getTranslatable().translate(x, y);
},
updateCls: function(cls) {
this.getElement().addCls(cls);
},
applyTranslatable: function(translatable, currentInstance) {
translatable = Ext.factory(translatable, Ext.util.Translatable, currentInstance);
translatable.setElement(this.getElement());
return translatable;
},
setExtraConstraint: function(constraint) {
this.extraConstraint = constraint || {};
this.refreshConstraint();
return this;
},
addExtraConstraint: function(constraint) {
Ext.merge(this.extraConstraint, constraint);
this.refreshConstraint();
return this;
},
applyConstraint: function(newConstraint) {
this.currentConstraint = newConstraint;
if (!newConstraint) {
newConstraint = this.defaultConstraint;
}
if (newConstraint === 'container') {
return Ext.merge(this.getContainerConstraint(), this.extraConstraint);
}
return Ext.merge({}, this.extraConstraint, newConstraint);
},
updateConstraint: function() {
this.refreshOffset();
},
getContainerConstraint: function() {
var container = this.getContainer(),
element = this.getElement();
if (!container || !element.dom) {
return this.defaultConstraint;
}
return {
min: { x: 0, y: 0 },
max: { x: this.containerWidth - this.width, y: this.containerHeight - this.height }
};
},
getContainer: function() {
var container = this.container;
if (!container) {
container = this.getElement().getParent();
if (container) {
this.container = container;
container.on({
resize: 'onContainerResize',
destroy: 'onContainerDestroy',
scope: this
});
}
}
return container;
},
onElementResize: function(element, info) {
this.width = info.width;
this.height = info.height;
this.refresh();
},
onContainerResize: function(container, info) {
this.containerWidth = info.width;
this.containerHeight = info.height;
this.refresh();
},
onContainerDestroy: function() {
delete this.container;
delete this.containerSizeMonitor;
},
detachListeners: function() {
this.getElement().un(this.listeners);
},
isAxisEnabled: function(axis) {
var direction = this.getDirection();
if (axis === 'x') {
return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_HORIZONTAL);
}
return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_VERTICAL);
},
onDragStart: function(e) {
if (this.getDisabled()) {
return false;
}
var offset = this.offset;
this.fireAction('dragstart', [this, e, offset.x, offset.y], this.initDragStart);
},
initDragStart: function(me, e, offsetX, offsetY) {
this.dragStartOffset = {
x: offsetX,
y: offsetY
};
this.isDragging = true;
this.getElement().addCls(this.getDraggingCls());
},
onDrag: function(e) {
if (!this.isDragging) {
return;
}
var startOffset = this.dragStartOffset;
this.fireAction('drag', [this, e, startOffset.x + e.deltaX, startOffset.y + e.deltaY], this.doDrag);
},
doDrag: function(me, e, offsetX, offsetY) {
me.setOffset(offsetX, offsetY);
},
onDragEnd: function(e) {
if (!this.isDragging) {
return;
}
this.onDrag(e);
this.isDragging = false;
this.getElement().removeCls(this.getDraggingCls());
this.fireEvent('dragend', this, e, this.offset.x, this.offset.y);
},
setOffset: function(x, y, animation) {
var currentOffset = this.offset,
constraint = this.getConstraint(),
minOffset = constraint.min,
maxOffset = constraint.max,
min = Math.min,
max = Math.max;
if (this.isAxisEnabled('x') && typeof x == 'number') {
x = min(max(x, minOffset.x), maxOffset.x);
}
else {
x = currentOffset.x;
}
if (this.isAxisEnabled('y') && typeof y == 'number') {
y = min(max(y, minOffset.y), maxOffset.y);
}
else {
y = currentOffset.y;
}
currentOffset.x = x;
currentOffset.y = y;
this.getTranslatable().translate(x, y, animation);
},
getOffset: function() {
return this.offset;
},
refreshConstraint: function() {
this.setConstraint(this.currentConstraint);
},
refreshOffset: function() {
var offset = this.offset;
this.setOffset(offset.x, offset.y);
},
refresh: function() {
this.refreshConstraint();
this.getTranslatable().refresh();
this.refreshOffset();
},
/**
* Enable the Draggable.
* @return {Ext.util.Draggable} This Draggable instance
*/
enable: function() {
return this.setDisabled(false);
},
/**
* Disable the Draggable.
* @return {Ext.util.Draggable} This Draggable instance
*/
disable: function() {
return this.setDisabled(true);
},
destroy: function() {
var translatable = this.getTranslatable();
var element = this.getElement();
if (element && !element.isDestroyed) {
element.removeCls(this.getCls());
}
this.detachListeners();
if (translatable) {
translatable.destroy();
}
}
}, function() {
});
/**
* @private
*/
Ext.define('Ext.behavior.Draggable', {
extend: 'Ext.behavior.Behavior',
requires: [
'Ext.util.Draggable'
],
setConfig: function(config) {
var draggable = this.draggable,
component = this.component;
if (config) {
if (!draggable) {
component.setTranslatable(true);
this.draggable = draggable = new Ext.util.Draggable(config);
draggable.setTranslatable(component.getTranslatable());
draggable.setElement(component.renderElement);
draggable.on('destroy', 'onDraggableDestroy', this);
component.on(this.listeners);
}
else if (Ext.isObject(config)) {
draggable.setConfig(config);
}
}
else if (draggable) {
draggable.destroy();
}
return this;
},
getDraggable: function() {
return this.draggable;
},
onDraggableDestroy: function() {
delete this.draggable;
},
onComponentDestroy: function() {
var draggable = this.draggable;
if (draggable) {
draggable.destroy();
}
}
});
/**
* 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;
}
});
(function(clsPrefix) {
/**
* Most of the visual classes you interact with in Sencha Touch are Components. Every Component in Sencha Touch is a
* subclass of Ext.Component, which means they can all:
*
* * Render themselves onto the page using a template
* * Show and hide themselves at any time
* * Center themselves on the screen
* * Enable and disable themselves
*
* They can also do a few more advanced things:
*
* * Float above other components (windows, message boxes and overlays)
* * Change size and position on the screen with animation
* * Dock other Components inside themselves (useful for toolbars)
* * Align to other components, allow themselves to be dragged around, make their content scrollable & more
*
* ## Available Components
*
* There are many components available in Sencha Touch, separated into 4 main groups:
*
* ### Navigation components
* * {@link Ext.Toolbar}
* * {@link Ext.Button}
* * {@link Ext.TitleBar}
* * {@link Ext.SegmentedButton}
* * {@link Ext.Title}
* * {@link Ext.Spacer}
*
* ### Store-bound components
* * {@link Ext.dataview.DataView}
* * {@link Ext.Carousel}
* * {@link Ext.List}
* * {@link Ext.NestedList}
*
* ### Form components
* * {@link Ext.form.Panel}
* * {@link Ext.form.FieldSet}
* * {@link Ext.field.Checkbox}
* * {@link Ext.field.Hidden}
* * {@link Ext.field.Slider}
* * {@link Ext.field.Text}
* * {@link Ext.picker.Picker}
* * {@link Ext.picker.Date}
*
* ### General components
* * {@link Ext.Panel}
* * {@link Ext.tab.Panel}
* * {@link Ext.Viewport Ext.Viewport}
* * {@link Ext.Img}
* * {@link Ext.Map}
* * {@link Ext.Audio}
* * {@link Ext.Video}
* * {@link Ext.Sheet}
* * {@link Ext.ActionSheet}
* * {@link Ext.MessageBox}
*
*
* ## Instantiating Components
*
* Components are created the same way as all other classes in Sencha Touch - using Ext.create. Here's how we can
* create a Text field:
*
* var panel = Ext.create('Ext.Panel', {
* html: 'This is my panel'
* });
*
* This will create a {@link Ext.Panel Panel} instance, configured with some basic HTML content. A Panel is just a
* simple Component that can render HTML and also contain other items. In this case we've created a Panel instance but
* it won't show up on the screen yet because items are not rendered immediately after being instantiated. This allows
* us to create some components and move them around before rendering and laying them out, which is a good deal faster
* than moving them after rendering.
*
* To show this panel on the screen now we can simply add it to the global Viewport:
*
* Ext.Viewport.add(panel);
*
* Panels are also Containers, which means they can contain other Components, arranged by a layout. Let's revisit the
* above example now, this time creating a panel with two child Components and a hbox layout:
*
* @example
* var panel = Ext.create('Ext.Panel', {
* layout: 'hbox',
*
* items: [
* {
* xtype: 'panel',
* flex: 1,
* html: 'Left Panel, 1/3rd of total size',
* style: 'background-color: #5E99CC;'
* },
* {
* xtype: 'panel',
* flex: 2,
* html: 'Right Panel, 2/3rds of total size',
* style: 'background-color: #759E60;'
* }
* ]
* });
*
* Ext.Viewport.add(panel);
*
* This time we created 3 Panels - the first one is created just as before but the inner two are declared inline using
* an xtype. Xtype is a convenient way of creating Components without having to go through the process of using
* Ext.create and specifying the full class name, instead you can just provide the xtype for the class inside an object
* and the framework will create the components for you.
*
* We also specified a layout for the top level panel - in this case hbox, which splits the horizontal width of the
* parent panel based on the 'flex' of each child. For example, if the parent Panel above is 300px wide then the first
* child will be flexed to 100px wide and the second to 200px because the first one was given `flex: 1` and the second
* `flex: 2`.
*
* ## Using xtype
*
* xtype is an easy way to create Components without using the full class name. This is especially useful when creating
* a {@link Ext.Container Container} that contains child Components. An xtype is simply a shorthand way of specifying a
* Component - for example you can use `xtype: 'panel'` instead of typing out Ext.panel.Panel.
*
* Sample usage:
*
* @example miniphone
* Ext.create('Ext.Container', {
* fullscreen: true,
* layout: 'fit',
*
* items: [
* {
* xtype: 'panel',
* html: 'This panel is created by xtype'
* },
* {
* xtype: 'toolbar',
* title: 'So is the toolbar',
* docked: 'top'
* }
* ]
* });
*
*
* ### Common xtypes
*
* These are the xtypes that are most commonly used. For an exhaustive list please see the
* [Components Guide](#!/guide/components).
*
* <pre>
xtype Class
----------------- ---------------------
actionsheet Ext.ActionSheet
audio Ext.Audio
button Ext.Button
image Ext.Img
label Ext.Label
loadmask Ext.LoadMask
map Ext.Map
panel Ext.Panel
segmentedbutton Ext.SegmentedButton
sheet Ext.Sheet
spacer Ext.Spacer
titlebar Ext.TitleBar
toolbar Ext.Toolbar
video Ext.Video
carousel Ext.carousel.Carousel
navigationview Ext.navigation.View
datepicker Ext.picker.Date
picker Ext.picker.Picker
slider Ext.slider.Slider
thumb Ext.slider.Thumb
tabpanel Ext.tab.Panel
viewport Ext.viewport.Default
DataView Components
---------------------------------------------
dataview Ext.dataview.DataView
list Ext.dataview.List
nestedlist Ext.dataview.NestedList
Form Components
---------------------------------------------
checkboxfield Ext.field.Checkbox
datepickerfield Ext.field.DatePicker
emailfield Ext.field.Email
hiddenfield Ext.field.Hidden
numberfield Ext.field.Number
passwordfield Ext.field.Password
radiofield Ext.field.Radio
searchfield Ext.field.Search
selectfield Ext.field.Select
sliderfield Ext.field.Slider
spinnerfield Ext.field.Spinner
textfield Ext.field.Text
textareafield Ext.field.TextArea
togglefield Ext.field.Toggle
urlfield Ext.field.Url
fieldset Ext.form.FieldSet
formpanel Ext.form.Panel
* </pre>
*
* ## Configuring Components
*
* Whenever you create a new Component you can pass in configuration options. All of the configurations for a given
* Component are listed in the "Config options" section of its class docs page. You can pass in any number of
* configuration options when you instantiate the Component, and modify any of them at any point later. For example, we
* can easily modify the {@link Ext.Panel#html html content} of a Panel after creating it:
*
* @example miniphone
* // we can configure the HTML when we instantiate the Component
* var panel = Ext.create('Ext.Panel', {
* fullscreen: true,
* html: 'This is a Panel'
* });
*
* // we can update the HTML later using the setHtml method:
* panel.setHtml('Some new HTML');
*
* // we can retrieve the current HTML using the getHtml method:
* Ext.Msg.alert(panel.getHtml()); // displays "Some new HTML"
*
* Every config has a getter method and a setter method - these are automatically generated and always follow the same
* pattern. For example, a config called `html` will receive `getHtml` and `setHtml` methods, a config called `defaultType`
* will receive `getDefaultType` and `setDefaultType` methods, and so on.
*
* ## Further Reading
*
* See the [Component & Container Guide](#!/guide/components) for more information, and check out the
* {@link Ext.Container} class docs also.
*
* @aside guide components
* @aside guide events
*
*/
Ext.define('Ext.Component', {
extend: 'Ext.AbstractComponent',
alternateClassName: 'Ext.lib.Component',
mixins: ['Ext.mixin.Traversable'],
requires: [
'Ext.ComponentManager',
'Ext.XTemplate',
'Ext.dom.Element',
'Ext.behavior.Translatable',
'Ext.behavior.Draggable'
],
/**
* @cfg {String} xtype
* The `xtype` configuration option can be used to optimize Component creation and rendering. It serves as a
* shortcut to the full component name. For example, the component `Ext.button.Button` has an xtype of `button`.
*
* You can define your own xtype on a custom {@link Ext.Component component} by specifying the
* {@link Ext.Class#alias alias} config option with a prefix of `widget`. For example:
*
* Ext.define('PressMeButton', {
* extend: 'Ext.button.Button',
* alias: 'widget.pressmebutton',
* text: 'Press Me'
* });
*
* Any Component can be created implicitly as an object config with an xtype specified, allowing it to be
* declared and passed into the rendering pipeline without actually being instantiated as an object. Not only is
* rendering deferred, but the actual creation of the object itself is also deferred, saving memory and resources
* until they are actually needed. In complex, nested layouts containing many Components, this can make a
* noticeable improvement in performance.
*
* // Explicit creation of contained Components:
* var panel = new Ext.Panel({
* // ...
* items: [
* Ext.create('Ext.button.Button', {
* text: 'OK'
* })
* ]
* });
*
* // Implicit creation using xtype:
* var panel = new Ext.Panel({
* // ...
* items: [{
* xtype: 'button',
* text: 'OK'
* }]
* });
*
* In the first example, the button will always be created immediately during the panel's initialization. With
* many added Components, this approach could potentially slow the rendering of the page. In the second example,
* the button will not be created or rendered until the panel is actually displayed in the browser. If the panel
* is never displayed (for example, if it is a tab that remains hidden) then the button will never be created and
* will never consume any resources whatsoever.
*/
xtype: 'component',
observableType: 'component',
cachedConfig: {
/**
* @cfg {String} baseCls
* The base CSS class to apply to this component's element. This will also be prepended to
* other elements within this component. To add specific styling for sub-classes, use the {@link #cls} config.
* @accessor
*/
baseCls: null,
/**
* @cfg {String/String[]} cls The CSS class to add to this component's element, in addition to the {@link #baseCls}
* @accessor
*/
cls: null,
/**
* @cfg {String} [floatingCls="x-floating"] The CSS class to add to this component when it is floatable.
* @accessor
*/
floatingCls: clsPrefix + 'floating',
/**
* @cfg {String} [hiddenCls="x-item-hidden"] The CSS class to add to the component when it is hidden
* @accessor
*/
hiddenCls: clsPrefix + 'item-hidden',
/**
* @cfg {String} ui The ui to be used on this Component
*/
ui: null,
/**
* @cfg {Number/String} margin The margin to use on this Component. Can be specified as a number (in which case
* all edges get the same margin) or a CSS string like '5 10 10 10'
* @accessor
*/
margin: null,
/**
* @cfg {Number/String} padding The padding to use on this Component. Can be specified as a number (in which
* case all edges get the same padding) or a CSS string like '5 10 10 10'
* @accessor
*/
padding: null,
/**
* @cfg {Number/String} border The border width to use on this Component. Can be specified as a number (in which
* case all edges get the same border width) or a CSS string like '5 10 10 10'.
*
* Please note that this will not add
* a `border-color` or `border-style` CSS property to the component; you must do that manually using either CSS or
* the {@link #style} configuration.
*
* ## Using {@link #style}:
*
* Ext.Viewport.add({
* centered: true,
* width: 100,
* height: 100,
*
* border: 3,
* style: 'border-color: blue; border-style: solid;'
* // ...
* });
*
* ## Using CSS:
*
* Ext.Viewport.add({
* centered: true,
* width: 100,
* height: 100,
*
* border: 3,
* cls: 'my-component'
* // ...
* });
*
* And your CSS file:
*
* .my-component {
* border-color: red;
* border-style: solid;
* }
*
* @accessor
*/
border: null,
/**
* @cfg {String} [styleHtmlCls="x-html"]
* The class that is added to the content target when you set `styleHtmlContent` to `true`.
* @accessor
*/
styleHtmlCls: clsPrefix + 'html',
/**
* @cfg {Boolean} [styleHtmlContent=false]
* `true` to automatically style the HTML inside the content target of this component (body for panels).
* @accessor
*/
styleHtmlContent: null
},
eventedConfig: {
/**
* @cfg {Number} flex
* The flex of this item *if* this item item is inside a {@link Ext.layout.HBox} or {@link Ext.layout.VBox}
* layout.
*
* You can also update the flex of a component dynamically using the {@link Ext.layout.FlexBox#setItemFlex}
* method.
*/
flex: null,
/**
* @cfg {Number/String} left
* The absolute left position of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* Explicitly setting this value will make this Component become 'floating', which means its layout will no
* longer be affected by the Container that it resides in.
* @accessor
* @evented
*/
left: null,
/**
* @cfg {Number/String} top
* The absolute top position of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* Explicitly setting this value will make this Component become 'floating', which means its layout will no
* longer be affected by the Container that it resides in.
* @accessor
* @evented
*/
top: null,
/**
* @cfg {Number/String} right
* The absolute right position of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* Explicitly setting this value will make this Component become 'floating', which means its layout will no
* longer be affected by the Container that it resides in.
* @accessor
* @evented
*/
right: null,
/**
* @cfg {Number/String} bottom
* The absolute bottom position of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* Explicitly setting this value will make this Component become 'floating', which means its layout will no
* longer be affected by the Container that it resides in.
* @accessor
* @evented
*/
bottom: null,
/**
* @cfg {Number/String} width
* The width of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* By default, if this is not explicitly set, this Component's element will simply have its own natural size.
* @accessor
* @evented
*/
width: null,
/**
* @cfg {Number/String} height
* The height of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* By default, if this is not explicitly set, this Component's element will simply have its own natural size.
* @accessor
* @evented
*/
height: null,
/**
* @cfg {Number/String} minWidth
* The minimum width of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* @accessor
* @evented
*/
minWidth: null,
/**
* @cfg {Number/String} minHeight
* The minimum height of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* @accessor
* @evented
*/
minHeight: null,
/**
* @cfg {Number/String} maxWidth
* The maximum width of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* Note that this config will not apply if the Component is 'floating' (absolutely positioned or centered)
* @accessor
* @evented
*/
maxWidth: null,
/**
* @cfg {Number/String} maxHeight
* The maximum height of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* Note that this config will not apply if the Component is 'floating' (absolutely positioned or centered)
* @accessor
* @evented
*/
maxHeight: null,
/**
* @cfg {String} docked
* The dock position of this component in its container. Can be `left`, `top`, `right` or `bottom`.
*
* __Notes__
*
* You must use a HTML5 doctype for {@link #docked} `bottom` to work. To do this, simply add the following code to the HTML file:
*
* <!doctype html>
*
* So your index.html file should look a little like this:
*
* <!doctype html>
* <html>
* <head>
* <title>MY application title</title>
* ...
*
* @accessor
* @evented
*/
docked: null,
/**
* @cfg {Boolean} centered
* Whether or not this Component is absolutely centered inside its Container
* @accessor
* @evented
*/
centered: null,
/**
* @cfg {Boolean} hidden
* Whether or not this Component is hidden (its CSS `display` property is set to `none`)
* @accessor
* @evented
*/
hidden: null,
/**
* @cfg {Boolean} disabled
* Whether or not this component is disabled
* @accessor
* @evented
*/
disabled: null
},
config: {
/**
* @cfg {String/Object} style Optional CSS styles that will be rendered into an inline style attribute when the
* Component is rendered.
*
* You can pass either a string syntax:
*
* style: 'background:red'
*
* Or by using an object:
*
* style: {
* background: 'red'
* }
*
* When using the object syntax, you can define CSS Properties by using a string:
*
* style: {
* 'border-left': '1px solid red'
* }
*
* Although the object syntax is much easier to read, we suggest you to use the string syntax for better performance.
*
* @accessor
*/
style: null,
/**
* @cfg {String/Ext.Element/HTMLElement} html Optional HTML content to render inside this Component, or a reference
* to an existing element on the page.
* @accessor
*/
html: null,
/**
* @cfg {Object} draggable Configuration options to make this Component draggable
* @accessor
*/
draggable: null,
/**
* @cfg {Object} translatable
* @private
* @accessor
*/
translatable: null,
/**
* @cfg {Ext.Element} renderTo Optional element to render this Component to. Usually this is not needed because
* a Component is normally full screen or automatically rendered inside another {@link Ext.Container Container}
* @accessor
*/
renderTo: null,
/**
* @cfg {Number} zIndex The z-index to give this Component when it is rendered
* @accessor
*/
zIndex: null,
/**
* @cfg {String/String[]/Ext.Template[]/Ext.XTemplate[]} tpl
* A {@link String}, {@link Ext.Template}, {@link Ext.XTemplate} or an {@link Array} of strings to form an {@link Ext.XTemplate}.
* Used in conjunction with the {@link #data} and {@link #tplWriteMode} configurations.
*
* __Note__
* The {@link #data} configuration _must_ be set for any content to be shown in the component when using this configuration.
* @accessor
*/
tpl: null,
/**
* @cfg {String/Mixed} enterAnimation
* Animation effect to apply when the Component is being shown. Typically you want to use an
* inbound animation type such as 'fadeIn' or 'slideIn'.
* @deprecated 2.0.0 Please use {@link #showAnimation} instead.
* @accessor
*/
enterAnimation: null,
/**
* @cfg {String/Mixed} exitAnimation
* Animation effect to apply when the Component is being hidden.
* @deprecated 2.0.0 Please use {@link #hideAnimation} instead. Typically you want to use an
* outbound animation type such as 'fadeOut' or 'slideOut'.
* @accessor
*/
exitAnimation: null,
/**
* @cfg {String/Mixed} showAnimation
* Animation effect to apply when the Component is being shown. Typically you want to use an
* inbound animation type such as 'fadeIn' or 'slideIn'.
* @accessor
*/
showAnimation: null,
/**
* @cfg {String/Mixed} hideAnimation
* Animation effect to apply when the Component is being hidden. Typically you want to use an
* outbound animation type such as 'fadeOut' or 'slideOut'.
* @accessor
*/
hideAnimation: null,
/**
* @cfg {String} tplWriteMode The Ext.(X)Template method to use when
* updating the content area of the Component.
* Valid modes are:
*
* - append
* - insertAfter
* - insertBefore
* - insertFirst
* - overwrite
* @accessor
*/
tplWriteMode: 'overwrite',
/**
* @cfg {Mixed} data
* The initial set of data to apply to the `{@link #tpl}` to
* update the content area of the Component.
* @accessor
*/
data: null,
/**
* @cfg {String} [disabledCls="x-item-disabled"] The CSS class to add to the component when it is disabled
* @accessor
*/
disabledCls: clsPrefix + 'item-disabled',
/**
* @cfg {Ext.Element/HTMLElement/String} contentEl The configured element will automatically be
* added as the content of this component. When you pass a string, we expect it to be an element id.
* If the content element is hidden, we will automatically show it.
* @accessor
*/
contentEl: null,
/**
* @cfg {String} id
* The **unique id of this component instance.**
*
* It should not be necessary to use this configuration except for singleton objects in your application. Components
* created with an id may be accessed globally using {@link Ext#getCmp Ext.getCmp}.
*
* Instead of using assigned ids, use the {@link #itemId} config, and {@link Ext.ComponentQuery ComponentQuery}
* which provides selector-based searching for Sencha Components analogous to DOM querying. The
* {@link Ext.Container} class contains {@link Ext.Container#down shortcut methods} to query
* its descendant Components by selector.
*
* Note that this id will also be used as the element id for the containing HTML element that is rendered to the
* page for this component. This allows you to write id-based CSS rules to style the specific instance of this
* component uniquely, and also to select sub-elements using this component's id as the parent.
*
* **Note**: to avoid complications imposed by a unique id also see `{@link #itemId}`.
*
* Defaults to an auto-assigned id.
*/
/**
* @cfg {String} itemId
* An itemId can be used as an alternative way to get a reference to a component when no object reference is
* available. Instead of using an `{@link #id}` with {@link Ext#getCmp}, use `itemId` with
* {@link Ext.Container#getComponent} which will retrieve `itemId`'s or {@link #id}'s. Since `itemId`'s are an
* index to the container's internal MixedCollection, the `itemId` is scoped locally to the container - avoiding
* potential conflicts with {@link Ext.ComponentManager} which requires a **unique** `{@link #id}`.
*
* Also see {@link #id}, {@link Ext.Container#query}, {@link Ext.Container#down} and {@link Ext.Container#child}.
*
* @accessor
*/
itemId: undefined,
/**
* @cfg {Ext.data.Model} record A model instance which updates the Component's html based on it's tpl. Similar to the data
* configuration, but tied to to a record to make allow dynamic updates. This must be a model
* instance and not a configuration of one.
* @accessor
*/
record: null,
/**
* @cfg {Object/Array} plugins
* @accessor
* An object or array of objects that will provide custom functionality for this component. The only
* requirement for a valid plugin is that it contain an init method that accepts a reference of type Ext.Component.
*
* When a component is created, if any plugins are available, the component will call the init method on each
* plugin, passing a reference to itself. Each plugin can then call methods or respond to events on the
* component as needed to provide its functionality.
*
* For examples of plugins, see Ext.plugin.PullRefresh and Ext.plugin.ListPaging
*
* ## Example code
*
* A plugin by alias:
*
* Ext.create('Ext.dataview.List', {
* config: {
* plugins: 'listpaging',
* itemTpl: '<div class="item">{title}</div>',
* store: 'Items'
* }
* });
*
* Multiple plugins by alias:
*
* Ext.create('Ext.dataview.List', {
* config: {
* plugins: ['listpaging', 'pullrefresh'],
* itemTpl: '<div class="item">{title}</div>',
* store: 'Items'
* }
* });
*
* Single plugin by class name with config options:
*
* Ext.create('Ext.dataview.List', {
* config: {
* plugins: {
* xclass: 'Ext.plugin.ListPaging', // Reference plugin by class
* autoPaging: true
* },
*
* itemTpl: '<div class="item">{title}</div>',
* store: 'Items'
* }
* });
*
* Multiple plugins by class name with config options:
*
* Ext.create('Ext.dataview.List', {
* config: {
* plugins: [
* {
* xclass: 'Ext.plugin.PullRefresh',
* pullRefreshText: 'Pull to refresh...'
* },
* {
* xclass: 'Ext.plugin.ListPaging',
* autoPaging: true
* }
* ],
*
* itemTpl: '<div class="item">{title}</div>',
* store: 'Items'
* }
* });
*
*/
plugins: null
},
/**
* @event show
* Fires whenever the Component is shown
* @param {Ext.Component} this The component instance
*/
/**
* @event hide
* Fires whenever the Component is hidden
* @param {Ext.Component} this The component instance
*/
/**
* @event fullscreen
* Fires whenever a Component with the fullscreen config is instantiated
* @param {Ext.Component} this The component instance
*/
/**
* @event floatingchange
* Fires whenever there is a change in the floating status of a component
* @param {Ext.Component} this The component instance
* @param {Boolean} floating The component's new floating state
*/
/**
* @event beforeorientationchange
* Fires before orientation changes.
* @removed 2.0.0 This event is now only available `onBefore` the Viewport's {@link Ext.Viewport#orientationchange}
*/
/**
* @event orientationchange
* Fires when orientation changes.
* @removed 2.0.0 This event is now only available on the Viewport's {@link Ext.Viewport#orientationchange}
*/
/**
* @event initialize
* Fires when the component has been initialized
* @param {Ext.Component} this The component instance
*/
/**
* @event painted
* @inheritdoc Ext.dom.Element#painted
*/
/**
* @event erased
* Fires when the component is no longer displayed in the DOM. Listening to this event will
* degrade performance not recommend for general use.
* @param {Ext.Component} this The component instance
*/
/**
* @event resize
* @inheritdoc Ext.dom.Element#resize
*/
/**
* @private
*/
listenerOptionsRegex: /^(?:delegate|single|delay|buffer|args|prepend|element)$/,
/**
* @private
*/
alignmentRegex: /^([a-z]+)-([a-z]+)(\?)?$/,
/**
* @private
*/
isComponent: true,
/**
* @private
*/
floating: false,
/**
* @private
*/
rendered: false,
/**
* @private
*/
isInner: true,
/**
* @readonly
* @private
*/
dockPositions: {
top: true,
right: true,
bottom: true,
left: true
},
innerElement: null,
element: null,
template: [],
widthLayoutSized: false,
heightLayoutSized: false,
layoutStretched: false,
sizeState: false,
sizeFlags: 0x0,
LAYOUT_WIDTH: 0x1,
LAYOUT_HEIGHT: 0x2,
LAYOUT_BOTH: 0x3,
LAYOUT_STRETCHED: 0x4,
/**
* Creates new Component.
* @param {Object} config The standard configuration object.
*/
constructor: function(config) {
var me = this,
currentConfig = me.config,
id;
me.onInitializedListeners = [];
me.initialConfig = config;
if (config !== undefined && 'id' in config) {
id = config.id;
}
else if ('id' in currentConfig) {
id = currentConfig.id;
}
else {
id = me.getId();
}
me.id = id;
me.setId(id);
Ext.ComponentManager.register(me);
me.initElement();
me.initConfig(me.initialConfig);
me.refreshSizeState = me.doRefreshSizeState;
me.refreshFloating = me.doRefreshFloating;
if (me.refreshSizeStateOnInitialized) {
me.refreshSizeState();
}
if (me.refreshFloatingOnInitialized) {
me.refreshFloating();
}
me.initialize();
me.triggerInitialized();
/**
* Force the component to take up 100% width and height available, by adding it to {@link Ext.Viewport}.
* @cfg {Boolean} fullscreen
*/
if (me.config.fullscreen) {
me.fireEvent('fullscreen', me);
}
me.fireEvent('initialize', me);
},
beforeInitConfig: function(config) {
this.beforeInitialize.apply(this, arguments);
},
/**
* @private
*/
beforeInitialize: Ext.emptyFn,
/**
* Allows addition of behavior to the rendering phase.
* @protected
* @template
*/
initialize: Ext.emptyFn,
getTemplate: function() {
return this.template;
},
/**
* @private
* @return {Object}
* @return {String} return.reference
* @return {Array} return.classList
* @return {Object} return.children
*/
getElementConfig: function() {
return {
reference: 'element',
classList: ['x-unsized'],
children: this.getTemplate()
};
},
/**
* @private
*/
triggerInitialized: function() {
var listeners = this.onInitializedListeners,
ln = listeners.length,
listener, fn, scope, args, i;
if (!this.initialized) {
this.initialized = true;
if (ln > 0) {
for (i = 0; i < ln; i++) {
listener = listeners[i];
fn = listener.fn;
scope = listener.scope;
args = listener.args;
if (typeof fn == 'string') {
scope[fn].apply(scope, args);
}
else {
fn.apply(scope, args);
}
}
listeners.length = 0;
}
}
},
/**
* @private
* @param fn
* @param scope
*/
onInitialized: function(fn, scope, args) {
var listeners = this.onInitializedListeners;
if (!scope) {
scope = this;
}
if (this.initialized) {
if (typeof fn == 'string') {
scope[fn].apply(scope, args);
}
else {
fn.apply(scope, args);
}
}
else {
listeners.push({
fn: fn,
scope: scope,
args: args
});
}
},
renderTo: function(container, insertBeforeElement) {
var dom = this.renderElement.dom,
containerDom = Ext.getDom(container),
insertBeforeChildDom = Ext.getDom(insertBeforeElement);
if (containerDom) {
if (insertBeforeChildDom) {
containerDom.insertBefore(dom, insertBeforeChildDom);
}
else {
containerDom.appendChild(dom);
}
this.setRendered(Boolean(dom.offsetParent));
}
},
/**
* @private
* @chainable
*/
setParent: function(parent) {
var currentParent = this.parent;
if (parent && currentParent && currentParent !== parent) {
currentParent.remove(this, false);
}
this.parent = parent;
return this;
},
applyPlugins: function(config) {
var ln, i, configObj;
if (!config) {
return config;
}
config = [].concat(config);
for (i = 0, ln = config.length; i < ln; i++) {
configObj = config[i];
config[i] = Ext.factory(configObj, 'Ext.plugin.Plugin', null, 'plugin');
}
return config;
},
updatePlugins: function(newPlugins, oldPlugins) {
var ln, i;
if (newPlugins) {
for (i = 0, ln = newPlugins.length; i < ln; i++) {
newPlugins[i].init(this);
}
}
if (oldPlugins) {
for (i = 0, ln = oldPlugins.length; i < ln; i++) {
Ext.destroy(oldPlugins[i]);
}
}
},
updateRenderTo: function(newContainer) {
this.renderTo(newContainer);
},
updateStyle: function(style) {
this.element.applyStyles(style);
},
updateBorder: function(border) {
this.element.setBorder(border);
},
updatePadding: function(padding) {
this.innerElement.setPadding(padding);
},
updateMargin: function(margin) {
this.element.setMargin(margin);
},
updateUi: function(newUi, oldUi) {
var baseCls = this.getBaseCls();
if (baseCls) {
if (oldUi) {
this.element.removeCls(oldUi, baseCls);
}
if (newUi) {
this.element.addCls(newUi, baseCls);
}
}
},
applyBaseCls: function(baseCls) {
return baseCls || clsPrefix + this.xtype;
},
updateBaseCls: function(newBaseCls, oldBaseCls) {
var me = this,
ui = me.getUi();
if (newBaseCls) {
this.element.addCls(newBaseCls);
if (ui) {
this.element.addCls(newBaseCls, null, ui);
}
}
if (oldBaseCls) {
this.element.removeCls(oldBaseCls);
if (ui) {
this.element.removeCls(oldBaseCls, null, ui);
}
}
},
/**
* Adds a CSS class (or classes) to this Component's rendered element.
* @param {String} cls The CSS class to add.
* @param {String} [prefix=""] Optional prefix to add to each class.
* @param {String} [suffix=""] Optional suffix to add to each class.
*/
addCls: function(cls, prefix, suffix) {
var oldCls = this.getCls(),
newCls = (oldCls) ? oldCls.slice() : [],
ln, i, cachedCls;
prefix = prefix || '';
suffix = suffix || '';
if (typeof cls == "string") {
cls = [cls];
}
ln = cls.length;
//check if there is currently nothing in the array and we don't need to add a prefix or a suffix.
//if true, we can just set the newCls value to the cls property, because that is what the value will be
//if false, we need to loop through each and add them to the newCls array
if (!newCls.length && prefix === '' && suffix === '') {
newCls = cls;
} else {
for (i = 0; i < ln; i++) {
cachedCls = prefix + cls[i] + suffix;
if (newCls.indexOf(cachedCls) == -1) {
newCls.push(cachedCls);
}
}
}
this.setCls(newCls);
},
/**
* Removes the given CSS class(es) from this Component's rendered element.
* @param {String} cls The class(es) to remove.
* @param {String} [prefix=""] Optional prefix to prepend before each class.
* @param {String} [suffix=""] Optional suffix to append to each class.
*/
removeCls: function(cls, prefix, suffix) {
var oldCls = this.getCls(),
newCls = (oldCls) ? oldCls.slice() : [],
ln, i;
prefix = prefix || '';
suffix = suffix || '';
if (typeof cls == "string") {
newCls = Ext.Array.remove(newCls, prefix + cls + suffix);
} else {
ln = cls.length;
for (i = 0; i < ln; i++) {
newCls = Ext.Array.remove(newCls, prefix + cls[i] + suffix);
}
}
this.setCls(newCls);
},
/**
* Replaces specified classes with the newly specified classes.
* It uses the {@link #addCls} and {@link #removeCls} methods, so if the class(es) you are removing don't exist, it will
* still add the new classes.
* @param {String} oldCls The class(es) to remove.
* @param {String} newCls The class(es) to add.
* @param {String} [prefix=""] Optional prefix to prepend before each class.
* @param {String} [suffix=""] Optional suffix to append to each class.
*/
replaceCls: function(oldCls, newCls, prefix, suffix) {
// We could have just called {@link #removeCls} and {@link #addCls}, but that would mean {@link #updateCls}
// would get called twice, which would have performance implications because it will update the dom.
var cls = this.getCls(),
array = (cls) ? cls.slice() : [],
ln, i, cachedCls;
prefix = prefix || '';
suffix = suffix || '';
//remove all oldCls
if (typeof oldCls == "string") {
array = Ext.Array.remove(array, prefix + oldCls + suffix);
} else if (oldCls) {
ln = oldCls.length;
for (i = 0; i < ln; i++) {
array = Ext.Array.remove(array, prefix + oldCls[i] + suffix);
}
}
//add all newCls
if (typeof newCls == "string") {
array.push(prefix + newCls + suffix);
} else if (newCls) {
ln = newCls.length;
//check if there is currently nothing in the array and we don't need to add a prefix or a suffix.
//if true, we can just set the array value to the newCls property, because that is what the value will be
//if false, we need to loop through each and add them to the array
if (!array.length && prefix === '' && suffix === '') {
array = newCls;
} else {
for (i = 0; i < ln; i++) {
cachedCls = prefix + newCls[i] + suffix;
if (array.indexOf(cachedCls) == -1) {
array.push(cachedCls);
}
}
}
}
this.setCls(array);
},
/**
* @private
* @chainable
*/
toggleCls: function(className, force) {
this.element.toggleCls(className, force);
return this;
},
/**
* @private
* Checks if the `cls` is a string. If it is, changed it into an array.
* @param {String/Array} cls
* @return {Array/null}
*/
applyCls: function(cls) {
if (typeof cls == "string") {
cls = [cls];
}
//reset it back to null if there is nothing.
if (!cls || !cls.length) {
cls = null;
}
return cls;
},
/**
* @private
* All cls methods directly report to the {@link #cls} configuration, so anytime it changes, {@link #updateCls} will be called
*/
updateCls: function(newCls, oldCls) {
if (oldCls != newCls && this.element) {
this.element.replaceCls(oldCls, newCls);
}
},
/**
* Updates the {@link #styleHtmlCls} configuration
*/
updateStyleHtmlCls: function(newHtmlCls, oldHtmlCls) {
var innerHtmlElement = this.innerHtmlElement,
innerElement = this.innerElement;
if (this.getStyleHtmlContent() && oldHtmlCls) {
if (innerHtmlElement) {
innerHtmlElement.replaceCls(oldHtmlCls, newHtmlCls);
} else {
innerElement.replaceCls(oldHtmlCls, newHtmlCls);
}
}
},
applyStyleHtmlContent: function(config) {
return Boolean(config);
},
updateStyleHtmlContent: function(styleHtmlContent) {
var htmlCls = this.getStyleHtmlCls(),
innerElement = this.innerElement,
innerHtmlElement = this.innerHtmlElement;
if (styleHtmlContent) {
if (innerHtmlElement) {
innerHtmlElement.addCls(htmlCls);
} else {
innerElement.addCls(htmlCls);
}
} else {
if (innerHtmlElement) {
innerHtmlElement.removeCls(htmlCls);
} else {
innerElement.addCls(htmlCls);
}
}
},
applyContentEl: function(contentEl) {
if (contentEl) {
return Ext.get(contentEl);
}
},
updateContentEl: function(newContentEl, oldContentEl) {
if (oldContentEl) {
oldContentEl.hide();
Ext.getBody().append(oldContentEl);
}
if (newContentEl) {
this.setHtml(newContentEl.dom);
newContentEl.show();
}
},
/**
* Returns the height and width of the Component.
* @return {Object} The current `height` and `width` of the Component.
* @return {Number} return.width
* @return {Number} return.height
*/
getSize: function() {
return {
width: this.getWidth(),
height: this.getHeight()
};
},
/**
* @private
* @return {Boolean}
*/
isCentered: function() {
return Boolean(this.getCentered());
},
isFloating: function() {
return this.floating;
},
isDocked: function() {
return Boolean(this.getDocked());
},
isInnerItem: function() {
return this.isInner;
},
setIsInner: function(isInner) {
if (isInner !== this.isInner) {
this.isInner = isInner;
if (this.initialized) {
this.fireEvent('innerstatechange', this, isInner);
}
}
},
filterPositionValue: function(value) {
if (value === '' || value === 'auto') {
value = null;
}
return value;
},
filterLengthValue: function(value) {
if (value === 'auto' || (!value && value !== 0)) {
return null;
}
return value;
},
applyTop: function(top) {
return this.filterPositionValue(top);
},
applyRight: function(right) {
return this.filterPositionValue(right);
},
applyBottom: function(bottom) {
return this.filterPositionValue(bottom);
},
applyLeft: function(left) {
return this.filterPositionValue(left);
},
applyWidth: function(width) {
return this.filterLengthValue(width);
},
applyHeight: function(height) {
return this.filterLengthValue(height);
},
applyMinWidth: function(width) {
return this.filterLengthValue(width);
},
applyMinHeight: function(height) {
return this.filterLengthValue(height);
},
applyMaxWidth: function(width) {
return this.filterLengthValue(width);
},
applyMaxHeight: function(height) {
return this.filterLengthValue(height);
},
doSetTop: function(top) {
this.element.setTop(top);
this.refreshFloating();
},
doSetRight: function(right) {
this.element.setRight(right);
this.refreshFloating();
},
doSetBottom: function(bottom) {
this.element.setBottom(bottom);
this.refreshFloating();
},
doSetLeft: function(left) {
this.element.setLeft(left);
this.refreshFloating();
},
doSetWidth: function(width) {
this.element.setWidth(width);
this.refreshSizeState();
},
doSetHeight: function(height) {
this.element.setHeight(height);
this.refreshSizeState();
},
applyFlex: function(flex) {
if (flex) {
flex = Number(flex);
if (isNaN(flex)) {
flex = null;
}
}
else {
flex = null
}
return flex;
},
doSetFlex: Ext.emptyFn,
refreshSizeState: function() {
this.refreshSizeStateOnInitialized = true;
},
doRefreshSizeState: function() {
var hasWidth = this.getWidth() !== null || this.widthLayoutSized || (this.getLeft() !== null && this.getRight() !== null),
hasHeight = this.getHeight() !== null || this.heightLayoutSized || (this.getTop() !== null && this.getBottom() !== null),
stretched = this.layoutStretched || (!hasHeight && this.getMinHeight() !== null),
state = hasWidth && hasHeight,
flags = (hasWidth && this.LAYOUT_WIDTH) | (hasHeight && this.LAYOUT_HEIGHT) | (stretched && this.LAYOUT_STRETCHED);
if (!state && stretched) {
state = null;
}
this.setSizeState(state);
this.setSizeFlags(flags);
},
setLayoutSizeFlags: function(flags) {
this.layoutStretched = !!(flags & this.LAYOUT_STRETCHED);
this.widthLayoutSized = !!(flags & this.LAYOUT_WIDTH);
this.heightLayoutSized = !!(flags & this.LAYOUT_HEIGHT);
this.refreshSizeState();
},
setSizeFlags: function(flags) {
if (flags !== this.sizeFlags) {
this.sizeFlags = flags;
if (this.initialized) {
this.fireEvent('sizeflagschange', this, flags);
}
}
},
getSizeFlags: function() {
if (!this.initialized) {
this.doRefreshSizeState();
}
return this.sizeFlags;
},
setSizeState: function(state) {
if (state !== this.sizeState) {
this.sizeState = state;
this.element.setSizeState(state);
if (this.initialized) {
this.fireEvent('sizestatechange', this, state);
}
}
},
getSizeState: function() {
if (!this.initialized) {
this.doRefreshSizeState();
}
return this.sizeState;
},
doSetMinWidth: function(width) {
this.element.setMinWidth(width);
},
doSetMinHeight: function(height) {
this.element.setMinHeight(height);
this.refreshSizeState();
},
doSetMaxWidth: function(width) {
this.element.setMaxWidth(width);
},
doSetMaxHeight: function(height) {
this.element.setMaxHeight(height);
},
/**
* @private
* @param {Boolean} centered
* @return {Boolean}
*/
applyCentered: function(centered) {
centered = Boolean(centered);
if (centered) {
this.refreshInnerState = Ext.emptyFn;
if (this.isFloating()) {
this.resetFloating();
}
if (this.isDocked()) {
this.setDocked(false);
}
this.setIsInner(false);
delete this.refreshInnerState;
}
return centered;
},
doSetCentered: function(centered) {
this.toggleCls(this.getFloatingCls(), centered);
if (!centered) {
this.refreshInnerState();
}
},
applyDocked: function(docked) {
if (!docked) {
return null;
}
//<debug error>
if (!/^(top|right|bottom|left)$/.test(docked)) {
Ext.Logger.error("Invalid docking position of '" + docked.position + "', must be either 'top', 'right', 'bottom', " +
"'left' or `null` (for no docking)", this);
return;
}
//</debug>
this.refreshInnerState = Ext.emptyFn;
if (this.isFloating()) {
this.resetFloating();
}
if (this.isCentered()) {
this.setCentered(false);
}
this.setIsInner(false);
delete this.refreshInnerState;
return docked;
},
doSetDocked: function(docked) {
if (!docked) {
this.refreshInnerState();
}
},
/**
* Resets {@link #top}, {@link #right}, {@link #bottom} and {@link #left} configurations to `null`, which
* will un-float this component.
*/
resetFloating: function() {
this.setTop(null);
this.setRight(null);
this.setBottom(null);
this.setLeft(null);
},
refreshInnerState: function() {
this.setIsInner(!this.isCentered() && !this.isFloating() && !this.isDocked());
},
refreshFloating: function() {
this.refreshFloatingOnInitialized = true;
},
doRefreshFloating: function() {
var floating = true,
floatingCls = this.getFloatingCls();
if (this.getTop() === null && this.getBottom() === null &&
this.getRight() === null && this.getLeft() === null) {
floating = false;
}
else {
this.refreshSizeState();
}
if (floating !== this.floating) {
this.floating = floating;
this.element.toggleCls(floatingCls, floating);
if (floating) {
this.refreshInnerState = Ext.emptyFn;
if (this.isCentered()) {
this.setCentered(false);
}
if (this.isDocked()) {
this.setDocked(false);
}
this.setIsInner(false);
delete this.refreshInnerState;
}
if (this.initialized) {
this.fireEvent('floatingchange', this, floating);
}
if (!floating) {
this.refreshInnerState();
}
}
},
/**
* Updates the floatingCls if the component is currently floating
* @private
*/
updateFloatingCls: function(newFloatingCls, oldFloatingCls) {
if (this.isFloating()) {
this.replaceCls(oldFloatingCls, newFloatingCls);
}
},
applyDisabled: function(disabled) {
return Boolean(disabled);
},
doSetDisabled: function(disabled) {
this.element[disabled ? 'addCls' : 'removeCls'](this.getDisabledCls());
},
updateDisabledCls: function(newDisabledCls, oldDisabledCls) {
if (this.isDisabled()) {
this.element.replaceCls(oldDisabledCls, newDisabledCls);
}
},
/**
* Disables this Component
*/
disable: function() {
this.setDisabled(true);
},
/**
* Enables this Component
*/
enable: function() {
this.setDisabled(false);
},
/**
* Returns `true` if this Component is currently disabled.
* @return {Boolean} `true` if currently disabled.
*/
isDisabled: function() {
return this.getDisabled();
},
applyZIndex: function(zIndex) {
if (!zIndex && zIndex !== 0) {
zIndex = null;
}
if (zIndex !== null) {
zIndex = Number(zIndex);
if (isNaN(zIndex)) {
zIndex = null;
}
}
return zIndex;
},
updateZIndex: function(zIndex) {
var element = this.element,
domStyle;
if (element && !element.isDestroyed) {
domStyle = element.dom.style;
if (zIndex !== null) {
domStyle.setProperty('z-index', zIndex, 'important');
}
else {
domStyle.removeProperty('z-index');
}
}
},
getInnerHtmlElement: function() {
var innerHtmlElement = this.innerHtmlElement,
styleHtmlCls = this.getStyleHtmlCls();
if (!innerHtmlElement || !innerHtmlElement.dom || !innerHtmlElement.dom.parentNode) {
this.innerHtmlElement = innerHtmlElement = this.innerElement.createChild({ cls: 'x-innerhtml ' });
if (this.getStyleHtmlContent()) {
this.innerHtmlElement.addCls(styleHtmlCls);
this.innerElement.removeCls(styleHtmlCls);
}
}
return innerHtmlElement;
},
updateHtml: function(html) {
var innerHtmlElement = this.getInnerHtmlElement();
if (Ext.isElement(html)){
innerHtmlElement.setHtml('');
innerHtmlElement.append(html);
}
else {
innerHtmlElement.setHtml(html);
}
},
applyHidden: function(hidden) {
return Boolean(hidden);
},
doSetHidden: function(hidden) {
var element = this.renderElement;
if (element.isDestroyed) {
return;
}
if (hidden) {
element.hide();
}
else {
element.show();
}
if (this.element) {
this.element[hidden ? 'addCls' : 'removeCls'](this.getHiddenCls());
}
this.fireEvent(hidden ? 'hide' : 'show', this);
},
updateHiddenCls: function(newHiddenCls, oldHiddenCls) {
if (this.isHidden()) {
this.element.replaceCls(oldHiddenCls, newHiddenCls);
}
},
/**
* Returns `true` if this Component is currently hidden.
* @return {Boolean} `true` if currently hidden.
*/
isHidden: function() {
return this.getHidden();
},
/**
* Hides this Component
* @param {Object/Boolean} animation (optional)
* @return {Ext.Component}
* @chainable
*/
hide: function(animation) {
if (!this.getHidden()) {
if (animation === undefined || (animation && animation.isComponent)) {
animation = this.getHideAnimation();
}
if (animation) {
if (animation === true) {
animation = 'fadeOut';
}
this.onBefore({
hiddenchange: 'animateFn',
scope: this,
single: true,
args: [animation]
});
}
this.setHidden(true);
}
return this;
},
/**
* Shows this component.
* @param {Object/Boolean} animation (optional)
* @return {Ext.Component}
* @chainable
*/
show: function(animation) {
var hidden = this.getHidden();
if (hidden || hidden === null) {
if (animation === true) {
animation = 'fadeIn';
}
else if (animation === undefined || (animation && animation.isComponent)) {
animation = this.getShowAnimation();
}
if (animation) {
this.onBefore({
hiddenchange: 'animateFn',
scope: this,
single: true,
args: [animation]
});
}
this.setHidden(false);
}
return this;
},
animateFn: function(animation, component, newState, oldState, options, controller) {
if (animation && (!newState || (newState && this.isPainted()))) {
var anim = new Ext.fx.Animation(animation);
anim.setElement(component.element);
if (newState) {
anim.setOnEnd(function() {
controller.resume();
});
controller.pause();
}
Ext.Animator.run(anim);
}
},
/**
* @private
*/
setVisibility: function(isVisible) {
this.renderElement.setVisibility(isVisible);
},
/**
* @private
*/
isRendered: function() {
return this.rendered;
},
/**
* @private
*/
isPainted: function() {
return this.renderElement.isPainted();
},
/**
* @private
*/
applyTpl: function(config) {
return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
},
applyData: function(data) {
if (Ext.isObject(data)) {
return Ext.apply({}, data);
} else if (!data) {
data = {};
}
return data;
},
/**
* @private
*/
updateData: function(newData) {
var me = this;
if (newData) {
var tpl = me.getTpl(),
tplWriteMode = me.getTplWriteMode();
if (tpl) {
tpl[tplWriteMode](me.getInnerHtmlElement(), newData);
}
/**
* @event updatedata
* Fires whenever the data of the component is updated
* @param {Ext.Component} this The component instance
* @param {Object} newData The new data
*/
this.fireEvent('updatedata', me, newData);
}
},
applyRecord: function(config) {
if (config && Ext.isObject(config) && config.isModel) {
return config;
}
return null;
},
updateRecord: function(newRecord, oldRecord) {
var me = this;
if (oldRecord) {
oldRecord.unjoin(me);
}
if (!newRecord) {
me.updateData('');
}
else {
newRecord.join(me);
me.updateData(newRecord.getData(true));
}
},
// @private Used to handle joining of a record to a tpl
afterEdit: function() {
this.updateRecord(this.getRecord());
},
// @private Used to handle joining of a record to a tpl
afterErase: function() {
this.setRecord(null);
},
applyItemId: function(itemId) {
return itemId || this.getId();
},
/**
* Tests whether or not this Component is of a specific xtype. This can test whether this Component is descended
* from the xtype (default) or whether it is directly of the xtype specified (`shallow = true`).
* __If using your own subclasses, be aware that a Component must register its own xtype
* to participate in determination of inherited xtypes.__
*
* For a list of all available xtypes, see the {@link Ext.Component} header.
*
* Example usage:
*
* var t = new Ext.field.Text();
* var isText = t.isXType('textfield'); // true
* var isBoxSubclass = t.isXType('field'); // true, descended from Ext.field.Field
* var isBoxInstance = t.isXType('field', true); // false, not a direct Ext.field.Field instance
*
* @param {String} xtype The xtype to check for this Component.
* @param {Boolean} shallow (optional) `false` to check whether this Component is descended from the xtype (this is
* the default), or `true` to check whether this Component is directly of the specified xtype.
* @return {Boolean} `true` if this component descends from the specified xtype, `false` otherwise.
*/
isXType: function(xtype, shallow) {
if (shallow) {
return this.xtypes.indexOf(xtype) != -1;
}
return Boolean(this.xtypesMap[xtype]);
},
/**
* Returns this Component's xtype hierarchy as a slash-delimited string. For a list of all
* available xtypes, see the {@link Ext.Component} header.
*
* __Note:__ If using your own subclasses, be aware that a Component must register its own xtype
* to participate in determination of inherited xtypes.
*
* Example usage:
*
* var t = new Ext.field.Text();
* alert(t.getXTypes()); // alerts 'component/field/textfield'
*
* @return {String} The xtype hierarchy string.
*/
getXTypes: function() {
return this.xtypesChain.join('/');
},
getDraggableBehavior: function() {
var behavior = this.draggableBehavior;
if (!behavior) {
behavior = this.draggableBehavior = new Ext.behavior.Draggable(this);
}
return behavior;
},
applyDraggable: function(config) {
this.getDraggableBehavior().setConfig(config);
},
getDraggable: function() {
return this.getDraggableBehavior().getDraggable();
},
getTranslatableBehavior: function() {
var behavior = this.translatableBehavior;
if (!behavior) {
behavior = this.translatableBehavior = new Ext.behavior.Translatable(this);
}
return behavior;
},
applyTranslatable: function(config) {
this.getTranslatableBehavior().setConfig(config);
},
getTranslatable: function() {
return this.getTranslatableBehavior().getTranslatable();
},
translateAxis: function(axis, value, animation) {
var x, y;
if (axis === 'x') {
x = value;
}
else {
y = value;
}
return this.translate(x, y, animation);
},
translate: function() {
var translatable = this.getTranslatable();
if (!translatable) {
this.setTranslatable(true);
translatable = this.getTranslatable();
}
translatable.translate.apply(translatable, arguments);
},
/**
* @private
* @param rendered
*/
setRendered: function(rendered) {
var wasRendered = this.rendered;
if (rendered !== wasRendered) {
this.rendered = rendered;
return true;
}
return false;
},
/**
* Sets the size of the Component.
* @param {Number} width The new width for the Component.
* @param {Number} height The new height for the Component.
*/
setSize: function(width, height) {
if (width != undefined) {
this.setWidth(width);
}
if (height != undefined) {
this.setHeight(height);
}
},
//@private
doAddListener: function(name, fn, scope, options, order) {
if (options && 'element' in options) {
//<debug error>
if (this.referenceList.indexOf(options.element) === -1) {
Ext.Logger.error("Adding event listener with an invalid element reference of '" + options.element +
"' for this component. Available values are: '" + this.referenceList.join("', '") + "'", this);
}
//</debug>
// The default scope is this component
return this[options.element].doAddListener(name, fn, scope || this, options, order);
}
if (name == 'painted' || name == 'resize') {
return this.element.doAddListener(name, fn, scope || this, options, order);
}
return this.callParent(arguments);
},
//@private
doRemoveListener: function(name, fn, scope, options, order) {
if (options && 'element' in options) {
//<debug error>
if (this.referenceList.indexOf(options.element) === -1) {
Ext.Logger.error("Removing event listener with an invalid element reference of '" + options.element +
"' for this component. Available values are: '" + this.referenceList.join('", "') + "'", this);
}
//</debug>
// The default scope is this component
this[options.element].doRemoveListener(name, fn, scope || this, options, order);
}
return this.callParent(arguments);
},
/**
* Shows this component by another component. If you specify no alignment, it will automatically
* position this component relative to the reference component.
*
* For example, say we are aligning a Panel next to a Button, the alignment string would look like this:
*
* [panel-vertical (t/b/c)][panel-horizontal (l/r/c)]-[button-vertical (t/b/c)][button-horizontal (l/r/c)]
*
* where t = top, b = bottom, c = center, l = left, r = right.
*
* ## Examples
*
* - `tl-tr` means top-left corner of the Panel to the top-right corner of the Button
* - `tc-bc` means top-center of the Panel to the bottom-center of the Button
*
* You can put a '?' at the end of the alignment string to constrain the floating element to the
* {@link Ext.Viewport Viewport}
*
* // show `panel` by `button` using the default positioning (auto fit)
* panel.showBy(button);
*
* // align the top left corner of `panel` with the top right corner of `button` (constrained to viewport)
* panel.showBy(button, "tl-tr?");
*
* // align the bottom right corner of `panel` with the center left edge of `button` (not constrained by viewport)
* panel.showBy(button, "br-cl");
*
* @param {Ext.Component} component The target component to show this component by.
* @param {String} alignment (optional) The specific alignment.
*/
showBy: function(component, alignment) {
var me = this,
viewport = Ext.Viewport,
parent = me.getParent();
me.setVisibility(false);
if (parent !== viewport) {
viewport.add(me);
}
me.show();
me.on({
hide: 'onShowByErased',
destroy: 'onShowByErased',
single: true,
scope: me
});
viewport.on('resize', 'alignTo', me, { args: [component, alignment] });
me.alignTo(component, alignment);
me.setVisibility(true);
},
/**
* @private
* @param component
*/
onShowByErased: function() {
Ext.Viewport.un('resize', 'alignTo', this);
},
/**
* @private
*/
alignTo: function(component, alignment) {
var alignToElement = component.isComponent ? component.renderElement : component,
element = this.renderElement,
alignToBox = alignToElement.getPageBox(),
constrainBox = this.getParent().element.getPageBox(),
box = element.getPageBox(),
alignToHeight = alignToBox.height,
alignToWidth = alignToBox.width,
height = box.height,
width = box.width;
// Keep off the sides...
constrainBox.bottom -= 5;
constrainBox.height -= 10;
constrainBox.left += 5;
constrainBox.right -= 5;
constrainBox.top += 5;
constrainBox.width -= 10;
if (!alignment || alignment === 'auto') {
if (constrainBox.bottom - alignToBox.bottom < height) {
if (alignToBox.top - constrainBox.top < height) {
if (alignToBox.left - constrainBox.left < width) {
alignment = 'cl-cr?';
}
else {
alignment = 'cr-cl?';
}
}
else {
alignment = 'bc-tc?';
}
}
else {
alignment = 'tc-bc?';
}
}
var matches = alignment.match(this.alignmentRegex);
//<debug error>
if (!matches) {
Ext.Logger.error("Invalid alignment value of '" + alignment + "'");
}
//</debug>
var from = matches[1].split(''),
to = matches[2].split(''),
constrained = (matches[3] === '?'),
fromVertical = from[0],
fromHorizontal = from[1] || fromVertical,
toVertical = to[0],
toHorizontal = to[1] || toVertical,
top = alignToBox.top,
left = alignToBox.left,
halfAlignHeight = alignToHeight / 2,
halfAlignWidth = alignToWidth / 2,
halfWidth = width / 2,
halfHeight = height / 2,
maxLeft, maxTop;
switch (fromVertical) {
case 't':
switch (toVertical) {
case 'c':
top += halfAlignHeight;
break;
case 'b':
top += alignToHeight;
}
break;
case 'b':
switch (toVertical) {
case 'c':
top -= (height - halfAlignHeight);
break;
case 't':
top -= height;
break;
case 'b':
top -= height - alignToHeight;
}
break;
case 'c':
switch (toVertical) {
case 't':
top -= halfHeight;
break;
case 'c':
top -= (halfHeight - halfAlignHeight);
break;
case 'b':
top -= (halfHeight - alignToHeight);
}
break;
}
switch (fromHorizontal) {
case 'l':
switch (toHorizontal) {
case 'c':
left += halfAlignHeight;
break;
case 'r':
left += alignToWidth;
}
break;
case 'r':
switch (toHorizontal) {
case 'r':
left -= (width - alignToWidth);
break;
case 'c':
left -= (width - halfWidth);
break;
case 'l':
left -= width;
}
break;
case 'c':
switch (toHorizontal) {
case 'l':
left -= halfWidth;
break;
case 'c':
left -= (halfWidth - halfAlignWidth);
break;
case 'r':
left -= (halfWidth - alignToWidth);
}
break;
}
if (constrained) {
maxLeft = (constrainBox.left + constrainBox.width) - width;
maxTop = (constrainBox.top + constrainBox.height) - height;
left = Math.max(constrainBox.left, Math.min(maxLeft, left));
top = Math.max(constrainBox.top, Math.min(maxTop, top));
}
this.setLeft(left);
this.setTop(top);
},
/**
* Walks up the `ownerCt` axis looking for an ancestor Container which matches
* the passed simple selector.
*
* Example:
*
* var owningTabPanel = grid.up('tabpanel');
*
* @param {String} selector (optional) The simple selector to test.
* @return {Ext.Container} The matching ancestor Container (or `undefined` if no match was found).
*/
up: function(selector) {
var result = this.parent;
if (selector) {
for (; result; result = result.parent) {
if (Ext.ComponentQuery.is(result, selector)) {
return result;
}
}
}
return result;
},
getBubbleTarget: function() {
return this.getParent();
},
/**
* Destroys this Component. If it is currently added to a Container it will first be removed from that Container.
* All Ext.Element references are also deleted and the Component is de-registered from Ext.ComponentManager
*/
destroy: function() {
this.destroy = Ext.emptyFn;
var parent = this.getParent(),
referenceList = this.referenceList,
i, ln, reference;
this.isDestroying = true;
Ext.destroy(this.getTranslatable(), this.getPlugins());
// Remove this component itself from the container if it's currently contained
if (parent) {
parent.remove(this, false);
}
// Destroy all element references
for (i = 0, ln = referenceList.length; i < ln; i++) {
reference = referenceList[i];
this[reference].destroy();
delete this[reference];
}
Ext.destroy(this.innerHtmlElement);
this.setRecord(null);
this.callSuper();
Ext.ComponentManager.unregister(this);
}
// Convert old properties in data into a config object
}, function() {
});
})(Ext.baseCSSPrefix);
/**
*
*/
Ext.define('Ext.layout.wrapper.Inner', {
config: {
sizeState: null,
container: null
},
constructor: function(config) {
this.initConfig(config);
},
getElement: function() {
return this.getContainer().bodyElement;
},
setInnerWrapper: Ext.emptyFn,
getInnerWrapper: Ext.emptyFn
});
/**
*
*/
Ext.define('Ext.layout.Abstract', {
mixins: ['Ext.mixin.Observable'],
isLayout: true,
constructor: function(config) {
this.initialConfig = config;
},
setContainer: function(container) {
this.container = container;
this.initConfig(this.initialConfig);
return this;
},
onItemAdd: function() {},
onItemRemove: function() {},
onItemMove: function() {},
onItemCenteredChange: function() {},
onItemFloatingChange: function() {},
onItemDockedChange: function() {},
onItemInnerStateChange: function() {}
});
/**
*
*/
Ext.define('Ext.mixin.Bindable', {
extend: 'Ext.mixin.Mixin',
mixinConfig: {
id: 'bindable'
},
bind: function(instance, boundMethod, bindingMethod, preventDefault) {
if (!bindingMethod) {
bindingMethod = boundMethod;
}
var boundFn = instance[boundMethod],
fn;
instance[boundMethod] = fn = function() {
var binding = fn.$binding,
scope = binding.bindingScope,
args = Array.prototype.slice.call(arguments);
args.push(arguments);
if (!binding.preventDefault && scope[binding.bindingMethod].apply(scope, args) !== false) {
return binding.boundFn.apply(this, arguments);
}
};
fn.$binding = {
preventDefault: !!preventDefault,
boundFn: boundFn,
bindingMethod: bindingMethod,
bindingScope: this
};
return this;
},
unbind: function(instance, boundMethod, bindingMethod) {
if (!bindingMethod) {
bindingMethod = boundMethod;
}
var fn = instance[boundMethod],
binding = fn.$binding,
boundFn, currentBinding;
while (binding) {
boundFn = binding.boundFn;
if (binding.bindingMethod === bindingMethod && binding.bindingScope === this) {
if (currentBinding) {
currentBinding.boundFn = boundFn;
}
else {
instance[boundMethod] = boundFn;
}
return this;
}
currentBinding = binding;
binding = binding.boundFn;
}
return this;
}
});
/**
*
*/
Ext.define('Ext.util.Wrapper', {
mixins: ['Ext.mixin.Bindable'],
constructor: function(elementConfig, wrappedElement) {
var element = this.link('element', Ext.Element.create(elementConfig));
if (wrappedElement) {
element.insertBefore(wrappedElement);
this.wrap(wrappedElement);
}
},
bindSize: function(sizeName) {
var wrappedElement = this.wrappedElement,
boundMethodName;
this.boundSizeName = sizeName;
this.boundMethodName = boundMethodName = sizeName === 'width' ? 'setWidth' : 'setHeight';
this.bind(wrappedElement, boundMethodName, 'onBoundSizeChange');
wrappedElement[boundMethodName].call(wrappedElement, wrappedElement.getStyleValue(sizeName));
},
onBoundSizeChange: function(size, args) {
var element = this.element;
if (typeof size === 'string' && size.substr(-1) === '%') {
args[0] = '100%';
}
else {
size = '';
}
element[this.boundMethodName].call(element, size);
},
wrap: function(wrappedElement) {
var element = this.element,
innerDom;
this.wrappedElement = wrappedElement;
innerDom = element.dom;
while (innerDom.firstElementChild !== null) {
innerDom = innerDom.firstElementChild;
}
innerDom.appendChild(wrappedElement.dom);
},
destroy: function() {
var element = this.element,
dom = element.dom,
wrappedElement = this.wrappedElement,
boundMethodName = this.boundMethodName,
parentNode = dom.parentNode,
size;
if (boundMethodName) {
this.unbind(wrappedElement, boundMethodName, 'onBoundSizeChange');
size = element.getStyle(this.boundSizeName);
if (size) {
wrappedElement[boundMethodName].call(wrappedElement, size);
}
}
if (parentNode) {
if (!wrappedElement.isDestroyed) {
parentNode.replaceChild(dom.firstElementChild, dom);
}
delete this.wrappedElement;
}
this.callSuper();
}
});
/**
*
*/
Ext.define('Ext.layout.wrapper.BoxDock', {
config: {
direction: 'horizontal',
element: {
className: 'x-dock'
},
bodyElement: {
className: 'x-dock-body'
},
innerWrapper: null,
sizeState: false,
container: null
},
positionMap: {
top: 'start',
left: 'start',
bottom: 'end',
right: 'end'
},
constructor: function(config) {
this.items = {
start: [],
end: []
};
this.itemsCount = 0;
this.initConfig(config);
},
addItems: function(items) {
var i, ln, item;
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
this.addItem(item);
}
},
addItem: function(item) {
var docked = item.getDocked(),
position = this.positionMap[docked],
wrapper = item.$dockWrapper,
container = this.getContainer(),
index = container.indexOf(item),
element = item.element,
items = this.items,
sideItems = items[position],
i, ln, sibling, referenceElement, siblingIndex;
if (wrapper) {
wrapper.removeItem(item);
}
item.$dockWrapper = this;
item.addCls('x-dock-item');
item.addCls('x-docked-' + docked);
for (i = 0, ln = sideItems.length; i < ln; i++) {
sibling = sideItems[i];
siblingIndex = container.indexOf(sibling);
if (siblingIndex > index) {
referenceElement = sibling.element;
sideItems.splice(i, 0, item);
break;
}
}
if (!referenceElement) {
sideItems.push(item);
referenceElement = this.getBodyElement();
}
this.itemsCount++;
if (position === 'start') {
element.insertBefore(referenceElement);
}
else {
element.insertAfter(referenceElement);
}
},
removeItem: function(item) {
var position = item.getDocked(),
items = this.items[this.positionMap[position]];
Ext.Array.remove(items, item);
item.element.detach();
delete item.$dockWrapper;
item.removeCls('x-dock-item');
item.removeCls('x-docked-' + position);
if (--this.itemsCount === 0) {
this.destroy();
}
},
getItemsSlice: function(index) {
var container = this.getContainer(),
items = this.items,
slice = [],
sideItems, i, ln, item;
for (sideItems = items.start, i = 0, ln = sideItems.length; i < ln; i++) {
item = sideItems[i];
if (container.indexOf(item) > index) {
slice.push(item);
}
}
for (sideItems = items.end, i = 0, ln = sideItems.length; i < ln; i++) {
item = sideItems[i];
if (container.indexOf(item) > index) {
slice.push(item);
}
}
return slice;
},
applyElement: function(element) {
return Ext.Element.create(element);
},
updateElement: function(element) {
element.addCls('x-dock-' + this.getDirection());
},
applyBodyElement: function(bodyElement) {
return Ext.Element.create(bodyElement);
},
updateBodyElement: function(bodyElement) {
this.getElement().append(bodyElement);
},
updateInnerWrapper: function(innerWrapper, oldInnerWrapper) {
var bodyElement = this.getBodyElement();
if (oldInnerWrapper && oldInnerWrapper.$outerWrapper === this) {
oldInnerWrapper.getElement().detach();
delete oldInnerWrapper.$outerWrapper;
}
if (innerWrapper) {
innerWrapper.setSizeState(this.getSizeState());
innerWrapper.$outerWrapper = this;
bodyElement.append(innerWrapper.getElement());
}
},
updateSizeState: function(state) {
var innerWrapper = this.getInnerWrapper();
this.getElement().setSizeState(state);
if (innerWrapper) {
innerWrapper.setSizeState(state);
}
},
destroy: function() {
var innerWrapper = this.getInnerWrapper(),
outerWrapper = this.$outerWrapper,
innerWrapperElement;
if (innerWrapper) {
if (outerWrapper) {
outerWrapper.setInnerWrapper(innerWrapper);
}
else {
innerWrapperElement = innerWrapper.getElement();
if (!innerWrapperElement.isDestroyed) {
innerWrapperElement.replace(this.getElement());
}
delete innerWrapper.$outerWrapper;
}
}
delete this.$outerWrapper;
this.setInnerWrapper(null);
this.unlink('_bodyElement', '_element');
this.callSuper();
}
});
/**
*
*/
Ext.define('Ext.layout.Default', {
extend: 'Ext.layout.Abstract',
isAuto: true,
alias: ['layout.default', 'layout.auto'],
requires: [
'Ext.util.Wrapper',
'Ext.layout.wrapper.BoxDock',
'Ext.layout.wrapper.Inner'
],
config: {
/**
* @cfg {Ext.fx.layout.Card} animation Layout animation configuration
* Controls how layout transitions are animated. Currently only available for
* Card Layouts.
*
* Possible values are:
*
* - cover
* - cube
* - fade
* - flip
* - pop
* - reveal
* - scroll
* - slide
* @accessor
*/
animation: null
},
centerWrapperClass: 'x-center',
dockWrapperClass: 'x-dock',
positionMap: {
top: 'start',
left: 'start',
middle: 'center',
bottom: 'end',
right: 'end'
},
positionDirectionMap: {
top: 'vertical',
bottom: 'vertical',
left: 'horizontal',
right: 'horizontal'
},
setContainer: function(container) {
var options = {
delegate: '> component'
};
this.dockedItems = [];
this.callSuper(arguments);
container.on('centeredchange', 'onItemCenteredChange', this, options, 'before')
.on('floatingchange', 'onItemFloatingChange', this, options, 'before')
.on('dockedchange', 'onBeforeItemDockedChange', this, options, 'before')
.on('dockedchange', 'onAfterItemDockedChange', this, options);
},
monitorSizeStateChange: function() {
this.monitorSizeStateChange = Ext.emptyFn;
this.container.on('sizestatechange', 'onContainerSizeStateChange', this);
},
monitorSizeFlagsChange: function() {
this.monitorSizeFlagsChange = Ext.emptyFn;
this.container.on('sizeflagschange', 'onContainerSizeFlagsChange', this);
},
onItemAdd: function(item) {
var docked = item.getDocked();
if (docked !== null) {
this.dockItem(item);
}
else if (item.isCentered()) {
this.onItemCenteredChange(item, true);
}
else if (item.isFloating()) {
this.onItemFloatingChange(item, true);
}
else {
this.onItemInnerStateChange(item, true);
}
},
/**
*
* @param item
* @param isInner
* @param [destroying]
*/
onItemInnerStateChange: function(item, isInner, destroying) {
if (isInner) {
this.insertInnerItem(item, this.container.innerIndexOf(item));
}
else {
this.removeInnerItem(item);
}
},
insertInnerItem: function(item, index) {
var container = this.container,
containerDom = container.innerElement.dom,
itemDom = item.element.dom,
nextSibling = container.getInnerAt(index + 1),
nextSiblingDom = nextSibling ? nextSibling.element.dom : null;
containerDom.insertBefore(itemDom, nextSiblingDom);
return this;
},
insertBodyItem: function(item) {
var container = this.container.setUseBodyElement(true),
bodyDom = container.bodyElement.dom;
if (item.getZIndex() === null) {
item.setZIndex((container.indexOf(item) + 1) * 2);
}
bodyDom.insertBefore(item.element.dom, bodyDom.firstChild);
return this;
},
removeInnerItem: function(item) {
item.element.detach();
},
removeBodyItem: function(item) {
item.setZIndex(null);
item.element.detach();
},
onItemRemove: function(item, index, destroying) {
var docked = item.getDocked();
if (docked) {
this.undockItem(item);
}
else if (item.isCentered()) {
this.onItemCenteredChange(item, false);
}
else if (item.isFloating()) {
this.onItemFloatingChange(item, false);
}
else {
this.onItemInnerStateChange(item, false, destroying);
}
},
onItemMove: function(item, toIndex, fromIndex) {
if (item.isCentered() || item.isFloating()) {
item.setZIndex((toIndex + 1) * 2);
}
else if (item.isInnerItem()) {
this.insertInnerItem(item, this.container.innerIndexOf(item));
}
else {
this.undockItem(item);
this.dockItem(item);
}
},
onItemCenteredChange: function(item, centered) {
var wrapperName = '$centerWrapper';
if (centered) {
this.insertBodyItem(item);
item.link(wrapperName, new Ext.util.Wrapper({
className: this.centerWrapperClass
}, item.element));
}
else {
item.unlink(wrapperName);
this.removeBodyItem(item);
}
},
onItemFloatingChange: function(item, floating) {
if (floating) {
this.insertBodyItem(item);
}
else {
this.removeBodyItem(item);
}
},
onBeforeItemDockedChange: function(item, docked, oldDocked) {
if (oldDocked) {
this.undockItem(item);
}
},
onAfterItemDockedChange: function(item, docked, oldDocked) {
if (docked) {
this.dockItem(item);
}
},
onContainerSizeStateChange: function() {
var dockWrapper = this.getDockWrapper();
if (dockWrapper) {
dockWrapper.setSizeState(this.container.getSizeState());
}
},
onContainerSizeFlagsChange: function() {
var items = this.dockedItems,
i, ln, item;
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
this.refreshDockedItemLayoutSizeFlags(item);
}
},
refreshDockedItemLayoutSizeFlags: function(item) {
var container = this.container,
dockedDirection = this.positionDirectionMap[item.getDocked()],
binaryMask = (dockedDirection === 'horizontal') ? container.LAYOUT_HEIGHT : container.LAYOUT_WIDTH,
flags = (container.getSizeFlags() & binaryMask);
item.setLayoutSizeFlags(flags);
},
dockItem: function(item) {
var DockClass = Ext.layout.wrapper.BoxDock,
dockedItems = this.dockedItems,
ln = dockedItems.length,
container = this.container,
itemIndex = container.indexOf(item),
positionDirectionMap = this.positionDirectionMap,
direction = positionDirectionMap[item.getDocked()],
dockInnerWrapper = this.dockInnerWrapper,
referenceDirection, i, dockedItem, index, previousItem, slice,
referenceItem, referenceDocked, referenceWrapper, newWrapper, nestedWrapper;
this.monitorSizeStateChange();
this.monitorSizeFlagsChange();
if (!dockInnerWrapper) {
dockInnerWrapper = this.link('dockInnerWrapper', new Ext.layout.wrapper.Inner({
container: this.container
}));
}
if (ln === 0) {
dockedItems.push(item);
newWrapper = new DockClass({
container: this.container,
direction: direction
});
newWrapper.addItem(item);
newWrapper.getElement().replace(dockInnerWrapper.getElement());
newWrapper.setInnerWrapper(dockInnerWrapper);
container.onInitialized('onContainerSizeStateChange', this);
}
else {
for (i = 0; i < ln; i++) {
dockedItem = dockedItems[i];
index = container.indexOf(dockedItem);
if (index > itemIndex) {
referenceItem = previousItem || dockedItems[0];
dockedItems.splice(i, 0, item);
break;
}
previousItem = dockedItem;
}
if (!referenceItem) {
referenceItem = dockedItems[ln - 1];
dockedItems.push(item);
}
referenceDocked = referenceItem.getDocked();
referenceWrapper = referenceItem.$dockWrapper;
referenceDirection = positionDirectionMap[referenceDocked];
if (direction === referenceDirection) {
referenceWrapper.addItem(item);
}
else {
slice = referenceWrapper.getItemsSlice(itemIndex);
newWrapper = new DockClass({
container: this.container,
direction: direction
});
if (slice.length > 0) {
if (slice.length === referenceWrapper.itemsCount) {
nestedWrapper = referenceWrapper;
newWrapper.setSizeState(nestedWrapper.getSizeState());
newWrapper.getElement().replace(nestedWrapper.getElement());
}
else {
nestedWrapper = new DockClass({
container: this.container,
direction: referenceDirection
});
nestedWrapper.setInnerWrapper(referenceWrapper.getInnerWrapper());
nestedWrapper.addItems(slice);
referenceWrapper.setInnerWrapper(newWrapper);
}
newWrapper.setInnerWrapper(nestedWrapper);
}
else {
newWrapper.setInnerWrapper(referenceWrapper.getInnerWrapper());
referenceWrapper.setInnerWrapper(newWrapper);
}
newWrapper.addItem(item);
}
}
container.onInitialized('refreshDockedItemLayoutSizeFlags', this, [item]);
},
getDockWrapper: function() {
var dockedItems = this.dockedItems;
if (dockedItems.length > 0) {
return dockedItems[0].$dockWrapper;
}
return null;
},
undockItem: function(item) {
var dockedItems = this.dockedItems;
if (item.$dockWrapper) {
item.$dockWrapper.removeItem(item);
}
Ext.Array.remove(dockedItems, item);
item.setLayoutSizeFlags(0);
},
destroy: function() {
this.dockedItems.length = 0;
delete this.dockedItems;
this.callSuper();
}
});
/**
*
*/
Ext.define('Ext.layout.Box', {
extend: 'Ext.layout.Default',
config: {
orient: 'horizontal',
align: 'start',
pack: 'start'
},
alias: 'layout.tablebox',
layoutBaseClass: 'x-layout-tablebox',
itemClass: 'x-layout-tablebox-item',
setContainer: function(container) {
this.callSuper(arguments);
container.innerElement.addCls(this.layoutBaseClass);
container.on('flexchange', 'onItemFlexChange', this, {
delegate: '> component'
});
},
onItemInnerStateChange: function(item, isInner) {
this.callSuper(arguments);
item.toggleCls(this.itemClass, isInner);
},
onItemFlexChange: function() {
}
});
/**
*
*/
Ext.define('Ext.layout.FlexBox', {
extend: 'Ext.layout.Box',
alias: 'layout.box',
config: {
align: 'stretch'
},
layoutBaseClass: 'x-layout-box',
itemClass: 'x-layout-box-item',
setContainer: function(container) {
this.callSuper(arguments);
this.monitorSizeFlagsChange();
},
applyOrient: function(orient) {
//<debug error>
if (orient !== 'horizontal' && orient !== 'vertical') {
Ext.Logger.error("Invalid box orient of: '" + orient + "', must be either 'horizontal' or 'vertical'");
}
//</debug>
return orient;
},
updateOrient: function(orient, oldOrient) {
var container = this.container,
delegation = {
delegate: '> component'
};
if (orient === 'horizontal') {
this.sizePropertyName = 'width';
}
else {
this.sizePropertyName = 'height';
}
container.innerElement.swapCls('x-' + orient, 'x-' + oldOrient);
if (oldOrient) {
container.un(oldOrient === 'horizontal' ? 'widthchange' : 'heightchange', 'onItemSizeChange', this, delegation);
this.redrawContainer();
}
container.on(orient === 'horizontal' ? 'widthchange' : 'heightchange', 'onItemSizeChange', this, delegation);
},
onItemInnerStateChange: function(item, isInner) {
this.callSuper(arguments);
var flex, size;
item.toggleCls(this.itemClass, isInner);
if (isInner) {
flex = item.getFlex();
size = item.get(this.sizePropertyName);
if (flex) {
this.doItemFlexChange(item, flex);
}
else if (size) {
this.doItemSizeChange(item, size);
}
}
this.refreshItemSizeState(item);
},
refreshItemSizeState: function(item) {
var isInner = item.isInnerItem(),
container = this.container,
LAYOUT_HEIGHT = container.LAYOUT_HEIGHT,
LAYOUT_WIDTH = container.LAYOUT_WIDTH,
dimension = this.sizePropertyName,
layoutSizeFlags = 0,
containerSizeFlags = container.getSizeFlags();
if (isInner) {
layoutSizeFlags |= container.LAYOUT_STRETCHED;
if (this.getAlign() === 'stretch') {
layoutSizeFlags |= containerSizeFlags & (dimension === 'width' ? LAYOUT_HEIGHT : LAYOUT_WIDTH);
}
if (item.getFlex()) {
layoutSizeFlags |= containerSizeFlags & (dimension === 'width' ? LAYOUT_WIDTH : LAYOUT_HEIGHT);
}
}
item.setLayoutSizeFlags(layoutSizeFlags);
},
refreshAllItemSizedStates: function() {
var innerItems = this.container.innerItems,
i, ln, item;
for (i = 0,ln = innerItems.length; i < ln; i++) {
item = innerItems[i];
this.refreshItemSizeState(item);
}
},
onContainerSizeFlagsChange: function() {
this.refreshAllItemSizedStates();
this.callSuper(arguments);
},
onItemSizeChange: function(item, size) {
if (item.isInnerItem()) {
this.doItemSizeChange(item, size);
}
},
doItemSizeChange: function(item, size) {
if (size) {
item.setFlex(null);
this.redrawContainer();
}
},
onItemFlexChange: function(item, flex) {
if (item.isInnerItem()) {
this.doItemFlexChange(item, flex);
this.refreshItemSizeState(item);
}
},
doItemFlexChange: function(item, flex) {
this.setItemFlex(item, flex);
if (flex) {
item.set(this.sizePropertyName, null);
}
else {
this.redrawContainer();
}
},
redrawContainer: function() {
var container = this.container,
renderedTo = container.element.dom.parentNode;
if (renderedTo && renderedTo.nodeType !== 11) {
container.innerElement.redraw();
}
},
/**
* Sets the flex of an item in this box layout.
* @param {Ext.Component} item The item of this layout which you want to update the flex of.
* @param {Number} flex The flex to set on this method
*/
setItemFlex: function(item, flex) {
var element = item.element;
element.toggleCls('x-flexed', !!flex);
element.setStyle('-webkit-box-flex', flex);
},
convertPosition: function(position) {
var positionMap = this.positionMap;
if (positionMap.hasOwnProperty(position)) {
return positionMap[position];
}
return position;
},
applyAlign: function(align) {
return this.convertPosition(align);
},
updateAlign: function(align, oldAlign) {
var container = this.container;
container.innerElement.swapCls(align, oldAlign, true, 'x-align');
if (oldAlign !== undefined) {
this.refreshAllItemSizedStates();
}
},
applyPack: function(pack) {
return this.convertPosition(pack);
},
updatePack: function(pack, oldPack) {
this.container.innerElement.swapCls(pack, oldPack, true, 'x-pack');
}
});
/**
*
*/
Ext.define('Ext.layout.HBox', {
extend: 'Ext.layout.FlexBox',
alias: 'layout.hbox'
});
/**
*
*/
Ext.define('Ext.layout.Fit', {
extend: 'Ext.layout.Default',
isFit: true,
alias: 'layout.fit',
layoutClass: 'x-layout-fit',
itemClass: 'x-layout-fit-item',
setContainer: function(container) {
this.callSuper(arguments);
container.innerElement.addCls(this.layoutClass);
this.onContainerSizeFlagsChange();
this.monitorSizeFlagsChange();
},
onContainerSizeFlagsChange: function() {
var container = this.container,
sizeFlags = container.getSizeFlags(),
stretched = Boolean(sizeFlags & container.LAYOUT_STRETCHED),
innerItems = container.innerItems,
i, ln, item;
this.callSuper();
for (i = 0,ln = innerItems.length; i < ln; i++) {
item = innerItems[i];
item.setLayoutSizeFlags(sizeFlags);
}
container.innerElement.toggleCls('x-stretched', stretched);
},
onItemInnerStateChange: function(item, isInner) {
this.callSuper(arguments);
item.toggleCls(this.itemClass, isInner);
item.setLayoutSizeFlags(isInner ? this.container.getSizeFlags() : 0);
}
});
/**
*
*/
Ext.define('Ext.layout.Float', {
extend: 'Ext.layout.Default',
alias: 'layout.float',
config: {
direction: 'left'
},
layoutClass: 'layout-float',
itemClass: 'layout-float-item',
setContainer: function(container) {
this.callSuper(arguments);
container.innerElement.addCls(this.layoutClass);
},
onItemInnerStateChange: function(item, isInner) {
this.callSuper(arguments);
item.toggleCls(this.itemClass, isInner);
},
updateDirection: function(direction, oldDirection) {
var prefix = 'direction-';
this.container.innerElement.swapCls(prefix + direction, prefix + oldDirection);
}
});
/**
*
*/
Ext.define('Ext.layout.wrapper.Dock', {
requires: [
'Ext.util.Wrapper'
],
config: {
direction: 'horizontal',
element: {
className: 'x-dock'
},
bodyElement: {
className: 'x-dock-body'
},
innerWrapper: null,
sizeState: false,
container: null
},
positionMap: {
top: 'start',
left: 'start',
bottom: 'end',
right: 'end'
},
constructor: function(config) {
this.items = {
start: [],
end: []
};
this.itemsCount = 0;
this.initConfig(config);
},
addItems: function(items) {
var i, ln, item;
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
this.addItem(item);
}
},
addItem: function(item) {
var docked = item.getDocked(),
position = this.positionMap[docked],
wrapper = item.$dockWrapper,
container = this.getContainer(),
index = container.indexOf(item),
items = this.items,
sideItems = items[position],
itemWrapper, element, i, ln, sibling, referenceElement, siblingIndex;
if (wrapper) {
wrapper.removeItem(item);
}
item.$dockWrapper = this;
itemWrapper = item.link('$dockItemWrapper', new Ext.util.Wrapper({
className: 'x-dock-item'
}));
item.addCls('x-docked-' + docked);
element = itemWrapper.element;
for (i = 0, ln = sideItems.length; i < ln; i++) {
sibling = sideItems[i];
siblingIndex = container.indexOf(sibling);
if (siblingIndex > index) {
referenceElement = sibling.element;
sideItems.splice(i, 0, item);
break;
}
}
if (!referenceElement) {
sideItems.push(item);
referenceElement = this.getBodyElement();
}
this.itemsCount++;
if (position === 'start') {
element.insertBefore(referenceElement);
}
else {
element.insertAfter(referenceElement);
}
itemWrapper.wrap(item.element);
itemWrapper.bindSize(this.getDirection() === 'horizontal' ? 'width' : 'height');
},
removeItem: function(item) {
var position = item.getDocked(),
items = this.items[this.positionMap[position]];
item.removeCls('x-docked-' + position);
Ext.Array.remove(items, item);
item.unlink('$dockItemWrapper');
item.element.detach();
delete item.$dockWrapper;
if (--this.itemsCount === 0) {
this.destroy();
}
},
getItemsSlice: function(index) {
var container = this.getContainer(),
items = this.items,
slice = [],
sideItems, i, ln, item;
for (sideItems = items.start, i = 0, ln = sideItems.length; i < ln; i++) {
item = sideItems[i];
if (container.indexOf(item) > index) {
slice.push(item);
}
}
for (sideItems = items.end, i = 0, ln = sideItems.length; i < ln; i++) {
item = sideItems[i];
if (container.indexOf(item) > index) {
slice.push(item);
}
}
return slice;
},
applyElement: function(element) {
return Ext.Element.create(element);
},
updateElement: function(element) {
element.addCls('x-dock-' + this.getDirection());
},
applyBodyElement: function(bodyElement) {
return Ext.Element.create(bodyElement);
},
updateBodyElement: function(bodyElement) {
this.getElement().append(bodyElement);
},
updateInnerWrapper: function(innerWrapper, oldInnerWrapper) {
var innerElement = this.getBodyElement();
if (oldInnerWrapper && oldInnerWrapper.$outerWrapper === this) {
innerElement.remove(oldInnerWrapper.getElement());
delete oldInnerWrapper.$outerWrapper;
}
if (innerWrapper) {
innerWrapper.setSizeState(this.getSizeState());
innerWrapper.$outerWrapper = this;
innerElement.append(innerWrapper.getElement());
}
},
updateSizeState: function(state) {
var innerWrapper = this.getInnerWrapper();
this.getElement().setSizeState(state);
if (innerWrapper) {
innerWrapper.setSizeState(state);
}
},
destroy: function() {
var innerWrapper = this.getInnerWrapper(),
outerWrapper = this.$outerWrapper;
if (innerWrapper) {
if (outerWrapper) {
outerWrapper.setInnerWrapper(innerWrapper);
}
else {
innerWrapper.getElement().replace(this.getElement());
delete innerWrapper.$outerWrapper;
}
}
delete this.$outerWrapper;
this.setInnerWrapper(null);
this.unlink('_bodyElement', '_element');
this.callSuper();
}
});
/**
*
*/
Ext.define('Ext.layout.VBox', {
extend: 'Ext.layout.FlexBox',
alias: 'layout.vbox',
config: {
orient: 'vertical'
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Abstract', {
extend: 'Ext.Evented',
isAnimation: true,
config: {
direction: 'left',
duration: null,
reverse: null,
layout: null
},
updateLayout: function() {
this.enable();
},
enable: function() {
var layout = this.getLayout();
if (layout) {
layout.onBefore('activeitemchange', 'onActiveItemChange', this);
}
},
disable: function() {
var layout = this.getLayout();
if (this.isAnimating) {
this.stopAnimation();
}
if (layout) {
layout.unBefore('activeitemchange', 'onActiveItemChange', this);
}
},
onActiveItemChange: Ext.emptyFn,
destroy: function() {
var layout = this.getLayout();
if (this.isAnimating) {
this.stopAnimation();
}
if (layout) {
layout.unBefore('activeitemchange', 'onActiveItemChange', this);
}
this.setLayout(null);
}
});
/**
* @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.animation.Abstract', {
extend: 'Ext.Evented',
isAnimation: true,
requires: [
'Ext.fx.State'
],
config: {
name: '',
element: null,
/**
* @cfg
* Before configuration.
*/
before: null,
from: {},
to: {},
after: null,
states: {},
duration: 300,
/**
* @cfg
* Easing type.
*/
easing: 'linear',
iteration: 1,
direction: 'normal',
delay: 0,
onBeforeStart: null,
onEnd: null,
onBeforeEnd: null,
scope: null,
reverse: null,
preserveEndState: false,
replacePrevious: true
},
STATE_FROM: '0%',
STATE_TO: '100%',
DIRECTION_UP: 'up',
DIRECTION_DOWN: 'down',
DIRECTION_LEFT: 'left',
DIRECTION_RIGHT: 'right',
stateNameRegex: /^(?:[\d\.]+)%$/,
constructor: function() {
this.states = {};
this.callParent(arguments);
return this;
},
applyElement: function(element) {
return Ext.get(element);
},
applyBefore: function(before, current) {
if (before) {
return Ext.factory(before, Ext.fx.State, current);
}
},
applyAfter: function(after, current) {
if (after) {
return Ext.factory(after, Ext.fx.State, current);
}
},
setFrom: function(from) {
return this.setState(this.STATE_FROM, from);
},
setTo: function(to) {
return this.setState(this.STATE_TO, to);
},
getFrom: function() {
return this.getState(this.STATE_FROM);
},
getTo: function() {
return this.getState(this.STATE_TO);
},
setStates: function(states) {
var validNameRegex = this.stateNameRegex,
name;
for (name in states) {
if (validNameRegex.test(name)) {
this.setState(name, states[name]);
}
}
return this;
},
getStates: function() {
return this.states;
},
stop: function() {
this.fireEvent('stop', this);
},
destroy: function() {
this.stop();
this.callParent();
},
setState: function(name, state) {
var states = this.getStates(),
stateInstance;
stateInstance = Ext.factory(state, Ext.fx.State, states[name]);
if (stateInstance) {
states[name] = stateInstance;
}
//<debug error>
else if (name === this.STATE_TO) {
Ext.Logger.error("Setting and invalid '100%' / 'to' state of: " + state);
}
//</debug>
return this;
},
getState: function(name) {
return this.getStates()[name];
},
getData: function() {
var states = this.getStates(),
statesData = {},
before = this.getBefore(),
after = this.getAfter(),
from = states[this.STATE_FROM],
to = states[this.STATE_TO],
fromData = from.getData(),
toData = to.getData(),
data, name, state;
for (name in states) {
if (states.hasOwnProperty(name)) {
state = states[name];
data = state.getData();
statesData[name] = data;
}
}
if (Ext.os.is.Android2) {
statesData['0.0001%'] = fromData;
}
return {
before: before ? before.getData() : {},
after: after ? after.getData() : {},
states: statesData,
from: fromData,
to: toData,
duration: this.getDuration(),
iteration: this.getIteration(),
direction: this.getDirection(),
easing: this.getEasing(),
delay: this.getDelay(),
onEnd: this.getOnEnd(),
onBeforeEnd: this.getOnBeforeEnd(),
onBeforeStart: this.getOnBeforeStart(),
scope: this.getScope(),
preserveEndState: this.getPreserveEndState(),
replacePrevious: this.getReplacePrevious()
};
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.Slide', {
extend: 'Ext.fx.animation.Abstract',
alternateClassName: 'Ext.fx.animation.SlideIn',
alias: ['animation.slide', 'animation.slideIn'],
config: {
/**
* @cfg {String} direction The direction of which the slide animates
* @accessor
*/
direction: 'left',
/**
* @cfg {Boolean} out True if you want to make this animation slide out, instead of slide in.
* @accessor
*/
out: false,
/**
* @cfg {Number} offset The offset that the animation should go offscreen before entering (or when exiting)
* @accessor
*/
offset: 0,
/**
* @cfg
* @inheritdoc
*/
easing: 'auto',
containerBox: 'auto',
elementBox: 'auto',
isElementBoxFit: true,
useCssTransform: true
},
reverseDirectionMap: {
up: 'down',
down: 'up',
left: 'right',
right: 'left'
},
applyEasing: function(easing) {
if (easing === 'auto') {
return 'ease-' + ((this.getOut()) ? 'in' : 'out');
}
return easing;
},
getContainerBox: function() {
var box = this._containerBox;
if (box === 'auto') {
box = this.getElement().getParent().getPageBox();
}
return box;
},
getElementBox: function() {
var box = this._elementBox;
if (this.getIsElementBoxFit()) {
return this.getContainerBox();
}
if (box === 'auto') {
box = this.getElement().getPageBox();
}
return box;
},
getData: function() {
var elementBox = this.getElementBox(),
containerBox = this.getContainerBox(),
box = elementBox ? elementBox : containerBox,
from = this.getFrom(),
to = this.getTo(),
out = this.getOut(),
offset = this.getOffset(),
direction = this.getDirection(),
useCssTransform = this.getUseCssTransform(),
reverse = this.getReverse(),
translateX = 0,
translateY = 0,
fromX, fromY, toX, toY;
if (reverse) {
direction = this.reverseDirectionMap[direction];
}
switch (direction) {
case this.DIRECTION_UP:
if (out) {
translateY = containerBox.top - box.top - box.height - offset;
}
else {
translateY = containerBox.bottom - box.bottom + box.height + offset;
}
break;
case this.DIRECTION_DOWN:
if (out) {
translateY = containerBox.bottom - box.bottom + box.height + offset;
}
else {
translateY = containerBox.top - box.height - box.top - offset;
}
break;
case this.DIRECTION_RIGHT:
if (out) {
translateX = containerBox.right - box.right + box.width + offset;
}
else {
translateX = containerBox.left - box.left - box.width - offset;
}
break;
case this.DIRECTION_LEFT:
if (out) {
translateX = containerBox.left - box.left - box.width - offset;
}
else {
translateX = containerBox.right - box.right + box.width + offset;
}
break;
}
fromX = (out) ? 0 : translateX;
fromY = (out) ? 0 : translateY;
if (useCssTransform) {
from.setTransform({
translateX: fromX,
translateY: fromY
});
}
else {
from.set('left', fromX);
from.set('top', fromY);
}
toX = (out) ? translateX : 0;
toY = (out) ? translateY : 0;
if (useCssTransform) {
to.setTransform({
translateX: toX,
translateY: toY
});
}
else {
to.set('left', toX);
to.set('top', toY);
}
return this.callParent(arguments);
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.SlideOut', {
extend: 'Ext.fx.animation.Slide',
alias: ['animation.slideOut'],
config: {
// @hide
out: true
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.Fade', {
extend: 'Ext.fx.animation.Abstract',
alternateClassName: 'Ext.fx.animation.FadeIn',
alias: ['animation.fade', 'animation.fadeIn'],
config: {
/**
* @cfg {Boolean} out True if you want to make this animation fade out, instead of fade in.
* @accessor
*/
out: false,
before: {
display: null,
opacity: 0
},
after: {
opacity: null
},
reverse: null
},
updateOut: function(newOut) {
var to = this.getTo(),
from = this.getFrom();
if (newOut) {
from.set('opacity', 1);
to.set('opacity', 0);
} else {
from.set('opacity', 0);
to.set('opacity', 1);
}
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.FadeOut', {
extend: 'Ext.fx.animation.Fade',
alias: 'animation.fadeOut',
config: {
// @hide
out: true,
before: {}
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.Flip', {
extend: 'Ext.fx.animation.Abstract',
alias: 'animation.flip',
config: {
easing: 'ease-in',
/**
* @cfg {String} direction The direction of which the slide animates
* @accessor
*/
direction: 'right',
half: false,
out: null
},
getData: function() {
var from = this.getFrom(),
to = this.getTo(),
direction = this.getDirection(),
out = this.getOut(),
half = this.getHalf(),
rotate = (half) ? 90 : 180,
fromScale = 1,
toScale = 1,
fromRotateX = 0,
fromRotateY = 0,
toRotateX = 0,
toRotateY = 0;
if (out) {
toScale = 0.8;
}
else {
fromScale = 0.8;
}
switch (direction) {
case this.DIRECTION_UP:
if (out) {
toRotateX = rotate;
}
else {
fromRotateX = -rotate;
}
break;
case this.DIRECTION_DOWN:
if (out) {
toRotateX = -rotate;
}
else {
fromRotateX = rotate;
}
break;
case this.DIRECTION_RIGHT:
if (out) {
toRotateY = -rotate;
}
else {
fromRotateY = rotate;
}
break;
case this.DIRECTION_LEFT:
if (out) {
toRotateY = -rotate;
}
else {
fromRotateY = rotate;
}
break;
}
from.setTransform({
rotateX: fromRotateX,
rotateY: fromRotateY,
scale: fromScale
});
to.setTransform({
rotateX: toRotateX,
rotateY: toRotateY,
scale: toScale
});
return this.callParent(arguments);
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.Pop', {
extend: 'Ext.fx.animation.Abstract',
alias: ['animation.pop', 'animation.popIn'],
alternateClassName: 'Ext.fx.animation.PopIn',
config: {
/**
* @cfg {Boolean} out True if you want to make this animation pop out, instead of pop in.
* @accessor
*/
out: false,
before: {
display: null,
opacity: 0
},
after: {
opacity: null
}
},
getData: function() {
var to = this.getTo(),
from = this.getFrom(),
out = this.getOut();
if (out) {
from.set('opacity', 1);
from.setTransform({
scale: 1
});
to.set('opacity', 0);
to.setTransform({
scale: 0
});
}
else {
from.set('opacity', 0);
from.setTransform({
scale: 0
});
to.set('opacity', 1);
to.setTransform({
scale: 1
});
}
return this.callParent(arguments);
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.PopOut', {
extend: 'Ext.fx.animation.Pop',
alias: 'animation.popOut',
config: {
// @hide
out: true,
before: {}
}
});
/**
* @private
* @author Jacky Nguyen <jacky@sencha.com>
*/
Ext.define('Ext.fx.Animation', {
requires: [
'Ext.fx.animation.Slide',
'Ext.fx.animation.SlideOut',
'Ext.fx.animation.Fade',
'Ext.fx.animation.FadeOut',
'Ext.fx.animation.Flip',
'Ext.fx.animation.Pop',
'Ext.fx.animation.PopOut'
// 'Ext.fx.animation.Cube'
],
constructor: function(config) {
var defaultClass = Ext.fx.animation.Abstract,
type;
if (typeof config == 'string') {
type = config;
config = {};
}
else if (config && config.type) {
type = config.type;
}
if (type) {
if (Ext.os.is.Android2) {
if (type == 'pop') {
type = 'fade';
}
if (type == 'popIn') {
type = 'fadeIn';
}
if (type == 'popOut') {
type = 'fadeOut';
}
}
defaultClass = Ext.ClassManager.getByAlias('animation.' + type);
//<debug error>
if (!defaultClass) {
Ext.Logger.error("Invalid animation type of: '" + type + "'");
}
//</debug>
}
return Ext.factory(config, defaultClass);
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Style', {
extend: 'Ext.fx.layout.card.Abstract',
requires: [
'Ext.fx.Animation'
],
config: {
inAnimation: {
before: {
visibility: null
},
preserveEndState: false,
replacePrevious: true
},
outAnimation: {
preserveEndState: false,
replacePrevious: true
}
},
constructor: function(config) {
var inAnimation, outAnimation;
this.initConfig(config);
this.endAnimationCounter = 0;
inAnimation = this.getInAnimation();
outAnimation = this.getOutAnimation();
inAnimation.on('animationend', 'incrementEnd', this);
outAnimation.on('animationend', 'incrementEnd', this);
},
updateDirection: function(direction) {
this.getInAnimation().setDirection(direction);
this.getOutAnimation().setDirection(direction);
},
updateDuration: function(duration) {
this.getInAnimation().setDuration(duration);
this.getOutAnimation().setDuration(duration);
},
updateReverse: function(reverse) {
this.getInAnimation().setReverse(reverse);
this.getOutAnimation().setReverse(reverse);
},
incrementEnd: function() {
this.endAnimationCounter++;
if (this.endAnimationCounter > 1) {
this.endAnimationCounter = 0;
this.fireEvent('animationend', this);
}
},
applyInAnimation: function(animation, inAnimation) {
return Ext.factory(animation, Ext.fx.Animation, inAnimation);
},
applyOutAnimation: function(animation, outAnimation) {
return Ext.factory(animation, Ext.fx.Animation, outAnimation);
},
updateInAnimation: function(animation) {
animation.setScope(this);
},
updateOutAnimation: function(animation) {
animation.setScope(this);
},
onActiveItemChange: function(cardLayout, newItem, oldItem, options, controller) {
var inAnimation = this.getInAnimation(),
outAnimation = this.getOutAnimation(),
inElement, outElement;
if (newItem && oldItem && oldItem.isPainted()) {
inElement = newItem.renderElement;
outElement = oldItem.renderElement;
inAnimation.setElement(inElement);
outAnimation.setElement(outElement);
outAnimation.setOnBeforeEnd(function(element, interrupted) {
if (interrupted || Ext.Animator.hasRunningAnimations(element)) {
controller.firingArguments[1] = null;
controller.firingArguments[2] = null;
}
});
outAnimation.setOnEnd(function() {
controller.resume();
});
inElement.dom.style.setProperty('visibility', 'hidden', '!important');
newItem.show();
Ext.Animator.run([outAnimation, inAnimation]);
controller.pause();
}
},
destroy: function () {
Ext.destroy(this.getInAnimation(), this.getOutAnimation());
this.callParent(arguments);
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Slide', {
extend: 'Ext.fx.layout.card.Style',
alias: 'fx.layout.card.slide',
config: {
inAnimation: {
type: 'slide',
easing: 'ease-out'
},
outAnimation: {
type: 'slide',
easing: 'ease-out',
out: true
}
},
updateReverse: function(reverse) {
this.getInAnimation().setReverse(reverse);
this.getOutAnimation().setReverse(reverse);
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Cover', {
extend: 'Ext.fx.layout.card.Style',
alias: 'fx.layout.card.cover',
config: {
reverse: null,
inAnimation: {
before: {
'z-index': 100
},
after: {
'z-index': 0
},
type: 'slide',
easing: 'ease-out'
},
outAnimation: {
easing: 'ease-out',
from: {
opacity: 0.99
},
to: {
opacity: 1
},
out: true
}
},
updateReverse: function(reverse) {
this.getInAnimation().setReverse(reverse);
this.getOutAnimation().setReverse(reverse);
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Reveal', {
extend: 'Ext.fx.layout.card.Style',
alias: 'fx.layout.card.reveal',
config: {
inAnimation: {
easing: 'ease-out',
from: {
opacity: 0.99
},
to: {
opacity: 1
}
},
outAnimation: {
before: {
'z-index': 100
},
after: {
'z-index': 0
},
type: 'slide',
easing: 'ease-out',
out: true
}
},
updateReverse: function(reverse) {
this.getInAnimation().setReverse(reverse);
this.getOutAnimation().setReverse(reverse);
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Fade', {
extend: 'Ext.fx.layout.card.Style',
alias: 'fx.layout.card.fade',
config: {
reverse: null,
inAnimation: {
type: 'fade',
easing: 'ease-out'
},
outAnimation: {
type: 'fade',
easing: 'ease-out',
out: true
}
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Flip', {
extend: 'Ext.fx.layout.card.Style',
alias: 'fx.layout.card.flip',
config: {
duration: 500,
inAnimation: {
type: 'flip',
half: true,
easing: 'ease-out',
before: {
'backface-visibility': 'hidden'
},
after: {
'backface-visibility': null
}
},
outAnimation: {
type: 'flip',
half: true,
easing: 'ease-in',
before: {
'backface-visibility': 'hidden'
},
after: {
'backface-visibility': null
},
out: true
}
},
updateDuration: function(duration) {
var halfDuration = duration / 2,
inAnimation = this.getInAnimation(),
outAnimation = this.getOutAnimation();
inAnimation.setDelay(halfDuration);
inAnimation.setDuration(halfDuration);
outAnimation.setDuration(halfDuration);
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Pop', {
extend: 'Ext.fx.layout.card.Style',
alias: 'fx.layout.card.pop',
config: {
duration: 500,
inAnimation: {
type: 'pop',
easing: 'ease-out'
},
outAnimation: {
type: 'pop',
easing: 'ease-in',
out: true
}
},
updateDuration: function(duration) {
var halfDuration = duration / 2,
inAnimation = this.getInAnimation(),
outAnimation = this.getOutAnimation();
inAnimation.setDelay(halfDuration);
inAnimation.setDuration(halfDuration);
outAnimation.setDuration(halfDuration);
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.Scroll', {
extend: 'Ext.fx.layout.card.Abstract',
requires: [
'Ext.fx.easing.Linear'
],
alias: 'fx.layout.card.scroll',
config: {
duration: 150
},
constructor: function(config) {
this.initConfig(config);
this.doAnimationFrame = Ext.Function.bind(this.doAnimationFrame, this);
},
getEasing: function() {
var easing = this.easing;
if (!easing) {
this.easing = easing = new Ext.fx.easing.Linear();
}
return easing;
},
updateDuration: function(duration) {
this.getEasing().setDuration(duration);
},
onActiveItemChange: function(cardLayout, newItem, oldItem, options, controller) {
var direction = this.getDirection(),
easing = this.getEasing(),
containerElement, inElement, outElement, containerWidth, containerHeight, reverse;
if (newItem && oldItem) {
if (this.isAnimating) {
this.stopAnimation();
}
newItem.setWidth('100%');
newItem.setHeight('100%');
containerElement = this.getLayout().container.innerElement;
containerWidth = containerElement.getWidth();
containerHeight = containerElement.getHeight();
inElement = newItem.renderElement;
outElement = oldItem.renderElement;
this.oldItem = oldItem;
this.newItem = newItem;
this.currentEventController = controller;
this.containerElement = containerElement;
this.isReverse = reverse = this.getReverse();
newItem.show();
if (direction == 'right') {
direction = 'left';
this.isReverse = reverse = !reverse;
}
else if (direction == 'down') {
direction = 'up';
this.isReverse = reverse = !reverse;
}
if (direction == 'left') {
if (reverse) {
easing.setConfig({
startValue: containerWidth,
endValue: 0
});
containerElement.dom.scrollLeft = containerWidth;
outElement.setLeft(containerWidth);
}
else {
easing.setConfig({
startValue: 0,
endValue: containerWidth
});
inElement.setLeft(containerWidth);
}
}
else {
if (reverse) {
easing.setConfig({
startValue: containerHeight,
endValue: 0
});
containerElement.dom.scrollTop = containerHeight;
outElement.setTop(containerHeight);
}
else {
easing.setConfig({
startValue: 0,
endValue: containerHeight
});
inElement.setTop(containerHeight);
}
}
this.startAnimation();
controller.pause();
}
},
startAnimation: function() {
this.isAnimating = true;
this.getEasing().setStartTime(Date.now());
this.timer = setInterval(this.doAnimationFrame, 20);
this.doAnimationFrame();
},
doAnimationFrame: function() {
var easing = this.getEasing(),
direction = this.getDirection(),
scroll = 'scrollTop',
value;
if (direction == 'left' || direction == 'right') {
scroll = 'scrollLeft';
}
if (easing.isEnded) {
this.stopAnimation();
}
else {
value = easing.getValue();
this.containerElement.dom[scroll] = value;
}
},
stopAnimation: function() {
var me = this,
direction = me.getDirection(),
scroll = 'setTop',
oldItem = me.oldItem,
newItem = me.newItem;
if (direction == 'left' || direction == 'right') {
scroll = 'setLeft';
}
me.currentEventController.resume();
if (me.isReverse && oldItem && oldItem.renderElement && oldItem.renderElement.dom) {
oldItem.renderElement[scroll](null);
}
else if (newItem && newItem.renderElement && newItem.renderElement.dom) {
newItem.renderElement[scroll](null);
}
clearInterval(me.timer);
me.isAnimating = false;
me.fireEvent('animationend', me);
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.Card', {
requires: [
'Ext.fx.layout.card.Slide',
'Ext.fx.layout.card.Cover',
'Ext.fx.layout.card.Reveal',
'Ext.fx.layout.card.Fade',
'Ext.fx.layout.card.Flip',
'Ext.fx.layout.card.Pop',
// 'Ext.fx.layout.card.Cube',
'Ext.fx.layout.card.Scroll'
],
constructor: function(config) {
var defaultClass = Ext.fx.layout.card.Abstract,
type;
if (!config) {
return null;
}
if (typeof config == 'string') {
type = config;
config = {};
}
else if (config.type) {
type = config.type;
}
config.elementBox = false;
if (type) {
if (Ext.os.is.Android2) {
// In Android 2 we only support scroll and fade. Otherwise force it to slide.
if (type != 'fade') {
type = 'scroll';
}
}
else if (type === 'slide' && Ext.browser.is.ChromeMobile) {
type = 'scroll';
}
defaultClass = Ext.ClassManager.getByAlias('fx.layout.card.' + type);
//<debug error>
if (!defaultClass) {
Ext.Logger.error("Unknown card animation type: '" + type + "'");
}
//</debug>
}
return Ext.factory(config, defaultClass);
}
});
/**
* @aside guide layouts
* @aside video layouts
*
* Sometimes you want to show several screens worth of information but you've only got a small screen to work with.
* TabPanels and Carousels both enable you to see one screen of many at a time, and underneath they both use a Card
* Layout.
*
* Card Layout takes the size of the Container it is applied to and sizes the currently active item to fill the
* Container completely. It then hides the rest of the items, allowing you to change which one is currently visible but
* only showing one at once:
*
* {@img ../guides/layouts/card.jpg}
*
*
* Here the gray box is our Container, and the blue box inside it is the currently active card. The three other cards
* are hidden from view, but can be swapped in later. While it's not too common to create Card layouts directly, you
* can do so like this:
*
* var panel = Ext.create('Ext.Panel', {
* layout: 'card',
* items: [
* {
* html: "First Item"
* },
* {
* html: "Second Item"
* },
* {
* html: "Third Item"
* },
* {
* html: "Fourth Item"
* }
* ]
* });
*
* panel.{@link Ext.Container#setActiveItem setActiveItem}(1);
*
* Here we create a Panel with a Card Layout and later set the second item active (the active item index is zero-based,
* so 1 corresponds to the second item). Normally you're better off using a {@link Ext.tab.Panel tab panel} or a
* {@link Ext.carousel.Carousel carousel}.
*
* For a more detailed overview of what layouts are and the types of layouts shipped with Sencha Touch 2, check out the
* [Layout Guide](#!/guide/layouts).
*/
Ext.define('Ext.layout.Card', {
extend: 'Ext.layout.Default',
alias: 'layout.card',
isCard: true,
/**
* @event activeitemchange
* @preventable doActiveItemChange
* Fires when an card is made active
* @param {Ext.layout.Card} this The layout instance
* @param {Mixed} newActiveItem The new active item
* @param {Mixed} oldActiveItem The old active item
*/
layoutClass: 'x-layout-card',
itemClass: 'x-layout-card-item',
requires: [
'Ext.fx.layout.Card'
],
/**
* @private
*/
applyAnimation: function(animation) {
return new Ext.fx.layout.Card(animation);
},
/**
* @private
*/
updateAnimation: function(animation, oldAnimation) {
if (animation && animation.isAnimation) {
animation.setLayout(this);
}
if (oldAnimation) {
oldAnimation.destroy();
}
},
setContainer: function(container) {
this.callSuper(arguments);
container.innerElement.addCls(this.layoutClass);
container.onInitialized('onContainerInitialized', this);
},
onContainerInitialized: function() {
var container = this.container,
activeItem = container.getActiveItem();
if (activeItem) {
activeItem.show();
}
container.on('activeitemchange', 'onContainerActiveItemChange', this);
},
/**
* @private
*/
onContainerActiveItemChange: function(container) {
this.relayEvent(arguments, 'doActiveItemChange');
},
onItemInnerStateChange: function(item, isInner, destroying) {
this.callSuper(arguments);
var container = this.container,
activeItem = container.getActiveItem();
item.toggleCls(this.itemClass, isInner);
item.setLayoutSizeFlags(isInner ? container.LAYOUT_BOTH : 0);
if (isInner) {
if (activeItem !== container.innerIndexOf(item) && activeItem !== item && item !== container.pendingActiveItem) {
item.hide();
}
}
else {
if (!destroying && !item.isDestroyed && item.isDestroying !== true) {
item.show();
}
}
},
/**
* @private
*/
doActiveItemChange: function(me, newActiveItem, oldActiveItem) {
if (oldActiveItem) {
oldActiveItem.hide();
}
if (newActiveItem) {
newActiveItem.show();
}
},
destroy: function () {
this.callParent(arguments);
Ext.destroy(this.getAnimation());
}
});
/**
* 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 32:
* 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) {
// <debug>
Ext.Logger.error('A Filter requires either a property and value, or a filterFn to be set');
// </debug>
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;
}
});
/**
* @private
*/
Ext.define('Ext.util.AbstractMixedCollection', {
requires: ['Ext.util.Filter'],
mixins: {
observable: 'Ext.mixin.Observable'
},
/**
* @event clear
* Fires when the collection is cleared.
*/
/**
* @event add
* Fires when an item is added to the collection.
* @param {Number} index The index at which the item was added.
* @param {Object} o The item added.
* @param {String} key The key associated with the added item.
*/
/**
* @event replace
* Fires when an item is replaced in the collection.
* @param {String} key he key associated with the new added.
* @param {Object} old The item being replaced.
* @param {Object} new The new item.
*/
/**
* @event remove
* Fires when an item is removed from the collection.
* @param {Object} o The item being removed.
* @param {String} key (optional) The key associated with the removed item.
*/
/**
* Creates new MixedCollection.
* @param {Boolean} [allowFunctions=false] Specify `true` if the {@link #addAll}
* function should add function references to the collection.
* @param {Function} [keyFn] A function that can accept an item of the type(s) stored in this MixedCollection
* and return the key value for that item. This is used when available to look up the key on items that
* were passed without an explicit key parameter to a MixedCollection method. Passing this parameter is
* equivalent to providing an implementation for the {@link #getKey} method.
*/
constructor: function(allowFunctions, keyFn) {
var me = this;
me.items = [];
me.map = {};
me.keys = [];
me.length = 0;
me.allowFunctions = allowFunctions === true;
if (keyFn) {
me.getKey = keyFn;
}
me.mixins.observable.constructor.call(me);
},
/**
* @cfg {Boolean} allowFunctions Specify `true` if the {@link #addAll}
* function should add function references to the collection.
*/
allowFunctions : false,
/**
* Adds an item to the collection. Fires the {@link #event-add} event when complete.
* @param {String} key The key to associate with the item, or the new item.
*
* If a {@link #getKey} implementation was specified for this MixedCollection,
* or if the key of the stored items is in a property called `id`,
* the MixedCollection will be able to _derive_ the key for the new item.
* In this case just pass the new item in this parameter.
* @param {Object} o The item to add.
* @return {Object} The item added.
*/
add: function(key, obj){
var me = this,
myObj = obj,
myKey = key,
old;
if (arguments.length == 1) {
myObj = myKey;
myKey = me.getKey(myObj);
}
if (typeof myKey != 'undefined' && myKey !== null) {
old = me.map[myKey];
if (typeof old != 'undefined') {
return me.replace(myKey, myObj);
}
me.map[myKey] = myObj;
}
me.length++;
me.items.push(myObj);
me.keys.push(myKey);
me.fireEvent('add', me.length - 1, myObj, myKey);
return myObj;
},
/**
* MixedCollection has a generic way to fetch keys if you implement `getKey`. The default implementation
* simply returns `item.id` but you can provide your own implementation
* to return a different value as in the following examples:
*
* // normal way
* var mc = new Ext.util.MixedCollection();
* mc.add(someEl.dom.id, someEl);
* mc.add(otherEl.dom.id, otherEl);
* //and so on
*
* // using getKey
* var mc = new Ext.util.MixedCollection();
* mc.getKey = function(el) {
* return el.dom.id;
* };
* mc.add(someEl);
* mc.add(otherEl);
*
* // or via the constructor
* var mc = new Ext.util.MixedCollection(false, function(el) {
* return el.dom.id;
* });
* mc.add(someEl);
* mc.add(otherEl);
*
* @param {Object} item The item for which to find the key.
* @return {Object} The key for the passed item.
*/
getKey: function(o){
return o.id;
},
/**
* Replaces an item in the collection. Fires the {@link #event-replace} event when complete.
* @param {String} key The key associated with the item to replace, or the replacement item.
*
* If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key
* of your stored items is in a property called `id`, then the MixedCollection
* will be able to _derive_ the key of the replacement item. If you want to replace an item
* with one having the same key value, then just pass the replacement item in this parameter.
* @param {Object} o (optional) If the first parameter passed was a key, the item to associate
* with that key.
* @return {Object} The new item.
*/
replace: function(key, o){
var me = this,
old,
index;
if (arguments.length == 1) {
o = arguments[0];
key = me.getKey(o);
}
old = me.map[key];
if (typeof key == 'undefined' || key === null || typeof old == 'undefined') {
return me.add(key, o);
}
index = me.indexOfKey(key);
me.items[index] = o;
me.map[key] = o;
me.fireEvent('replace', key, old, o);
return o;
},
/**
* Adds all elements of an Array or an Object to the collection.
* @param {Object/Array} objs An Object containing properties which will be added
* to the collection, or an Array of values, each of which are added to the collection.
* Functions references will be added to the collection if `{@link #allowFunctions}`
* has been set to `true`.
*/
addAll: function(objs){
var me = this,
i = 0,
args,
len,
key;
if (arguments.length > 1 || Ext.isArray(objs)) {
args = arguments.length > 1 ? arguments : objs;
for (len = args.length; i < len; i++) {
me.add(args[i]);
}
} else {
for (key in objs) {
if (objs.hasOwnProperty(key)) {
if (me.allowFunctions || typeof objs[key] != 'function') {
me.add(key, objs[key]);
}
}
}
}
},
/**
* Executes the specified function once for every item in the collection.
*
* @param {Function} fn The function to execute for each item.
* @param {Mixed} fn.item The collection item.
* @param {Number} fn.index The item's index.
* @param {Number} fn.length The total number of items in the collection.
* @param {Boolean} fn.return Returning `false` will stop the iteration.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
* Defaults to the current item in the iteration.
*/
each: function(fn, scope){
var items = [].concat(this.items), // each safe for removal
i = 0,
len = items.length,
item;
for (; i < len; i++) {
item = items[i];
if (fn.call(scope || item, item, i, len) === false) {
break;
}
}
},
/**
* Executes the specified function once for every key in the collection, passing each
* key, and its associated item as the first two parameters.
* @param {Function} fn The function to execute for each item.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the browser window.
*/
eachKey: function(fn, scope){
var keys = this.keys,
items = this.items,
i = 0,
len = keys.length;
for (; i < len; i++) {
fn.call(scope || window, keys[i], items[i], i, len);
}
},
/**
* Returns the first item in the collection which elicits a `true` return value from the
* passed selection function.
* @param {Function} fn The selection function to execute for each item.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the browser window.
* @return {Object} The first item in the collection which returned `true` from the selection function.
*/
findBy: function(fn, scope) {
var keys = this.keys,
items = this.items,
i = 0,
len = items.length;
for (; i < len; i++) {
if (fn.call(scope || window, items[i], keys[i])) {
return items[i];
}
}
return null;
},
/**
* Inserts an item at the specified index in the collection. Fires the `{@link #event-add}` event when complete.
* @param {Number} index The index to insert the item at.
* @param {String} key The key to associate with the new item, or the item itself.
* @param {Object} o (optional) If the second parameter was a key, the new item.
* @return {Object} The item inserted.
*/
insert: function(index, key, obj){
var me = this,
myKey = key,
myObj = obj;
if (arguments.length == 2) {
myObj = myKey;
myKey = me.getKey(myObj);
}
if (me.containsKey(myKey)) {
me.suspendEvents();
me.removeAtKey(myKey);
me.resumeEvents();
}
if (index >= me.length) {
return me.add(myKey, myObj);
}
me.length++;
Ext.Array.splice(me.items, index, 0, myObj);
if (typeof myKey != 'undefined' && myKey !== null) {
me.map[myKey] = myObj;
}
Ext.Array.splice(me.keys, index, 0, myKey);
me.fireEvent('add', index, myObj, myKey);
return myObj;
},
/**
* Remove an item from the collection.
* @param {Object} o The item to remove.
* @return {Object} The item removed or `false` if no item was removed.
*/
remove: function(o){
return this.removeAt(this.indexOf(o));
},
/**
* Remove all items in the passed array from the collection.
* @param {Array} items An array of items to be removed.
* @return {Ext.util.MixedCollection} this object
*/
removeAll: function(items){
Ext.each(items || [], function(item) {
this.remove(item);
}, this);
return this;
},
/**
* Remove an item from a specified index in the collection. Fires the `{@link #event-remove}` event when complete.
* @param {Number} index The index within the collection of the item to remove.
* @return {Object/Boolean} The item removed or `false` if no item was removed.
*/
removeAt: function(index){
var me = this,
o,
key;
if (index < me.length && index >= 0) {
me.length--;
o = me.items[index];
Ext.Array.erase(me.items, index, 1);
key = me.keys[index];
if (typeof key != 'undefined') {
delete me.map[key];
}
Ext.Array.erase(me.keys, index, 1);
me.fireEvent('remove', o, key);
return o;
}
return false;
},
/**
* Removed an item associated with the passed key from the collection.
* @param {String} key The key of the item to remove.
* @return {Object/Boolean} The item removed or `false` if no item was removed.
*/
removeAtKey: function(key){
return this.removeAt(this.indexOfKey(key));
},
/**
* Returns the number of items in the collection.
* @return {Number} the number of items in the collection.
*/
getCount: function(){
return this.length;
},
/**
* Returns index within the collection of the passed Object.
* @param {Object} o The item to find the index of.
* @return {Number} index of the item. Returns -1 if not found.
*/
indexOf: function(o){
return Ext.Array.indexOf(this.items, o);
},
/**
* Returns index within the collection of the passed key.
* @param {String} key The key to find the index of.
* @return {Number} The index of the key.
*/
indexOfKey: function(key){
return Ext.Array.indexOf(this.keys, key);
},
/**
* Returns the item associated with the passed key OR index.
* Key has priority over index. This is the equivalent
* of calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
* @param {String/Number} key The key or index of the item.
* @return {Object} If the item is found, returns the item. If the item was not found, returns `undefined`.
* If an item was found, but is a Class, returns `null`.
*/
get: function(key) {
var me = this,
mk = me.map[key],
item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined;
return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype!
},
/**
* Returns the item at the specified index.
* @param {Number} index The index of the item.
* @return {Object} The item at the specified index.
*/
getAt: function(index) {
return this.items[index];
},
/**
* Returns the item associated with the passed key.
* @param {String/Number} key The key of the item.
* @return {Object} The item associated with the passed key.
*/
getByKey: function(key) {
return this.map[key];
},
/**
* Returns `true` if the collection contains the passed Object as an item.
* @param {Object} o The Object to look for in the collection.
* @return {Boolean} `true` if the collection contains the Object as an item.
*/
contains: function(o){
return Ext.Array.contains(this.items, o);
},
/**
* Returns `true` if the collection contains the passed Object as a key.
* @param {String} key The key to look for in the collection.
* @return {Boolean} `true` if the collection contains the Object as a key.
*/
containsKey: function(key){
return typeof this.map[key] != 'undefined';
},
/**
* Removes all items from the collection. Fires the `{@link #event-clear}` event when complete.
*/
clear: function(){
var me = this;
me.length = 0;
me.items = [];
me.keys = [];
me.map = {};
me.fireEvent('clear');
},
/**
* Returns the first item in the collection.
* @return {Object} the first item in the collection..
*/
first: function() {
return this.items[0];
},
/**
* Returns the last item in the collection.
* @return {Object} the last item in the collection..
*/
last: function() {
return this.items[this.length - 1];
},
/**
* Collects all of the values of the given property and returns their sum.
* @param {String} property The property to sum by.
* @param {String} [root] Optional 'root' property to extract the first argument from. This is used mainly when
* summing fields in records, where the fields are all stored inside the `data` object
* @param {Number} [start=0] (optional) The record index to start at.
* @param {Number} [end=-1] (optional) The record index to end at.
* @return {Number} The total
*/
sum: function(property, root, start, end) {
var values = this.extractValues(property, root),
length = values.length,
sum = 0,
i;
start = start || 0;
end = (end || end === 0) ? end : length - 1;
for (i = start; i <= end; i++) {
sum += values[i];
}
return sum;
},
/**
* Collects unique values of a particular property in this MixedCollection.
* @param {String} property The property to collect on.
* @param {String} [root] Optional 'root' property to extract the first argument from. This is used mainly when
* summing fields in records, where the fields are all stored inside the `data` object.
* @param {Boolean} allowBlank (optional) Pass `true` to allow `null`, `undefined`, or empty string values.
* @return {Array} The unique values.
*/
collect: function(property, root, allowNull) {
var values = this.extractValues(property, root),
length = values.length,
hits = {},
unique = [],
value, strValue, i;
for (i = 0; i < length; i++) {
value = values[i];
strValue = String(value);
if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
hits[strValue] = true;
unique.push(value);
}
}
return unique;
},
/**
* @private
* Extracts all of the given property values from the items in the MixedCollection. Mainly used as a supporting method for
* functions like `sum()` and `collect()`.
* @param {String} property The property to extract.
* @param {String} [root] Optional 'root' property to extract the first argument from. This is used mainly when
* extracting field data from Model instances, where the fields are stored inside the `data` object.
* @return {Array} The extracted values.
*/
extractValues: function(property, root) {
var values = this.items;
if (root) {
values = Ext.Array.pluck(values, root);
}
return Ext.Array.pluck(values, property);
},
/**
* Returns a range of items in this collection.
* @param {Number} [startIndex=0] (optional) The starting index.
* @param {Number} [endIndex=-1] (optional) The ending index.
* @return {Array} An array of items
*/
getRange: function(start, end){
var me = this,
items = me.items,
range = [],
i;
if (items.length < 1) {
return range;
}
start = start || 0;
end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
if (start <= end) {
for (i = start; i <= end; i++) {
range[range.length] = items[i];
}
} else {
for (i = start; i >= end; i--) {
range[range.length] = items[i];
}
}
return range;
},
/**
* Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single
* property/value pair with optional parameters for substring matching and case sensitivity. See
* {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively,
* MixedCollection can be easily filtered by property like this:
*
* // create a simple store with a few people defined
* var people = new Ext.util.MixedCollection();
* people.addAll([
* {id: 1, age: 25, name: 'Ed'},
* {id: 2, age: 24, name: 'Tommy'},
* {id: 3, age: 24, name: 'Arne'},
* {id: 4, age: 26, name: 'Aaron'}
* ]);
*
* // a new MixedCollection containing only the items where age == 24
* var middleAged = people.filter('age', 24);
*
* @param {Ext.util.Filter[]/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects
* @param {String/RegExp} value Either string that the property values
* should start with or a RegExp to test against the property.
* @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning
* @param {Boolean} [caseSensitive=false] (optional) `true` for case sensitive comparison.
* @return {Ext.util.MixedCollection} The new filtered collection
*/
filter: function(property, value, anyMatch, caseSensitive) {
var filters = [],
filterFn;
//support for the simple case of filtering by property/value
if (Ext.isString(property)) {
filters.push(Ext.create('Ext.util.Filter', {
property : property,
value : value,
anyMatch : anyMatch,
caseSensitive: caseSensitive
}));
} else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
filters = filters.concat(property);
}
//at this point we have an array of zero or more Ext.util.Filter objects to filter with,
//so here we construct a function that combines these filters by ANDing them together
filterFn = function(record) {
var isMatch = true,
length = filters.length,
i;
for (i = 0; i < length; i++) {
var filter = filters[i],
fn = filter.getFilterFn(),
scope = filter.getScope();
isMatch = isMatch && fn.call(scope, record);
}
return isMatch;
};
return this.filterBy(filterFn);
},
/**
* Filter by a function. Returns a _new_ collection that has been filtered.
* The passed function will be called with each object in the collection.
* If the function returns `true`, the value is included otherwise it is filtered.
* @param {Function} fn The function to be called, it will receive the args `o` (the object), `k` (the key)
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this MixedCollection.
* @return {Ext.util.MixedCollection} The new filtered collection.
*/
filterBy: function(fn, scope) {
var me = this,
newMC = new this.self(),
keys = me.keys,
items = me.items,
length = items.length,
i;
newMC.getKey = me.getKey;
for (i = 0; i < length; i++) {
if (fn.call(scope || me, items[i], keys[i])) {
newMC.add(keys[i], items[i]);
}
}
return newMC;
},
/**
* Finds the index of the first matching object in this collection by a specific property/value.
* @param {String} property The name of a property on your objects.
* @param {String/RegExp} value A string that the property values.
* should start with or a RegExp to test against the property.
* @param {Number} [start=0] (optional) The index to start searching at.
* @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning.
* @param {Boolean} caseSensitive (optional) `true` for case sensitive comparison.
* @return {Number} The matched index or -1.
*/
findIndex: function(property, value, start, anyMatch, caseSensitive){
if(Ext.isEmpty(value, false)){
return -1;
}
value = this.createValueMatcher(value, anyMatch, caseSensitive);
return this.findIndexBy(function(o){
return o && value.test(o[property]);
}, null, start);
},
/**
* Find the index of the first matching object in this collection by a function.
* If the function returns `true` it is considered a match.
* @param {Function} fn The function to be called, it will receive the args `o` (the object), `k` (the key).
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this MixedCollection.
* @param {Number} [start=0] (optional) The index to start searching at.
* @return {Number} The matched index or -1.
*/
findIndexBy: function(fn, scope, start){
var me = this,
keys = me.keys,
items = me.items,
i = start || 0,
len = items.length;
for (; i < len; i++) {
if (fn.call(scope || me, items[i], keys[i])) {
return i;
}
}
return -1;
},
/**
* Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering,
* and by Ext.data.Store#filter
* @private
* @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe
* @param {Boolean} [anyMatch=false] `true` to allow any match - no regex start/end line anchors will be added.
* @param {Boolean} [caseSensitive=false] `true` to make the regex case sensitive (adds 'i' switch to regex).
* @param {Boolean} [exactMatch=false] `true` to force exact match (^ and $ characters added to the regex). Ignored if `anyMatch` is `true`.
*/
createValueMatcher: function(value, anyMatch, caseSensitive, exactMatch) {
if (!value.exec) { // not a regex
var er = Ext.String.escapeRegex;
value = String(value);
if (anyMatch === true) {
value = er(value);
} else {
value = '^' + er(value);
if (exactMatch === true) {
value += '$';
}
}
value = new RegExp(value, caseSensitive ? '' : 'i');
}
return value;
},
/**
* Creates a shallow copy of this collection.
* @return {Ext.util.MixedCollection}
*/
clone: function() {
var me = this,
copy = new this.self(),
keys = me.keys,
items = me.items,
i = 0,
len = items.length;
for(; i < len; i++){
copy.add(keys[i], items[i]);
}
copy.getKey = me.getKey;
return copy;
}
});
/**
* 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. Valid values are "ASC", and "DESC".
*/
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);
},
// <debug>
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;
},
// </debug>
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"));
}
});
/**
* @docauthor Tommy Maintz <tommy@sencha.com>
*
* A mixin which allows a data component to be sorted. This is used by e.g. {@link Ext.data.Store} and {@link Ext.data.TreeStore}.
*
* __Note:__ This mixin is mainly for internal library use and most users should not need to use it directly. It
* is more likely you will want to use one of the component classes that import this mixin, such as
* {@link Ext.data.Store} or {@link Ext.data.TreeStore}.
*/
Ext.define("Ext.util.Sortable", {
extend: 'Ext.mixin.Mixin',
/**
* @property {Boolean} isSortable
* Flag denoting that this object is sortable. Always `true`.
* @readonly
*/
isSortable: true,
mixinConfig: {
hooks: {
destroy: 'destroy'
}
},
/**
* @property {String} defaultSortDirection
* The default sort direction to use if one is not specified.
*/
defaultSortDirection: "ASC",
requires: [
'Ext.util.Sorter'
],
/**
* @property {String} sortRoot
* The property in each item that contains the data to sort.
*/
/**
* Performs initialization of this mixin. Component classes using this mixin should call this method during their
* own initialization.
*/
initSortable: function() {
var me = this,
sorters = me.sorters;
/**
* @property {Ext.util.MixedCollection} sorters
* The collection of {@link Ext.util.Sorter Sorters} currently applied to this Store
*/
me.sorters = Ext.create('Ext.util.AbstractMixedCollection', false, function(item) {
return item.id || item.property;
});
if (sorters) {
me.sorters.addAll(me.decodeSorters(sorters));
}
},
/**
* Sorts the data in the Store by one or more of its properties. Example usage:
*
* //sort by a single field
* myStore.sort('myField', 'DESC');
*
* //sorting by multiple fields
* myStore.sort([
* {
* property : 'age',
* direction: 'ASC'
* },
* {
* property : 'name',
* direction: 'DESC'
* }
* ]);
*
* Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter} instances, and delegates
* the actual sorting to its internal {@link Ext.util.MixedCollection}.
*
* When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field, so this code:
*
* store.sort('myField');
* store.sort('myField');
*
* Is equivalent to this code, because Store handles the toggling automatically:
*
* store.sort('myField', 'ASC');
* store.sort('myField', 'DESC');
*
* @param {String/Ext.util.Sorter[]} sorters Either a string name of one of the fields in this Store's configured
* {@link Ext.data.Model Model}, or an array of sorter configurations.
* @param {String} [direction="ASC"] The overall direction to sort the data by.
* @param {String} [where]
* @param {Boolean} [doSort]
* @return {Ext.util.Sorter[]}
*/
sort: function(sorters, direction, where, doSort) {
var me = this,
sorter, sorterFn,
newSorters;
if (Ext.isArray(sorters)) {
doSort = where;
where = direction;
newSorters = sorters;
}
else if (Ext.isObject(sorters)) {
doSort = where;
where = direction;
newSorters = [sorters];
}
else if (Ext.isString(sorters)) {
sorter = me.sorters.get(sorters);
if (!sorter) {
sorter = {
property : sorters,
direction: direction
};
newSorters = [sorter];
}
else if (direction === undefined) {
sorter.toggle();
}
else {
sorter.setDirection(direction);
}
}
if (newSorters && newSorters.length) {
newSorters = me.decodeSorters(newSorters);
if (Ext.isString(where)) {
if (where === 'prepend') {
sorters = me.sorters.clone().items;
me.sorters.clear();
me.sorters.addAll(newSorters);
me.sorters.addAll(sorters);
}
else {
me.sorters.addAll(newSorters);
}
}
else {
me.sorters.clear();
me.sorters.addAll(newSorters);
}
if (doSort !== false) {
me.onBeforeSort(newSorters);
}
}
if (doSort !== false) {
sorters = me.sorters.items;
if (sorters.length) {
//construct an amalgamated sorter function which combines all of the Sorters passed
sorterFn = function(r1, r2) {
var result = sorters[0].sort(r1, r2),
length = sorters.length,
i;
//if we have more than one sorter, OR any additional sorter functions together
for (i = 1; i < length; i++) {
result = result || sorters[i].sort.call(this, r1, r2);
}
return result;
};
me.doSort(sorterFn);
}
}
return sorters;
},
onBeforeSort: Ext.emptyFn,
/**
* @private
* Normalizes an array of sorter objects, ensuring that they are all {@link Ext.util.Sorter} instances.
* @param {Array} sorters The sorters array.
* @return {Array} Array of {@link Ext.util.Sorter} objects.
*/
decodeSorters: function(sorters) {
if (!Ext.isArray(sorters)) {
if (sorters === undefined) {
sorters = [];
} else {
sorters = [sorters];
}
}
var length = sorters.length,
Sorter = Ext.util.Sorter,
fields = this.model ? this.model.prototype.fields : null,
field,
config, i;
for (i = 0; i < length; i++) {
config = sorters[i];
if (!(config instanceof Sorter)) {
if (Ext.isString(config)) {
config = {
property: config
};
}
Ext.applyIf(config, {
root : this.sortRoot,
direction: "ASC"
});
if (config.fn) {
config.sorterFn = config.fn;
}
//support a function to be passed as a sorter definition
if (typeof config == 'function') {
config = {
sorterFn: config
};
}
// ensure sortType gets pushed on if necessary
if (fields && !config.transform) {
field = fields.get(config.property);
config.transform = field ? field.sortType : undefined;
}
sorters[i] = Ext.create('Ext.util.Sorter', config);
}
}
return sorters;
},
getSorters: function() {
return this.sorters.items;
},
destroy: function () {
this.callSuper();
Ext.destroy(this.sorters);
}
});
/**
* Represents a collection of a set of key and value pairs. Each key in the MixedCollection must be unique, the same key
* cannot exist twice. This collection is ordered, items in the collection can be accessed by index or via the key.
* Newly added items are added to the end of the collection. This class is similar to {@link Ext.util.HashMap} however
* it is heavier and provides more functionality. Sample usage:
*
* @example
* var coll = new Ext.util.MixedCollection();
* coll.add('key1', 'val1');
* coll.add('key2', 'val2');
* coll.add('key3', 'val3');
*
* alert(coll.get('key1')); // 'val1'
* alert(coll.indexOfKey('key3')); // 2
*
* The MixedCollection also has support for sorting and filtering of the values in the collection.
*
* @example
* var coll = new Ext.util.MixedCollection();
* coll.add('key1', 100);
* coll.add('key2', -100);
* coll.add('key3', 17);
* coll.add('key4', 0);
* var biggerThanZero = coll.filterBy(function(value){
* return value > 0;
* });
* alert(biggerThanZero.getCount()); // 2
*/
Ext.define('Ext.util.MixedCollection', {
extend: 'Ext.util.AbstractMixedCollection',
mixins: {
sortable: 'Ext.util.Sortable'
},
/**
* @event sort
* Fires whenever MixedCollection is sorted.
* @param {Ext.util.MixedCollection} this
*/
constructor: function() {
var me = this;
me.callParent(arguments);
me.mixins.sortable.initSortable.call(me);
},
doSort: function(sorterFn) {
this.sortBy(sorterFn);
},
/**
* @private
* Performs the actual sorting based on a direction and a sorting function. Internally,
* this creates a temporary array of all items in the MixedCollection, sorts it and then writes
* the sorted array data back into `this.items` and `this.keys`.
* @param {String} property Property to sort by ('key', 'value', or 'index')
* @param {String} [dir=ASC] (optional) Direction to sort 'ASC' or 'DESC'.
* @param {Function} fn (optional) Comparison function that defines the sort order.
* Defaults to sorting by numeric value.
*/
_sort: function(property, dir, fn){
var me = this,
i, len,
dsc = String(dir).toUpperCase() == 'DESC' ? -1 : 1,
//this is a temporary array used to apply the sorting function
c = [],
keys = me.keys,
items = me.items;
//default to a simple sorter function if one is not provided
fn = fn || function(a, b) {
return a - b;
};
//copy all the items into a temporary array, which we will sort
for(i = 0, len = items.length; i < len; i++){
c[c.length] = {
key : keys[i],
value: items[i],
index: i
};
}
//sort the temporary array
Ext.Array.sort(c, function(a, b){
var v = fn(a[property], b[property]) * dsc;
if(v === 0){
v = (a.index < b.index ? -1 : 1);
}
return v;
});
//copy the temporary array back into the main this.items and this.keys objects
for(i = 0, len = c.length; i < len; i++){
items[i] = c[i].value;
keys[i] = c[i].key;
}
me.fireEvent('sort', me);
},
/**
* Sorts the collection by a single sorter function.
* @param {Function} sorterFn The function to sort by.
*/
sortBy: function(sorterFn) {
var me = this,
items = me.items,
keys = me.keys,
length = items.length,
temp = [],
i;
//first we create a copy of the items array so that we can sort it
for (i = 0; i < length; i++) {
temp[i] = {
key : keys[i],
value: items[i],
index: i
};
}
Ext.Array.sort(temp, function(a, b) {
var v = sorterFn(a.value, b.value);
if (v === 0) {
v = (a.index < b.index ? -1 : 1);
}
return v;
});
//copy the temporary array back into the main this.items and this.keys objects
for (i = 0; i < length; i++) {
items[i] = temp[i].value;
keys[i] = temp[i].key;
}
me.fireEvent('sort', me, items, keys);
},
/**
* Reorders each of the items based on a mapping from old index to new index. Internally this just translates into a
* sort. The `sort` event is fired whenever reordering has occured.
* @param {Object} mapping Mapping from old item index to new item index.
*/
reorder: function(mapping) {
var me = this,
items = me.items,
index = 0,
length = items.length,
order = [],
remaining = [],
oldIndex;
me.suspendEvents();
//object of {oldPosition: newPosition} reversed to {newPosition: oldPosition}
for (oldIndex in mapping) {
order[mapping[oldIndex]] = items[oldIndex];
}
for (index = 0; index < length; index++) {
if (mapping[index] == undefined) {
remaining.push(items[index]);
}
}
for (index = 0; index < length; index++) {
if (order[index] == undefined) {
order[index] = remaining.shift();
}
}
me.clear();
me.addAll(order);
me.resumeEvents();
me.fireEvent('sort', me);
},
/**
* Sorts this collection by **key**s.
* @param {String} [direction=ASC] 'ASC' or 'DESC'.
* @param {Function} [fn] Comparison function that defines the sort order. Defaults to sorting by case insensitive
* string.
*/
sortByKey: function(dir, fn){
this._sort('key', dir, fn || function(a, b){
var v1 = String(a).toUpperCase(), v2 = String(b).toUpperCase();
return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
});
}
});
/**
* @private
*/
Ext.define('Ext.ItemCollection', {
extend: 'Ext.util.MixedCollection',
getKey: function(item) {
return item.getItemId();
},
has: function(item) {
return this.map.hasOwnProperty(item.getId());
}
});
/**
* @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;
}
});
/**
* @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
*
* 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.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;
}
});
/**
* @class Ext.scroll.Scroller
* @author Jacky Nguyen <jacky@sencha.com>
*
* Momentum scrolling is one of the most important part of the framework's UI layer. In Sencha Touch there are
* several scroller implementations so we can have the best performance on all mobile devices and browsers.
*
* Scroller settings can be changed using the {@link Ext.Container#scrollable scrollable} configuration in
* {@link Ext.Container}. Anything you pass to that method will be passed to the scroller when it is
* instantiated in your container.
*
* Please note that the {@link Ext.Container#getScrollable} method returns an instance of {@link Ext.scroll.View}.
* So if you need to get access to the scroller after your container has been instantiated, you must used the
* {@link Ext.scroll.View#getScroller} method.
*
* // lets assume container is a container you have
* // created which is scrollable
* container.getScrollable().getScroller().setFps(10);
*
* ## Example
*
* Here is a simple example of how to adjust the scroller settings when using a {@link Ext.Container} (or anything
* that extends it).
*
* @example
* var container = Ext.create('Ext.Container', {
* fullscreen: true,
* html: 'This container is scrollable!',
* scrollable: {
* direction: 'vertical'
* }
* });
*
* As you can see, we are passing the {@link #direction} configuration into the scroller instance in our container.
*
* You can pass any of the configs below in that {@link Ext.Container#scrollable scrollable} configuration and it will
* just work.
*
* Go ahead and try it in the live code editor above!
*/
Ext.define('Ext.scroll.Scroller', {
extend: 'Ext.Evented',
requires: [
'Ext.fx.easing.BoundMomentum',
'Ext.fx.easing.EaseOut',
'Ext.util.Translatable'
],
/**
* @event maxpositionchange
* Fires whenever the maximum position has changed.
* @param {Ext.scroll.Scroller} this
* @param {Number} maxPosition The new maximum position.
*/
/**
* @event refresh
* Fires whenever the Scroller is refreshed.
* @param {Ext.scroll.Scroller} this
*/
/**
* @event scrollstart
* Fires whenever the scrolling is started.
* @param {Ext.scroll.Scroller} this
* @param {Number} x The current x position.
* @param {Number} y The current y position.
*/
/**
* @event scrollend
* Fires whenever the scrolling is ended.
* @param {Ext.scroll.Scroller} this
* @param {Number} x The current x position.
* @param {Number} y The current y position.
*/
/**
* @event scroll
* Fires whenever the Scroller is scrolled.
* @param {Ext.scroll.Scroller} this
* @param {Number} x The new x position.
* @param {Number} y The new y position.
*/
config: {
/**
* @cfg element
* @private
*/
element: null,
/**
* @cfg {String} direction
* Possible values: 'auto', 'vertical', 'horizontal', or 'both'.
* @accessor
*/
direction: 'auto',
/**
* @cfg fps
* @private
*/
fps: 'auto',
/**
* @cfg {Boolean} disabled
* Whether or not this component is disabled.
* @accessor
*/
disabled: null,
/**
* @cfg {Boolean} directionLock
* `true` to lock the direction of the scroller when the user starts scrolling.
* This is useful when putting a scroller inside a scroller or a {@link Ext.Carousel}.
* @accessor
*/
directionLock: false,
/**
* @cfg {Object} momentumEasing
* A valid config for {@link Ext.fx.easing.BoundMomentum}. The default value is:
*
* {
* momentum: {
* acceleration: 30,
* friction: 0.5
* },
* bounce: {
* acceleration: 30,
* springTension: 0.3
* }
* }
*
* Note that supplied object will be recursively merged with the default object. For example, you can simply
* pass this to change the momentum acceleration only:
*
* {
* momentum: {
* acceleration: 10
* }
* }
*
* @accessor
*/
momentumEasing: {
momentum: {
acceleration: 30,
friction: 0.5
},
bounce: {
acceleration: 30,
springTension: 0.3
},
minVelocity: 1
},
/**
* @cfg bounceEasing
* @private
*/
bounceEasing: {
duration: 400
},
/**
* @cfg outOfBoundRestrictFactor
* @private
*/
outOfBoundRestrictFactor: 0.5,
/**
* @cfg startMomentumResetTime
* @private
*/
startMomentumResetTime: 300,
/**
* @cfg maxAbsoluteVelocity
* @private
*/
maxAbsoluteVelocity: 6,
/**
* @cfg containerSize
* @private
*/
containerSize: 'auto',
/**
* @cfg size
* @private
*/
size: 'auto',
/**
* @cfg autoRefresh
* @private
*/
autoRefresh: true,
/**
* @cfg {Object/Number} initialOffset
* The initial scroller position. When specified as Number,
* both x and y will be set to that value.
*/
initialOffset: {
x: 0,
y: 0
},
/**
* @cfg {Number/Object} slotSnapSize
* The size of each slot to snap to in 'px', can be either an object with `x` and `y` values, i.e:
*
* {
* x: 50,
* y: 100
* }
*
* or a number value to be used for both directions. For example, a value of `50` will be treated as:
*
* {
* x: 50,
* y: 50
* }
*
* @accessor
*/
slotSnapSize: {
x: 0,
y: 0
},
/**
* @cfg slotSnapOffset
* @private
*/
slotSnapOffset: {
x: 0,
y: 0
},
slotSnapEasing: {
duration: 150
},
translatable: {
translationMethod: 'auto',
useWrapper: false
}
},
cls: Ext.baseCSSPrefix + 'scroll-scroller',
containerCls: Ext.baseCSSPrefix + 'scroll-container',
dragStartTime: 0,
dragEndTime: 0,
isDragging: false,
isAnimating: false,
/**
* @private
* @constructor
* @chainable
*/
constructor: function(config) {
var element = config && config.element;
if (Ext.os.is.Android4 && !Ext.browser.is.Chrome) {
this.onDrag = Ext.Function.createThrottled(this.onDrag, 20, this);
}
this.listeners = {
scope: this,
touchstart: 'onTouchStart',
touchend: 'onTouchEnd',
dragstart: 'onDragStart',
drag: 'onDrag',
dragend: 'onDragEnd'
};
this.minPosition = { x: 0, y: 0 };
this.startPosition = { x: 0, y: 0 };
this.position = { x: 0, y: 0 };
this.velocity = { x: 0, y: 0 };
this.isAxisEnabledFlags = { x: false, y: false };
this.flickStartPosition = { x: 0, y: 0 };
this.flickStartTime = { x: 0, y: 0 };
this.lastDragPosition = { x: 0, y: 0 };
this.dragDirection = { x: 0, y: 0};
this.initialConfig = config;
if (element) {
this.setElement(element);
}
return this;
},
/**
* @private
*/
applyElement: function(element) {
if (!element) {
return;
}
return Ext.get(element);
},
/**
* @private
* @chainable
*/
updateElement: function(element) {
this.initialize();
element.addCls(this.cls);
if (!this.getDisabled()) {
this.attachListeneners();
}
this.onConfigUpdate(['containerSize', 'size'], 'refreshMaxPosition');
this.on('maxpositionchange', 'snapToBoundary');
this.on('minpositionchange', 'snapToBoundary');
return this;
},
applyTranslatable: function(config, translatable) {
return Ext.factory(config, Ext.util.Translatable, translatable);
},
updateTranslatable: function(translatable) {
translatable.setConfig({
element: this.getElement(),
listeners: {
animationframe: 'onAnimationFrame',
animationend: 'onAnimationEnd',
scope: this
}
});
},
updateFps: function(fps) {
if (fps !== 'auto') {
this.getTranslatable().setFps(fps);
}
},
/**
* @private
*/
attachListeneners: function() {
this.getContainer().on(this.listeners);
},
/**
* @private
*/
detachListeners: function() {
this.getContainer().un(this.listeners);
},
/**
* @private
*/
updateDisabled: function(disabled) {
if (disabled) {
this.detachListeners();
}
else {
this.attachListeneners();
}
},
updateInitialOffset: function(initialOffset) {
if (typeof initialOffset == 'number') {
initialOffset = {
x: initialOffset,
y: initialOffset
};
}
var position = this.position,
x, y;
position.x = x = initialOffset.x;
position.y = y = initialOffset.y;
this.getTranslatable().translate(-x, -y);
},
/**
* @private
* @return {String}
*/
applyDirection: function(direction) {
var minPosition = this.getMinPosition(),
maxPosition = this.getMaxPosition(),
isHorizontal, isVertical;
this.givenDirection = direction;
if (direction === 'auto') {
isHorizontal = maxPosition.x > minPosition.x;
isVertical = maxPosition.y > minPosition.y;
if (isHorizontal && isVertical) {
direction = 'both';
}
else if (isHorizontal) {
direction = 'horizontal';
}
else {
direction = 'vertical';
}
}
return direction;
},
/**
* @private
*/
updateDirection: function(direction) {
var isAxisEnabled = this.isAxisEnabledFlags;
isAxisEnabled.x = (direction === 'both' || direction === 'horizontal');
isAxisEnabled.y = (direction === 'both' || direction === 'vertical');
},
/**
* Returns `true` if a specified axis is enabled.
* @param {String} axis The axis to check (`x` or `y`).
* @return {Boolean} `true` if the axis is enabled.
*/
isAxisEnabled: function(axis) {
this.getDirection();
return this.isAxisEnabledFlags[axis];
},
/**
* @private
* @return {Object}
*/
applyMomentumEasing: function(easing) {
var defaultClass = Ext.fx.easing.BoundMomentum;
return {
x: Ext.factory(easing, defaultClass),
y: Ext.factory(easing, defaultClass)
};
},
/**
* @private
* @return {Object}
*/
applyBounceEasing: function(easing) {
var defaultClass = Ext.fx.easing.EaseOut;
return {
x: Ext.factory(easing, defaultClass),
y: Ext.factory(easing, defaultClass)
};
},
updateBounceEasing: function(easing) {
this.getTranslatable().setEasingX(easing.x).setEasingY(easing.y);
},
/**
* @private
* @return {Object}
*/
applySlotSnapEasing: function(easing) {
var defaultClass = Ext.fx.easing.EaseOut;
return {
x: Ext.factory(easing, defaultClass),
y: Ext.factory(easing, defaultClass)
};
},
/**
* @private
* @return {Object}
*/
getMinPosition: function() {
var minPosition = this.minPosition;
if (!minPosition) {
this.minPosition = minPosition = {
x: 0,
y: 0
};
this.fireEvent('minpositionchange', this, minPosition);
}
return minPosition;
},
/**
* @private
* @return {Object}
*/
getMaxPosition: function() {
var maxPosition = this.maxPosition,
size, containerSize;
if (!maxPosition) {
size = this.getSize();
containerSize = this.getContainerSize();
this.maxPosition = maxPosition = {
x: Math.max(0, size.x - containerSize.x),
y: Math.max(0, size.y - containerSize.y)
};
this.fireEvent('maxpositionchange', this, maxPosition);
}
return maxPosition;
},
/**
* @private
*/
refreshMaxPosition: function() {
this.maxPosition = null;
this.getMaxPosition();
},
/**
* @private
* @return {Object}
*/
applyContainerSize: function(size) {
var containerDom = this.getContainer().dom,
x, y;
if (!containerDom) {
return;
}
this.givenContainerSize = size;
if (size === 'auto') {
x = containerDom.offsetWidth;
y = containerDom.offsetHeight;
}
else {
x = size.x;
y = size.y;
}
return {
x: x,
y: y
};
},
/**
* @private
* @param {String/Object} size
* @return {Object}
*/
applySize: function(size) {
var dom = this.getElement().dom,
x, y;
if (!dom) {
return;
}
this.givenSize = size;
if (size === 'auto') {
x = dom.offsetWidth;
y = dom.offsetHeight;
}
else if (typeof size == 'number') {
x = size;
y = size;
}
else {
x = size.x;
y = size.y;
}
return {
x: x,
y: y
};
},
/**
* @private
*/
updateAutoRefresh: function(autoRefresh) {
this.getElement().toggleListener(autoRefresh, 'resize', 'onElementResize', this);
this.getContainer().toggleListener(autoRefresh, 'resize', 'onContainerResize', this);
},
applySlotSnapSize: function(snapSize) {
if (typeof snapSize == 'number') {
return {
x: snapSize,
y: snapSize
};
}
return snapSize;
},
applySlotSnapOffset: function(snapOffset) {
if (typeof snapOffset == 'number') {
return {
x: snapOffset,
y: snapOffset
};
}
return snapOffset;
},
/**
* @private
* Returns the container for this scroller
*/
getContainer: function() {
var container = this.container;
if (!container) {
this.container = container = this.getElement().getParent();
//<debug error>
if (!container) {
Ext.Logger.error("Making an element scrollable that doesn't have any container");
}
//</debug>
container.addCls(this.containerCls);
}
return container;
},
/**
* @private
* @return {Ext.scroll.Scroller} this
* @chainable
*/
refresh: function() {
this.stopAnimation();
this.getTranslatable().refresh();
this.setSize(this.givenSize);
this.setContainerSize(this.givenContainerSize);
this.setDirection(this.givenDirection);
this.fireEvent('refresh', this);
return this;
},
onElementResize: function(element, info) {
this.setSize({
x: info.width,
y: info.height
});
this.refresh();
},
onContainerResize: function(container, info) {
this.setContainerSize({
x: info.width,
y: info.height
});
this.refresh();
},
/**
* Scrolls to the given location.
*
* @param {Number} x The scroll position on the x axis.
* @param {Number} y The scroll position on the y axis.
* @param {Boolean/Object} animation (optional) Whether or not to animate the scrolling to the new position.
*
* @return {Ext.scroll.Scroller} this
* @chainable
*/
scrollTo: function(x, y, animation) {
var translatable = this.getTranslatable(),
position = this.position,
positionChanged = false,
translationX, translationY;
if (this.isAxisEnabled('x')) {
if (typeof x != 'number') {
x = position.x;
}
else {
if (position.x !== x) {
position.x = x;
positionChanged = true;
}
}
translationX = -x;
}
if (this.isAxisEnabled('y')) {
if (typeof y != 'number') {
y = position.y;
}
else {
if (position.y !== y) {
position.y = y;
positionChanged = true;
}
}
translationY = -y;
}
if (positionChanged) {
if (animation !== undefined) {
translatable.translateAnimated(translationX, translationY, animation);
}
else {
this.fireEvent('scroll', this, position.x, position.y);
translatable.translate(translationX, translationY);
}
}
return this;
},
/**
* @private
* @return {Ext.scroll.Scroller} this
* @chainable
*/
scrollToTop: function(animation) {
var initialOffset = this.getInitialOffset();
return this.scrollTo(initialOffset.x, initialOffset.y, animation);
},
/**
* Scrolls to the end of the scrollable view.
* @return {Ext.scroll.Scroller} this
* @chainable
*/
scrollToEnd: function(animation) {
var size = this.getSize(),
cntSize = this.getContainerSize();
return this.scrollTo(size.x - cntSize.x, size.y - cntSize.y, animation);
},
/**
* Change the scroll offset by the given amount.
* @param {Number} x The offset to scroll by on the x axis.
* @param {Number} y The offset to scroll by on the y axis.
* @param {Boolean/Object} animation (optional) Whether or not to animate the scrolling to the new position.
* @return {Ext.scroll.Scroller} this
* @chainable
*/
scrollBy: function(x, y, animation) {
var position = this.position;
x = (typeof x == 'number') ? x + position.x : null;
y = (typeof y == 'number') ? y + position.y : null;
return this.scrollTo(x, y, animation);
},
/**
* @private
*/
onTouchStart: function() {
this.isTouching = true;
this.stopAnimation();
},
/**
* @private
*/
onTouchEnd: function() {
var position = this.position;
this.isTouching = false;
if (!this.isDragging && this.snapToSlot()) {
this.fireEvent('scrollstart', this, position.x, position.y);
}
},
/**
* @private
*/
onDragStart: function(e) {
var direction = this.getDirection(),
absDeltaX = e.absDeltaX,
absDeltaY = e.absDeltaY,
directionLock = this.getDirectionLock(),
startPosition = this.startPosition,
flickStartPosition = this.flickStartPosition,
flickStartTime = this.flickStartTime,
lastDragPosition = this.lastDragPosition,
currentPosition = this.position,
dragDirection = this.dragDirection,
x = currentPosition.x,
y = currentPosition.y,
now = Ext.Date.now();
this.isDragging = true;
if (directionLock && direction !== 'both') {
if ((direction === 'horizontal' && absDeltaX > absDeltaY)
|| (direction === 'vertical' && absDeltaY > absDeltaX)) {
e.stopPropagation();
}
else {
this.isDragging = false;
return;
}
}
lastDragPosition.x = x;
lastDragPosition.y = y;
flickStartPosition.x = x;
flickStartPosition.y = y;
startPosition.x = x;
startPosition.y = y;
flickStartTime.x = now;
flickStartTime.y = now;
dragDirection.x = 0;
dragDirection.y = 0;
this.dragStartTime = now;
this.isDragging = true;
this.fireEvent('scrollstart', this, x, y);
},
/**
* @private
*/
onAxisDrag: function(axis, delta) {
if (!this.isAxisEnabled(axis)) {
return;
}
var flickStartPosition = this.flickStartPosition,
flickStartTime = this.flickStartTime,
lastDragPosition = this.lastDragPosition,
dragDirection = this.dragDirection,
old = this.position[axis],
min = this.getMinPosition()[axis],
max = this.getMaxPosition()[axis],
start = this.startPosition[axis],
last = lastDragPosition[axis],
current = start - delta,
lastDirection = dragDirection[axis],
restrictFactor = this.getOutOfBoundRestrictFactor(),
startMomentumResetTime = this.getStartMomentumResetTime(),
now = Ext.Date.now(),
distance;
if (current < min) {
current *= restrictFactor;
}
else if (current > max) {
distance = current - max;
current = max + distance * restrictFactor;
}
if (current > last) {
dragDirection[axis] = 1;
}
else if (current < last) {
dragDirection[axis] = -1;
}
if ((lastDirection !== 0 && (dragDirection[axis] !== lastDirection))
|| (now - flickStartTime[axis]) > startMomentumResetTime) {
flickStartPosition[axis] = old;
flickStartTime[axis] = now;
}
lastDragPosition[axis] = current;
},
/**
* @private
*/
onDrag: function(e) {
if (!this.isDragging) {
return;
}
var lastDragPosition = this.lastDragPosition;
this.onAxisDrag('x', e.deltaX);
this.onAxisDrag('y', e.deltaY);
this.scrollTo(lastDragPosition.x, lastDragPosition.y);
},
/**
* @private
*/
onDragEnd: function(e) {
var easingX, easingY;
if (!this.isDragging) {
return;
}
this.dragEndTime = Ext.Date.now();
this.onDrag(e);
this.isDragging = false;
easingX = this.getAnimationEasing('x');
easingY = this.getAnimationEasing('y');
if (easingX || easingY) {
this.getTranslatable().animate(easingX, easingY);
}
else {
this.onScrollEnd();
}
},
/**
* @private
*/
getAnimationEasing: function(axis) {
if (!this.isAxisEnabled(axis)) {
return null;
}
var currentPosition = this.position[axis],
flickStartPosition = this.flickStartPosition[axis],
flickStartTime = this.flickStartTime[axis],
minPosition = this.getMinPosition()[axis],
maxPosition = this.getMaxPosition()[axis],
maxAbsVelocity = this.getMaxAbsoluteVelocity(),
boundValue = null,
dragEndTime = this.dragEndTime,
easing, velocity, duration;
if (currentPosition < minPosition) {
boundValue = minPosition;
}
else if (currentPosition > maxPosition) {
boundValue = maxPosition;
}
// Out of bound, to be pulled back
if (boundValue !== null) {
easing = this.getBounceEasing()[axis];
easing.setConfig({
startTime: dragEndTime,
startValue: -currentPosition,
endValue: -boundValue
});
return easing;
}
// Still within boundary, start deceleration
duration = dragEndTime - flickStartTime;
if (duration === 0) {
return null;
}
velocity = (currentPosition - flickStartPosition) / (dragEndTime - flickStartTime);
if (velocity === 0) {
return null;
}
if (velocity < -maxAbsVelocity) {
velocity = -maxAbsVelocity;
}
else if (velocity > maxAbsVelocity) {
velocity = maxAbsVelocity;
}
easing = this.getMomentumEasing()[axis];
easing.setConfig({
startTime: dragEndTime,
startValue: -currentPosition,
startVelocity: -velocity,
minMomentumValue: -maxPosition,
maxMomentumValue: 0
});
return easing;
},
/**
* @private
*/
onAnimationFrame: function(translatable, x, y) {
var position = this.position;
position.x = -x;
position.y = -y;
this.fireEvent('scroll', this, position.x, position.y);
},
/**
* @private
*/
onAnimationEnd: function() {
this.snapToBoundary();
this.onScrollEnd();
},
/**
* @private
* Stops the animation of the scroller at any time.
*/
stopAnimation: function() {
this.getTranslatable().stopAnimation();
},
/**
* @private
*/
onScrollEnd: function() {
var position = this.position;
if (this.isTouching || !this.snapToSlot()) {
this.fireEvent('scrollend', this, position.x, position.y);
}
},
/**
* @private
* @return {Boolean}
*/
snapToSlot: function() {
var snapX = this.getSnapPosition('x'),
snapY = this.getSnapPosition('y'),
easing = this.getSlotSnapEasing();
if (snapX !== null || snapY !== null) {
this.scrollTo(snapX, snapY, {
easingX: easing.x,
easingY: easing.y
});
return true;
}
return false;
},
/**
* @private
* @return {Number/null}
*/
getSnapPosition: function(axis) {
var snapSize = this.getSlotSnapSize()[axis],
snapPosition = null,
position, snapOffset, maxPosition, mod;
if (snapSize !== 0 && this.isAxisEnabled(axis)) {
position = this.position[axis];
snapOffset = this.getSlotSnapOffset()[axis];
maxPosition = this.getMaxPosition()[axis];
mod = (position - snapOffset) % snapSize;
if (mod !== 0) {
if (Math.abs(mod) > snapSize / 2) {
snapPosition = position + ((mod > 0) ? snapSize - mod : mod - snapSize);
if (snapPosition > maxPosition) {
snapPosition = position - mod;
}
}
else {
snapPosition = position - mod;
}
}
}
return snapPosition;
},
/**
* @private
*/
snapToBoundary: function() {
var position = this.position,
minPosition = this.getMinPosition(),
maxPosition = this.getMaxPosition(),
minX = minPosition.x,
minY = minPosition.y,
maxX = maxPosition.x,
maxY = maxPosition.y,
x = Math.round(position.x),
y = Math.round(position.y);
if (x < minX) {
x = minX;
}
else if (x > maxX) {
x = maxX;
}
if (y < minY) {
y = minY;
}
else if (y > maxY) {
y = maxY;
}
this.scrollTo(x, y);
},
destroy: function() {
var element = this.getElement(),
sizeMonitors = this.sizeMonitors;
if (sizeMonitors) {
sizeMonitors.element.destroy();
sizeMonitors.container.destroy();
}
if (element && !element.isDestroyed) {
element.removeCls(this.cls);
this.getContainer().removeCls(this.containerCls);
}
Ext.destroy(this.getTranslatable());
this.callParent(arguments);
}
}, function() {
});
/**
* @private
*/
Ext.define('Ext.scroll.indicator.Abstract', {
extend: 'Ext.Component',
config: {
baseCls: 'x-scroll-indicator',
axis: 'x',
value: 0,
length: null,
minLength: 6,
hidden: true,
ui: 'dark'
},
cachedConfig: {
ratio: 1,
barCls: 'x-scroll-bar',
active: true
},
barElement: null,
barLength: 0,
gapLength: 0,
getElementConfig: function() {
return {
reference: 'barElement',
children: [this.callParent()]
};
},
applyRatio: function(ratio) {
if (isNaN(ratio)) {
ratio = 1;
}
return ratio;
},
refresh: function() {
var bar = this.barElement,
barDom = bar.dom,
ratio = this.getRatio(),
axis = this.getAxis(),
barLength = (axis === 'x') ? barDom.offsetWidth : barDom.offsetHeight,
length = barLength * ratio;
this.barLength = barLength;
this.gapLength = barLength - length;
this.setLength(length);
this.updateValue(this.getValue());
},
updateBarCls: function(barCls) {
this.barElement.addCls(barCls);
},
updateAxis: function(axis) {
this.element.addCls(this.getBaseCls(), null, axis);
this.barElement.addCls(this.getBarCls(), null, axis);
},
updateValue: function(value) {
this.setOffset(this.gapLength * value);
},
updateActive: function(active) {
this.barElement[active ? 'addCls' : 'removeCls']('active');
},
doSetHidden: function(hidden) {
var elementDomStyle = this.element.dom.style;
if (hidden) {
elementDomStyle.opacity = '0';
}
else {
elementDomStyle.opacity = '';
}
},
applyLength: function(length) {
return Math.max(this.getMinLength(), length);
},
updateLength: function(length) {
if (!this.isDestroyed) {
var axis = this.getAxis(),
element = this.element;
if (axis === 'x') {
element.setWidth(length);
}
else {
element.setHeight(length);
}
}
},
setOffset: function(offset) {
var axis = this.getAxis(),
element = this.element;
if (axis === 'x') {
element.setLeft(offset);
}
else {
element.setTop(offset);
}
}
});
/**
* @private
*/
Ext.define('Ext.scroll.indicator.Default', {
extend: 'Ext.scroll.indicator.Abstract',
config: {
cls: 'default'
},
setOffset: function(offset) {
var axis = this.getAxis(),
domStyle = this.element.dom.style;
if (axis === 'x') {
domStyle.webkitTransform = 'translate3d(' + offset + 'px, 0, 0)';
}
else {
domStyle.webkitTransform = 'translate3d(0, ' + offset + 'px, 0)';
}
},
updateValue: function(value) {
var barLength = this.barLength,
gapLength = this.gapLength,
length = this.getLength(),
newLength, offset, extra;
if (value <= 0) {
offset = 0;
this.updateLength(this.applyLength(length + value * barLength));
}
else if (value >= 1) {
extra = Math.round((value - 1) * barLength);
newLength = this.applyLength(length - extra);
extra = length - newLength;
this.updateLength(newLength);
offset = gapLength + extra;
}
else {
offset = gapLength * value;
}
this.setOffset(offset);
}
});
/**
* @private
*/
Ext.define('Ext.scroll.indicator.ScrollPosition', {
extend: 'Ext.scroll.indicator.Abstract',
config: {
cls: 'scrollposition'
},
getElementConfig: function() {
var config = this.callParent(arguments);
config.children.unshift({
className: 'x-scroll-bar-stretcher'
});
return config;
},
updateValue: function(value) {
if (this.gapLength === 0) {
if (value > 1) {
value = value - 1;
}
this.setOffset(this.barLength * value);
}
else {
this.setOffset(this.gapLength * value);
}
},
updateLength: function() {
var scrollOffset = this.barLength,
barDom = this.barElement.dom,
element = this.element;
this.callParent(arguments);
if (this.getAxis() === 'x') {
barDom.scrollLeft = scrollOffset;
element.setLeft(scrollOffset);
}
else {
barDom.scrollTop = scrollOffset;
element.setTop(scrollOffset);
}
},
setOffset: function(offset) {
var barLength = this.barLength,
minLength = this.getMinLength(),
barDom = this.barElement.dom;
offset = Math.min(barLength - minLength, Math.max(offset, minLength - this.getLength()));
offset = barLength - offset;
if (this.getAxis() === 'x') {
barDom.scrollLeft = offset;
}
else {
barDom.scrollTop = offset;
}
}
});
/**
* @private
*/
Ext.define('Ext.scroll.indicator.CssTransform', {
extend: 'Ext.scroll.indicator.Abstract',
config: {
cls: 'csstransform'
},
getElementConfig: function() {
var config = this.callParent();
config.children[0].children = [
{
reference: 'startElement'
},
{
reference: 'middleElement'
},
{
reference: 'endElement'
}
];
return config;
},
refresh: function() {
var axis = this.getAxis(),
startElementDom = this.startElement.dom,
endElementDom = this.endElement.dom,
middleElement = this.middleElement,
startElementLength, endElementLength;
if (axis === 'x') {
startElementLength = startElementDom.offsetWidth;
endElementLength = endElementDom.offsetWidth;
middleElement.setLeft(startElementLength);
}
else {
startElementLength = startElementDom.offsetHeight;
endElementLength = endElementDom.offsetHeight;
middleElement.setTop(startElementLength);
}
this.startElementLength = startElementLength;
this.endElementLength = endElementLength;
this.callParent();
},
updateLength: function(length) {
var axis = this.getAxis(),
endElementStyle = this.endElement.dom.style,
middleElementStyle = this.middleElement.dom.style,
endElementLength = this.endElementLength,
endElementOffset = length - endElementLength,
middleElementLength = endElementOffset - this.startElementLength;
if (axis === 'x') {
endElementStyle.webkitTransform = 'translate3d(' + endElementOffset + 'px, 0, 0)';
middleElementStyle.webkitTransform = 'translate3d(0, 0, 0) scaleX(' + middleElementLength + ')';
}
else {
endElementStyle.webkitTransform = 'translate3d(0, ' + endElementOffset + 'px, 0)';
middleElementStyle.webkitTransform = 'translate3d(0, 0, 0) scaleY(' + middleElementLength + ')';
}
},
updateValue: function(value) {
var barLength = this.barLength,
gapLength = this.gapLength,
length = this.getLength(),
newLength, offset, extra;
if (value <= 0) {
offset = 0;
this.updateLength(this.applyLength(length + value * barLength));
}
else if (value >= 1) {
extra = Math.round((value - 1) * barLength);
newLength = this.applyLength(length - extra);
extra = length - newLength;
this.updateLength(newLength);
offset = gapLength + extra;
}
else {
offset = gapLength * value;
}
this.setOffset(offset);
},
setOffset: function(offset) {
var axis = this.getAxis(),
elementStyle = this.element.dom.style;
offset = Math.round(offset);
if (axis === 'x') {
elementStyle.webkitTransform = 'translate3d(' + offset + 'px, 0, 0)';
}
else {
elementStyle.webkitTransform = 'translate3d(0, ' + offset + 'px, 0)';
}
}
});
/**
* @private
*/
Ext.define('Ext.scroll.indicator.Throttled', {
extend:'Ext.scroll.indicator.Default',
config: {
cls: 'throttled'
},
constructor: function() {
this.callParent(arguments);
this.updateLength = Ext.Function.createThrottled(this.updateLength, 75, this);
this.setOffset = Ext.Function.createThrottled(this.setOffset, 50, this);
},
doSetHidden: function(hidden) {
if (hidden) {
this.setOffset(-10000);
} else {
delete this.lastLength;
delete this.lastOffset;
this.updateValue(this.getValue());
}
},
updateLength: function(length) {
length = Math.round(length);
if (this.lastLength === length || this.lastOffset === -10000) {
return;
}
this.lastLength = length;
Ext.TaskQueue.requestWrite('doUpdateLength', this,[length]);
},
doUpdateLength: function(length){
if (!this.isDestroyed) {
var axis = this.getAxis(),
element = this.element;
if (axis === 'x') {
element.setWidth(length);
}
else {
element.setHeight(length);
}
}
},
setOffset: function(offset) {
offset = Math.round(offset);
if (this.lastOffset === offset || this.lastOffset === -10000) {
return;
}
this.lastOffset = offset;
Ext.TaskQueue.requestWrite('doSetOffset', this,[offset]);
},
doSetOffset: function(offset) {
if (!this.isDestroyed) {
var axis = this.getAxis(),
domStyle = this.element.dom.style;
if (axis === 'x') {
domStyle.webkitTransform = 'translate3d(' + offset + 'px, 0, 0)';
}
else {
domStyle.webkitTransform = 'translate3d(0, ' + offset + 'px, 0)';
}
}
}
});
/**
* @private
*/
Ext.define('Ext.scroll.Indicator', {
requires: [
'Ext.scroll.indicator.Default',
'Ext.scroll.indicator.ScrollPosition',
'Ext.scroll.indicator.CssTransform',
'Ext.scroll.indicator.Throttled'
],
alternateClassName: 'Ext.util.Indicator',
constructor: function(config) {
if (Ext.os.is.Android2 || Ext.os.is.Android3 || Ext.browser.is.ChromeMobile) {
return new Ext.scroll.indicator.ScrollPosition(config);
}
else if (Ext.os.is.iOS) {
return new Ext.scroll.indicator.CssTransform(config);
}
else if (Ext.os.is.Android4) {
return new Ext.scroll.indicator.Throttled(config);
}
else {
return new Ext.scroll.indicator.Default(config);
}
}
});
/**
* This is a simple container that is used to compile content and a {@link Ext.scroll.View} instance. It also
* provides scroll indicators.
*
* 99% of the time all you need to use in this class is {@link #getScroller}.
*
* This should never should be extended.
*/
Ext.define('Ext.scroll.View', {
extend: 'Ext.Evented',
alternateClassName: 'Ext.util.ScrollView',
requires: [
'Ext.scroll.Scroller',
'Ext.scroll.Indicator'
],
config: {
/**
* @cfg {String} indicatorsUi
* The style of the indicators of this view. Available options are `dark` or `light`.
*/
indicatorsUi: 'dark',
element: null,
scroller: {},
indicators: {
x: {
axis: 'x'
},
y: {
axis: 'y'
}
},
indicatorsHidingDelay: 100,
cls: Ext.baseCSSPrefix + 'scroll-view'
},
/**
* @method getScroller
* Returns the scroller instance in this view. Checkout the documentation of {@link Ext.scroll.Scroller} and
* {@link Ext.Container#getScrollable} for more information.
* @return {Ext.scroll.View} The scroller
*/
/**
* @private
*/
processConfig: function(config) {
if (!config) {
return null;
}
if (typeof config == 'string') {
config = {
direction: config
};
}
config = Ext.merge({}, config);
var scrollerConfig = config.scroller,
name;
if (!scrollerConfig) {
config.scroller = scrollerConfig = {};
}
for (name in config) {
if (config.hasOwnProperty(name)) {
if (!this.hasConfig(name)) {
scrollerConfig[name] = config[name];
delete config[name];
}
}
}
return config;
},
constructor: function(config) {
config = this.processConfig(config);
this.useIndicators = { x: true, y: true };
this.doHideIndicators = Ext.Function.bind(this.doHideIndicators, this);
this.initConfig(config);
},
setConfig: function(config) {
return this.callParent([this.processConfig(config)]);
},
updateIndicatorsUi: function(newUi) {
var indicators = this.getIndicators();
indicators.x.setUi(newUi);
indicators.y.setUi(newUi);
},
applyScroller: function(config, currentScroller) {
return Ext.factory(config, Ext.scroll.Scroller, currentScroller);
},
applyIndicators: function(config, indicators) {
var defaultClass = Ext.scroll.Indicator,
useIndicators = this.useIndicators;
if (!config) {
config = {};
}
if (!config.x) {
useIndicators.x = false;
config.x = {};
}
if (!config.y) {
useIndicators.y = false;
config.y = {};
}
return {
x: Ext.factory(config.x, defaultClass, indicators && indicators.x),
y: Ext.factory(config.y, defaultClass, indicators && indicators.y)
};
},
updateIndicators: function(indicators) {
this.indicatorsGrid = Ext.Element.create({
className: 'x-scroll-bar-grid-wrapper',
children: [{
className: 'x-scroll-bar-grid',
children: [
{
children: [{}, {
children: [indicators.y.barElement]
}]
},
{
children: [{
children: [indicators.x.barElement]
}, {}]
}
]
}]
});
},
updateScroller: function(scroller) {
scroller.on({
scope: this,
scrollstart: 'onScrollStart',
scroll: 'onScroll',
scrollend: 'onScrollEnd',
refresh: 'refreshIndicators'
});
},
isAxisEnabled: function(axis) {
return this.getScroller().isAxisEnabled(axis) && this.useIndicators[axis];
},
applyElement: function(element) {
if (element) {
return Ext.get(element);
}
},
updateElement: function(element) {
var scrollerElement = element.getFirstChild().getFirstChild(),
scroller = this.getScroller();
element.addCls(this.getCls());
element.insertFirst(this.indicatorsGrid);
scroller.setElement(scrollerElement);
this.refreshIndicators();
return this;
},
showIndicators: function() {
var indicators = this.getIndicators();
if (this.hasOwnProperty('indicatorsHidingTimer')) {
clearTimeout(this.indicatorsHidingTimer);
delete this.indicatorsHidingTimer;
}
if (this.isAxisEnabled('x')) {
indicators.x.show();
}
if (this.isAxisEnabled('y')) {
indicators.y.show();
}
},
hideIndicators: function() {
var delay = this.getIndicatorsHidingDelay();
if (delay > 0) {
this.indicatorsHidingTimer = setTimeout(this.doHideIndicators, delay);
}
else {
this.doHideIndicators();
}
},
doHideIndicators: function() {
var indicators = this.getIndicators();
if (this.isAxisEnabled('x')) {
indicators.x.hide();
}
if (this.isAxisEnabled('y')) {
indicators.y.hide();
}
},
onScrollStart: function() {
this.onScroll.apply(this, arguments);
this.showIndicators();
},
onScrollEnd: function() {
this.hideIndicators();
},
onScroll: function(scroller, x, y) {
this.setIndicatorValue('x', x);
this.setIndicatorValue('y', y);
//<debug>
if (this.isBenchmarking) {
this.framesCount++;
}
//</debug>
},
//<debug>
isBenchmarking: false,
framesCount: 0,
getCurrentFps: function() {
var now = Date.now(),
fps;
if (!this.isBenchmarking) {
this.isBenchmarking = true;
fps = 0;
}
else {
fps = Math.round(this.framesCount * 1000 / (now - this.framesCountStartTime));
}
this.framesCountStartTime = now;
this.framesCount = 0;
return fps;
},
//</debug>
setIndicatorValue: function(axis, scrollerPosition) {
if (!this.isAxisEnabled(axis)) {
return this;
}
var scroller = this.getScroller(),
scrollerMaxPosition = scroller.getMaxPosition()[axis],
scrollerContainerSize = scroller.getContainerSize()[axis],
value;
if (scrollerMaxPosition === 0) {
value = scrollerPosition / scrollerContainerSize;
if (scrollerPosition >= 0) {
value += 1;
}
}
else {
if (scrollerPosition > scrollerMaxPosition) {
value = 1 + ((scrollerPosition - scrollerMaxPosition) / scrollerContainerSize);
}
else if (scrollerPosition < 0) {
value = scrollerPosition / scrollerContainerSize;
}
else {
value = scrollerPosition / scrollerMaxPosition;
}
}
this.getIndicators()[axis].setValue(value);
},
refreshIndicator: function(axis) {
if (!this.isAxisEnabled(axis)) {
return this;
}
var scroller = this.getScroller(),
indicator = this.getIndicators()[axis],
scrollerContainerSize = scroller.getContainerSize()[axis],
scrollerSize = scroller.getSize()[axis],
ratio = scrollerContainerSize / scrollerSize;
indicator.setRatio(ratio);
indicator.refresh();
},
refresh: function() {
return this.getScroller().refresh();
},
refreshIndicators: function() {
var indicators = this.getIndicators();
indicators.x.setActive(this.isAxisEnabled('x'));
indicators.y.setActive(this.isAxisEnabled('y'));
this.refreshIndicator('x');
this.refreshIndicator('y');
},
destroy: function() {
var element = this.getElement(),
indicators = this.getIndicators();
Ext.destroy(this.getScroller(), this.indicatorsGrid);
if (this.hasOwnProperty('indicatorsHidingTimer')) {
clearTimeout(this.indicatorsHidingTimer);
delete this.indicatorsHidingTimer;
}
if (element && !element.isDestroyed) {
element.removeCls(this.getCls());
}
indicators.x.destroy();
indicators.y.destroy();
delete this.indicatorsGrid;
this.callParent(arguments);
}
});
/**
* @private
*/
Ext.define('Ext.behavior.Scrollable', {
extend: 'Ext.behavior.Behavior',
requires: [
'Ext.scroll.View'
],
constructor: function() {
this.listeners = {
painted: 'onComponentPainted',
scope: this
};
this.callParent(arguments);
},
onComponentPainted: function() {
this.scrollView.refresh();
},
setConfig: function(config) {
var scrollView = this.scrollView,
component = this.component,
scrollerElement;
if (config) {
if (!scrollView) {
this.scrollView = scrollView = new Ext.scroll.View(config);
scrollView.on('destroy', 'onScrollViewDestroy', this);
component.setUseBodyElement(true);
this.scrollerElement = scrollerElement = component.innerElement;
this.scrollContainer = scrollerElement.wrap();
scrollView.setElement(component.bodyElement);
if (component.isPainted()) {
this.onComponentPainted(component);
}
component.on(this.listeners);
}
else if (Ext.isString(config) || Ext.isObject(config)) {
scrollView.setConfig(config);
}
}
else if (scrollView) {
scrollView.destroy();
}
return this;
},
getScrollView: function() {
return this.scrollView;
},
onScrollViewDestroy: function() {
var component = this.component,
scrollerElement = this.scrollerElement;
if (!scrollerElement.isDestroyed) {
this.scrollerElement.unwrap();
}
this.scrollContainer.destroy();
if (!component.isDestroyed) {
component.un(this.listeners);
}
delete this.scrollerElement;
delete this.scrollView;
delete this.scrollContainer;
},
onComponentDestroy: function() {
var scrollView = this.scrollView;
if (scrollView) {
scrollView.destroy();
}
}
});
/**
* A simple class used to mask any {@link Ext.Container}.
*
* This should rarely be used directly, instead look at the {@link Ext.Container#masked} configuration.
*
* ## Example
*
* @example miniphone
* // Create our container
* var container = Ext.create('Ext.Container', {
* html: 'My container!'
* });
*
* // Add the container to the Viewport
* Ext.Viewport.add(container);
*
* // Mask the container
* container.setMasked(true);
*/
Ext.define('Ext.Mask', {
extend: 'Ext.Component',
xtype: 'mask',
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'mask',
/**
* @cfg {Boolean} transparent True to make this mask transparent.
*/
transparent: false,
/**
* @cfg
* @hide
*/
top: 0,
/**
* @cfg
* @hide
*/
left: 0,
/**
* @cfg
* @hide
*/
right: 0,
/**
* @cfg
* @hide
*/
bottom: 0
},
/**
* @event tap
* A tap event fired when a user taps on this mask
* @param {Ext.Mask} this The mask instance
* @param {Ext.EventObject} e The event object
*/
initialize: function() {
this.callSuper();
this.element.on('*', 'onEvent', this);
},
onEvent: function(e) {
var controller = arguments[arguments.length - 1];
if (controller.info.eventName === 'tap') {
this.fireEvent('tap', this, e);
return false;
}
if (e && e.stopEvent) {
e.stopEvent();
}
return false;
},
updateTransparent: function(newTransparent) {
this[newTransparent ? 'addCls' : 'removeCls'](this.getBaseCls() + '-transparent');
}
});
/**
* A Container has all of the abilities of {@link Ext.Component Component}, but lets you nest other Components inside
* it. Applications are made up of lots of components, usually nested inside one another. Containers allow you to
* render and arrange child Components inside them. Most apps have a single top-level Container called a Viewport,
* which takes up the entire screen. Inside of this are child components, for example in a mail app the Viewport
* Container's two children might be a message List and an email preview pane.
*
* Containers give the following extra functionality:
*
* - Adding child Components at instantiation and run time
* - Removing child Components
* - Specifying a [Layout](#!/guide/layouts)
*
* Layouts determine how the child Components should be laid out on the screen. In our mail app example we'd use an
* HBox layout so that we can pin the email list to the left hand edge of the screen and allow the preview pane to
* occupy the rest. There are several layouts in Sencha Touch 2, each of which help you achieve your desired
* application structure, further explained in the [Layout guide](#!/guide/layouts).
*
* ## Adding Components to Containers
*
* As we mentioned above, Containers are special Components that can have child Components arranged by a Layout. One of
* the code samples above showed how to create a Panel with 2 child Panels already defined inside it but it's easy to
* do this at run time too:
*
* @example miniphone
* //this is the Panel we'll be adding below
* var aboutPanel = Ext.create('Ext.Panel', {
* html: 'About this app'
* });
*
* //this is the Panel we'll be adding to
* var mainPanel = Ext.create('Ext.Panel', {
* fullscreen: true,
*
* layout: 'hbox',
* defaults: {
* flex: 1
* },
*
* items: {
* html: 'First Panel',
* style: 'background-color: #5E99CC;'
* }
* });
*
* //now we add the first panel inside the second
* mainPanel.add(aboutPanel);
*
* Here we created three Panels in total. First we made the aboutPanel, which we might use to tell the user a little
* about the app. Then we create one called mainPanel, which already contains a third Panel in its
* {@link Ext.Container#cfg-items items} configuration, with some dummy text ("First Panel"). Finally, we add the first
* panel to the second by calling the {@link Ext.Container#method-add add} method on `mainPanel`.
*
* In this case we gave our mainPanel another hbox layout, but we also introduced some
* {@link Ext.Container#defaults defaults}. These are applied to every item in the Panel, so in this case every child
* inside `mainPanel` will be given a `flex: 1` configuration. The effect of this is that when we first render the screen
* only a single child is present inside `mainPanel`, so that child takes up the full width available to it. Once the
* `mainPanel.add` line is called though, the `aboutPanel` is rendered inside of it and also given a `flex` of 1, which will
* cause it and the first panel to both receive half the full width of the `mainPanel`.
*
* Likewise, it's easy to remove items from a Container:
*
* mainPanel.remove(aboutPanel);
*
* After this line is run everything is back to how it was, with the first child panel once again taking up the full
* width inside `mainPanel`.
*
* ## Further Reading
*
* See the [Component & Container Guide](#!/guide/components) for more information, and check out the
* {@link Ext.Container} class docs also.
*
* @aside guide components
* @aside guide layouts
*/
Ext.define('Ext.Container', {
extend: 'Ext.Component',
alternateClassName: 'Ext.lib.Container',
requires: [
'Ext.layout.*',
'Ext.ItemCollection',
'Ext.behavior.Scrollable',
'Ext.Mask'
],
xtype: 'container',
/**
* @event add
* Fires whenever item added to the Container.
* @param {Ext.Container} this The Container instance.
* @param {Object} item The item added to the Container.
* @param {Number} index The index of the item within the Container.
*/
/**
* @event remove
* Fires whenever item removed from the Container.
* @param {Ext.Container} this The Container instance.
* @param {Object} item The item removed from the Container.
* @param {Number} index The index of the item that was removed.
*/
/**
* @event move
* Fires whenever item moved within the Container.
* @param {Ext.Container} this The Container instance.
* @param {Object} item The item moved within the Container.
* @param {Number} toIndex The new index of the item.
* @param {Number} fromIndex The old index of the item.
*/
/**
* @private
* @event renderedchange
* Fires whenever an item is rendered into a container or derendered
* from a Container.
* @param {Ext.Container} this The Container instance.
* @param {Object} item The item in the Container.
* @param {Boolean} rendered The current rendered status of the item.
*/
/**
* @event activate
* Fires whenever item within the Container is activated.
* @param {Ext.Container} this The Container instance.
* @param {Object} newActiveItem The new active item within the container.
* @param {Object} oldActiveItem The old active item within the container.
*/
/**
* @event deactivate
* Fires whenever item within the Container is deactivated.
* @param {Ext.Container} this The Container instance.
* @param {Object} newActiveItem The new active item within the container.
* @param {Object} oldActiveItem The old active item within the container.
*/
eventedConfig: {
/**
* @cfg {Object/String/Number} activeItem The item from the {@link #cfg-items} collection that will be active first. This is
* usually only meaningful in a {@link Ext.layout.Card card layout}, where only one item can be active at a
* time. If passes a string, it will be assumed to be a {@link Ext.ComponentQuery} selector.
* @accessor
* @evented
*/
activeItem: 0,
/**
* @cfg {Boolean/String/Object} scrollable
* Configuration options to make this Container scrollable. Acceptable values are:
*
* - `'horizontal'`, `'vertical'`, `'both'` to enabling scrolling for that direction.
* - `true`/`false` to explicitly enable/disable scrolling.
*
* Alternatively, you can give it an object which is then passed to the scroller instance:
*
* scrollable: {
* direction: 'vertical',
* directionLock: true
* }
*
* Please look at the {@link Ext.scroll.Scroller} documentation for more example on how to use this.
* @accessor
* @evented
*/
scrollable: null
},
config: {
/**
* @cfg {String/Object/Boolean} cardSwitchAnimation
* Animation to be used during transitions of cards.
* @removed 2.0.0 Please use {@link Ext.layout.Card#animation} instead
*/
/**
* @cfg {Object/String} layout Configuration for this Container's layout. Example:
*
* Ext.create('Ext.Container', {
* layout: {
* type: 'hbox',
* align: 'middle'
* },
* items: [
* {
* xtype: 'panel',
* flex: 1,
* style: 'background-color: red;'
* },
* {
* xtype: 'panel',
* flex: 2,
* style: 'background-color: green'
* }
* ]
* });
*
* See the [Layouts Guide](#!/guide/layouts) for more information.
*
* @accessor
*/
layout: null,
/**
* @cfg {Object} control Enables you to easily control Components inside this Container by listening to their
* events and taking some action. For example, if we had a container with a nested Disable button, and we
* wanted to hide the Container when the Disable button is tapped, we could do this:
*
* Ext.create('Ext.Container', {
* control: {
* 'button[text=Disable]': {
* tap: 'hideMe'
* }
* },
*
* hideMe: function () {
* this.hide();
* }
* });
*
* We used a {@link Ext.ComponentQuery} selector to listen to the {@link Ext.Button#tap tap} event on any
* {@link Ext.Button button} anywhere inside the Container that has the {@link Ext.Button#text text} 'Disable'.
* Whenever a Component matching that selector fires the `tap` event our `hideMe` function is called. `hideMe` is
* called with scope: `this` (e.g. `this` is the Container instance).
*
*/
control: {},
/**
* @cfg {Object} defaults A set of default configurations to apply to all child Components in this Container.
* It's often useful to specify defaults when creating more than one items with similar configurations. For
* example here we can specify that each child is a panel and avoid repeating the xtype declaration for each
* one:
*
* Ext.create('Ext.Container', {
* defaults: {
* xtype: 'panel'
* },
* items: [
* {
* html: 'Panel 1'
* },
* {
* html: 'Panel 2'
* }
* ]
* });
*
* @accessor
*/
defaults: null,
/**
* @cfg {Array/Object} items The child items to add to this Container. This is usually an array of Component
* configurations or instances, for example:
*
* Ext.create('Ext.Container', {
* items: [
* {
* xtype: 'panel',
* html: 'This is an item'
* }
* ]
* });
* @accessor
*/
items: null,
/**
* @cfg {Boolean} autoDestroy If `true`, child items will be destroyed as soon as they are {@link #method-remove removed}
* from this container.
* @accessor
*/
autoDestroy: true,
/** @cfg {String} defaultType
* The default {@link Ext.Component xtype} of child Components to create in this Container when a child item
* is specified as a raw configuration object, rather than as an instantiated Component.
* @accessor
*/
defaultType: null,
//@private
useBodyElement: null,
/**
* @cfg {Boolean/Object/Ext.Mask/Ext.LoadMask} masked
* A configuration to allow you to mask this container.
* You can optionally pass an object block with and xtype of `loadmask`, and an optional `message` value to
* display a loading mask. Please refer to the {@link Ext.LoadMask} component to see other configurations.
*
* masked: {
* xtype: 'loadmask',
* message: 'My message'
* }
*
* Alternatively, you can just call the setter at any time with `true`/`false` to show/hide the mask:
*
* setMasked(true); //show the mask
* setMasked(false); //hides the mask
*
* There are also two convenient methods, {@link #method-mask} and {@link #unmask}, to allow you to mask and unmask
* this container at any time.
*
* Remember, the {@link Ext.Viewport} is always a container, so if you want to mask your whole application at anytime,
* can call:
*
* Ext.Viewport.setMasked({
* xtype: 'loadmask',
* message: 'Hello'
* });
*
* @accessor
*/
masked: null,
/**
* @cfg {Boolean} modal `true` to make this Container modal. This will create a mask underneath the Container
* that covers its parent and does not allow the user to interact with any other Components until this
* Container is dismissed.
* @accessor
*/
modal: null,
/**
* @cfg {Boolean} hideOnMaskTap When using a {@link #modal} Component, setting this to `true` will hide the modal
* mask and the Container when the mask is tapped on.
* @accessor
*/
hideOnMaskTap: null
},
isContainer: true,
constructor: function(config) {
var me = this;
me._items = me.items = new Ext.ItemCollection();
me.innerItems = [];
me.onItemAdd = me.onFirstItemAdd;
me.callParent(arguments);
},
getElementConfig: function() {
return {
reference: 'element',
classList: ['x-container', 'x-unsized'],
children: [{
reference: 'innerElement',
className: 'x-inner'
}]
};
},
/**
* Changes the {@link #masked} configuration when its setter is called, which will convert the value
* into a proper object/instance of {@link Ext.Mask}/{@link Ext.LoadMask}. If a mask already exists,
* it will use that instead.
* @param masked
* @param currentMask
* @return {Object}
*/
applyMasked: function(masked, currentMask) {
var isVisible = true;
if (masked === false) {
masked = true;
isVisible = false;
}
currentMask = Ext.factory(masked, Ext.Mask, currentMask);
if (currentMask) {
this.add(currentMask);
currentMask.setHidden(!isVisible);
}
return currentMask;
},
/**
* Convenience method which calls {@link #setMasked} with a value of `true` (to show the mask). For additional
* functionality, call the {@link #setMasked} function direction (See the {@link #masked} configuration documentation
* for more information).
*/
mask: function(mask) {
this.setMasked(mask || true);
},
/**
* Convenience method which calls {@link #setMasked} with a value of false (to hide the mask). For additional
* functionality, call the {@link #setMasked} function direction (See the {@link #masked} configuration documentation
* for more information).
*/
unmask: function() {
this.setMasked(false);
},
setParent: function(container) {
this.callSuper(arguments);
if (container) {
var modal = this.getModal();
if (modal) {
container.insertBefore(modal, this);
modal.setZIndex(this.getZIndex() - 1);
}
}
},
applyModal: function(modal, currentModal) {
var isVisible = true;
if (modal === false) {
modal = true;
isVisible = false;
}
currentModal = Ext.factory(modal, Ext.Mask, currentModal);
if (currentModal) {
currentModal.setVisibility(isVisible);
}
return currentModal;
},
updateModal: function(modal) {
var container = this.getParent();
if (container) {
if (modal) {
container.insertBefore(modal, this);
modal.setZIndex(this.getZIndex() - 1);
}
else {
container.remove(modal);
}
}
},
updateHideOnMaskTap : function(hide) {
var mask = this.getModal();
if (mask) {
mask[hide ? 'on' : 'un'].call(mask, 'tap', 'hide', this);
}
},
updateZIndex: function(zIndex) {
var modal = this.getModal();
this.callParent(arguments);
if (modal) {
modal.setZIndex(zIndex - 1);
}
},
updateBaseCls: function(newBaseCls, oldBaseCls) {
var me = this,
ui = me.getUi();
if (newBaseCls) {
this.element.addCls(newBaseCls);
this.innerElement.addCls(newBaseCls, null, 'inner');
if (ui) {
this.element.addCls(newBaseCls, null, ui);
}
}
if (oldBaseCls) {
this.element.removeCls(oldBaseCls);
this.innerElement.removeCls(newBaseCls, null, 'inner');
if (ui) {
this.element.removeCls(oldBaseCls, null, ui);
}
}
},
updateUseBodyElement: function(useBodyElement) {
if (useBodyElement) {
this.link('bodyElement', this.innerElement.wrap({
cls: 'x-body'
}));
}
},
applyItems: function(items, collection) {
if (items) {
var me = this;
me.getDefaultType();
me.getDefaults();
if (me.initialized && collection.length > 0) {
me.removeAll();
}
me.add(items);
//Don't need to call setActiveItem when Container is first initialized
if (me.initialized) {
var activeItem = me.initialConfig.activeItem || me.config.activeItem || 0;
me.setActiveItem(activeItem);
}
}
},
/**
* @private
*/
applyControl: function(selectors) {
var selector, key, listener, listeners;
for (selector in selectors) {
listeners = selectors[selector];
for (key in listeners) {
listener = listeners[key];
if (Ext.isObject(listener)) {
listener.delegate = selector;
}
}
listeners.delegate = selector;
this.addListener(listeners);
}
return selectors;
},
/**
* Initialize layout and event listeners the very first time an item is added
* @private
*/
onFirstItemAdd: function() {
delete this.onItemAdd;
if (this.innerHtmlElement && !this.getHtml()) {
this.innerHtmlElement.destroy();
delete this.innerHtmlElement;
}
this.on('innerstatechange', 'onItemInnerStateChange', this, {
delegate: '> component'
});
return this.onItemAdd.apply(this, arguments);
},
//<debug error>
updateLayout: function(newLayout, oldLayout) {
if (oldLayout && oldLayout.isLayout) {
Ext.Logger.error('Replacing a layout after one has already been initialized is not currently supported.');
}
},
//</debug>
getLayout: function() {
var layout = this.layout;
if (!layout) {
layout = this.link('_layout', this.link('layout', Ext.factory(this._layout || 'default', Ext.layout.Default, null, 'layout')));
layout.setContainer(this);
}
return layout;
},
updateDefaultType: function(defaultType) {
// Cache the direct reference to the default item class here for performance
this.defaultItemClass = Ext.ClassManager.getByAlias('widget.' + defaultType);
//<debug error>
if (!this.defaultItemClass) {
Ext.Logger.error("Invalid defaultType of: '" + defaultType + "', must be a valid component xtype");
}
//</debug>
},
applyDefaults: function(defaults) {
if (defaults) {
this.factoryItem = this.factoryItemWithDefaults;
return defaults;
}
},
factoryItem: function(item) {
//<debug error>
if (!item) {
Ext.Logger.error("Invalid item given: " + item + ", must be either the config object to factory a new item, " +
"or an existing component instance");
}
//</debug>
return Ext.factory(item, this.defaultItemClass);
},
factoryItemWithDefaults: function(item) {
//<debug error>
if (!item) {
Ext.Logger.error("Invalid item given: " + item + ", must be either the config object to factory a new item, " +
"or an existing component instance");
}
//</debug>
var me = this,
defaults = me.getDefaults(),
instance;
if (!defaults) {
return Ext.factory(item, me.defaultItemClass);
}
// Existing instance
if (item.isComponent) {
instance = item;
// Apply defaults only if this is not already an item of this container
if (defaults && item.isInnerItem() && !me.has(instance)) {
instance.setConfig(defaults, true);
}
}
// Config object
else {
if (defaults && !item.ignoreDefaults) {
// Note:
// - defaults is only applied to inner items
// - we merge the given config together with defaults into a new object so that the original object stays intact
if (!(
item.hasOwnProperty('left') &&
item.hasOwnProperty('right') &&
item.hasOwnProperty('top') &&
item.hasOwnProperty('bottom') &&
item.hasOwnProperty('docked') &&
item.hasOwnProperty('centered')
)) {
item = Ext.mergeIf({}, item, defaults);
}
}
instance = Ext.factory(item, me.defaultItemClass);
}
return instance;
},
/**
* Adds one or more Components to this Container. Example:
*
* var myPanel = Ext.create('Ext.Panel', {
* html: 'This will be added to a Container'
* });
*
* myContainer.add([myPanel]);
*
* @param {Object/Object[]/Ext.Component/Ext.Component[]} newItems The new items to add to the Container.
* @return {Ext.Component} The last item added to the Container from the `newItems` array.
*/
add: function(newItems) {
var me = this,
i, ln, item, newActiveItem;
newItems = Ext.Array.from(newItems);
ln = newItems.length;
for (i = 0; i < ln; i++) {
item = me.factoryItem(newItems[i]);
this.doAdd(item);
if (!newActiveItem && !this.getActiveItem() && this.innerItems.length > 0 && item.isInnerItem()) {
newActiveItem = item;
}
}
if (newActiveItem) {
this.setActiveItem(newActiveItem);
}
return item;
},
/**
* @private
* @param item
*/
doAdd: function(item) {
var me = this,
items = me.getItems(),
index;
if (!items.has(item)) {
index = items.length;
items.add(item);
if (item.isInnerItem()) {
me.insertInner(item);
}
item.setParent(me);
me.onItemAdd(item, index);
}
},
/**
* Removes an item from this Container, optionally destroying it.
* @param {Object} item The item to remove.
* @param {Boolean} destroy Calls the Component's {@link Ext.Component#destroy destroy} method if `true`.
* @return {Ext.Component} this
*/
remove: function(item, destroy) {
var me = this,
index = me.indexOf(item),
innerItems = me.getInnerItems();
if (destroy === undefined) {
destroy = me.getAutoDestroy();
}
if (index !== -1) {
if (!me.removingAll && innerItems.length > 1 && item === me.getActiveItem()) {
me.on({
activeitemchange: 'doRemove',
scope: me,
single: true,
order: 'after',
args: [item, index, destroy]
});
me.doResetActiveItem(innerItems.indexOf(item));
}
else {
me.doRemove(item, index, destroy);
if (innerItems.length === 0) {
me.setActiveItem(null);
}
}
}
return me;
},
doResetActiveItem: function(innerIndex) {
if (innerIndex === 0) {
this.setActiveItem(1);
}
else {
this.setActiveItem(0);
}
},
doRemove: function(item, index, destroy) {
var me = this;
me.items.remove(item);
if (item.isInnerItem()) {
me.removeInner(item);
}
me.onItemRemove(item, index, destroy);
item.setParent(null);
if (destroy) {
item.destroy();
}
},
/**
* Removes all items currently in the Container, optionally destroying them all.
* @param {Boolean} destroy If `true`, {@link Ext.Component#destroy destroys} each removed Component.
* @param {Boolean} everything If `true`, completely remove all items including docked / centered and floating items.
* @return {Ext.Component} this
*/
removeAll: function(destroy, everything) {
var items = this.items,
ln = items.length,
i = 0,
item;
if (destroy === undefined) {
destroy = this.getAutoDestroy();
}
everything = Boolean(everything);
// removingAll flag is used so we don't unnecessarily change activeItem while removing all items.
this.removingAll = true;
for (; i < ln; i++) {
item = items.getAt(i);
if (item && (everything || item.isInnerItem())) {
this.doRemove(item, i, destroy);
i--;
ln--;
}
}
this.setActiveItem(null);
this.removingAll = false;
return this;
},
/**
* Returns the Component for a given index in the Container's {@link #property-items}.
* @param {Number} index The index of the Component to return.
* @return {Ext.Component} The item at the specified `index`, if found.
*/
getAt: function(index) {
return this.items.getAt(index);
},
getInnerAt: function(index) {
return this.innerItems[index];
},
/**
* Removes the Component at the specified index:
*
* myContainer.removeAt(0); // removes the first item
*
* @param {Number} index The index of the Component to remove.
*/
removeAt: function(index) {
var item = this.getAt(index);
if (item) {
this.remove(item);
}
return this;
},
/**
* Removes an inner Component at the specified index:
*
* myContainer.removeInnerAt(0); // removes the first item of the innerItems property
*
* @param {Number} index The index of the Component to remove.
*/
removeInnerAt: function(index) {
var item = this.getInnerItems()[index];
if (item) {
this.remove(item);
}
return this;
},
/**
* @private
*/
has: function(item) {
return this.getItems().indexOf(item) != -1;
},
/**
* @private
*/
hasInnerItem: function(item) {
return this.innerItems.indexOf(item) != -1;
},
/**
* @private
*/
indexOf: function(item) {
return this.getItems().indexOf(item);
},
innerIndexOf: function(item) {
return this.innerItems.indexOf(item);
},
/**
* @private
* @param item
* @param index
*/
insertInner: function(item, index) {
var items = this.getItems().items,
innerItems = this.innerItems,
currentInnerIndex = innerItems.indexOf(item),
newInnerIndex = -1,
nextSibling;
if (currentInnerIndex !== -1) {
innerItems.splice(currentInnerIndex, 1);
}
if (typeof index == 'number') {
do {
nextSibling = items[++index];
} while (nextSibling && !nextSibling.isInnerItem());
if (nextSibling) {
newInnerIndex = innerItems.indexOf(nextSibling);
innerItems.splice(newInnerIndex, 0, item);
}
}
if (newInnerIndex === -1) {
innerItems.push(item);
newInnerIndex = innerItems.length - 1;
}
if (currentInnerIndex !== -1) {
this.onInnerItemMove(item, newInnerIndex, currentInnerIndex);
}
return this;
},
onInnerItemMove: Ext.emptyFn,
/**
* @private
* @param item
*/
removeInner: function(item) {
Ext.Array.remove(this.innerItems, item);
return this;
},
/**
* Adds a child Component at the given index. For example, here's how we can add a new item, making it the first
* child Component of this Container:
*
* myContainer.insert(0, {xtype: 'panel', html: 'new item'});
*
* @param {Number} index The index to insert the Component at.
* @param {Object} item The Component to insert.
*/
insert: function(index, item) {
var me = this,
i;
if (Ext.isArray(item)) {
for (i = item.length - 1; i >= 0; i--) {
me.insert(index, item[i]);
}
return me;
}
item = this.factoryItem(item);
this.doInsert(index, item);
return item;
},
/**
* @private
* @param index
* @param item
*/
doInsert: function(index, item) {
var me = this,
items = me.items,
itemsLength = items.length,
currentIndex, isInnerItem;
isInnerItem = item.isInnerItem();
if (index > itemsLength) {
index = itemsLength;
}
if (items[index - 1] === item) {
return me;
}
currentIndex = me.indexOf(item);
if (currentIndex !== -1) {
if (currentIndex < index) {
index -= 1;
}
items.removeAt(currentIndex);
}
else {
item.setParent(me);
}
items.insert(index, item);
if (isInnerItem) {
me.insertInner(item, index);
}
if (currentIndex !== -1) {
me.onItemMove(item, index, currentIndex);
}
else {
me.onItemAdd(item, index);
}
},
/**
* @private
*/
insertFirst: function(item) {
return this.insert(0, item);
},
/**
* @private
*/
insertLast: function(item) {
return this.insert(this.getItems().length, item);
},
/**
* @private
*/
insertBefore: function(item, relativeToItem) {
var index = this.indexOf(relativeToItem);
if (index !== -1) {
this.insert(index, item);
}
return this;
},
/**
* @private
*/
insertAfter: function(item, relativeToItem) {
var index = this.indexOf(relativeToItem);
if (index !== -1) {
this.insert(index + 1, item);
}
return this;
},
/**
* @private
*/
onItemAdd: function(item, index) {
this.doItemLayoutAdd(item, index);
if (this.initialized) {
this.fireEvent('add', this, item, index);
}
},
doItemLayoutAdd: function(item, index) {
var layout = this.getLayout();
if (this.isRendered() && item.setRendered(true)) {
item.fireAction('renderedchange', [this, item, true], 'onItemAdd', layout, { args: [item, index] });
}
else {
layout.onItemAdd(item, index);
}
},
/**
* @private
*/
onItemRemove: function(item, index) {
this.doItemLayoutRemove(item, index);
this.fireEvent('remove', this, item, index);
},
doItemLayoutRemove: function(item, index) {
var layout = this.getLayout();
if (this.isRendered() && item.setRendered(false)) {
item.fireAction('renderedchange', [this, item, false], 'onItemRemove', layout, { args: [item, index, undefined] });
}
else {
layout.onItemRemove(item, index);
}
},
/**
* @private
*/
onItemMove: function(item, toIndex, fromIndex) {
if (item.isDocked()) {
item.setDocked(null);
}
this.doItemLayoutMove(item, toIndex, fromIndex);
this.fireEvent('move', this, item, toIndex, fromIndex);
},
doItemLayoutMove: function(item, toIndex, fromIndex) {
this.getLayout().onItemMove(item, toIndex, fromIndex);
},
onItemInnerStateChange: function(item, isInner) {
var layout = this.getLayout();
if (isInner) {
this.insertInner(item, this.items.indexOf(item));
}
else {
this.removeInner(item);
}
layout.onItemInnerStateChange.apply(layout, arguments);
},
/**
* Returns all inner {@link #property-items} of this container. `inner` means that the item is not `docked` or
* `floating`.
* @return {Array} The inner items of this container.
*/
getInnerItems: function() {
return this.innerItems;
},
/**
* Returns all the {@link Ext.Component#docked} items in this container.
* @return {Array} The docked items of this container.
*/
getDockedItems: function() {
var items = this.getItems().items,
dockedItems = [],
ln = items.length,
item, i;
for (i = 0; i < ln; i++) {
item = items[i];
if (item.isDocked()) {
dockedItems.push(item);
}
}
return dockedItems;
},
/**
* @private
*/
applyActiveItem: function(activeItem, currentActiveItem) {
var innerItems = this.getInnerItems();
// Make sure the items are already initialized
this.getItems();
// No items left to be active, reset back to 0 on falsy changes
if (!activeItem && innerItems.length === 0) {
return 0;
}
else if (typeof activeItem == 'number') {
activeItem = Math.max(0, Math.min(activeItem, innerItems.length - 1));
activeItem = innerItems[activeItem];
if (activeItem) {
return activeItem;
}
else if (currentActiveItem) {
return null;
}
}
else if (activeItem) {
var item;
//ComponentQuery selector?
if (typeof activeItem == 'string') {
item = this.child(activeItem);
activeItem = {
xtype : activeItem
};
}
if (!item || !item.isComponent) {
item = this.factoryItem(activeItem);
}
this.pendingActiveItem = item;
//<debug error>
if (!item.isInnerItem()) {
Ext.Logger.error("Setting activeItem to be a non-inner item");
}
//</debug>
if (!this.has(item)) {
this.add(item);
}
return item;
}
},
/**
* Animates to the supplied `activeItem` with a specified animation. Currently this only works
* with a Card layout. This passed animation will override any default animations on the
* container, for a single card switch. The animation will be destroyed when complete.
* @param {Object/Number} activeItem The item or item index to make active.
* @param {Object/Ext.fx.layout.Card} animation Card animation configuration or instance.
*/
animateActiveItem: function(activeItem, animation) {
var layout = this.getLayout(),
defaultAnimation;
if (this.activeItemAnimation) {
this.activeItemAnimation.destroy();
}
this.activeItemAnimation = animation = new Ext.fx.layout.Card(animation);
if (animation && layout.isCard) {
animation.setLayout(layout);
defaultAnimation = layout.getAnimation();
if (defaultAnimation) {
defaultAnimation.disable();
animation.on('animationend', function() {
defaultAnimation.enable();
animation.destroy();
}, this);
}
}
return this.setActiveItem(activeItem);
},
/**
* @private
*/
doSetActiveItem: function(newActiveItem, oldActiveItem) {
delete this.pendingActiveItem;
if (oldActiveItem) {
oldActiveItem.fireEvent('deactivate', oldActiveItem, this, newActiveItem);
}
if (newActiveItem) {
newActiveItem.fireEvent('activate', newActiveItem, this, oldActiveItem);
}
},
doSetHidden: function(hidden) {
var modal = this.getModal();
if (modal) {
modal.setHidden(hidden);
}
this.callSuper(arguments);
},
/**
* @private
*/
setRendered: function(rendered) {
if (this.callParent(arguments)) {
var items = this.items.items,
i, ln;
for (i = 0,ln = items.length; i < ln; i++) {
items[i].setRendered(rendered);
}
return true;
}
return false;
},
/**
* @private
*/
getScrollableBehavior: function() {
var behavior = this.scrollableBehavior;
if (!behavior) {
behavior = this.scrollableBehavior = new Ext.behavior.Scrollable(this);
}
return behavior;
},
/**
* @private
*/
applyScrollable: function(config) {
if (config && !config.isObservable) {
this.getScrollableBehavior().setConfig(config);
}
return config;
},
doSetScrollable: function() {
// Used for plugins when they need to reinitialize scroller listeners
},
/**
* Returns an the scrollable instance for this container, which is a {@link Ext.scroll.View} class.
*
* Please checkout the documentation for {@link Ext.scroll.View}, {@link Ext.scroll.View#getScroller}
* and {@link Ext.scroll.Scroller} for more information.
* @return {Ext.scroll.View} The scroll view.
*/
getScrollable: function() {
return this.getScrollableBehavior().getScrollView();
},
// Used by ComponentQuery to retrieve all of the items
// which can potentially be considered a child of this Container.
// This should be overridden by components which have child items
// that are not contained in items. For example `dockedItems`, `menu`, etc
// @private
getRefItems: function(deep) {
var items = this.getItems().items.slice(),
ln = items.length,
i, item;
if (deep) {
for (i = 0; i < ln; i++) {
item = items[i];
if (item.getRefItems) {
items = items.concat(item.getRefItems(true));
}
}
}
return items;
},
/**
* Examines this container's `{@link #property-items}` property
* and gets a direct child component of this container.
* @param {String/Number} component This parameter may be any of the following:
*
* - {String} : representing the `itemId`
* or `{@link Ext.Component#getId id}` of the child component.
* - {Number} : representing the position of the child component
* within the `{@link #property-items}` property.
*
* For additional information see {@link Ext.util.MixedCollection#get}.
* @return {Ext.Component} The component (if found).
*/
getComponent: function(component) {
if (Ext.isObject(component)) {
component = component.getItemId();
}
return this.getItems().get(component);
},
/**
* Finds a docked item of this container using a reference, `id `or an `index` of its location
* in {@link #getDockedItems}.
* @param {String/Number} component The `id` or `index` of the component to find.
* @return {Ext.Component/Boolean} The docked component, if found.
*/
getDockedComponent: function(component) {
if (Ext.isObject(component)) {
component = component.getItemId();
}
var dockedItems = this.getDockedItems(),
ln = dockedItems.length,
item, i;
if (Ext.isNumber(component)) {
return dockedItems[component];
}
for (i = 0; i < ln; i++) {
item = dockedItems[i];
if (item.id == component) {
return item;
}
}
return false;
},
/**
* Retrieves all descendant components which match the passed selector.
* Executes an Ext.ComponentQuery.query using this container as its root.
* @param {String} selector Selector complying to an Ext.ComponentQuery selector.
* @return {Array} Ext.Component's which matched the selector.
*/
query: function(selector) {
return Ext.ComponentQuery.query(selector, this);
},
/**
* Retrieves the first direct child of this container which matches the passed selector.
* The passed in selector must comply with an {@link Ext.ComponentQuery} selector.
* @param {String} selector An {@link Ext.ComponentQuery} selector.
* @return {Ext.Component}
*/
child: function(selector) {
return this.query('> ' + selector)[0] || null;
},
/**
* Retrieves the first descendant of this container which matches the passed selector.
* The passed in selector must comply with an {@link Ext.ComponentQuery} selector.
* @param {String} selector An {@link Ext.ComponentQuery} selector.
* @return {Ext.Component}
*/
down: function(selector) {
return this.query(selector)[0] || null;
},
destroy: function() {
var me = this,
modal = me.getModal();
if (modal) {
modal.destroy();
}
me.removeAll(true, true);
me.unlink('_scrollable');
Ext.destroy(me.items);
me.callSuper();
}
}, function() {
this.addMember('defaultItemClass', this);
});
/**
* 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 {@link 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 {@link 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 {@link Ext.util.Point}. Otherwise, returns the given point itself.
* @param object
* @return {Ext.util.Point}
*/
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.
* @return {Ext.util.Point} The new point.
*/
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 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 {@link 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 {@link 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.
* @return {Boolean}
*/
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. For example:
* [100.3,199.8] will equals to [100, 200].
* @param {Ext.util.Point/Object} point 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;
}
});
/**
* @class Ext.util.LineSegment
*
* Utility class that represents a line segment, constructed by two {@link Ext.util.Point}
*/
Ext.define('Ext.util.LineSegment', {
requires: ['Ext.util.Point'],
/**
* Creates new LineSegment out of two points.
* @param {Ext.util.Point} point1
* @param {Ext.util.Point} point2
*/
constructor: function(point1, point2) {
var Point = Ext.util.Point;
this.point1 = Point.from(point1);
this.point2 = Point.from(point2);
},
/**
* Returns the point where two lines intersect.
* @param {Ext.util.LineSegment} lineSegment The line to intersect with.
* @return {Ext.util.Point}
*/
intersects: function(lineSegment) {
var point1 = this.point1,
point2 = this.point2,
point3 = lineSegment.point1,
point4 = lineSegment.point2,
x1 = point1.x,
x2 = point2.x,
x3 = point3.x,
x4 = point4.x,
y1 = point1.y,
y2 = point2.y,
y3 = point3.y,
y4 = point4.y,
d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4),
xi, yi;
if (d == 0) {
return null;
}
xi = ((x3 - x4) * (x1 * y2 - y1 * x2) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d;
yi = ((y3 - y4) * (x1 * y2 - y1 * x2) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d;
if (xi < Math.min(x1, x2) || xi > Math.max(x1, x2)
|| xi < Math.min(x3, x4) || xi > Math.max(x3, x4)
|| yi < Math.min(y1, y2) || yi > Math.max(y1, y2)
|| yi < Math.min(y3, y4) || yi > Math.max(y3, y4)) {
return null;
}
return new Ext.util.Point(xi, yi);
},
/**
* Returns string representation of the line. Useful for debugging.
* @return {String} For example `Point[12,8] Point[0,0]`
*/
toString: function() {
return this.point1.toString() + " " + this.point2.toString();
}
});
/**
* @aside guide floating_components
*
* Panels are most useful as Overlays - containers that float over your application. They contain extra styling such
* that when you {@link #showBy} another component, the container will appear in a rounded black box with a 'tip'
* pointing to a reference component.
*
* If you don't need this extra functionality, you should use {@link Ext.Container} instead. See the
* [Overlays example](#!/example/overlays) for more use cases.
*
* @example miniphone preview
*
* var button = Ext.create('Ext.Button', {
* text: 'Button',
* id: 'rightButton'
* });
*
* Ext.create('Ext.Container', {
* fullscreen: true,
* items: [
* {
* docked: 'top',
* xtype: 'titlebar',
* items: [
* button
* ]
* }
* ]
* });
*
* Ext.create('Ext.Panel', {
* html: 'Floating Panel',
* left: 0,
* padding: 10
* }).showBy(button);
*
*/
Ext.define('Ext.Panel', {
extend: 'Ext.Container',
requires: ['Ext.util.LineSegment'],
alternateClassName: 'Ext.lib.Panel',
xtype: 'panel',
isPanel: true,
config: {
baseCls: Ext.baseCSSPrefix + 'panel',
/**
* @cfg {Number/Boolean/String} bodyPadding
* A shortcut for setting a padding style on the body element. The value can either be
* a number to be applied to all sides, or a normal CSS string describing padding.
* @deprecated 2.0.0
*/
bodyPadding: null,
/**
* @cfg {Number/Boolean/String} bodyMargin
* A shortcut for setting a margin style on the body element. The value can either be
* a number to be applied to all sides, or a normal CSS string describing margins.
* @deprecated 2.0.0
*/
bodyMargin: null,
/**
* @cfg {Number/Boolean/String} bodyBorder
* A shortcut for setting a border style on the body element. The value can either be
* a number to be applied to all sides, or a normal CSS string describing borders.
* @deprecated 2.0.0
*/
bodyBorder: null
},
getElementConfig: function() {
var config = this.callParent();
config.children.push({
reference: 'tipElement',
className: 'x-anchor',
hidden: true
});
return config;
},
applyBodyPadding: function(bodyPadding) {
if (bodyPadding === true) {
bodyPadding = 5;
}
if (bodyPadding) {
bodyPadding = Ext.dom.Element.unitizeBox(bodyPadding);
}
return bodyPadding;
},
updateBodyPadding: function(newBodyPadding) {
this.element.setStyle('padding', newBodyPadding);
},
applyBodyMargin: function(bodyMargin) {
if (bodyMargin === true) {
bodyMargin = 5;
}
if (bodyMargin) {
bodyMargin = Ext.dom.Element.unitizeBox(bodyMargin);
}
return bodyMargin;
},
updateBodyMargin: function(newBodyMargin) {
this.element.setStyle('margin', newBodyMargin);
},
applyBodyBorder: function(bodyBorder) {
if (bodyBorder === true) {
bodyBorder = 1;
}
if (bodyBorder) {
bodyBorder = Ext.dom.Element.unitizeBox(bodyBorder);
}
return bodyBorder;
},
updateBodyBorder: function(newBodyBorder) {
this.element.setStyle('border-width', newBodyBorder);
},
alignTo: function(component) {
var tipElement = this.tipElement;
tipElement.hide();
if (this.currentTipPosition) {
tipElement.removeCls('x-anchor-' + this.currentTipPosition);
}
this.callParent(arguments);
var LineSegment = Ext.util.LineSegment,
alignToElement = component.isComponent ? component.renderElement : component,
element = this.renderElement,
alignToBox = alignToElement.getPageBox(),
box = element.getPageBox(),
left = box.left,
top = box.top,
right = box.right,
bottom = box.bottom,
centerX = left + (box.width / 2),
centerY = top + (box.height / 2),
leftTopPoint = { x: left, y: top },
rightTopPoint = { x: right, y: top },
leftBottomPoint = { x: left, y: bottom },
rightBottomPoint = { x: right, y: bottom },
boxCenterPoint = { x: centerX, y: centerY },
alignToCenterX = alignToBox.left + (alignToBox.width / 2),
alignToCenterY = alignToBox.top + (alignToBox.height / 2),
alignToBoxCenterPoint = { x: alignToCenterX, y: alignToCenterY },
centerLineSegment = new LineSegment(boxCenterPoint, alignToBoxCenterPoint),
offsetLeft = 0,
offsetTop = 0,
tipSize, tipWidth, tipHeight, tipPosition, tipX, tipY;
tipElement.setVisibility(false);
tipElement.show();
tipSize = tipElement.getSize();
tipWidth = tipSize.width;
tipHeight = tipSize.height;
if (centerLineSegment.intersects(new LineSegment(leftTopPoint, rightTopPoint))) {
tipX = Math.min(Math.max(alignToCenterX, left + tipWidth), right - (tipWidth));
tipY = top;
offsetTop = tipHeight + 10;
tipPosition = 'top';
}
else if (centerLineSegment.intersects(new LineSegment(leftTopPoint, leftBottomPoint))) {
tipX = left;
tipY = Math.min(Math.max(alignToCenterY + (tipWidth / 2), tipWidth * 1.6), bottom - (tipWidth / 2.2));
offsetLeft = tipHeight + 10;
tipPosition = 'left';
}
else if (centerLineSegment.intersects(new LineSegment(leftBottomPoint, rightBottomPoint))) {
tipX = Math.min(Math.max(alignToCenterX, left + tipWidth), right - tipWidth);
tipY = bottom;
offsetTop = -tipHeight - 10;
tipPosition = 'bottom';
}
else if (centerLineSegment.intersects(new LineSegment(rightTopPoint, rightBottomPoint))) {
tipX = right;
tipY = Math.max(Math.min(alignToCenterY - tipHeight, bottom - tipWidth * 1.3), tipWidth / 2);
offsetLeft = -tipHeight - 10;
tipPosition = 'right';
}
if (tipX || tipY) {
this.currentTipPosition = tipPosition;
tipElement.addCls('x-anchor-' + tipPosition);
tipElement.setLeft(tipX - left);
tipElement.setTop(tipY - top);
tipElement.setVisibility(true);
this.setLeft(this.getLeft() + offsetLeft);
this.setTop(this.getTop() + offsetTop);
}
}
});
/**
* A general sheet class. This renderable container provides base support for orientation-aware transitions for popup or
* side-anchored sliding Panels.
*
* In most cases, you should use {@link Ext.ActionSheet}, {@link Ext.MessageBox}, {@link Ext.picker.Picker}, or {@link Ext.picker.Date}.
*/
Ext.define('Ext.Sheet', {
extend: 'Ext.Panel',
xtype: 'sheet',
requires: ['Ext.fx.Animation'],
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'sheet',
/**
* @cfg
* @inheritdoc
*/
modal: true,
/**
* @cfg {Boolean} centered
* Whether or not this component is absolutely centered inside its container.
* @accessor
* @evented
*/
centered: true,
/**
* @cfg {Boolean} stretchX `true` to stretch this sheet horizontally.
*/
stretchX: null,
/**
* @cfg {Boolean} stretchY `true` to stretch this sheet vertically.
*/
stretchY: null,
/**
* @cfg {String} enter
* The viewport side used as the enter point when shown. Valid values are 'top', 'bottom', 'left', and 'right'.
* Applies to sliding animation effects only.
*/
enter: 'bottom',
/**
* @cfg {String} exit
* The viewport side used as the exit point when hidden. Valid values are 'top', 'bottom', 'left', and 'right'.
* Applies to sliding animation effects only.
*/
exit: 'bottom',
/**
* @cfg
* @inheritdoc
*/
showAnimation: !Ext.os.is.Android2 ? {
type: 'slideIn',
duration: 250,
easing: 'ease-out'
} : null,
/**
* @cfg
* @inheritdoc
*/
hideAnimation: !Ext.os.is.Android2 ? {
type: 'slideOut',
duration: 250,
easing: 'ease-in'
} : null
},
applyHideAnimation: function(config) {
var exit = this.getExit(),
direction = exit;
if (exit === null) {
return null;
}
if (config === true) {
config = {
type: 'slideOut'
};
}
if (Ext.isString(config)) {
config = {
type: config
};
}
var anim = Ext.factory(config, Ext.fx.Animation);
if (anim) {
if (exit == 'bottom') {
direction = 'down';
}
if (exit == 'top') {
direction = 'up';
}
anim.setDirection(direction);
}
return anim;
},
applyShowAnimation: function(config) {
var enter = this.getEnter(),
direction = enter;
if (enter === null) {
return null;
}
if (config === true) {
config = {
type: 'slideIn'
};
}
if (Ext.isString(config)) {
config = {
type: config
};
}
var anim = Ext.factory(config, Ext.fx.Animation);
if (anim) {
if (enter == 'bottom') {
direction = 'down';
}
if (enter == 'top') {
direction = 'up';
}
anim.setBefore({
display: null
});
anim.setReverse(true);
anim.setDirection(direction);
}
return anim;
},
updateStretchX: function(newStretchX) {
this.getLeft();
this.getRight();
if (newStretchX) {
this.setLeft(0);
this.setRight(0);
}
},
updateStretchY: function(newStretchY) {
this.getTop();
this.getBottom();
if (newStretchY) {
this.setTop(0);
this.setBottom(0);
}
}
});
/**
* A simple class to display a button in Sencha Touch.
*
* There are various different styles of Button you can create by using the {@link #icon},
* {@link #iconCls}, {@link #iconAlign}, {@link #iconMask}, {@link #ui}, and {@link #text}
* configurations.
*
* ## Simple Button
*
* Here is a Button in it's simplest form:
*
* @example miniphone
* var button = Ext.create('Ext.Button', {
* text: 'Button'
* });
* Ext.Viewport.add({ xtype: 'container', padding: 10, items: [button] });
*
* ## Icons
*
* You can also create a Button with just an icon using the {@link #iconCls} configuration:
*
* @example miniphone
* var button = Ext.create('Ext.Button', {
* iconCls: 'refresh',
* iconMask: true
* });
* Ext.Viewport.add({ xtype: 'container', padding: 10, items: [button] });
*
* Note that the {@link #iconMask} configuration is required when you want to use any of the
* bundled Pictos icons.
*
* Here are the included icons available (if {@link Global_CSS#$include-default-icons $include-default-icons}
* is set to `true`):
*
* - ![action](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAFI0lEQVRoBe2YW4hVVRjHZ0yzq6lFEaMlE0PShYRAJIl6iEqKHnqI6WJB0IvdICkfEk0aIyo0KFCph8giCitI7CkoohQL7SoZDaQmXSgKo4uWNf1+zt7DOXvOOXuvvc85bc+cD36ssy/r+77/Xmt9e+3TOzIy0jORbNJEEqvWruBOH/HuCHdHuMOeQOmmdO+ozaA5oxXPunSC2Re4MbgCNiB6vvqbKbx0giNxp9BeBU/BIJqnRecLN2UVrLDj4GIYgscRfSltYSuzYMUdA/0wCI8ieglM5XduK7vgWJhTegGshucRfQHkyj1XpziLNrfmOh2ug1dhMaJn0gbZZDpNpsexQb2y3azfKXCAwns4W5dMd7m2B2ANLCT/x/A/nKknN5mUhWFp1g4Z7vM14jrbBZvgEwi1tAdkDEf3ZrgI0S/RrkP4IdqGpuA+cJo0yw7iyNfJmzAcMrokfjp93HC4XrPYCdzkgPXDPPqvJN7eRh0VrBWqfKMuev6k3Qzr4SP4HWqOFIkZ73iYA/NhLpwPZ4LLS+FZzUp+GtwAA/heS/sGwv+irWnXc9bdTRF20/8eOBWmEKwnCectOrPhSlgF2+Bb+Bl+AxP8B/6FvLn8Td8fYQXMSubgsVZU8Cv4mAeNhC7k+jLYCopzrRURlvZA9P8WLIJJlcI5zi1Ypw+Dr4oqp3EAzlsbLCjfg1PeEUxLtlnXXU4/wQboq8gpl2BHx2l5UuyosuW8I6rQb8Bp1iwRefy4VN6FReaopU3pX7jnhwSO7MmVIiNnJ3L+DtgHCm3ltA0RH4/26rhKk1tdu4kr7yeuHkKgU3rMqI5ncfAQDIKbg14oi1nJv4OvTShthC9LjmTyGB8XwhZw+oQ8+Xbc68C8AOboK6+YYPpfDV+B06YdAkJiuMtzhvrOP1JYafMLpu/Z8CmEJNGOe60fz0J/cjZmWcP0G2+sWZ/aUnCqhFosOq7gyf6uOT888th+Ot0HmxF7MOkgt2AcXQNLkg5rHPv+dffjVvPX6PdeWtf7MJhUssD578ZtEGL6sY4MIfTjeh1zCWZ0Z+DwQXAkapkjtzviPdoPYB+JuJVMNfy7QQkR7MbGPfRaYhi7ruUSjLcbwe1k0tw2vgivwy6C70/ekPE4JK+N+HySWDuz+A5xXOnvlsqD6Lf/QjwBnxNc4a02YwzBeuIdyBosWDDT7RKcn1MRYA+/V8ImAv9Rcb5VP53ufoQ8AB8S0+PMFiwYz5fDzCjCF7SLCbojOm514zZ3HViYLIZVxmD4h8B0rtWtFXkEn4tTv22thPe2SawVeDs8TTz/NqoyhLqDGoC7wervt3lNCxKMY/fIc+BLuJXgn9G20pyuVuA1sJF4vt7GjHx8nZnT7XAXzIXnoK4FCcbLVHAqLW+DWF8v78Aq2EY8v7zGDK2+EmfBI3AtTAPNTU1dCxXs/a6ht+t6bM4FNykvw/0IdYSrDLHu8iyeQ7Cg6mLKQahgd0pbSOJwit/cl6Np6p+BrxGn6hNUp1z3m/tOWAH+DrIgwSTQcBcTFLnOzcRwSjZ6j/vdvQyCxRrSanu0mWvZqp3LjkbBuYTGnSac4CxreCQqJPFD+r/bhq+dtOSyCO7DyWzIcm9avKLXXb+FcskiYjlBfB0lP9KLJp+nv6N7ZL+cp7N9sgg+L6/zMvabcEWrK7iM07CZOXVHuJlPs4y+rNJ74JkyJpczp62N+vWOfpw0uqWzrnXXcGeN53g13REe/0w660x3hDtrPMer+Q9LNCcV91c+jgAAAABJRU5ErkJggg==) action
* - ![add](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAABqUlEQVRoBe2awWnDUBBE843B4NxcQSAFOC4lJeTkoxtJDykgvRhcgCFNJCFgIs+ChEHSJX93YT6ZD4ssmR3NztNFH5Wu6+6iVynlEZpbp+4J3s5OjWm7DRxZuMMCdUB9oyzNmrJe01hEejMtM5exIh6bCI3JbFkDT27EckEDs5DI8iHCWcmy6IowC4ksHyKclSyLrgizkMjyIcJZybLoijALiSwfIpyVLItuOGFso/xiuEvAgJdeK0DqJrHEhtsTTh9ul9y/ChR2KE+Y1ruDt2ccI7d6PszcK+oFFblWELt3Cn6i/8epMW5/W+LKGrUZ/0NwboF5QxuPsfY8dmOxJs41cBOYHCZF2BFeE60i3AQmh0kRdoTXRKsIN4HJYVKEHeE10frvCNvr4RH1HojH3rGHr3hqA7VdkxPKvuKJ3AA4hn7BM3xxA5N71Fdv1gz/tax3P+hFHmsJwM/8wraMadqOh5GuXda76rVqNWb7wgeevQvRRQ1MBCPFiginxEokKsJEMFKsiHBKrESiIkwEI8WKCKfESiQqwkQwUqyIcEqsRKIiTAQjxcoVrP83/9czD9EAAAAASUVORK5CYII=) add
* - ![arrow_down](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGRTdGMTE3NDA3MjA2ODExOTJDQUMyNUQwRUE4NjdEQiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxQTFBMDFDQ0I5NEYxMURGQUU1RjlGMEFERUNDQTVEMCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyMkRCMDIxMkI5NEUxMURGQUU1RjlGMEFERUNDQTVEMCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjMwRTE0QzVBNDIyMjY4MTFCQ0ZCOTAzOTcwNzcyRkVCIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkZFN0YxMTc0MDcyMDY4MTE5MkNBQzI1RDBFQTg2N0RCIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+HfrH/AAAAeVJREFUeNrs2cFHBGEUAPA3zYqIiIhOnTpFRHSKrp26RqeuEV077R/QqWtE166dOkVERHRa9hQRnZalFcv0Hk/W1Mx+38z3vvlm5j3eZW+/9+abne+9KEkSaFPMQMtCwQpWsIIVrGAFK1jBClawgo2ik/4hiqJGwLKuvfpIc5xSkWqYr5hzU1s/mRNxXTPsJ+ZqluvXlwOmSj3XBDvG3M1rpAmYYoUrFzr4ZNqTawqm2MH8Dhh7ZXJUbcAUx4FinzBnJcAUl4FhP/jIgRSYKvkYCJaO2LbNv08RMMUy5nsA4COTLy0XYIqtil9iF6aflq7AwBWuAvuQ9ZKSBgNX2ieWjtKSzeXBNZgqfe8J+4W5aXtbcg0GrvibB/BhkeuhBJhigzsghT0veh+WAlMcCGHvMOMQwcCdcIntYy6WmXhIg2PuiAvsEHO97IhHGgzckb4D8L6LmZYPMHBnhiWwXVdDPF9g4A4Vwd66nFr6BAN3ygbbw1yoMzjmjplgB5hrrufSvsHAHesZDOD2JAbxVYCBOzfIAZ9JbR6qAgN3cPwP9kZy1VIlGLiTdluCmoOBO/pnS9Bk8DzmS3pL4BMcpZEe1qX0GI/atC4dQYXRMa1MU0IX4gpWsIIVrGAFK1jBCnYUPwIMAPUPAyFL+nRdAAAAAElFTkSuQmCC) arrow_down
* - ![arrow_left](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGRTdGMTE3NDA3MjA2ODExOTJDQUMyNUQwRUE4NjdEQiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGMDZEQTFBREFDOTMxMURGOEQ2MUVDMjM0MzY2NTBDQSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGMDZEQTFBQ0FDOTMxMURGOEQ2MUVDMjM0MzY2NTBDQSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkFGQzJEMjQxRjIyMDY4MTE4QTZEQzUxMDg5Q0Y0RTRFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkZFN0YxMTc0MDcyMDY4MTE5MkNBQzI1RDBFQTg2N0RCIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+FXGmxAAAAghJREFUeNrsm09ERFEUxt+rxBAxqyFm1SqiRYpMSpFapUVaRGpTRIpIbWLaFJEoRZtilChRWiRKsyklilYRERERERGZvsN57Wfmvnnnznkfv+WM+bn3e/ePN24mk3E0pcRRllC42FOWy4dc1w30R+fz3LFthEs1TelZ0KlBuAIcgmRgHS5gqlm2RsNTmqbvrUlZycLT4BhUiliWfEwEbII+UeuwT4nzqNZq2Gm1gTu/ZaUIj4NTEBW7tTTY1zUwKH4vbaive6BBw2kpAa6DkA1CeBicgZhVx8McUg5WWNi+83CWiXFfE9ZeAGQR6ukBqJKyu/Gzw7TcXEiS9UuYbiWWeU8ckXYqMT2lozyFW6SeOU0K1/FhPS75RsHUlKbj3KV0WRPC1Nd5sCuxr6anNPV12zFwk2jLCCdtk81XeAIsahL+BVOgH3xrEPayA5rAixZhyj2oB2ktwpR30A5WtQh7vR4DQ+BHg7CXLdAMXrUIU26411dahClvoBVsaBF2uMsjYFRCrwt5a7kOOnjUVQg7vE43cr9VCDu8I6Nep7QIO7z3HgCTvHYXvbCXJe71hxZhyjmv1w9ahCnP/DDb1yLs9boXzGgR9rIAusCnFmHKCff6UYsw5Ymlj7QIU75AN5gz9YVuLu8eB/S+dA+v1+l83pe2Sfg/BRe2OeGfPELhUDgUtip/AgwAw4tbozZtKFwAAAAASUVORK5CYII=) arrow_left
* - ![arrow_right](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGRTdGMTE3NDA3MjA2ODExOTJDQUMyNUQwRUE4NjdEQiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGMDZEQTFCMUFDOTMxMURGOEQ2MUVDMjM0MzY2NTBDQSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGMDZEQTFCMEFDOTMxMURGOEQ2MUVDMjM0MzY2NTBDQSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkFGQzJEMjQxRjIyMDY4MTE4QTZEQzUxMDg5Q0Y0RTRFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkZFN0YxMTc0MDcyMDY4MTE5MkNBQzI1RDBFQTg2N0RCIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+xvZexwAAAhhJREFUeNrsm8FHRFEUxu9rxhARsxqiVauYXWoTpTYtUkRqlWkz0WaiTW2iNi3atGhTm4k2E5GYSJRaZcZQtIqIISIiYhgyfZdv/oF59913X+cdfst5733u+c495743XqvVUpKiSwmLWPB/j2QnP/I8L9SH9lN3/KxwQlpKT4FtaR7eAhegR1LRmgEVMCCpSg+CGtNczLbUC8pgQ9I+rCv3LiiBbkmNxwJ93S+p08qCRzAhqbVMg2tQkNRLa1/vg6ILvrY5POTAXdi+tj0tDbOYjUoaDzPgBuQlzcMpcEhSkg4A8lztjBTBin6u0d8iBOvoYwXPSRGsuEcXuWcnJAhuR4G+TksRrGOMfXhWimDFjqzCyUuE4LavS5yxExIEt0OfopRN+DpKbx6MHAtHSfAeWPN7kWQEhDbAMjg1cTHXBdfBLHiSUKXvwZBJsS4LPgCT4NP0hV1L6SZYAcdB3cAlwe9gDlQlTEsP9Gs16Bu5IPgIjIOP/34AoP26Ss82bd00LA/r1Vzk1mM1whCsfTrPpsJ62E7pE/q1HpaPbAn+Betgib1xaGEjpb+Ywrcu7H9BC35m8//mSncTZEqfgRGXxAYpeJNp3FCOhemU/ub+euXqzGlS8AuYBq8unyiYSulLNv9OizUleIcr+6MiEF4n3x7ze2n9OkSfE5/bfmg/30v7ERxaWBcc5Yj/5BELjgXHgiMVfwIMAGPkXbHq6ClAAAAAAElFTkSuQmCC) arrow_right
* - ![arrow_up](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGRTdGMTE3NDA3MjA2ODExOTJDQUMyNUQwRUE4NjdEQiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDQUZBQUM3NEFDOTMxMURGOEQ2MUVDMjM0MzY2NTBDQSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDQUZBQUM3M0FDOTMxMURGOEQ2MUVDMjM0MzY2NTBDQSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkFGQzJEMjQxRjIyMDY4MTE4QTZEQzUxMDg5Q0Y0RTRFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkZFN0YxMTc0MDcyMDY4MTE5MkNBQzI1RDBFQTg2N0RCIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+ar3jxgAAAbFJREFUeNrs2j9ExGEcx/H71YmmpoiIaIq4KSKi6dabbo1oiqamm1qboimiNZpuuikiIqLppiPipqYjIuLp+/D95vy6X/frfr/n730e3sst53XP9x7u+V2ilKpM05qpTNkCGGCAAQYYYIABBhhggAEGeNSqpl9IkiQKWNbvfBc7PDdNIz1PPVK7Trd+OMPrRr8l9Uat2nT9+CyCW4yVnnnHowTXqa8UWHcdI3iNGozASscxgReo7h9YxTtfjwXcHoOVBjwJQYNPcmKlLk9EkODGP7FSO0TwOvU+IVjxZAQD1iPZK4CVGiGAZ6lOCVjFE7LhO/i0JKzUK3KImQY3S8ZKHZ4cr8A16sMQWPHkeANepF4MYqWmD2A9arcWsIonqOYafGYJK73yRDkB71nGSnd5r4jKBG9Sn47AunOb4CWq7xAr7dsA61G69wCreMK2TIMvPMFKfZ44I+ADz7DSQ9YhVgS87fiQGtdlmeBlvkNWnndYBljfGT8FgJVDbKco+CoQrBp6mrEyKfgoMOyvpxlZ4CT9vcXj0shWNe8nE8vCfzwABhhggAEGGGCATa1vAQYAZekAmr8OukgAAAAASUVORK5CYII=) arrow_up
* - ![bookmarks](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAHC0lEQVRoBe2aW4hVVRiAx8t4qXFMvGZGeLcblUVWdJEoiTIhI9KoHiIyKyh6SOvBh166vPTQQ2IXkKyIktIyLQzLUoMkSbKoVEwtK2+VZWrl9H3bs4Y1e/a5eDxzDsycHz7X2muv9f/r//+11p6zt91aWloaupJ070rO6mvd4c6e8XqGO3uGe5biYDck188y1LOGeuS3Hvs8AVrrWZ0LtUU27VbIbrCRlMVsluQwBptgHEyHS+BcGAxBDlLZCOvhY/gQ/oD/oFxxuw2Fy2AKTIIJ0AuUf2EbrIF18A7shcOQX0xCPhh1KsyEVWAES+U7+j4Co/PpLtTOOB2bA7uhVJu/0fdZmFRQd9ZNBvWB6+AjKNVgVr+vGX8fNEO3LFuhzftgRu+HrZClr5S2fYydC8Ohe9AfynbZpdPJ8CTsgSwDLiWXjcs4cIj6P3AUssYsoH0kZDptO4yHFZA13rYjoJ1g8+9cWz6bn3D/UmjjdDIBGhPhoOhL5WmYBY1J47F/gkGNfAEb4Ptjt5J9ehp19/XF4N7uDToRxL28Gu4m0mavVXKH02ganoGprTeOVXTG4Bp8HdgEv4L7WxsT4WoYlLvuQRmLc50Nn2NXHwhnbg9T9QDTWTMYR9nM7YTH4WzoDy55HQp4kPQDHX8AvgEzEuuxvhD6BZu5OZxO23JIZ8rxHkj3wDBoApMQbOq0q3E43AKr4U9I61lP25hgM3GYBpVMASMZT/IvrpdCwYMgKAsl/UfAc+CKiPUZPAPXI+esWZqf6mP//eD4gUFnsZK+JuEx2AGxTesvQHNiM2fYCfooiTsaYU+9IcWMZd1nnBl4Anw8xXpdkpPB+zMgvaJ09mHI3O9ZtuI2xt0EuyC2adZd2tpM9oKHVNzBTLwKJ8XKyqmjw1PXgybWv5LrK+CrVPsBrm8rx048Bh3T4KeUbgM9CZI9kI7Il7SPjZWUW0ePS+098OAKTptF92ccCIP8FPQs11YYhw4zOQ888IJNy9eh4cZUo0tsdhhciRJ90+GXlJ14ItYN8qhK2FMH0gye7LGdI0aiF8RipN+IGypQfxcdnxXQo81lTHRrgT7HdQtdnh2LUoMadTgJR3TDa5daxQTjHoBvgqd+lvjYW5Z14wTb2vmRnFoZSn1MVVqWoNBHRloMsEtvXfpGBa7b+ZHP4QrYaqsit8QWt21Nrn7n35e576Ojw6VqDuc8WUuZdsy95oldFam2w+7ltBwlu/5FVhWptsPt9lRVvIyMVNvhyHRtqnWHaxP36lmtZ7h6sa6NpXqGaxP36lmtZ7h6sa6NpXqGaxP36lntchn25XtJkvtC0JfOvhLyxVz8Q8Af8f4SksP8+vGVTUUk9zVEm841/TrKn5q+qNNmSb+4ijqMwQEoHA5nwjlwBoyHeHX4RnI7+PbzW8b4iWMHk/iZ8riF8QZUm+PgPBgDg8EvELEc4sL3YNsYs4FyC+zCrm9FMyWfw4dQ0MSIa+F6uAb6gxH2c0c60jQl35XMrFl2Ip+iYznlKibgpIoK/Z3PRXADTIFRoPPa9F4PiMWV5Qcz7WrTd2YfoOctSl8ZOZd24itUBwZcGnfB27AbVOLSCfdLLZ3APlgLD0JvmAzx+2l1bSEgFMmHsYWUm8G3IOkvEqXadb6+dPcD+SuQHpe8M44bde5HcMJxe1y3T0AHCgXE6DsBjT8EaUd20nYnuA0MdiFd3tNeMZvO1b3tx7V43i0ePGY4/XLNTvGhxGWDX9j3ghnbAlvBfhofASPB5egydN93h1gMoJkbEjdSNwDqHQTpJWsAfMm3AQyIifDaubmtxsBYuBAc3wwFxX2RJbGzLmv3w4uwHpy4WZMg6hH323i4AybDaAjiPUmL44amGn2fvBH8ILAEDJQZMzhmWXGOjTk8b66EaXA5DIO8YobbpD26XkHdyRu9Xu61YtBPB8ywE1gE+yGf/qz2TfR/FAxWUzF74T59DeZAmAFrIEu3be32sI1Ocg64RMr6uMU4l7TP7anwA+SbQGg3c/NhApQU3OBsXDLWgJvhueAqDPpD2c5h9+pM6BMrKreOHidwFbgHg9F0qbMvgSuprO/C6fmhx6fCLNgDsb02Duvs7dCYVnAi1+jzMDofXK6x8VB/nvZTTsRG1lh0erDNBvd/sNXqsI33QkWdDRNBr0vc88KgBuOWK2Fw6FfpEt06vQB8mmiv4eZc5X3KAZU2GOtDv8t7HriENe7z+YK4T0fUsXEW+GhLHL6VymaY2BHG0jqx0w9eA4273Nr8P6p0/0pcawOmwEEj7jNvPoo9VDpcsHOAv3VdYp7gS7k22x0qORv+jb3Yh/co2E+jj6KqCIZ93PnM3I5d91ZVBLtjdVj8gyJZ39WwjOHEZi3stvmvh9VwttY23MxdSuoOd/Z01zPc2TP8PxKYOEKWmL1pAAAAAElFTkSuQmCC) bookmarks
* - ![compose](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAF/0lEQVRoBe2aW4hVVRjH54xa6nSzm92sHiZNorJowMpxrDEoyECiUUpztIkeeumpoCB6rAwi6FHwIXqKXkqiEE0no0QNLWwyspmGsruWlVqp0+9/2t9hz3Lty+mcfTnpB/9Za397Xf7//a219lr7TGVsbKztZLL2k0mstJ4S/H+P+ESfwEqlMhn/VNAJpoOjoGibAIFfwDbWnT/DZOCrex34D4b9vvw4wVScRKEu0AcWgQtBmYb9DvgsA6OganCWhgFwL/lHEf35v3ci/mqVFrAO8AT4FugJHge6URZsg0s3aDfOAe+H8f0INAo3gavD9928iT2bgqvBYVAWgWEeG+E1G0wwAeQ18hTZ/cDKSvROECnaBD9Iod9DFa2BMqSDEgAqjtiH8H3v4XwM32ZwlZUPp/jbLgHDoAziXA7r4aXIhsVqgZLYA8Atb9eK9BbQGRarvOwxEDdfdU9D/UiOUH9bwTixAWGJ/QmYuKhUojU6xomu4HgL3AV89ipO3ZdYlc3LJOJTsAeR1bAEr56V+J4H00Aa0/D+BNxPM0NW4Wcyvqe0G7+Gu5b9IhAexnrYq8A+4OMa55PoDaA6p0kjG1jHvVqnetBFQBxAP9CrJ27qxYm2OX25IhdlxxGoRgqzYFOxHAIvgHMbIKKF7iIwVe+yMtsA5F4CjYiVPu2+lhG/z3QRNRTeKGIIB4NKgXgEHIrhF8Xb9WuxmmVayhphLVDPgimgEdtL5VWI3RNuxH0idp17hCGlAOg924zISmyXRdbSskVYYjVnmxFZvXt14DjBLKJummuEYXU3iNsuuvyirnXam2cRddNSRJjXj1bjteAc0Ih9QeU+RG6JayTqSeUSYYhpu/griOKR1j9MGze7EXWvKRPZUaaC6VebAYltxrFUYue64nzXRQ7pfki+CDpAI6bVWJuKD9M0Ere1TFO/7jLMV+2NbTXWh8JGTDuoxYjVySqVFRFhfV15DjQqdoQ2BuoRS/mqRS0KTZ3D9KTISuxvIKrPtP5R2rjFnaP4Ek93lInsvGmC6eM00A+asRp/RTu3esRej3+G63evKZOL4HvoJ/x1MW0k3XI/0E6PR0Q3/o/AHPeee53XHO6DzDRgw5ls3fYlNZYgYHO4JmvgfVy/DjqBPhDEWuaCIXQpDOYELNaQPg4SiQXlLfmazErEvmsOpbQ9j+RlcAH4G6Qyd9jYdVPmMAx6wDEgkXOBHrK+lIqg9RWXSmy3OzTxzQcjwOrq29x1bjn3mjK1ClbR0oYF07Z2U08FfewiPV8EMK3YOu8midYCNd9DWpHVSm1clZZC8HkQ2R4Qe4Z0kpEnr5Vb36oU+TBxy2uB6rXyluK7AehAb+UsTSU46zl8BcRuBBrSg5CuzTPyf+HTfPbNaUVvKWU2kLq2BMdM15n2OmvBd0BEw3cHGPaQ0r1XwNuhe/r2vAKxG0O+cNbWg7AvdT6zvTQrqH5rXhowWYeAqmD8Z+DTqroA9IKFYDqQSewDlN2kiywsM8GQnR3gCOkQQmeRanhL4J1Av2qY6SP7XvBklmLVWZaCV9D+6eAQ0DxVVK8EZiNkPgDvAS1sQ4jV2ThTy0Qw0ZwM69sD5joVdQV5iV8P9DOOxO5DpL5j5WaZCIb9AqAV+ij4A+hw/maA/XlEkr68lpXga+ltKxgE2sDs9vZegDMrwWsQuboAPYldtieW+A8F8p6X9VDMRHA9BPIuGyd4LG8yKfuL46WdW6xJcFQDU3i96LRTGoOPBGmnligsirQWre/AxZ4C1+DrpY/3PfeKcl1Gxz3AJ1inrsR3uiquBf3AZ9/g1FFMjZXBZkBCW1Sf7WSx1NEx0bSv1QZBQ7tVoYA8jeDEf7yhXNuZ4B2gSq0qeBjuM1MJViGsB6hSK4rW598BMO6/bKPE14YAFXQ2HQWtMrwVnINAYmufjqKEmr8mOIj0bVTWSUYb/qQPbBoaRUABOQz03znLwUQTkyat/hZDpZrxGjqLi4VgMbgJ6L1XFlNUPwYKymvgACL10FPbCYJT12zRgnFbyxaVFE/7lOD459P6d/8Bhs9x6sTqrJgAAAAASUVORK5CYII=) compose
* - ![delete](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAGcElEQVRoBdWbzYscRRjGexY1EPK9u9mVoJH4cVBPCYR8mB0IbkISyB/gOYIeFSUQQaIX8eBBDKuCsBFFxJuieFCMEb9RiZrcxKOgB7+i0RjN+vwm9Q41Nd0z1d3Vk9mGh6rufut93l93dc9katNaWlrKymytVmuD4mek7zX2YpmxqWJVwwrl2iL9qBp+LpN3okywjNYo/qh0Sjqi/ZVlxqeIdZ5HXA1HXU3xqbnDMVJGYJ+UzktMi1+le6VrY8aniMHLeeJNDdRCTWti88fCTirpSemChJHpT/Uflq6LNawah4fzwtP8aanppDQZk3sosBJNS4tSCGumf+jcMWlFjGGVGHI7D7zM12+pjRqnh+UfCKwE66SXpL8k3yDsc/4+KfmdJqfLHVMDta4bBF0IrIFrpaeloqsaQvM83S8lgyaXy2nvjdAz3KdWal5bBJ0LrAGz0rPS31KYdNA+8Y9Jtac3OVyuKjVQ+2wedB+wAqekE9Iv0iC4onNMvUelytCMdTmGTeOiGqgdhqkQugdYAdzZBakqrBXAXXlCWhkaDttnjBtb9s6at7UwwNJzp7vAOsE3KKaCfcbZwKrtP8r1oBR9p4l1Yxhb1dcfBwtMG+xCd4A5IHFHfpL8AXX7fFw8YGbDWmIlxtT19cfDBFsHWm22UVqUfpP8wFR97tbxCNjjikt1Z8PaYYMR1uwRidd5GJRyn39k8PaeCME55s4Rk9IzzAUjrNmcdEb6VwqDUu5fUv6npGsMmr47xrmUXmEu2GCcs2d4v3Y+kZqaUlbAf/J4SOKuIvocs/NNtDDBtp8L7b+lt+vgaWkU0M/IB40CFqbt3VllnQ59lu3Tyc+kpqfYZXmgJu6o5YQBln09jD07WdZSwF6JKdA0tBXWREvtMMDS6mH0d6yvoLb0sdT0lGsClpqpvW08ftt9hv2D9LVxdb6Vmn57p4SmVmreG/LYfiGwg96hwd8sE2hgqXWHweW1A4Ed9AElOTfm0MBS44E8SP/YUGAHzfQ+O6bQwFJb4TQuDexBj9v0tmkcBdvh8OmH9XUVt0nvSE1/7415kVEDtWwbVrd/PmpK9wzIsq0y+VLi6sYU1kQM3tSw1a8tpl8amKTa2s7wakAbbDsGMIypBOygdwr6C6npr4j+DMELz50hSOx+ZWAHvVvmX0mj+EaGB167Y+Hy4iaUoM7GW/sHiSvf9IYHXnhW3/KuQswxOa6SFqSqP6X6UzW2jxeeq2JqzIupNKVlyEri81K4sBVbeJ04PPGOXjH0wUsDy2i19IJ0QapTeJ2xeFPDah8mpl8KWAbc2cel36U6BacYSw3UUupORwMr8aS0KF3NOxteKGqhpqi1YWZAFLASrpdelMYJ1uCpidrWJ5nSSjQtvSyNI6wPTY1JFsRJNMqPHoMo21IjtVZeEJ9xCZYDrF0cg54pmt65z7BAp6QT0nKC9aGpvW9tOPel5WAX1KZaNrVCRtlSOwx90D13WAEsiD8nLWdYu7AwwDJwQZypUHf13wwHtWfkgwbFpDhnf/rQtyC+SeZ8Px3FnX1LPpud6KcAG5QDJtg2dZ5hdTZKi1JTC+J+MZ/K5yZ7g9KXOObHNNHvWRA/JsPzIzB9Xx53GKy1HJM41wSonxNGWLN56Wupyd+nTiv/rQYZtpyTiPELTNmHDcb5zltanTnplHRRSmlErjek60PIcJ8YF5vaHybY5vDsfizpwB4p9TLp68p5SwhXtE+sxJhU0JeUC6Y95tkF7tBn2SGd/FxK8VcAHyjPzVLP+qwZ57XEujGMrQsNAyyHfK8eYAfNM82bsw40KwJ3Sn1/teOb5/UZ48aSoyo0tcMwH3r0ATvogwrmzwWq/Pz6nsbdLpWGteIY63KQqyw0NVP7Qcvnt7nADpq1YZYzeA5iTV9T7I1S9DT2i/H75HC5yBnrT63UXLhGXAjsoNsafFaKudOvKG6zVBvWwMnlcpJ7GDQ1Umvbxue1A4EZoO2wSzToc/ptxdwgJYO1YsnpcuNRBE1twB62cUXtUGAHzTN9TsqDflPHb5OSw1rR5HYeeIXQ1ERtuc+s5bA2CthB80yHn9P8pDIrNQbbLfQKNF54GjTPLDUVPrM23tpoYAe9S8k/kjB6VdoiNQ7bLfYKNJ54UwO17LLzMW2nWA2K3vQ/we5S8N0SL5LvZHI5enCCQPnzkcU3snukd+X/YZm0/wPdHqnTTpY+CgAAAABJRU5ErkJggg==) delete
* - ![download](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAGb0lEQVRoBd2aX4gVVRzH3V1dU5JMk9Q2wVxCo0QNTYRYS4l6CBFBomA1qjcjSOgPPUgR0VNBFBT0Bx96qAiSXipCH4rKIhGNUqE2SK3MqKwsLbXPZ7rnMo73jnPnzF6v9wefPefMnPP7/b7z58yZudtz6tSpMaNlPT09E/DdDxPhMpgNJyBtfTRG4AAchePk9BflqFhP1YIRqbCZsACWwjWwGIrYZ3TaDZ/ATjhIfh6IyqwywQhdRlaLYBVcB5Mgxn5n8HbYAjsQ/lGMs/pYz3AMOFLgG/AzeH+MBvo2xqqYXB1bSiyBe2EJvAaH4SSMhtC0T2MYy5jG7i0jvmXBBJoMj4D3VjuEpkVbN6axzWFyq6JbEkyAhfAqOJtmE2l32xzMZWErogsLxvE62As+Vtotrlk8czGndUVFFxKMw41wEM7FJdxMbNhuTua2sYjoXME4cVHwEDhZhACdWpqjufblCW8qmIHOxHfCT9CpIrN5mas5N53B8wS7kPgKOumezQrMts3VnJc1O8sNV1qsmq5k0LNwI3hZx9ovONgEPk4amcvRR+HiRjtb3KborbAB0fvOGJs9EnRwwf88HIHsESzbVuisbKzQdh/Yp6z/7DhzV8OEECOU3qd148z20FgDK+DC+o74in59Y2pm7rNPVWbualhT01T3e5pgts6D9eARrzIB3LXVzF0N60FNdasL5kj0sXUtzIf+eo/zt6IGtaytaUuU1AXTugKuhyomjsR5B/xRi5rUllgimCMwltYQzAHr3WJqUdNQTWOyuFDcpbASptnoMlOT2tQ4phfl3uBzwes9byZl93lpalLbXLV6SXtzr4BuPLvISkxtauxX8DjwW5Qv9t1qalPjOAX7vJoB3TRZIec0U5saZyl4ELr57CIvMTUOKngAqlxGJt478I8aBxQ8Hbpxds4eczVOV/BUuCC7twvbapyq4Ha8JPQVOIBF+hRwk9slWVLm9miy8xjbj0PRA/YHfU828eVm99mnyFziu6/9XT+Mh5as7KPIoE/BB/BPgYgeoP05/dx3OxQR4LrBF4IHoWUrK9j7wZeNzXxJGGk5amYAPvyovj2zuWGT1eEcdjwOpeYdL8mytpyBr5BAW5akroOxy4n5MiyFUqZg78W8+yvPsZfWEyQy3WzyOsbsq/n2Q9+TYMwypsbjCj4EXlJlzPHDcD/48W+0TN8PgF9kyh5YNR4y4e/AGbKsOVveC8OcCSeUSg2fir0H7oayc445qVGtY5bBHnDmjeFXxt8GY8Mn0dhSX+Ds/RvE5OZYNao1eQ/+kNJrPNapoocg9/edIgdCH3AL6DM2L7WpcZqXtKd6L/wJsXYRDl6ABVyK+i5ltbGLGfw06DPW1KbG5NY1MS+bbyD2SIbxO/G1HFo+046BG+ALCP5iS7WpsTf5MY3KPPgYTkCs8zD+XXzNLHL5hj70dwb2WbsNgp/YUk1qm2ecINh/MXoMfoTYAGG8gV6ES4Kgs5X2hZegivkk5KEmtU2qC04q/082u9gROlZRmvgmSH6lzBNMHx9pJlZF3LQPNQ2F2PXfh9noEvF18AGdHhBb/xd/d4SAzUr63AX2jY2XHq8WNU0LceuC3YCtBiecqgP7HF0XgmZL9m2AI5BONrauBrWsTsfLCnbV9AxU8ezLJnwAv2vSwa27DX6AbP/YthrU0p+OeZrgWgLO2FvB99zYoNnx+/B5dUiA+kL4FrL9YtvmroZkZg7xEn3pRqjTcRhGIDZwo/E+rpyNZ4D1Rn1it43gdzjoSZdnnGF3Yq5h74Oq76sg5D18b4PQrrI0Z3NvuKZvKLgmegqDNkPVs3aV4rK+zNWcp6TParreVHBN9ACDt8DfkHXeaW1zNNeBtMBsPVdwTfQgTt6CThZtbuY4mBWYbZ9VcEr0mx0qWrHmdlaxiZbsEWjWxuFkeBhcm7pkPNeXtDmYizkV/r/pQmc4HAQc+934ZtgBVa/GWjmAxjYHcxkf8itStiQ4OCTIbHgO9kM7z7axjGns2SGfVspSgkMAgq4EZ0b/i3U0hevbGMZaGeKXKRv+cylOCxufY/xCcS3cCl5ii6AXqjCFeum+A2/D54j0Pbu0RQsOkRHu+6zP7avgJvDsz4VWxStyD7wPrsi+hP0ILfIbFl3zrTLB6TCId3KbCK6X58MSmAOuocW69jUcrmH9U9gF38NRRB6jrNT+AwkLDdxcvfCRAAAAAElFTkSuQmCC) download
* - ![favorites](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAFfUlEQVRoBd2aXahVRRTHz/Ujv+2mZRGZB7W6mtpFikC7+UWUZiqBD0JPFdRL1EMFPfjoU4baS0FUD/UWZBEVShA+BCpmWApRSkgllNpDmZWZt9//eOay72afvWfWOTPn3rvgz8yeWbPW+s/XmT379AwODtZSSQ+CryVgA/gVfIx/pelEhFMBVlvBOaBeFo6Cean8y09KsnMg932TqCOs9M2UhMfhMJVsxtHcAmcbmekLCsqjFKUkvAYG1xSwmEHZqoLyKEVJCDOCNxH9HUCbVl6mULAuXxjrOQlhgl8Bbi0h0Uen3FBS37GqVIQHiHh2SdR16jTlo0t0woycpuxiUDSdHcFeMv3uIWYanTDB3wIWVZBQHP10zuQKvbarUxDWT1HRz1E++Ds99fLtgp6jEmbExhPNcs+IbkZPiCpRCRP5TPCQJ4MJ6A3QSUqjSWzC2ozuC4j+fnSnB+gHq8YmvJKIJgVEpRPX9QH6waqxCa8PjEhHT981H2j6qno0wqzF63BhOUxsom3Zb7aJqGsUjTAONFJlpysXQz7VuXpavrBTzzEJaz1adlzNjHs6RTBvJyZhjZTF/kTaWZZCnlvhsyWgQkPZQpagzsX1bFlAXjGtDdAPUu1p3PPQhCCXkdwG/mta0PWLds060AuAnqtEOjpdbQR3VymX1P9F3UfgGJA9X9F92c/ADaQ2P8V0DJ4/kDbeYKaSvgI2AN0+OGJK1VAbSIhTOXEOybYll2kte77yD4rqrHyb85S9Cl4HtReAyI11/A7HpRq5PSD6oR0f3Rad+H7S1DvV7UgS+tc1cU3n3V/AWJ/SX8BxVuMinow2rNNjlPQVeH0GFg378kDBfLAPXARjZbTPwmUXmOG+bgz71EKFfqKeAUWfREZbJxyCxyOOqEuHER4qrNUWovwy0CFktBHV4eNZMNvxyaaFhKWAaBt/HJwEo4W0luSKLMF8viVhp4iBeeBd8CcYqcQ1qi+CKS7uVmklYdcQY0+C42Ckkf6EmO51cVal3oRlCFkCdKgfCWtbo7obDO3AVWQbHHyUsjo40E6uq9cvQbdG+wN892fj8s0HjXDWKA51/t4JUo72H/jTDtybjSUkbyYsJ0gdfAtSjfTn+JoWQjCv2+57a4M1QaQSvZvrMsIs7RJejGcdUlLJUhzpZsYsZsJcCen6ZwCE3IaYA2021OfUdU3fJltmwni7Fvh+KDMF16KR3ux0lWuSdgjPxeNdJq/tNdKNqJaSSUyEmVK6JNPomtqbIh3eSKNsEmvAarfJ5LEzjbbR59MtpqyEb8eZjpndkhtxvNri3Er4YZxpx+yW6Jdhi8V5MOHm+n0QZ9afo0u0fQO8A5S3iPaQ1cTSG9w4f/SqesZBH/gRWI6T+gyyxfkgvw2cMdrS+/lTzpZvGnyWxsnTwHLRd4R2a/OBqQyoztKBe/P2qp6DCBOUptKHhuA+pU1fq2Co0/F0L9CVaghxXTbWW9ktKg8lrFfCrwODeh/9wgu1bEDo6OT2Fvgb+JLWq+nQEsnaa5UPJbwKBxc8A9KXPG1O3u+u6E4F24GvD3XMDjCxFcF8uTdhjGpHfwn49L42lCeAdyDZwGi3HpwAPr6+Q29htn1ZPoSwfuz3ewShXVcBNz62lzkvq6O9DjZHgQ9p72kdQljvob9VBPAN9Q+UEQmpw5b+Sf8e0FotI/4a9ZN8bIcQXlnh9AD1y3ychuhgU0tpJyhb14epn+ljN+Sk9S9G1ct50d8SdgF9x9EO3lHB5hXwPEYfA8dbGD9LuWZBtfj0inSQWUDTKzu1dAB5Dkz2tdOOHn70LvwVyMag/FYwzse295Rukq5j+G1wEOib66PAy5FPMD46+NPmqTV7CpwGGvkJPm2l8z8GWDNDloqpGQAAAABJRU5ErkJggg==) favorites
* - ![home](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAEK0lEQVRoBe2Zy28NURzHe/vwqEepYkFIQzxWaCOC2HhELEgQImhXIrqyIEXikVQi+gdIwx9AItg1NiJELMSGhKQbobY2VY9Srfp8m5lmTO/cOXN7Zu656f0ln8zMnTNnft/z+505j5sbGxurmk5WPZ3ESuu0E1xbigjncrka3jsbftClIvsU5RZ65aLK5Lj/C75SzSjHWCuJYLxqhPXwBgYhylq4sRaixChDP8EzGIJ4UwNnCR6tgFswANegKer93LsLim4herm/JKqO8O+ZRdhL42acOwunYAacg2Hu3ePYj3Ph1A1fU2ySmZSZeCiTjxaC1LAboRs6QGJl8+AKXIU1kLqlHmHEqlFboQv2gD40QdPHqx3qKdtJkD8Hb9o+TzXCXmT1cboB+cT6evTVPgIXeWYl6DoVSy3COF2Hx0rjTthp4L0a/4xXrofn33OeqH8avKMqFcE4O4uXb4ULsNfEEa+M0v00LIIuCKc/P03NrAtGrD5Iiuh10Dia1JTOR0EZsjjpw3HlrQpGbD0v3AzFig36e4CLkeAPNs6tCUbsHBxS+mpsLSayYT2KtLBqVgQjdgFe7QP1u9VWPbRc2ZQFe2LV5zSBWG7ZP+vVTUkwYhvx6DicB+fFqvWKFuyJ1QxJ00It48rCNNgnNi+N23hQaVw2YiU0cYQRq9Q9CJdBKV1q02zMeEaWSDBil1L5JTgBDeCCzcUJ8cXImfACOeqayjbBffgDfqu6cPyJP3dgVZTvwd9jdzuoSFmgicRDGAYXRIZ9+I5fPbA6KC7feUHBVKD5rJZ1EutaZMOiv+HjbWjJJ9T/LVIwDyqyh+ApuC7WFy/RCk4r5HyRwWNewRSW2N3wGv6CX2E5HBWcB9AaFOqfTxJMQa1lNewosqNQDiLDPmqv+hFsgzpfrI7/CeamVjwnQZEtV7G+eEX6MeyHGl/0hGB+1MJdYt+B/1C5H9UdX8J2qJ6IMBfz4Ri8hXIXGfZfmdoLWr5W1zJ7ktg2aId18BuiTHNvDVUumQSNxDikLSdtBzdok0yCD8MyiLNmCqhxXBL9An+egNI3yqRT9z+O92FO/O2UuOMuymoqF06bUl53489MQw21Gm8lWmkRa6R/oVaMfT6lAmrsUVMNRa2HU3I8k2orgjNp5hK+ZLwPp/x+fR+0ONfMp9BfJ+qLmulpyze1zMtC8AACbkI/xAneQZkO0JiZimUheAjPn0MfxAnWVo3RiEG5oiwLwXJsmGFDK5iCxrCnGZNSOzVLra+EPDZ9T6EMCFVZ3KWpI8XV7uBTFcEOBsWqS5UIW21OByurRNjBoFh1qRJhq83pYGWVCDsYFKsuVSJstTkdrGz8L0VTv1i+NVF2CyTJDC0LX7E8HIx7D/Vrb3wDaLvY1D5QsI/6jXZUEwk29cDlckki5bIOY9+mneB/GfbU3e4Ey5kAAAAASUVORK5CYII=) home
* - ![info](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAHOElEQVRoBdWbXYgVZRjHXdf8ysjUQl011lbRIFEjM6Uu0iyiEDG86EItKoIuuhDJCgoioouugqKbgi4CKwulILG0mxLTUtMyTWQNPzLTPszU1cx+v+OZw9nZM3POmZl3zQd+zMz7zvs8z//MvF+z2nLhwoU+oaylpWUQvvvDYGiDdjgP1dbKRSccglNwlpxOcwxiLUULRqTCRsNUmAk3wS3QiG3hpp2wCbbDYfLzhyjMChOM0FlkNR3mw61wFeSxv2j8FayBrQjfmMdZpa1POA84UuD7cBzsHyHQtzHm58nVtpnEErgvzIB34Rj8CyGEVvs0hrGMaey+WcQ3LZhAQ+FZsG/1htBq0Z4b09jmMLRZ0U0JJsA0eAccTeOJ9Pa1OZjLtGZENywYx0tgDzit9La4pHjmYk5LGhXdkGAcLoPDcCle4SSxUbk5mduyRkSnCsaJi4IV4GARBSj6eALfR8sxunLEMUdzbU0TniiYho7ED8GvULRI/UV9cDbnrsauheXQCVnjmas5J47gaYJdSPwAIfqsPlfEnwRl/eBBOAlZROvXnGfFfUfXNQXTYCKsg38gS+B6bT6MEogfiTcKNuaIa87mPjHu2+segrnRBf8bYN+ql3jW+ntrJVNK6OJGw+VkVt+2M3c1DIrHsZ9WjPVwCxcLYQ4MqVQUf/Jjikt3VnnX4eauhoVlTZVw3QRTOhmWwjhQfCi7ppZjkjOf62FCrfomysxdDUtBTRWrCCZYK6WLYAo4aoa0JxKcu2x9CsYk1DdTrAa1LCpru9g2ese58lddD+cgT/9ppK2j8ONR7HLf9Um8B0XOCmpR04QoVmnQosDp4BHYD40kXMQ9zsPfgSI/hyNQhN+4j/34VVu/0g9b/nXbKFgJf0O8weV+rSa1tam1b3kUm0SB77sj5KUw18OhTE1qm6RWBy07t0O4S7veto8J6FLwbng+YHC1qbE0GDtnrYXeGKzsHj7NT2AejKgMJn36DODaASZEF1KbGof4hJ2vXM45cIW2nwjwKDyA0HXgDicyl4RpC5LovixHtalxnCcd4PwX0hTjcvEFRO5ICBRyoWNINXYo2Ek+5DJyP/6fgZWI9XVNs3r1aW3r1alxjIJHQqjR+Vt8L0fnpxzrmU+45pKzXsMG69U4UsHDYWCDjRq9zYFpCzwGLi5K5qyA+KQpSMHt5VtDHNQ4XMEh+s5R/L4CuxSIUKeDO8BX1pG4lrlDmlqrosCy0jxcoL+KK5PvgFbEOka8CKsgbRd0u/dDUPMJh7ArcXon/A4PwwxwyvkKkuwuKi5bwYqaDbdBNAP8wvn3kGQ+4RDdq1u8UE/YINUjv313L/35bLfo5Qte+xs5va5WXdFlrrRMImnkLCreaRxtSnE2i7q8n3VS3Jeq1HhWwY6o7k1Dmn/r3ZgSYCZ1g1Lqi6hS41EFHwC/QIQ0P5D7vbiH8Tq7DnD7Frr/qvGAgvfBnxDSNqcsOJx7Xe2FNjXuU/BeOAah1rHn8f0FJJkDlk85pKlNjXsV7KPeA34KCWUuM5OsN760qE2NJxXcBevBfhbCOnFqsB5G/72aQj8vVVuIN01tauyKFvPbuHBhEGJ6+hK/SSLaqBsPmrFfhZe9KND0q7ZtjiM+Ye0guIXzPS/atuPQflzLxlI4Go6AOys/wq+Gn6EoU5Pa1Fj6G7Dfpp0nfeT+EkXaOZx9jf+kJ+xqbAPcxy1vwhnOd8MuKMrUtB7fauz2HcsgBuuAQVCEHcLJ8RRHrr42kExpWqRPu3mYDTektGmmyhVe9x+QYJU/mVK5AHwF/QblU8nLWnyMrY6Rds69T4Kvd964tleDWhZUx6yItRBzo+7A8QcUEXQVfkZVB6x1zj3GfQ587YqIqw81qKV/dcxugsuiJ3OT/cr+lzf4S/gYXB0wfk69HwX8YRxN88aL2pu7Gib3iBcv8BpbDJ0QOch6fB0fNf+1HOVXwD2wE7L6T2rXic/FNbXVLLw4mNmfTuRMZi/tx8djUDYHPgAHlaSks5abs7mX/lrYI3a8ILqmwTB4G9xWZQ1uu7egHQbC/aBQR+88PpPamqs5D4t0xI89+nD1DTT0A9waOANJQeqVu+j4Ddx3u26vd3/WenM01zHVGuLnqYK9GXNeXg15RGcV0Wg7czPHjrjA+HVdwVWifRX/j6LNydzqii1pif8CSdc4HApPg0u1IqeQRp9i/D5zMBdzqjkT1NLS0BOOGuLYv+E6lWyFolZjcSGNXBvbHMxlQJRfI8emBEcOCeKo+xq4A+nNp20sYxq7PcqnmWMmwVEAgs4FR0Y32CGF69sYxpobxc9yzP3feMo7nJtJxDnWV2w6RPtsTnOZQn1118JH8A0ik/bWVNe33IKjEAh3qei87Ue5eeDTnwTNilfkbvgM1oHb1oMIdX2c2woTXJ0J4h3c3NyPgikwA9zjjigT7Xf3ce0XCfF8M+wAv3icQmQXx0LtP/qKurS9uZqyAAAAAElFTkSuQmCC) info
* - ![locate](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAIDklEQVRoBe2aaaxeQxiA3eqCltpLkWotLUUtsUuJrbUFtSSaiIjljz8kQhOJiAQRQYREYvmFSPrDFiSExFpL49JSS6u0Re1bLUVRz3N7ph1z53zfud8956sf3uS5s7/zvjNzZuac7/asXr16g25IT0/PKPrZAfaFXWAMvAEL4GNYgS1/EjYqPU07jKNb4sGZcBocB0MhlYVkPAgPYM+itLDWtA43BYY6m7PBZVSFXuqd2ZQ96m3S2ZkY/0lFR+PBcFlf3ZTTjTiMwQfCR4WzfxO+D8/BTxA7Vxb/nXqzmnC6docxdDg8WTj2F+EtMBrMPxiqzvqn1N2nbqebcHg6hoaZfJn4sNho0hdB2cym+bOoOzRuP9j4EBTWJuzII1F2OngEuZQfwcBVhLG8FifaxM+jfHybOgMqrtVhet4OfH6VHsjpn9xXWu3PRKrtXK1qtVo5g6q1zNfyzJ1UFOnwCcz6ZqEq8bHErwzpCqE6JtHOsBap2+FNsGrjyLIjid+PvYfBDOJPwJSovEp0wyqVqtbJ3Xqqts3Vy83EKVSUTiWns1Nd2WesY2U0XAHfDkZBpu3vbHzu3rVI3Uv6G6z6oBbL1il5b1108LG6Hf4ak+YO3qy1Gl4ltnhtqoZIrQ6z8lZi06PwWw22qUJdn9Wkq09NrQ4Xhs0hfLgGI99Fx30MotfT+sT9oG6wbhzMAzebTviRdufUbZf6anc2GInBh8A7HTj8A23Ogw2DrjrDxhzuG80118KHMP7XCo57934Ljq/TwVRX4594cGADblmXEEyDqeCrYiy+XPhC8RzcioHfETYmXXE4WI/jXi1PDOkiXE44CUd9pWxcmtilWxnt0k5lVbecteNuO+xsplLrOZsqT9PddviL1ADSn2fyGsvqtsO5N59c3v8O1zUC3Z7hDzHcm1cs5nVNuu2wr4+pNHrupp3V/cUj1d+X5vwdTsS+RmYqjKDcT0N/cjz9kSmvNav2iwfGj8HCfcDflXaGbcGPezpsuBfEsoTEMvAnFmf7K1gCXjPnMwhfEtYmg3YYB30s9oeT4TDYCbYocGY7EWf6+wJ/qZgDj0MvA+Cdu2PpyOFiifrJ9SS4AHYDv1bW+oURfUF8J/bjgj+l3gteUZd38ggMyGEc1aHJcDb4k4nLtZW4RMMy/YW4LwonQHz29hZ1NiV0yW9VhASl4rK/G2bDAhyv/JGgssM4668K58OFMB5io0muFZ+518CPb34EWAga9VuxMvxlMIhH1FGUvUCZb1G7wu4wBfaAg8E9ISe2/RjugbvQUe1rKRXbvhOj8Ax4AxxJO0pxw3kEnHk3pezLO/mbgV81Q3v17ZmzgXxXk7rU+TSENmlo3y/C9JyeNK+lsyix08vAWUs7Mq3BL8GxMDpVnqapMwqc/aDL9lum9dI0ddwETwX7ctMK7UNonndybc0OdtBZ6jANh8GV4DMYFMfhj+TfCBsFZe1C6urwXAh6Kjkc9NLO5/wW+DXSEXQZausVUPoTa9ZhGvh8OqI+F7HCEP+I/JnBkKohbXS4N9HZdoZT/bR3JssmwpmelrYJ6aEU5mRPMp09l1JOlpI5lo1mFmHYvDyPXfqzUb6CMCc+b4thv6LQgTMvK8VGdhaFblwu2yD2uQRy9m1L/s20XYYd7xH/twTPQ0ipl4XrwY/pYUbT0DKPmBgNnwc7BV1pSJm674Sg73Xio9J6IW0Z+MyrO+7Li0nZsla39unD8KArhLkZ9iw8F0ZAmbQq+6asEfnO0nx4rIgvIiydYYz8mZnSATfPVNxjysSB9X/DboWv40o5h4+igod/Tj4j02XoaOdkHkauzBWYR5nOOcNSVeZQ0UtLTrR/AuyYFLrkvQn66HikrZMw1SGk5BooW84ukxGh7voOsWUjuBnCIxKHDvylqY1uNKnEm0Na5kiOTjPXR5ql7ixuD3uU9G/55mlZzuGfqeRI5cQb11T6yj0KufpN5vlcHwRHl3TixH2YluUMf5NKXghysgmZHuzzcXoRy6VsYHJt/QXCAZ4A6gkyoMu/jQo9vm9fBWUbqD4shH9LusYp9WxbBo5Q/EzE8Qcom5i2bZemjTelBYnerdq1S8tpvzf4Y3lsUxzXdk+ALfq17ZexZiO4g8q+1cRK0vjblM9I27dKawD8EOl1FgZ006L+TNCZ1J44re03Qb8Ntt/Vkko+7FOh7OoWK/bMdefeoZWjoYx6nvFx+8oO2wdcB98nOmJ9Ie6V+PDQbxz2c9hCZGNwhNrNspU1+hO4FiZDq5uTDls/GGZ869igOK4uUKe67SNuG3SkoUeq9fvdsvp8izuI4zTYBeZClU5Cp559D8GFcCCMh82DXuJukrE+nzV/OewbeOuCbQ4FdahLnUF/u9CLzfMwLuhMw5ZfPNgNp9H4NtgdXOoDkRVUfh/cKX3mloM76u0QdOmA1793wSW7G0yEKTAcBiIOnndzLxvev/OSjkCappVL6hlw9NqN8PoqX4Vt3s/Hp/an6ewz3K/SmhvNDSj86T/otDZp25jU7ly6ksM2RIbADHgFBvJcNTXrOvpCYdOQnHO5vMoOh8Z0sA1cDi9Cq3fSphy1z2fhYsjuxMHWXNhy00JhqbCheWtyJ54Ox8D+0KT0ovwp0NmXcMYjc8DSscOhJxwfRnxHGAfHwQFwBIyEwcgvNNY5HyHxHF6Kox5rHcugHY57xnnPWS8t4lHmIHjEeNyMBXf67WACeJNbDH+Ag+ax5fE1D5YWcd/cVuKkR04t8g94XuILUVeybgAAAABJRU5ErkJggg==) locate
* - ![maps](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAADl0lEQVRoBe2b24tNURzHjfutXEPycDAltwhJbuMSJUqSB/HiES/+AK9ePc6T8uCFkImQW5KGkdwSxYyMGkZu45bbDOPzyZyTrJnjnDkGrVm/+szas2bv397f33ftPS+/Vdba2toj5igj0NcfRkG/3qWIJdcIrs/AO6gDq7cKPkOjUNAmxr8ePJsix8NUWAvLoapowSQawIUzYCZUwAqohF3QAjtgGTyCy5x/nfEu1MNDCmAxuiS4Vy8ST4DZMB9WwiTIRUGC26q1gKtWwyyYBsPB5aLIL5CNTxzotDeWTeA5DUKuO4xXoQbxHpcUbSIzJFkDi0EzdLYnBNGuYJJ4ch+YAhvB5TAORsKvib4x97vwPpk2FjJuhibu85zxAlyCangBLRQib06u68t5vk4uVYVqgO+oqy9v5ASTRLd0LQNLYB24bAfBnw5zikX0HtuhGW5ANY9ylvEBvIY3FOArcz7rWHCpboBFMAxyGjguKIZy1jzYCqfAD5BLslB8J3dCP/AdOgo+fKHXd3Sebh+EctCMieBK6Oj8QuYrXZ7roQr88PiSD4b/IVyyfhB9jQy/uppTUijYhANLytJ1F/sxzL7POpg97vQdFfwVTNYtQsHdKpLg2O1ODieHI6tAWtKRGRrISQ4HJYlsIjkcmaGBnORwUJLIJpLDkRkayEkOByWJbCI5HJmhgZzkcFCSyCaSw5EZGshJDgcliWwiORyZoYGc5HBQksgmksORGRrISQ4HJYlsIjkcmaGBnORwUJLIJpLDkRkayEkOByWJbKLbOVx0r3E7httIbttwNvzddt//JWxIfQynYX8pgu2TbgBbjw9Ds53sNHJv49gOehu5bUe2DfjXojDVpWG/9iu4CEegBp7xfO+LFfyGC5+AiQ7BFXj/c8s+xw+Z24PwvYwKnQxLoQLccGEB7Hsu9t5ckjcU2QjuozgA5+Apz9PCmItCbvqWs2vhJpwBl8ZrEuVtOebPtiWLbf2ymyL0ZVT8XJgDbgHIgFsPOhPmr4d7oAnHue9txg6jI8EfueIaHIOrcAuafieSc/IG19vw7TYD6UEBbE4vhwxMB7cizIYhYPT6MeR+WjBFPoCToEgF1hb6bD8LNpHLwT0L56EOGkhUchc6edoNcruvQWoQ7/6GMTAa3E2zACxGNjRhH9wHV4zP9oGxqCjj7C0wA06Ay/YliRT/T4MCuGnEfQ4feJ5mfvdfaG+OXSWdju+VpAoIK3D9tAAAAABJRU5ErkJggg==) maps
* - ![more](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAADJ0lEQVRoBe2YS2sUQRSFp5MgvmLU+CAMiBJFDBHcCeoPEFciuHMjroMK4lZBcONG0JW60U1UEgRx59IXuNMoKEElKL7GRwyIqNHxO0N66FT3UNU9IHRNFXz0VNW5t+vW6RcT1ev1Sie1rk4qVrWGgn13PDgcHPZsB8Il7ZmhqXKCw6kt8WwgOOyZoalygsOpLfFsIDjsmaGpcoLDqS3xbCA47JmhqXKCw6kt8Wyg6XAURV2wEy7BM5iFtzAKu2BB0dqJ7YEtcBYmQblfwzjshUVt5O4mfhjOwwQodw3GYA8snpd77n9pFXMYvoP+qDaZZewcVKXPAzE64Qn4CmZe9f/AFSiSu4e4IzANrXJfZ24gXjO/KxEcg9+QFZQcU/CSONh2RKsraMQhr85xE/psOeN5tCr2APyA5Bqzfl9D06tYtX3wC7KE5pg2ZX98UtsR7XZo5ayZW/1DENnyzi18CO1nyMqTNXYcrTapcitHkBLJiZW2RaGRuxcg6+Stxu6i73fI3Y3uZM7cU+hXQeVvzsBP6Dc5LupxztzaiEGH3AvR3S+Qe4dc0D2cp/Uj1oPI1pR7g030n+erWlTe9pMA3cu2Jre+2ERtzBdZe01BL3Ke9Al6vQZsTbfKQ5vImH9PXxtqa3qVPbWJjHk94J6r4DPGhK17A8EHm4j7UAWP2nTG/GX6NWMs1SW3rrCroLeLaxtDqDdG4368zbHVkzM5Polus+2hEs+j7YNxx9zv0FkfhoncvegvOuZ+iW6rYhtfTXTWgV7OyeLM3w+Y3xaf0PVIzAqwFf0IzW7XnLGOmLUg58y1JvsTzA83Y5o/eLcyMQISJAN0z56G9bE275HYNXAU7kAy9xv6p2Bj3pyxntjVcBDuQTL3FH19Dg/FWh0bXzUMNhsf23JkOQzCK9B1P4NY39OFG3kjgpeB8g/AR/gG0+3mJkeF9Lp9lkIVZkDfC1r3vPs8VTAir1uRd1mpNyQUXGr7HBYfHHbYpFJLgsOlts9h8cFhh00qtSQ4XGr7HBYfHHbYpFJLgsOlts9h8cFhh00qtSQ4XGr7HBYfHHbYpFJLOs7hf5j4Vg3iLoGkAAAAAElFTkSuQmCC) more
* - ![organize](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAEdUlEQVRoBe2aS2xMURjHjbbqUaLoI7RChQUiGo9YaEqkoolIkCASSki68dixsLIVYmHbkJA03UgkFRI2QgRBKl4RgtJFK0jUI+o5fv/p68ztmUlHzpzO9PZLfjP3fOfcO9//fOeee+69E4lGo6PCZKPDJFZaQyc4N1mGI5FIMfUVkAfZMPaVwE54yqn6i+8BllQwravgAEyEv5DppsQ8gYPw3hqsJi0bNJ4El0GZzSa6iHcbjLbpsp7DDGX5V8ByyDbLJ+CdUGQLPNGQnkzj3TDFspN68BNkwhDPIY5poG/T1lBYR+LOkuW4uSeR4KXssN48grF9h20NdeukYLRL96Y6vAD2wCwwbQyFvXARPpoVA85fKnXiN4HtvP2Gf0tPG3XWUKNYT4E6PxjvD3x1EDHPZZvgxTTSDBc8gMrKbql5gKHeJh7NM6/AFu91/EVmjHGTFmN+HA3qYSoE7SuO8+zcEawY4vJdfr8Z/ljiqMS3AV2RvjpTPc7V0A623rqJv8RsnynbxDUXXieJuy/LfRmmEzSd7wKtroL2Hcc5BL4LVmRCmbheEIfmHduVQ1muQV/3BN2bJZyqaANbdm/jL+xtm4nfxKcsP08Q/zX8MxV3TDXqx+PYBGUQNHVAI9AsYrsuB9sPVflDT5xH+O7OZn8kK9msJf6G3ooFOOr66+O2NOVL6A7oP/njmmREQcN5LGhy1cLJtBwK++FSLqrVSGvPcrCZGu8DZTqTBSs+zUkarTZTUrerYh50gHYY7rSpRxZCCYTByvouS2FQK42hE9w7S/tKsOaIt/AGfoMWO3OgFLyYb8FaGByHl6C1r27jlsAh8HaN14LD1+x8jN/KNVdqlAvhgq8YfJ/DLYjVUDatk8J905HObd+Cf1rEaHTp5sSL+RacaKWWyO+8E3wLdi4g1QOOCE61x7Kt/UiGsy1jqcY7kuFUeyzF9ok6WA8ZvJjLtbQWEI/hXpLIW4N1rLyiPHV5hP9MsM4or2V7hlH+702XghWE3gAcTRKN3mjY7AZOdZbNCnAug4wTrNXSItCrmmYSZ3tGTNVAo+1nvCLOyLyeT9WC7WlqXNtUCq7vlpTlGkQMeG+Vio9j6NbxMOjtn8u7udjzaJcH1H3uLViVikCzLftqEtsKbeAyNh3LuWAdVM+yr8JsU8hgt9mvGh6ATousEKwgdcvXCMWDFap2mOYBTWK6b3YtNvYDrs9hM0i9BTgB+YMRTbvp0AS6bzaP43I7LUPaDFBvHPVmIy+ZaOp1+TkJX8Dc3/V22gUrYF1jN4L1r0T4NSPXg+sZ2dZZXgRr5m6BymCW8en6rc54BrYAXfu8CFbQmoQ0c1eYoilXw0NQp7gWZzueN8H68S44DbG/IPA9H66AL7FR12tpYk9qetOwGfSaVjcMNVAFie6iqHJv6bws2YaUfLpctYP+S5WoTVr8vjOMvphN4FN4N69Dybs6yw+OCLZ0yrByhS7DmrRaoQE0Kw5707JOf/UvH/ZKewTG/kscFrHSGbpzOHSC/wHSRhVOrpN3ggAAAABJRU5ErkJggg==) organize
* - ![refresh](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAG1ElEQVRoBc2aa6hVRRiGO17yrmloWpqhllH2wyKSEIsIo8QorLSbqVRgJd3DyFAQIyIiKQz61cUgpB+B0EWii4VkGFRUJpWKphZaaVZeutjz6FmwOnuvNbPWXvvs88HD2nvNzDfzrpn55tvrnLYjR44c1wpra2vrRr8jYC9j+KOzxmCnrTL7ng2LEN+rswbRSsH/ItL+Fwqij+8M0a0UrD5Fa0vg2c4Q3WrBik3sVj480WzRXUlwG4Lnw9OI7p08haqvXUmw2tzH8+AhRPf1RtXW1QSrz4i9CJYjepA3qrSuKFh9PeEWcE9XOtMtE0yyYYROojQfa0zRc8GZ7l9TWvJGj5LtCjdj0AYll6uD90HLQMizZKZ70vzOKjKypgpmkONQMxpGwWlwAvg9STLG8jlkip4FO/H3GKJ/DzXIK2/DQV554TIGdQaNpsNkmAAjoYpj5i/8rIIFjPlXruVMwY1Czy7X8+Al+B4OgU+yag7i0wjereyYqxDrDD4Ku6FqgfX87aGfR6BPGdENCabTqfAh/A31Btesez/T32LoXVR0KcF0NByeBPdSs0SF/Nr33VBIdOEoTVDyKFkCN0OlSQH+Ys2HsReMF66ueCuyJPDqzD4HvqEIzUCzyk1WtsAcKBy8opc0zgfBU+A52CwxIb+K3Qw3FJmodN0owXTgseNxsA9Cg2pm+S76vyktoOjn2D3sfjVAhFJBqmSax8km+BZ2gBnUlXAmhMyH+B3cj8DVocq55aEnROOJsB7MdIrOnnt9DVwD48G3lAPAB21evRRCPl3G22FaaKwx5blLmk4c2DNQdN+aaa2DKdAvayCULYQ8wYnYhpZxuv+QYGf3a/gnMLD0oH+h7mIYnO6o42fK/bX0MKTbpj8nYmd1bNvI98w9zHnbh8FcDSPBwcWYe/ReWMOgfEhlTbH6ugs/75Z1Urdd1tOi8qnwGcTO7j7qXgU9snym71Mva4bt70uYmq5f1ee6M8zsOphJoOiY2XVGlsEbDKxY5kOjlLmkt4Iz+z7Xyi1LjD/QJ4PLOsbWUmklGMkbsc00fqBZYh1Y3RnmvjnyWeDREbL9VHgVdjNQZ6is/URDxb5e1kFMuyzBij0ZzLBC5n5bzUAbmV2Titvx8V6os0bLs5b0aBz3j3CuyA/A36dlzK2zFTpFrAPMmuFRlPWzQsDMpN6BMoGqO+2+h9tiZ7Y9mBpXQivPIHoYvzXjyhKsUwcUsoNU2IRjj5JCRhtXx8rYRohV5Bh4EExP8+KFK24VfAT/syzBLmeT+5Ap9LdQpYrKFTwMrgcF55k/Tj6FGsFZe/gUKhupu5q5VGOCo7Nv3RrLEryLmgdqarf2hjPsyssac9ToshobjGKepO1jzuqowQQqGVNOj+zvMPVMdWssS/Cf1IwJRAa3CcSTmABX03nBG451DMTEFleniUyNZQneQk0zqJC5xHw3HTOIkK9QuYHqQsgKtOn2Ct6ZvpF8zhK8jQou65DZ+UXQ1ADHCrKfyTAWQubK/AH8XV5jWYI3UtOzLMZMQ2cyqGbOshnZDPBYCpn79xuouyWzBLskPodDEDJf394IXiu39vgwEccXQyjDsn/H/gkovMayBCt0Hdg4xi6g0rVNmuUT8b0AzA1C5vnryjT7q3sOZ77TopH7ZQOYj+oohH89NAuKeuPBgDL7Tsrw5SmwHEJ9J+W+bLR+/8RHx2tmpzRy3yyCfZA4DF23UfcK6Nmxo6Lf8WFUfhzM10P9JuUeRZfl9ZUp2EaYeycJAInT0NU/ct0HQ/M6ziqjnft0PLwCsavLMbkNV8OQLN9HNeUWHjtfn8eJiUhIaLrcCPkaTIHo2aau+3UmbIS0v5jPnrtz8vQEBR+tcOxVz3qcmWrGdJyu42y/BXfAJKjZW9w7CaaBy/djKDKrSV/mDCsg+HCj/qmF6DsPZ8tgOJQxV8geMBnwszPobCp2IAyFYVDGXE1fwAwmaEvQQWgJtM+ySYWC90PyVLvC1aPHQHl5jI6jWqIrHpuFl3F+oAuJ/pGxzIXoP4znRumODwPHI+BFcFm2eoZ907IEBnQcZ973QoJ1hLnnXoBWiXYZ74D50CtPXL2ywoLbRRtwloKBqDNnWrEGvOugVEZXSnC76O506o8GX8QbKZst3KPnTTi33szF3istOOmAAZgVrYBm/SeeD/MruAf6Jv2WvUadw3QUNM5q30ZcCrNhDMT8lKNapil0LayCtxG4JbNmgYLKBNsnortxccbPh+lgBuUvnlhzW3iumpaaofkzbzvXyqxSwelRIb4f3w1u58AlMA6GwNkwGEwhN4PZl0vWWLABDEr7EVr3BzxlDdl/zhnCj3tOo0oAAAAASUVORK5CYII=) refresh
* - ![reply](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAES0lEQVRoBe2ZSWgUQRSGM24YTdSo4AYRTcxBEZJDJCoigrtGg6CIgihqogfRgEERguhB40UP6kHw4kEET4J4E9wPAdeg4ALigjuKcSMuMX7/mAmdSU/SXdM9PTPpBx/T3al67/31urq6K5G2trac3mR9epNYaQ0FZ3vFwwqHFc6yEQhv6SwraBc5YYW7DEmWXUhZhSORSC7UwKIgxzAlghE5CZFHoAEKgxTcz8/gCI3gfzHsh6l+xnLq2zfBaC0miXpYDvmgu+kXBGqeC0aohK2D7TAF+kPamKeCETseZdugGgZDSp4RxHFsnghGqKo4H/aB5uoASEtLWjBiZ6KsFlaAHlJpbUkJRmwl6rTcFKW1SktyRoIROhofdbARhlr8OTkMdBPNlWCE6iG0AA5AqRN1Nm1cxbTpn9Qlx8ERO4pIG0Br6yDDqH3pV4kvPdRewCd4C+/ZPdWx7xZxsk1LgqvIZDeUeZzRT/xJ8Dt4BQ/gGjSSVzO/3psEJ4JoY+A4fATNvVTwhjh34RSshMGJ8jO5biuWIJqrc6AJ/kIqhNrF+EFs3fqHYRoMMxFp7dNFME5Hwi5QMLskgrqmgb8M+hgZYRXh5riTYBxpFM9CUKKcxlWOSyHPjVi1jQqmYy7shQ/gNGjQ7f6Q6yWY7UY07XNK4CK0QtAiTOK/J29tLOQ7EU67nIGgtfU1mARMhz6a3zegtCfRHXOYxhXtndJBgGkOT9FQ1Z3oDsFqhBXAFngJpkGD7veN3NclEt1JcKwRHaaD3niCTt40vh6+q2N6rL+2gtUA03p8FL6AaeAg++ntsNwqNqor/kL8OZ2WgF71vEpeq8FvC36uDveJM8qqyenHwzg67oE1MAxMTeLOQyNod0SDqO2hCaDVIma6u3R9OAxq/9WxW9PT+wRsQ7RiE7Gbj4f4v9F8Fujxb1ptfR2tj/cbf04bfbbqZWgsFEM5LITNcBLc3HF6iM2IxXAlWJ0wJXEQfoFb4RJcEwtu8kv/PCiEGdAAevFQJbvL5Rh/j351uRbcLloVmA83ewgUn0TSgq2DRGzloVt9E9yDFoiPqfOvUBHN3erA7TFOtG6fBqdfVp4KtuZLDqr8DrgDdqIPcb2/UYXjAmmu1cLDBIGswX0THMuJHIrgDGglsMZu4nxI0oItgcbjUHP7MyRaanwXrHywvlAFj8E6v+dqZ8MTI9BzHO2DtaC9KY1wIEYurXCO4JrbjyA6CvzO80wwznS3tMAFDpfBKdArnkY4ECOXqwTWUqZvA1mJp4L/+4wKf8ZxDeyE26AlLBBD9HUC14GWr8mezWEc2/oiiNZM/TumGbRLkdQ6nChOT9eJWw3ffakwjjuMRF5wUg9b4QnE5hOHKTVNsSuO3qW9SosN/Yn4KmAQbnnl040f4pelVLCb5Pxq6/st7Vfipn5DwaYjlyn9wgpnSqVM8wwrbDpymdIvrHCmVMo0z15X4X9rh8wHLEjawQAAAABJRU5ErkJggg==) reply
* - ![search](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAGdElEQVRoBdWaa4ycUxjHd9rpbm2bqKhiUavbVZdo0LCyLl3iHhGEkkZsKBYJX4RISHwQIYIPNJoQlUjTuCakUZ9oVGRF0GywslvqbgkpDarqsn7/6XsmM5n38pzzvtudeZL/nplznvM8z//cz5ktTU5OtuWRUqk0i/qdoAN0gcXgP+CkzIcx8APYBXbi82/SaZFSKGGILiTibnA+GADHgbkgSXZT8CF4GwyDEXxvI92r4k0Yoj1EeAG4CvSDEggRkX8VbID4lhADQXXUwxZgfAF4CGwFmgdFYQJb68HJljjy6mSSJZAZ4CLwESiKZJydb7A/CGblJZVWP5UwzueBB8AfIC7IovO0mK0B89KCzlOWSBinWoBeAkWTstiT3948xJLqxhLG2Xzw4jSRdQ0yiv/upMBD8xsI40Rzdu00k3WknyeO+aHk4urFEb4TJ/80CWEdYB4BhS1kdfswe+zpGNf80RYUIr9QSdgOdNCYCfaLcABpqFxBbymu3FIlDFkdD18B5wRYHaHOJvAeGCU4fa8IdnXUPAaoMZeDk4CvfEKFM7CrhswnbpxjZQX4C7j5Y0m1d64EXc5OWoqeFsPLwTvAYt/p/Iv+6jTb1rLKHMbYgWCjZxCb0T/e6qhWj3o6hz8HRMSRykp17l5WayfksyN8oafzTegfHOLQ1aG+blc6ZGQRdeVawB4GlWno7Pim1G9rB08AZzgrfRfdw3wdxelHvl/38K01Itc2Rf22Q8BPIIuoynXQL/SQj71DwcfA4n8nev1xjWfN0yGjD2gxsYh6432LolWHQL9F91Gj/j7oacUPFhE+11hbLxbrCFBzqWh5A4PDRqN90RZqVK9XE+ET67MSv41D9s3E0nwFX1Ndu4RFjkZpjkUxTkeEdTDIEvXqW1lKoeU0pOavXj10OsuSI1CYnaWUVC7COvpliR7f9CQzlaK5/LPBQRc6mstBIsIW0WXiO4tiDh35mIr1oS4kK2ENOctwqzPu+SX0MdDLjZWw9Pb1suyv7EPYR7cuEithLRLL6moW/0VriaVRtT1qTQkSER411Cyjc4pBL4/KEirPNRj4FZ3gXy5EWM+vWaIhtJQNf2GWYkg5dtWzui9bhuqn6OkVNUhE+ANjTZG91Kjrq6bDxHnGStqvcxHWsU5bQpZ0orCK3rDs21m2quXY6+DLTWBBNTP9wxbOKZZ4E63omLYZWG4r0nkQtOtwVASwdYeH723o9uTxS/3Ks+ytHk5/R3cI5LqIK2hEDw86XVkb+wV0Z+YiHDnWCjnu4Vj3Ug3DzhDn1NPacTX4HljJ6gFPr5e5RpZ74tFz6l0ezhWk5tFTYJFPEOjrLKxhrEazktWR8zVQ9vEVp1ttLYyplyeANQinN0ydIXBUnAOXR7nsrwAbgatrTbX3nu1s5Ul1oKgIRsZYMR/jy72gY0+u6a8OJMJX1P+C9MsaqDcPAseCHtANQkRTwHIoybZd21qR0Q2k1pZP0tNJSIubLhxJOr75egO/sjbekM/VIe0qY1RDb6p//PYl6/QniO0sF2tI2kBYRpBTgVrUOWqm9DPiGgghW+GWVBGj/UCvEM1E1sWinr4sKfa0/NgedhUwqsVITzvOUTOl6gxv0qmERRw5HOi/bHz2zb3VMHp28hremYQj0rq23QhGwFSQ0ZVPu8NvAfa3Use8kJkI1wzxxRhfDcYDAotrKF0GngYnRA17D599f7KVXcVzmoszLfUi7AxhfBG4GKwFPudhBacnmpfBStDwnzrkrQIhpDW8L3ExJqXV/wBA2Vs4WelquT9Qzy8FvdHnDlKR01RQ8OrJMaAp8TnYQUA7SBsEm6pzPXgcyI6PaCG7Hdu6VcVLUkuE5ONBR8ByDGb42sPGteBPEDcV0vK0ZZ2Z5C9oSCcZKzqfwO8OJK2FbCAunqYmrICRQaA3rLRejSvTWtGwTzc94Yj0DQS/O4C05nQd6VYhrIVMpEN6Wqv3crBngY4b582aR9DXgJCFTPt05T+AtKq2jNARzxLs/UBbnY/0onwLO97sXPuwj8cidQn8OuytAe0edjUyuluqh2vIPcNnPS1rIbOKfkRf0pKEGdqSJyFwM/AZ3j+2JGHXpZDWWf4+sMvlpaTal7e3xLYEsdQ4ITIIsras29AppxrKctRM5ZDRLUvv13GnLl1p5yjellylCb5BolvWkRQMgT6g6apXmnVgPWQrc/1/boJCaHVWyukAAAAASUVORK5CYII=) search
* - ![settings](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAIkklEQVRoBdWZd6yeUxjAe2lLUbVKrFaLUhUVo1pbQtqqESOECGLGH2IkCP8YQewYtUoTKmkJ/2hVEDFixN5FadXWBjFaq0b9fl/vuc5973nf9xtvez9P8rtnPeec5zn7/W7HsmXL+vzfpKOjYxVs3hR2hlXhT/gcX94iLBYd/r+BR2vB+eBsyVJ4FPqX+eJItbUwm8rmMEZDTRAMhG1Nd4p+bABbmUZlAGwLI0D9Lmlrh7HV5boHOHuPkL6LcCisDztCEJ1aBxwYwyvgMbgfToD/pGwJ9FY5FjoZ42AuhKX7N/HX4Er4Psq33PQ0eBz+APP+gbfhAOjQl7bdvxjYH86F4Gwc/pWT74DEesYXwWWwtg6385L25J0FH0JWXOopyfrjDC+AmTj7sxWyCua1hWCgs6Ox58GPTRr1FfVmwBuhfts6rIH47NJ9Eu6BWBwM9+xU8HqaDA5OLL+ReAmm044zXZPlGzmk2iDklHUSvF4mwU4wHEbCuqDo7OdwKXgK/w4DwEfIdVC7vgjVcxnPg/fhHZjVdocWRmn8faDBKRaTf4srPoa81eFocABS9cy7ra2XNAam5BcyvZqy4vL/Er7OFsTpdnW4yK5+OBCWd+yLjw9neY04Mxsvajiru7LS3qXut2/Aq8mZ6zp0iPuOnsBeH0wYi1thL8jmW99l7ux/1G0fxHui2TiNOojdaLQt6vcF38tbwyHg0zLel57AD8Io2Ay2h+sh3r++tl6AI2AbWBv62XAlwogPoyFPVhvuJpRpyCwc/7hbQU4CPWdlMfWWEFrX2YvFpXskTIRFsD4Mgqy4Qr6gPZ+ny6XR0c/Tp7Up4GdaPBNx/KG8unn5tOV+vLOgzbj9VNwD7gHYMPRRyR5mJpyBIVDU3lD0/ISrS9B19U2A4+uqkFZywMbCYbTnqig00PJ6xYNCPCnzZD0KRuQVJvJty089PyJicdY+hfggs7y2fAl/MBGJk+DJ7grgb+YCz6ZRceY8OHaEftly08ho+AQ0IrW0zPsWjkrV72zDg+VwGB50iHse3AbhpJ5P/AzYBz6E0Jf9egqfDieBZ4Vl38E1MKirzRBJhSh6ED0D7k0bvAA2gVVifdITwQd+MCAVOgMXx/WMIx42J8M88Ep6E7YJesSd5SthBuwOzvxweBhCPw6IV5nL1y+pPWEqXAJd+7fWX2g4G6K4HTwHGhoaNnwZDoLVQh3iZ4NXRayXinuV1N7vtc779NmN9NOZejr9FowL7WdDyjyVb4TQhzY+A7Vv3qBPuquvrrwQiUMUR8JMyDobOlhI2dXgIbQaXAvhV4agkwqfQs+DxH11PrhqUnou0TkwNrYrxMn3ADoMXgUnwIm5Ano4GOqEsMceppJ76REomzGX0bNwCrgMnZmU8XGeA3UizIK8wQz6Ou0+HROMjUPyXboOngyArhUX62XjKYcvp7IHTOi4N0MH5eGs0a2kXVpZ8fBYnM3spbSrxqVdnWRHi5Y9Ne+Gn6E3Z1dnn4fBWRtbSfdY0jaGjAYf3u6j3nLabbVfK86l6qaWNP3UllGYZdMrWzzxJ8OLVXdcO8ZTjfL29CP7VvD4r71DU3qJvPnkfQ1hZWxGfMuEXl7WXxQ8AacwQ9/kKTWdn5r2kEejO8DbUM+V8yR6x8II8CM9XBdbEffJ6FVXtkUsXwC7BhuqDpN7OHRCx951flgvgTBj2XApZX7CDYHci5+ywXAOFD1QbGsq9A02VB32pXH/26Zj/cEL3JkZCs6MT7+DwfyU6PwUuBDDCq8yyr+ln5vQ3RB8ZaXOD+2xv2XovkK4AD4CB9yB+o12XG1Niw/xLeBA2Alcji5jr6Z6xJfWQRihQXULzsxG2T7rER8fbqu54J08m/7eIWxarqJm0TLLLuGQ1pCjYFUMKNwa2XLq7Au/Q2ir3tDZfQoa7jPY4LLym9Pl3Kg42q/TUDNLzDv+tUY7RF973RJNS2of1duYDv9Sr3JGz9P4jUxePUlXgnWbllYcdmY1oFnxvl3p0orDrdTV0VbrNzVYrXS6NT3mXVdlxng7bF+mlCi3Xkuiw57QzRw8Xl9DuGKaGbSNqbsrNCpuIX+YaFq86KfDuuA97AnorPl2Lju51TkTXoe6Dy8GyFm6CLwdysSJ0EH5CfwFZEqTNwNVO5+CtcjymRpKfDsY1UlI+6NZaiZ19CyYhhHey6WCv0egdDf4a2RKfiDzPVgI78OczvAD+mjphKYdjtmSRwMqPh1/VTWHz8g/AZK/Wcfto7MfzIO8thy0B+M6VccLHaZzD6aXQEPyjDTfc8CtcQD0eAWRtwdMBWevqB1n0FkdVbWjob2i7+GBdHwpnAZrQj3yPUoLQKMXwXowEhy4wVCPOLjT4AKMtL1qJXieDellEvgzS9GMrKgyz4ZTszZVkU4uaTobBrPB19CKcqqoXZf2fBhdhZNxGz0cphOvm5uhbL8VGVxFmYP9BAyMDW41nrpqDqGT8ZB3bVC0UsQfJfYGr73KJOXwLrS+QQM9NHo3NqLvw2hcA7aUqqYcdu/6ovG0LJM5KNwBX4LLuEz8Geh28OebMrE9T/p7yhQbKk/tCRrw55eXwaddaj/6a8VMGAP+93AyeBendOO85zr1hxNOA5+McXmIuwr8ifaklH2t5PU4tEJjdDYWfCdnHx1zyTsG1lAX6YAzIc/44ITh/epHffhQ8feqWEdnXWGTgl6VYa7Dnc7sQ8fvgiems3ov+M7u9poifSh4d8aGp+JXZ42nzibgP7eXgM5+CuOzelWlCx3udNqZvgGOg+QVQb467mMNTjlqnl87J6cMJ9+zZH+4BfZN6VSVV+pwPR1hpA+VNyFvz+vwJ7B3Pe2tSJ3UKY1dDctX1PBzTsfyxGeq26NXpRKHmZGleOEV4pLOk4Xo+XrrVfFir0r8bh4EG0E8057i3r8eTL0u/wJCZSL2DoplLgAAAABJRU5ErkJggg==) settings
* - ![star](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAFfUlEQVRoBd2aXahVRRTHz/Ujv+2mZRGZB7W6mtpFikC7+UWUZiqBD0JPFdRL1EMFPfjoU4baS0FUD/UWZBEVShA+BCpmWApRSkgllNpDmZWZt9//eOay72afvWfWOTPn3rvgz8yeWbPW+s/XmT379AwODtZSSQ+CryVgA/gVfIx/pelEhFMBVlvBOaBeFo6Cean8y09KsnMg932TqCOs9M2UhMfhMJVsxtHcAmcbmekLCsqjFKUkvAYG1xSwmEHZqoLyKEVJCDOCNxH9HUCbVl6mULAuXxjrOQlhgl8Bbi0h0Uen3FBS37GqVIQHiHh2SdR16jTlo0t0woycpuxiUDSdHcFeMv3uIWYanTDB3wIWVZBQHP10zuQKvbarUxDWT1HRz1E++Ds99fLtgp6jEmbExhPNcs+IbkZPiCpRCRP5TPCQJ4MJ6A3QSUqjSWzC2ozuC4j+fnSnB+gHq8YmvJKIJgVEpRPX9QH6waqxCa8PjEhHT981H2j6qno0wqzF63BhOUxsom3Zb7aJqGsUjTAONFJlpysXQz7VuXpavrBTzzEJaz1adlzNjHs6RTBvJyZhjZTF/kTaWZZCnlvhsyWgQkPZQpagzsX1bFlAXjGtDdAPUu1p3PPQhCCXkdwG/mta0PWLds060AuAnqtEOjpdbQR3VymX1P9F3UfgGJA9X9F92c/ADaQ2P8V0DJ4/kDbeYKaSvgI2AN0+OGJK1VAbSIhTOXEOybYll2kte77yD4rqrHyb85S9Cl4HtReAyI11/A7HpRq5PSD6oR0f3Rad+H7S1DvV7UgS+tc1cU3n3V/AWJ/SX8BxVuMinow2rNNjlPQVeH0GFg378kDBfLAPXARjZbTPwmUXmOG+bgz71EKFfqKeAUWfREZbJxyCxyOOqEuHER4qrNUWovwy0CFktBHV4eNZMNvxyaaFhKWAaBt/HJwEo4W0luSKLMF8viVhp4iBeeBd8CcYqcQ1qi+CKS7uVmklYdcQY0+C42Ckkf6EmO51cVal3oRlCFkCdKgfCWtbo7obDO3AVWQbHHyUsjo40E6uq9cvQbdG+wN892fj8s0HjXDWKA51/t4JUo72H/jTDtybjSUkbyYsJ0gdfAtSjfTn+JoWQjCv2+57a4M1QaQSvZvrMsIs7RJejGcdUlLJUhzpZsYsZsJcCen6ZwCE3IaYA2021OfUdU3fJltmwni7Fvh+KDMF16KR3ux0lWuSdgjPxeNdJq/tNdKNqJaSSUyEmVK6JNPomtqbIh3eSKNsEmvAarfJ5LEzjbbR59MtpqyEb8eZjpndkhtxvNri3Er4YZxpx+yW6Jdhi8V5MOHm+n0QZ9afo0u0fQO8A5S3iPaQ1cTSG9w4f/SqesZBH/gRWI6T+gyyxfkgvw2cMdrS+/lTzpZvGnyWxsnTwHLRd4R2a/OBqQyoztKBe/P2qp6DCBOUptKHhuA+pU1fq2Co0/F0L9CVaghxXTbWW9ktKg8lrFfCrwODeh/9wgu1bEDo6OT2Fvgb+JLWq+nQEsnaa5UPJbwKBxc8A9KXPG1O3u+u6E4F24GvD3XMDjCxFcF8uTdhjGpHfwn49L42lCeAdyDZwGi3HpwAPr6+Q29htn1ZPoSwfuz3ewShXVcBNz62lzkvq6O9DjZHgQ9p72kdQljvob9VBPAN9Q+UEQmpw5b+Sf8e0FotI/4a9ZN8bIcQXlnh9AD1y3ychuhgU0tpJyhb14epn+ljN+Sk9S9G1ct50d8SdgF9x9EO3lHB5hXwPEYfA8dbGD9LuWZBtfj0inSQWUDTKzu1dAB5Dkz2tdOOHn70LvwVyMag/FYwzse295Rukq5j+G1wEOib66PAy5FPMD46+NPmqTV7CpwGGvkJPm2l8z8GWDNDloqpGQAAAABJRU5ErkJggg==) star
* - ![team](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAFI0lEQVRoBe2ZSYgdVRSG+yUmnagRQYU4NbZKNLYKWTgg4gQOaDYqJIIGl4LixhBwoy50LSIiulEjCkpAUBBRURpdGceFMQ7YtgkOJE4xTjGa9vuedUl1Vd2qevSrFqvrwJ97695zzj3/PXd6nd7MzMzIQpJFC4msXDvCbc94l+Euwy2bgW5JtyyhOTpdhnNT0rKGLsMtS2iOTpfh3JS0rOGQ+eLT6/VWMNYJ4NjUmN9T/xLs4WfqvPxO7TU9DkTdNmvBbeAskJ7kv/n+AjwKXiSW7yibFQk3BSIPZHdTl5xZzML238DDYFlTsQS/jZF1AGQ1mAZZkkXfe9FbGwJrqmz6lL4cEmOgjhyO0jq2gGVj0hhhAl9M1FeB3gDRn4Pu/5NwQnJ0ALKqrgKHDmgzkHpjGR4oioPKP1H96+Dn8GvpKyLqneV5Lp0XgnHggTMFJjlYPqAcpnyLsz/LHBLL0fRfCzwbvNN3gLeI5WXKaik7DbF2/20A28HPYF+CPZQfg9tj9vS5h18DRSdyrO0j9FeW+PQenwTe138AJ+d34OPFa215zDa0l15LOLgamM0DIBukbQ60JjhLl7RL+HWQtSv7jhLGz1FgM3DJZ30Yy69gYzqGonrVHr4eJ+OgB7Ji2xi4lGUW8+PsD0vOwNGNwInMirF42K0nlmXZzvR3LNARDN3fx6WVI3VJF50Fzvr7EZtY8zQdLtUiOYXGIrJpXUmvTDdk61HCKEqiagD9SSwnLCeX3RYwSJafRd/zoUj2FzVm2hyzMJ6gV0Y46Myl/BzjeqfnyMg36G5NJqpoTPvnLGWEnS0f9lVStL/7NgT/C5XNoHTW6XesV4En/1wlGo+Oo4QJ1ivoxxqju+fKCG2lf1uFH7P3eEl2K8xndRt3VKKEE4sPKWOHiCreg28TaPR1RN/X6GwEO0GReJ3cg95kUWeqzT8W6KtMpujcVaZQRfgFjL8qcbCDvndi/Zz0h4Hr6L8JHBHRW0L7DejdAU6K6Nj8CfBQi4mH4xYmrmy1sXlK/gCAAyfkQaAT91kWj9HW/6tJ8MO3NmeC+4CHlqdu1q7o25Xk5Hqynw+WBp+hpO1K4JItsnfr5GyCbSirCHstnQpcKulBXMK+o1frCPGgWAomwL2gLsm0z3S9ny38XARWgEXJOI7xNMiS9ns9MN5ZCQhEQ1lIGCOXmZf4ZeAW8C4IAblv3wBXAIn6sjkZ3Arc80FvGKW/nu4H/nhZDiR0IngI+LYPY3i43gWuAeNgFBQSn0UYJZejRH3CPQ8cMDi19Jp6AviuVfd48ADwRZXWG3Z9J/6fApeAJUm2TYRE02OZjPfA3WAM9HVDdvt2iXHI1HkoPQd2g7SjUHef+NyU7AXgFRD65qOcZrybQXgFmtUDIDu2xE3CBuCWWBxIU+8vk9MozdQukDUO3x4qm5IJOp36ZyW6waaJci/jrkviWEV9qiQOdd8Ebr/+T0fKkYvBp6AqOB2fnQz0SA39Kn9z6Z9mfPeze/UlUOXrB3Q2AW36a77KwP7tYCwh7Mupjk1TOmZuNInlyZqxuN8n3ItrQF1xryvRl9W/3Y3/60QGCTGF71h5JB0Tbn7vsDqyP6Vkva5dymxoVQ+lIE6+3+lJCH3Zcp+E78y2Fny7Evw7kstC8YA7BtQZRP1hiwTDKnuGun8aSiekaDxXwrbG/zOtaOT/ss3MLSjpCLc93V2Guwy3bAa6Jd2yhObodBnOTUnLGroMtyyhOTpdhnNT0rKGfwD3f6JVZi/xSQAAAABJRU5ErkJggg==) team
* - ![time](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAIPElEQVRoBdWae4gVVRzH97qr66vyhWbmurY+MA111dRMkLIXRuhG/pMVSUKGBGYPMTLDR0iaJBFUlIp/FJJlpWJS6vrAlCwTe1iaippSZipmPjL7fC/3XGbnzjkzc3fudTvwYWbO73d+jzlnzjkz96YuX75cUqiSSqWaYVs0hvZQBY3AW/7gYg/8A+fgPDFd5FiQkko6YZJUYj2hNwyDAXADlIOrHEO4A3bDVvgZ9hLfBY6JlUQSJkn14CAYAiNgFPh7kqpY5SDay2EjbCfxo7Fa25TVw/UBuw/BWvgT9HwUgl3YnQXX1ydWtc0rWRyr9zRcV8FpKESSfpuX8LMXnoDm+SYeO2GcXQfz4Cz4gyrGtSa3TaDHp1HcxGMljIN+sAGKkViYj+PEMRkax0k6csIYfgoOQVggxZa/R0ydoiYdaZZmFp6C0ZmgNTVu0YSzBQ6A1tuTYEqKk5ugA/SFkdAU4pbVNHiYpLWmu4vrztBSy83TcAai9pyeba2lz0E1tIFysD5vyMrgKugIY0GToW5MVJ/SWwltXPlIZh3SNNbdV9B/QRTH59GrhQehSZhjl5z2pucXc/4rRPEvHfV0B6dtm5CGI+B3iOLse/SehVgTiM23tx6bGuafwb8QJRY909ZlK7CHadATtOZFcfAmel28QSZ9jn0914/AYQiLScvW45Cen/yx5CSMYhNYA2GGtdGfDS38Rm3X6GpO0PNsKLPpBtXTbij8BGGxaWQODrThr0RxEuguuYzqeZ0Opf72tmt09TKxHU57+JLz7rY2QfXo3wpRkt6MXs7QrtPDKHSDfeBKVpPYjKBgXHW0mQVBz+HzrnZBMuwo6b3gilNb0Yn+9v6E30UpKCiv4WnoBD4ffuPea9q8YrE91asX9Rxb2loeBG9s/nO9YlZ6bWZf4dhc9EB4B2hJsBXtYd/AgAzHLfm0cfnYhvBlUE/aSlcE473CdMIkqyTvhU5eoe9cE8E8cvXulHwqxbvM3PRFeFzn8FqKbDTpdTQ6pof1BlQDtt5V7yzDySemYUM4Eo8mz4WgFwlb0RJbbYQm4e5U6JmwFe125tiEV7KepLWlFJp7goqW2WH0spbEkkacqOJ+UPfbylIMK+mGWl4lsLOO4DR69Tynv1y04DhSF5aiDcY7FllDqdbLSq0jmB7IKiXXkNYDrXFuK+sRHLMJG0I9o09zzEeOWDQ3DWI0lyphPbuqsJU1CFzDxdau2PVfhMSpiaupEh7uiEyJfsUNtE0IjqZFF2mmdi1R+j6eTriLI7T9yLT+/h/KBYLUHttWtPSWqYevtWlQfxjOOORJiJIaPRcJ5pAjIC1LnZVwL4fSEWSFTvhqh//IoszEtSekQYUSdpUTCLUsFbI8wOw5HvRNq75Fb3LOEpawa/Z2Gg4Q2mxpjdQ6v4KkBwa0i1Nl85G1EZZwVjGBE/Mx0GbqNgQfkvQECA3cZiSkPqWEtQG3lQoEiTxj2FkCW8E1SXVG/josJecqjnGLNlGuck4Jf+PQaIcsn4/vOSaZVLTE3Q0LwLVz095en3rXknQNlHMeWtBTLl1DFHdIri2ZtmZBaFnqo51bkmBT79660UE+vXV6DOZCVZh/dJrDUvC2956fRtYeSmaAV+A/vy/MWT5yfGr4PQNa9vw+/df6VDMRrB8NkWk0/gL+tuZ6G7JroOQeh5KU50Csz6lRbwB2NQyHwhYI+1Kqbe770D7IPvXaOmp+MAn6j5pDmkH6hywZ8yuY653I2gY5SaoO+y1hKujHMOPXdnwJnZwOoG52SNsJildFzlaCzYHqRyWVnMsOfsaAetsVyzTkdX674lrP7z5HO80F/U3CGlb6G4HLSS3ynLvqCj5fGX5ag37o/g38MX1HXc6Qzui7HolPTbv07MtFPzgKfgfm+m9kY/JNIp92+BsCmmhMDJrcJvltUaeXn689ekbfe3wSefrnWpOw9rHa3nmV/OebkLf2OyzkNf606XkNDsLbkPPrJHUa4hfAH6+51kipNnFm11cqtTa6Gko20zRsCEfiuREOgEku6LgKeXY58yasRTlsaGgjkr1bVzJp4tDHx8UQlKSp0+ozzhtnNmFVUh6DsI3At+hUeo0U+xz/KVgIJjHbcTU6dR4Df8Lat34cwdAGdDoWO9FMp5Tiezq4Hj/dAHVceinyxlkn4YxB7ViibADWo1fUnsafOmQW6KOErVdN/Yvo5PzKmZNwJmmtg6ah66gXgAHeO1ioc/y0g7kR49qIXqugWGwJl9EgyjOim6GJbCaE/mUoKIAoddgeDdvBdfONTDuuXja7gQlLmdIKwrZ5xol2ObqrYyC7BNicRq3HVm9YBPpUbHy5jifQe9Rl35pwJunBGNgV0ZkC0Z5V29BR0AHKXc79MvS1zdVmoy/Mg+PgStAr0yQ1BZw3PP1Qo2QtfEnQJLYY+liVggVHqF4O60DDXjsezax6ETf7Xo0iTUQ6toZb4Ha4E+IUbX1f4AbOD2sUmrAMkLR6egHo3TWfcopGO0G9oG2ieR2t4lw92g0qIZ+iz0XzSVYjIrz4h5XtGkvqgagTmXeoFfJcb0+B/8ey5mETBNVjvClMhjjPViES1s8qy6AiKE5XnXPSCmqIE23rBsIK0PNYiIRcNn/E53jI6/08dsLem4DTcbADdMddQSYh0we6t6BeW9pIkxZOrIUJrS3Cm6EG7gJ9TE+qaFbXLP8BbOZm76mv4XonbAIg8ZacV0B/GAvDQRNdPkVfOvQe+znsJ1HXh/tY9hNL2OuV5PWu2hyqQZsIra/6FCO6gClapn6AU7AbtDfXxuUknCHRSxwTLf8Bgi31NJnvpzwAAAAASUVORK5CYII=) time
* - ![trash](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAFBElEQVRoBe2aS4gdRRRA8+L/m0QIJkYNLlQUNOAvigpRcCEIcSsiCLoLLoILcaM7QVBX4koRshDxt9CFKCoiuvGDCP5QkxCiJhInRo2Ovzie80gPNWX1dL3uesM09IUz3V1169a9daur+031aG5ubkUpGY1GK7G1Dq4Cz9vKiIY74Sv8+72tkWQ7Ay4Bxo+Hu2E3/AuOZBf+ov2TsL6Ef5WNUsGazXvgEHQJMm77N/aeg3Mrh7seOweMM2bWYH+B2OES1/9g9w0oEnSngHHCYO+FGSgRXJ0NM/0idA565BRpKyxSt9J2B5xWY+Mw5Udq6uqKT6XimESlmX4d7sTnA4n6rKJjs7QSSgTrSno7nJyodtFyGr4AP4G6TeLIHweb4A44C0LR1xtgCzwP7aTtIkBvLlSfQjwNZyl7FNa0sU077V4DX0Js25X7cRjPzDb2Nd5FnK7xPbGXskdwxsxOLLRzdnwIj8GvkQFnypqobKLLrgGnOjMzP6cqJijzfn0NXPljmXRNWNC+dcBHM7HA2NELp10nwbaz5iC4OsdidTyrYp3a68ZFi7XJFfNsOBGcUmFnPpbiBWkVZefT7g+OXcTF0EUsFPtaje0Lw0LOzfoM49B4Gy36WMKwK+WDcC2cAmGwXK7YAAYdym9c+NiIdUOdnHODc6DjpPioix9LBvwtPE3QOzjWi7MjBS0M8CGY1huUA1ISg/4cNqXiqcqSwVqJ3AQ/QEmnpm3LR+IzsLYKMD4mA6bBOfAKuFpO28nS9v0Bcxckn9V1Ad9Pg2m/H5cONLT3Mf5fFGfX63hBQG8s7/LXxcdV0nvjMtgKp0MojuaroM60xYB8Z78ZTog6c515B1ylXey+ARe3/0tqFNCy0RjrkdvgOwhH0TeiB2A1uMBNGx9Ta+FZiP34mrIrQR39cECSUzqZYYIcR0mjJtmFwmHUvdenLjwmnUl7Eh05+LP40fjvoGTACYN1Rc6CecGhM7lw2lt+AA7Fg4fOespXgYO0j3pvnXmh3rY+/52+vrXtRSd841rQJ/WV1JVX9eNj14DnjeHnJVw8DBeAnX8A2ynfXwXN+cWUPQUOjNl6i7Jt1I9nCOe+1V0NT4AB/wkvw31QRIoFjDfnwRXgfVbJGZzsry44boTNUGVjlvOToPpV5FvbjXApKE7VLZ6UkpWlDGHH+96pV93/4TSsujGA8MeF51Xw6njuO3soKTth/UTnJQOeqONFlKsBW0SlfdVyDLh9NBkth4AzBqnXKkOGe52+DOeHDGcMUq9Vhgz3On0Zzg8ZzhikXqsMGe51+jKcHzKcMUi9Vhky3Ov0ZTg/ZDhjkHqtMmS41+nLcH7IcMYg9VplOWY4/Md88cEtHbDOVg5Xx9jpsM9Yx52JeAcw1ontTXRdcm9pFz3vBveHdNJN6YPVRhrnivtMlruZ5g7DFxBuXLut8j7sA/d43Yr5CIpJsYAJ7DN2/27Bsw1gwAb3I8wLOp+g4w6+nw/6HddOyszqWDg/Qv2bXFwH4+1SyhyUYtI1YLc85wXn/ORAagWdPVRKUqh3AJwtdTLeWq2rbCoP76cm3bjeLG6ELjZim03XJujyJqXF6rtmeDvGNzMN/ajEAZi2rKOD67t00jVgN7+3dnFgqdsu5XRc6tiS/eUGvBTTNengBIVZPuYG7LcYPjdluYk++bTw++pGyQ34bSy9B35Vs5zEYGfgJfg+x7H/ADoy2VfnrtXoAAAAAElFTkSuQmCC) trash
* - ![user](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAEWElEQVRoBe2aS0gVYRiGO1lmF8nQQlETutGFokAiqEV0ISKwgmrdMtzUpnW7drWKbFGbQAKpJIhuUGIUFUkW0T1Jq4V2U4ui7GLPexpDD+ecuX1jHqcPHseZ+f9vvnf++e8n0d/fPyZONjZOYqU1doLHRV3CiURCz5gMxTANJsJg+8XJJ+iBt9BHNdO1SCwRZR1GbAFRl8F8WAFLoRwGLME/ffAM7kETvIYPxPWDo7lFIhiheURaCVtgBywHXXOzbhJcggZoRvR7twy+76uELSEAtQsqySPwGdQN+KWDPHuh2DI2+TIVm3T455M9G0Bk6ktRvd4NBZaiTQUT3AQnSNW/VAFBzl/iZw0kq56FcOtuaQHB7QIv9ZVkrqZ2YA9Mck3pMYGZYKeh2sBz1SJb2mqcmfk0E0xQ6l9rwNoKcWjm11JwEYFVW6t1/K218mspeB5B5VsFluKnIuU88Kml4PGBo3DPqBGZiVkKNgvKRFkGJ5aCv2Z4xoi6bCm4DWUaXERhZhMJS8FfolDq+DSbRFgKjrIOa8poYpaCTQKK2sl/wSHfcFSNlll1sSzhn7ys3pAvLFP275lu+L1uKVhBPfYbgMf0zz2mc01mKfgbT7vi+kT/CeT3sv9s6XNYCtbg4CJ0pX9U4Kv3yXk3cO6UjGaCWX5Rg/UArqY8I8yp1qdPQ08YJ4Pzmgl2nCqwc2DVyKjunuddqkE0MVPBBKYSuQ7tJtEhFj9apDczU8FOVB0ctZiuHYUw9obMjbxErW2bmblgApTQengVIkq1B83QEsJH2qzmgp2n3ObYCEGndZ3krbcuXcUWiWACldCjoA0yv6a8J6HJb0Yv6SMRrAcj+gmHA+B3aneDPHXk/8jR3LR3a2rOfnAlTmfDVPDb6Khrq8bPDI5PoRPxZpMSk+1SgtOKpTa8l8BC0JaLmAkloA1xr/aOhJqEtINGWeqW7jjHXrQHbRdw4WxSJf8L8Aeh2m1QaWoBfiUsA61PTwGtUYeZ1qlP1zhan3YraBSnz/0mdAUVHqiEESoxKs0a2AxloJIMI5DsWU0vQH2z2oZToAnFI7+fu2/BiF3PgzbCKqgC1bXhNH3S6rba4BocR7TquifzLBih5XjcCSrROaAGKbJWHt9uJuGq67fgAki4zrNaVsGIzCP3dNgE20B1VJ+uro8UUz3Xr39UvxugCeEZl3UzCkZsBZn1+W6HRaB6qtZ4pJp2PtTna+58DFoR3sVxqHFxyM8euFsIW6EeXoDeoPrBXEEbAlpqqoN1kD9YY6rYxSQ4DGoE9KOSXBGZLk4NYB7CfigZEP1XMBfVEJ0BJUznIFevaSBzEEolOimYkyo4AfocclVYtrjViB0C9SzJEdE+jrn+CWcTrHvdUKuRUSm0gPrZ0W7tGjjMhTiIVWFWSbAGEnGxhAT/y+HhsL9oiVWFjo3FqnRVqrETrG5pFmiSEAuTYC3TFMVCLSIzTg9H6wuIXR2OneDfMJq1NmzzbS8AAAAASUVORK5CYII=) user
*
* You can also use other pictos icons by using the {@link Global_CSS#pictos-iconmask pictos-iconmask} mixin in your Sass.
*
* ## Badges
*
* Buttons can also have a badge on them, by using the {@link #badgeText} configuration:
*
* @example
* Ext.create('Ext.Container', {
* fullscreen: true,
* padding: 10,
* items: {
* xtype: 'button',
* text: 'My Button',
* badgeText: '2'
* }
* });
*
* ## UI
*
* Buttons also come with a range of different default UIs. Here are the included UIs
* available (if {@link #$include-button-uis $include-button-uis} is set to `true`):
*
* - **normal** - a basic gray button
* - **back** - a back button
* - **forward** - a forward button
* - **round** - a round button
* - **action** - shaded using the {@link Global_CSS#$active-color $active-color} (dark blue by default)
* - **decline** - shaded using the {@link Global_CSS#$alert-color $alert-color} (red by default)
* - **confirm** - shaded using the {@link Global_CSS#$confirm-color $confirm-color} (green by default)
*
* And setting them is very simple:
*
* var uiButton = Ext.create('Ext.Button', {
* text: 'My Button',
* ui: 'action'
* });
*
* And how they look:
*
* @example miniphone preview
* Ext.create('Ext.Container', {
* fullscreen: true,
* padding: 4,
* defaults: {
* xtype: 'button',
* margin: 5
* },
* layout: {
* type: 'vbox',
* align: 'center'
* },
* items: [
* { ui: 'normal', text: 'normal' },
* { ui: 'round', text: 'round' },
* { ui: 'action', text: 'action' },
* { ui: 'decline', text: 'decline' },
* { ui: 'confirm', text: 'confirm' }
* ]
* });
*
* Note that the default {@link #ui} is **normal**.
*
* You can also use the {@link #sencha-button-ui sencha-button-ui} CSS Mixin to create your own UIs.
*
* ## Example
*
* This example shows a bunch of icons on the screen in two toolbars. When you click on the center
* button, it switches the {@link #iconCls} on every button on the page.
*
* @example preview
* Ext.createWidget('container', {
* fullscreen: true,
* layout: {
* type: 'vbox',
* pack:'center',
* align: 'center'
* },
* items: [
* {
* xtype: 'button',
* text: 'Change iconCls',
* handler: function() {
* // classes for all the icons to loop through.
* var availableIconCls = [
* 'action', 'add', 'arrow_down', 'arrow_left',
* 'arrow_right', 'arrow_up', 'compose', 'delete',
* 'organize', 'refresh', 'reply', 'search',
* 'settings', 'star', 'trash', 'maps', 'locate',
* 'home'
* ];
* // get the text of this button,
* // so we know which button we don't want to change
* var text = this.getText();
*
* // use ComponentQuery to find all buttons on the page
* // and loop through all of them
* Ext.Array.forEach(Ext.ComponentQuery.query('button'), function(button) {
* // if the button is the change iconCls button, continue
* if (button.getText() === text) {
* return;
* }
*
* // get the index of the new available iconCls
* var index = availableIconCls.indexOf(button.getIconCls()) + 1;
*
* // update the iconCls of the button with the next iconCls, if one exists.
* // if not, use the first one
* button.setIconCls(availableIconCls[(index === availableIconCls.length) ? 0 : index]);
* });
* }
* },
* {
* xtype: 'toolbar',
* docked: 'top',
* defaults: {
* iconMask: true
* },
* items: [
* { xtype: 'spacer' },
* { iconCls: 'action' },
* { iconCls: 'add' },
* { iconCls: 'arrow_down' },
* { iconCls: 'arrow_left' },
* { iconCls: 'arrow_up' },
* { iconCls: 'compose' },
* { iconCls: 'delete' },
* { iconCls: 'organize' },
* { iconCls: 'refresh' },
* { xtype: 'spacer' }
* ]
* },
* {
* xtype: 'toolbar',
* docked: 'bottom',
* ui: 'light',
* defaults: {
* iconMask: true
* },
* items: [
* { xtype: 'spacer' },
* { iconCls: 'reply' },
* { iconCls: 'search' },
* { iconCls: 'settings' },
* { iconCls: 'star' },
* { iconCls: 'trash' },
* { iconCls: 'maps' },
* { iconCls: 'locate' },
* { iconCls: 'home' },
* { xtype: 'spacer' }
* ]
* }
* ]
* });
*
*/
Ext.define('Ext.Button', {
extend: 'Ext.Component',
xtype: 'button',
/**
* @event tap
* @preventable doTap
* Fires whenever a button is tapped.
* @param {Ext.Button} this The item added to the Container.
* @param {Ext.EventObject} e The event object.
*/
/**
* @event release
* @preventable doRelease
* Fires whenever the button is released.
* @param {Ext.Button} this The item added to the Container.
* @param {Ext.EventObject} e The event object.
*/
cachedConfig: {
/**
* @cfg {String} pressedCls
* The CSS class to add to the Button when it is pressed.
* @accessor
*/
pressedCls: Ext.baseCSSPrefix + 'button-pressing',
/**
* @cfg {String} badgeCls
* The CSS class to add to the Button's badge, if it has one.
* @accessor
*/
badgeCls: Ext.baseCSSPrefix + 'badge',
/**
* @cfg {String} hasBadgeCls
* The CSS class to add to the Button if it has a badge (note that this goes on the
* Button element itself, not on the badge element).
* @private
* @accessor
*/
hasBadgeCls: Ext.baseCSSPrefix + 'hasbadge',
/**
* @cfg {String} labelCls
* The CSS class to add to the field's label element.
* @accessor
*/
labelCls: Ext.baseCSSPrefix + 'button-label',
/**
* @cfg {String} iconMaskCls
* @private
* The CSS class to add to the icon element as allowed by {@link #iconMask}.
* @accessor
*/
iconMaskCls: Ext.baseCSSPrefix + 'icon-mask',
/**
* @cfg {String} iconCls
* Optional CSS class to add to the icon element. This is useful if you want to use a CSS
* background image to create your Button icon.
* @accessor
*/
iconCls: null
},
config: {
/**
* @cfg {String} badgeText
* Optional badge text.
* @accessor
*/
badgeText: null,
/**
* @cfg {String} text
* The Button text.
* @accessor
*/
text: null,
/**
* @cfg {String} icon
* Url to the icon image to use if you want an icon to appear on your button.
* @accessor
*/
icon: null,
/**
* @cfg {String} iconAlign
* The position within the Button to render the icon Options are: `top`, `right`, `bottom`, `left` and `center` (when you have
* no {@link #text} set).
* @accessor
*/
iconAlign: 'left',
/**
* @cfg {Number/Boolean} pressedDelay
* The amount of delay between the `tapstart` and the moment we add the `pressedCls` (in milliseconds).
* Settings this to `true` defaults to 100ms.
*/
pressedDelay: 0,
/**
* @cfg {Boolean} iconMask
* Whether or not to mask the icon with the `iconMask` configuration.
* This is needed if you want to use any of the bundled pictos icons in the Sencha Touch Sass.
* @accessor
*/
iconMask: null,
/**
* @cfg {Function} handler
* The handler function to run when the Button is tapped on.
* @accessor
*/
handler: null,
/**
* @cfg {Object} scope
* The scope to fire the configured {@link #handler} in.
* @accessor
*/
scope: null,
/**
* @cfg {String} autoEvent
* Optional event name that will be fired instead of `tap` when the Button is tapped on.
* @accessor
*/
autoEvent: null,
/**
* @cfg {String} ui
* The ui style to render this button with. The valid default options are:
*
* - `'normal'` - a basic gray button (default).
* - `'back'` - a back button.
* - `'forward'` - a forward button.
* - `'round'` - a round button.
* - `'action'` - shaded using the {@link Global_CSS#$active-color $active-color} (dark blue by default).
* - `'decline'` - shaded using the {@link Global_CSS#$alert-color $alert-color} (red by default).
* - `'confirm'` - shaded using the {@link Global_CSS#$confirm-color $confirm-color} (green by default).
* - `'plain'`
*
* @accessor
*/
ui: 'normal',
/**
* @cfg {String} html The HTML to put in this button.
*
* If you want to just add text, please use the {@link #text} configuration.
*/
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'button'
},
template: [
{
tag: 'span',
reference: 'badgeElement',
hidden: true
},
{
tag: 'span',
className: Ext.baseCSSPrefix + 'button-icon',
reference: 'iconElement',
hidden: true
},
{
tag: 'span',
reference: 'textElement',
hidden: true
}
],
initialize: function() {
this.callParent();
this.element.on({
scope : this,
tap : 'onTap',
touchstart : 'onPress',
touchend : 'onRelease'
});
},
/**
* @private
*/
updateBadgeText: function(badgeText) {
var element = this.element,
badgeElement = this.badgeElement;
if (badgeText) {
badgeElement.show();
badgeElement.setText(badgeText);
}
else {
badgeElement.hide();
}
element[(badgeText) ? 'addCls' : 'removeCls'](this.getHasBadgeCls());
},
/**
* @private
*/
updateText: function(text) {
var textElement = this.textElement;
if (textElement) {
if (text) {
textElement.show();
textElement.setHtml(text);
}
else {
textElement.hide();
}
}
},
/**
* @private
*/
updateHtml: function(html) {
var textElement = this.textElement;
if (html) {
textElement.show();
textElement.setHtml(html);
}
else {
textElement.hide();
}
},
/**
* @private
*/
updateBadgeCls: function(badgeCls, oldBadgeCls) {
this.badgeElement.replaceCls(oldBadgeCls, badgeCls);
},
/**
* @private
*/
updateHasBadgeCls: function(hasBadgeCls, oldHasBadgeCls) {
var element = this.element;
if (element.hasCls(oldHasBadgeCls)) {
element.replaceCls(oldHasBadgeCls, hasBadgeCls);
}
},
/**
* @private
*/
updateLabelCls: function(labelCls, oldLabelCls) {
this.textElement.replaceCls(oldLabelCls, labelCls);
},
/**
* @private
*/
updatePressedCls: function(pressedCls, oldPressedCls) {
var element = this.element;
if (element.hasCls(oldPressedCls)) {
element.replaceCls(oldPressedCls, pressedCls);
}
},
/**
* @private
*/
updateIcon: function(icon) {
var me = this,
element = me.iconElement;
if (icon) {
me.showIconElement();
element.setStyle('background-image', icon ? 'url(' + icon + ')' : '');
me.refreshIconAlign();
me.refreshIconMask();
}
else {
me.hideIconElement();
me.setIconAlign(false);
}
},
/**
* @private
*/
updateIconCls: function(iconCls, oldIconCls) {
var me = this,
element = me.iconElement;
if (iconCls) {
me.showIconElement();
element.replaceCls(oldIconCls, iconCls);
me.refreshIconAlign();
me.refreshIconMask();
}
else {
me.hideIconElement();
me.setIconAlign(false);
}
},
/**
* @private
*/
updateIconAlign: function(alignment, oldAlignment) {
var element = this.element,
baseCls = Ext.baseCSSPrefix + 'iconalign-';
if (!this.getText()) {
alignment = "center";
}
element.removeCls(baseCls + "center");
element.removeCls(baseCls + oldAlignment);
if (this.getIcon() || this.getIconCls()) {
element.addCls(baseCls + alignment);
}
},
refreshIconAlign: function() {
this.updateIconAlign(this.getIconAlign());
},
/**
* @private
*/
updateIconMaskCls: function(iconMaskCls, oldIconMaskCls) {
var element = this.iconElement;
if (this.getIconMask()) {
element.replaceCls(oldIconMaskCls, iconMaskCls);
}
},
/**
* @private
*/
updateIconMask: function(iconMask) {
this.iconElement[iconMask ? "addCls" : "removeCls"](this.getIconMaskCls());
},
refreshIconMask: function() {
this.updateIconMask(this.getIconMask());
},
applyAutoEvent: function(autoEvent) {
var me = this;
if (typeof autoEvent == 'string') {
autoEvent = {
name : autoEvent,
scope: me.scope || me
};
}
return autoEvent;
},
/**
* @private
*/
updateAutoEvent: function(autoEvent) {
var name = autoEvent.name,
scope = autoEvent.scope;
this.setHandler(function() {
scope.fireEvent(name, scope, this);
});
this.setScope(scope);
},
/**
* Used by `icon` and `iconCls` configurations to hide the icon element.
* We do this because Tab needs to change the visibility of the icon, not make
* it `display:none;`.
* @private
*/
hideIconElement: function() {
this.iconElement.hide();
},
/**
* Used by `icon` and `iconCls` configurations to show the icon element.
* We do this because Tab needs to change the visibility of the icon, not make
* it `display:node;`.
* @private
*/
showIconElement: function() {
this.iconElement.show();
},
/**
* We override this to check for '{ui}-back'. This is because if you have a UI of back, you need to actually add two class names.
* The ui class, and the back class:
*
* `ui: 'action-back'` would turn into:
*
* `class="x-button-action x-button-back"`
*
* But `ui: 'action'` would turn into:
*
* `class="x-button-action"`
*
* So we just split it up into an array and add both of them as a UI, when it has `back`.
* @private
*/
applyUi: function(config) {
if (config && Ext.isString(config)) {
var array = config.split('-');
if (array && (array[1] == "back" || array[1] == "forward")) {
return array;
}
}
return config;
},
getUi: function() {
//Now that the UI can sometimes be an array, we need to check if it an array and return the proper value.
var ui = this._ui;
if (Ext.isArray(ui)) {
return ui.join('-');
}
return ui;
},
applyPressedDelay: function(delay) {
if (Ext.isNumber(delay)) {
return delay;
}
return (delay) ? 100 : 0;
},
// @private
onPress: function() {
var me = this,
element = me.element,
pressedDelay = me.getPressedDelay(),
pressedCls = me.getPressedCls();
if (!me.getDisabled()) {
if (pressedDelay > 0) {
me.pressedTimeout = setTimeout(function() {
delete me.pressedTimeout;
if (element) {
element.addCls(pressedCls);
}
}, pressedDelay);
}
else {
element.addCls(pressedCls);
}
}
},
// @private
onRelease: function(e) {
this.fireAction('release', [this, e], 'doRelease');
},
// @private
doRelease: function(me, e) {
if (!me.getDisabled()) {
if (me.hasOwnProperty('pressedTimeout')) {
clearTimeout(me.pressedTimeout);
delete me.pressedTimeout;
}
else {
me.element.removeCls(me.getPressedCls());
}
}
},
// @private
onTap: function(e) {
if (this.getDisabled()) {
return false;
}
this.fireAction('tap', [this, e], 'doTap');
},
/**
* @private
*/
doTap: function(me, e) {
var handler = me.getHandler(),
scope = me.getScope() || me;
if (!handler) {
return;
}
if (typeof handler == 'string') {
handler = scope[handler];
}
//this is done so if you hide the button in the handler, the tap event will not fire on the new element
//where the button was.
if (e && e.preventDefault) {
e.preventDefault();
}
handler.apply(scope, arguments);
}
}, function() {
});
/**
* {@link Ext.ActionSheet ActionSheets} are used to display a list of {@link Ext.Button buttons} in a popup dialog.
*
* The key difference between ActionSheet and {@link Ext.Sheet} is that ActionSheets are docked at the bottom of the
* screen, and the {@link #defaultType} is set to {@link Ext.Button button}.
*
* ## Example
*
* @example preview miniphone
* var actionSheet = Ext.create('Ext.ActionSheet', {
* items: [
* {
* text: 'Delete draft',
* ui : 'decline'
* },
* {
* text: 'Save draft'
* },
* {
* text: 'Cancel',
* ui : 'confirm'
* }
* ]
* });
*
* Ext.Viewport.add(actionSheet);
* actionSheet.show();
*
* As you can see from the code above, you no longer have to specify a `xtype` when creating buttons within a {@link Ext.ActionSheet ActionSheet},
* because the {@link #defaultType} is set to {@link Ext.Button button}.
*
*/
Ext.define('Ext.ActionSheet', {
extend: 'Ext.Sheet',
alias : 'widget.actionsheet',
requires: ['Ext.Button'],
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'sheet-action',
/**
* @cfg
* @inheritdoc
*/
left: 0,
/**
* @cfg
* @inheritdoc
*/
right: 0,
/**
* @cfg
* @inheritdoc
*/
bottom: 0,
// @hide
centered: false,
/**
* @cfg
* @inheritdoc
*/
height: 'auto',
/**
* @cfg
* @inheritdoc
*/
defaultType: 'button'
}
});
/**
* The Connection class encapsulates a connection to the page's originating domain, allowing requests to be made either
* to a configured URL, or to a URL specified at request time.
*
* Requests made by this class are asynchronous, and will return immediately. No data from the server will be available
* to the statement immediately following the {@link #request} call. To process returned data, use a success callback
* in the request options object, or an {@link #requestcomplete event listener}.
*
* # File Uploads
*
* File uploads are not performed using normal "Ajax" techniques, that is they are not performed using XMLHttpRequests.
* Instead the form is submitted in the standard manner with the DOM `<form>` element temporarily modified to have its
* target set to refer to a dynamically generated, hidden `<iframe>` which is inserted into the document but removed
* after the return data has been gathered.
*
* The server response is parsed by the browser to create the document for the IFRAME. If the server is using JSON to
* send the return object, then the Content-Type header must be set to "text/html" in order to tell the browser to
* insert the text unchanged into the document body.
*
* Characters which are significant to an HTML parser must be sent as HTML entities, so encode `<` as `&lt;`, `&` as
* `&amp;` etc.
*
* The response text is retrieved from the document, and a fake XMLHttpRequest object is created containing a
* responseText property in order to conform to the requirements of event handlers and callbacks.
*
* Be aware that file upload packets are sent with the content type multipart/form and some server technologies
* (notably JEE) may require some custom processing in order to retrieve parameter names and parameter values from the
* packet content.
*
* __Note:__ It is not possible to check the response code of the hidden iframe, so the success handler will _always_ fire.
*/
Ext.define('Ext.data.Connection', {
mixins: {
observable: 'Ext.mixin.Observable'
},
statics: {
requestId: 0
},
config: {
/**
* @cfg {String} url
* The default URL to be used for requests to the server.
* @accessor
*/
url: null,
async: true,
/**
* @cfg {String} [method=undefined]
* The default HTTP method to be used for requests.
*
* __Note:__ This is case-sensitive and should be all caps.
*
* Defaults to `undefined`; if not set but params are present will use "POST", otherwise "GET".
*/
method: null,
username: '',
password: '',
/**
* @cfg {Boolean} disableCaching
* `true` to add a unique cache-buster param to GET requests.
* @accessor
*/
disableCaching: true,
/**
* @cfg {String} disableCachingParam
* Change the parameter which is sent went disabling caching through a cache buster.
* @accessor
*/
disableCachingParam: '_dc',
/**
* @cfg {Number} timeout
* The timeout in milliseconds to be used for requests.
* @accessor
*/
timeout : 30000,
/**
* @cfg {Object} extraParams
* Any parameters to be appended to the request.
* @accessor
*/
extraParams: null,
/**
* @cfg {Object} defaultHeaders
* An object containing request headers which are added to each request made by this object.
* @accessor
*/
defaultHeaders: null,
useDefaultHeader : true,
defaultPostHeader : 'application/x-www-form-urlencoded; charset=UTF-8',
/**
* @cfg {Boolean} useDefaultXhrHeader
* Set this to false to not send the default Xhr header (X-Requested-With) with every request.
* This should be set to false when making CORS (cross-domain) requests.
* @accessor
*/
useDefaultXhrHeader : true,
/**
* @cfg {String} defaultXhrHeader
* The value of the default Xhr header (X-Requested-With). This is only used when {@link #useDefaultXhrHeader}
* is set to `true`.
*/
defaultXhrHeader : 'XMLHttpRequest',
autoAbort: false
},
textAreaRe: /textarea/i,
multiPartRe: /multipart\/form-data/i,
lineBreakRe: /\r\n/g,
constructor : function(config) {
this.initConfig(config);
/**
* @event beforerequest
* Fires before a network request is made to retrieve a data object.
* @param {Ext.data.Connection} conn This Connection object.
* @param {Object} options The options config object passed to the {@link #request} method.
*/
/**
* @event requestcomplete
* Fires if the request was successfully completed.
* @param {Ext.data.Connection} conn This Connection object.
* @param {Object} response The XHR object containing the response data.
* See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
* @param {Object} options The options config object passed to the {@link #request} method.
*/
/**
* @event requestexception
* Fires if an error HTTP status was returned from the server.
* See [HTTP Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
* for details of HTTP status codes.
* @param {Ext.data.Connection} conn This Connection object.
* @param {Object} response The XHR object containing the response data.
* See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
* @param {Object} options The options config object passed to the {@link #request} method.
*/
this.requests = {};
},
/**
* Sends an HTTP request to a remote server.
*
* **Important:** Ajax server requests are asynchronous, and this call will
* return before the response has been received. Process any returned data
* in a callback function.
*
* Ext.Ajax.request({
* url: 'ajax_demo/sample.json',
* success: function(response, opts) {
* var obj = Ext.decode(response.responseText);
* console.dir(obj);
* },
* failure: function(response, opts) {
* console.log('server-side failure with status code ' + response.status);
* }
* });
*
* To execute a callback function in the correct scope, use the `scope` option.
*
* @param {Object} options An object which may contain the following properties:
*
* (The options object may also contain any other property which might be needed to perform
* post-processing in a callback because it is passed to callback functions.)
*
* @param {String/Function} options.url The URL to which to send the request, or a function
* to call which returns a URL string. The scope of the function is specified by the `scope` option.
* Defaults to the configured `url`.
*
* @param {Object/String/Function} options.params An object containing properties which are
* used as parameters to the request, a url encoded string or a function to call to get either. The scope
* of the function is specified by the `scope` option.
*
* @param {String} options.method The HTTP method to use
* for the request. Defaults to the configured method, or if no method was configured,
* "GET" if no parameters are being sent, and "POST" if parameters are being sent.
*
* __Note:__ The method name is case-sensitive and should be all caps.
*
* @param {Function} options.callback The function to be called upon receipt of the HTTP response.
* The callback is called regardless of success or failure and is passed the following parameters:
* @param {Object} options.callback.options The parameter to the request call.
* @param {Boolean} options.callback.success `true` if the request succeeded.
* @param {Object} options.callback.response The XMLHttpRequest object containing the response data.
* See [www.w3.org/TR/XMLHttpRequest/](http://www.w3.org/TR/XMLHttpRequest/) for details about
* accessing elements of the response.
*
* @param {Function} options.success The function to be called upon success of the request.
* The callback is passed the following parameters:
* @param {Object} options.success.response The XMLHttpRequest object containing the response data.
* @param {Object} options.success.options The parameter to the request call.
*
* @param {Function} options.failure The function to be called upon failure of the request.
* The callback is passed the following parameters:
* @param {Object} options.failure.response The XMLHttpRequest object containing the response data.
* @param {Object} options.failure.options The parameter to the request call.
*
* @param {Object} options.scope The scope in which to execute the callbacks: The "this" object for
* the callback function. If the `url`, or `params` options were specified as functions from which to
* draw values, then this also serves as the scope for those function calls. Defaults to the browser
* window.
*
* @param {Number} [options.timeout=30000] The timeout in milliseconds to be used for this request.
*
* @param {HTMLElement/HTMLElement/String} options.form The `<form>` Element or the id of the `<form>`
* to pull parameters from.
*
* @param {Boolean} options.isUpload **Only meaningful when used with the `form` option.**
*
* True if the form object is a file upload (will be set automatically if the form was configured
* with **`enctype`** `"multipart/form-data"`).
*
* File uploads are not performed using normal "Ajax" techniques, that is they are **not**
* performed using XMLHttpRequests. Instead the form is submitted in the standard manner with the
* DOM `<form>` element temporarily modified to have its [target][] set to refer to a dynamically
* generated, hidden `<iframe>` which is inserted into the document but removed after the return data
* has been gathered.
*
* The server response is parsed by the browser to create the document for the IFRAME. If the
* server is using JSON to send the return object, then the [Content-Type][] header must be set to
* "text/html" in order to tell the browser to insert the text unchanged into the document body.
*
* The response text is retrieved from the document, and a fake XMLHttpRequest object is created
* containing a `responseText` property in order to conform to the requirements of event handlers
* and callbacks.
*
* Be aware that file upload packets are sent with the content type [multipart/form][] and some server
* technologies (notably JEE) may require some custom processing in order to retrieve parameter names
* and parameter values from the packet content.
*
* [target]: http://www.w3.org/TR/REC-html40/present/frames.html#adef-target
* [Content-Type]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17
* [multipart/form]: http://www.faqs.org/rfcs/rfc2388.html
*
* @param {Object} options.headers Request headers to set for the request.
*
* @param {Object} options.xmlData XML document to use for the post.
*
* __Note:__ This will be used instead
* of params for the post data. Any params will be appended to the URL.
*
* @param {Object/String} options.jsonData JSON data to use as the post.
*
* __Note:__ This will be used
* instead of params for the post data. Any params will be appended to the URL.
*
* @param {Boolean} options.disableCaching True to add a unique cache-buster param to GET requests.
*
* @return {Object/null} The request object. This may be used to cancel the request.
*/
request : function(options) {
options = options || {};
var me = this,
scope = options.scope || window,
username = options.username || me.getUsername(),
password = options.password || me.getPassword() || '',
async, requestOptions, request, headers, xhr;
if (me.fireEvent('beforerequest', me, options) !== false) {
requestOptions = me.setOptions(options, scope);
if (this.isFormUpload(options) === true) {
this.upload(options.form, requestOptions.url, requestOptions.data, options);
return null;
}
// if autoabort is set, cancel the current transactions
if (options.autoAbort === true || me.getAutoAbort()) {
me.abort();
}
// create a connection object
xhr = this.getXhrInstance();
async = options.async !== false ? (options.async || me.getAsync()) : false;
// open the request
if (username) {
xhr.open(requestOptions.method, requestOptions.url, async, username, password);
} else {
xhr.open(requestOptions.method, requestOptions.url, async);
}
headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params);
// create the transaction object
request = {
id: ++Ext.data.Connection.requestId,
xhr: xhr,
headers: headers,
options: options,
async: async,
timeout: setTimeout(function() {
request.timedout = true;
me.abort(request);
}, options.timeout || me.getTimeout())
};
me.requests[request.id] = request;
// bind our statechange listener
if (async) {
xhr.onreadystatechange = Ext.Function.bind(me.onStateChange, me, [request]);
}
// start the request!
xhr.send(requestOptions.data);
if (!async) {
return this.onComplete(request);
}
return request;
} else {
Ext.callback(options.callback, options.scope, [options, undefined, undefined]);
return null;
}
},
/**
* Uploads a form using a hidden iframe.
* @param {String/HTMLElement/Ext.Element} form The form to upload.
* @param {String} url The url to post to.
* @param {String} params Any extra parameters to pass.
* @param {Object} options The initial options.
*/
upload: function(form, url, params, options) {
form = Ext.getDom(form);
options = options || {};
var id = Ext.id(),
frame = document.createElement('iframe'),
hiddens = [],
encoding = 'multipart/form-data',
buf = {
target: form.target,
method: form.method,
encoding: form.encoding,
enctype: form.enctype,
action: form.action
}, addField = function(name, value) {
hiddenItem = document.createElement('input');
Ext.fly(hiddenItem).set({
type: 'hidden',
value: value,
name: name
});
form.appendChild(hiddenItem);
hiddens.push(hiddenItem);
}, hiddenItem;
/*
* Originally this behavior was modified for Opera 10 to apply the secure URL after
* the frame had been added to the document. It seems this has since been corrected in
* Opera so the behavior has been reverted, the URL will be set before being added.
*/
Ext.fly(frame).set({
id: id,
name: id,
cls: Ext.baseCSSPrefix + 'hide-display',
src: Ext.SSL_SECURE_URL
});
document.body.appendChild(frame);
// This is required so that IE doesn't pop the response up in a new window.
if (document.frames) {
document.frames[id].name = id;
}
Ext.fly(form).set({
target: id,
method: 'POST',
enctype: encoding,
encoding: encoding,
action: url || buf.action
});
// add dynamic params
if (params) {
Ext.iterate(Ext.Object.fromQueryString(params), function(name, value) {
if (Ext.isArray(value)) {
Ext.each(value, function(v) {
addField(name, v);
});
} else {
addField(name, value);
}
});
}
Ext.fly(frame).on('load', Ext.Function.bind(this.onUploadComplete, this, [frame, options]), null, {single: true});
form.submit();
Ext.fly(form).set(buf);
Ext.each(hiddens, function(h) {
Ext.removeNode(h);
});
},
onUploadComplete: function(frame, options) {
var me = this,
// bogus response object
response = {
responseText: '',
responseXML: null
}, doc, firstChild;
try {
doc = frame.contentWindow.document || frame.contentDocument || window.frames[id].document;
if (doc) {
if (doc.body) {
if (this.textAreaRe.test((firstChild = doc.body.firstChild || {}).tagName)) { // json response wrapped in textarea
response.responseText = firstChild.value;
} else {
response.responseText = doc.body.innerHTML;
}
}
//in IE the document may still have a body even if returns XML.
response.responseXML = doc.XMLDocument || doc;
}
} catch (e) {
}
me.fireEvent('requestcomplete', me, response, options);
Ext.callback(options.success, options.scope, [response, options]);
Ext.callback(options.callback, options.scope, [options, true, response]);
setTimeout(function() {
Ext.removeNode(frame);
}, 100);
},
/**
* Detects whether the form is intended to be used for an upload.
* @private
*/
isFormUpload: function(options) {
var form = this.getForm(options);
if (form) {
return (options.isUpload || (this.multiPartRe).test(form.getAttribute('enctype')));
}
return false;
},
/**
* Gets the form object from options.
* @private
* @param {Object} options The request options.
* @return {HTMLElement/null} The form, `null` if not passed.
*/
getForm: function(options) {
return Ext.getDom(options.form) || null;
},
/**
* Sets various options such as the url, params for the request.
* @param {Object} options The initial options.
* @param {Object} scope The scope to execute in.
* @return {Object} The params for the request.
*/
setOptions: function(options, scope) {
var me = this,
params = options.params || {},
extraParams = me.getExtraParams(),
urlParams = options.urlParams,
url = options.url || me.getUrl(),
jsonData = options.jsonData,
method,
disableCache,
data;
// allow params to be a method that returns the params object
if (Ext.isFunction(params)) {
params = params.call(scope, options);
}
// allow url to be a method that returns the actual url
if (Ext.isFunction(url)) {
url = url.call(scope, options);
}
url = this.setupUrl(options, url);
//<debug>
if (!url) {
Ext.Logger.error('No URL specified');
}
//</debug>
// check for xml or json data, and make sure json data is encoded
data = options.rawData || options.xmlData || jsonData || null;
if (jsonData && !Ext.isPrimitive(jsonData)) {
data = Ext.encode(data);
}
// make sure params are a url encoded string and include any extraParams if specified
if (Ext.isObject(params)) {
params = Ext.Object.toQueryString(params);
}
if (Ext.isObject(extraParams)) {
extraParams = Ext.Object.toQueryString(extraParams);
}
params = params + ((extraParams) ? ((params) ? '&' : '') + extraParams : '');
urlParams = Ext.isObject(urlParams) ? Ext.Object.toQueryString(urlParams) : urlParams;
params = this.setupParams(options, params);
// decide the proper method for this request
method = (options.method || me.getMethod() || ((params || data) ? 'POST' : 'GET')).toUpperCase();
this.setupMethod(options, method);
disableCache = options.disableCaching !== false ? (options.disableCaching || me.getDisableCaching()) : false;
// append date to prevent caching
if (disableCache) {
url = Ext.urlAppend(url, (options.disableCachingParam || me.getDisableCachingParam()) + '=' + (new Date().getTime()));
}
// if the method is get or there is json/xml data append the params to the url
if ((method == 'GET' || data) && params) {
url = Ext.urlAppend(url, params);
params = null;
}
// allow params to be forced into the url
if (urlParams) {
url = Ext.urlAppend(url, urlParams);
}
return {
url: url,
method: method,
data: data || params || null
};
},
/**
* Template method for overriding url.
* @private
* @param {Object} options
* @param {String} url
* @return {String} The modified url
*/
setupUrl: function(options, url) {
var form = this.getForm(options);
if (form) {
url = url || form.action;
}
return url;
},
/**
* Template method for overriding params.
* @private
* @param {Object} options
* @param {String} params
* @return {String} The modified params.
*/
setupParams: function(options, params) {
var form = this.getForm(options),
serializedForm;
if (form && !this.isFormUpload(options)) {
serializedForm = Ext.Element.serializeForm(form);
params = params ? (params + '&' + serializedForm) : serializedForm;
}
return params;
},
/**
* Template method for overriding method.
* @private
* @param {Object} options
* @param {String} method
* @return {String} The modified method.
*/
setupMethod: function(options, method) {
if (this.isFormUpload(options)) {
return 'POST';
}
return method;
},
/**
* Setup all the headers for the request.
* @private
* @param {Object} xhr The xhr object.
* @param {Object} options The options for the request.
* @param {Object} data The data for the request.
* @param {Object} params The params for the request.
*/
setupHeaders: function(xhr, options, data, params) {
var me = this,
headers = Ext.apply({}, options.headers || {}, me.getDefaultHeaders() || {}),
contentType = me.getDefaultPostHeader(),
jsonData = options.jsonData,
xmlData = options.xmlData,
key,
header;
if (!headers['Content-Type'] && (data || params)) {
if (data) {
if (options.rawData) {
contentType = 'text/plain';
} else {
if (xmlData && Ext.isDefined(xmlData)) {
contentType = 'text/xml';
} else if (jsonData && Ext.isDefined(jsonData)) {
contentType = 'application/json';
}
}
}
headers['Content-Type'] = contentType;
}
if (((me.getUseDefaultXhrHeader() && options.useDefaultXhrHeader !== false) || options.useDefaultXhrHeader) && !headers['X-Requested-With']) {
headers['X-Requested-With'] = me.getDefaultXhrHeader();
}
// set up all the request headers on the xhr object
try {
for (key in headers) {
if (headers.hasOwnProperty(key)) {
header = headers[key];
xhr.setRequestHeader(key, header);
}
}
} catch(e) {
me.fireEvent('exception', key, header);
}
if (options.withCredentials) {
xhr.withCredentials = options.withCredentials;
}
return headers;
},
/**
* Creates the appropriate XHR transport for the browser.
* @private
*/
getXhrInstance: (function() {
var options = [function() {
return new XMLHttpRequest();
}, function() {
return new ActiveXObject('MSXML2.XMLHTTP.3.0');
}, function() {
return new ActiveXObject('MSXML2.XMLHTTP');
}, function() {
return new ActiveXObject('Microsoft.XMLHTTP');
}], i = 0,
len = options.length,
xhr;
for (; i < len; ++i) {
try {
xhr = options[i];
xhr();
break;
} catch(e) {
}
}
return xhr;
})(),
/**
* Determines whether this object has a request outstanding.
* @param {Object} request The request to check.
* @return {Boolean} True if there is an outstanding request.
*/
isLoading : function(request) {
if (!(request && request.xhr)) {
return false;
}
// if there is a connection and readyState is not 0 or 4
var state = request.xhr.readyState;
return !(state === 0 || state == 4);
},
/**
* Aborts any outstanding request.
* @param {Object} request (Optional) Defaults to the last request.
*/
abort : function(request) {
var me = this,
requests = me.requests,
id;
if (request && me.isLoading(request)) {
/*
* Clear out the onreadystatechange here, this allows us
* greater control, the browser may/may not fire the function
* depending on a series of conditions.
*/
request.xhr.onreadystatechange = null;
request.xhr.abort();
me.clearTimeout(request);
if (!request.timedout) {
request.aborted = true;
}
me.onComplete(request);
me.cleanup(request);
} else if (!request) {
for (id in requests) {
if (requests.hasOwnProperty(id)) {
me.abort(requests[id]);
}
}
}
},
/**
* Aborts all outstanding requests.
*/
abortAll: function() {
this.abort();
},
/**
* Fires when the state of the XHR changes.
* @private
* @param {Object} request The request
*/
onStateChange : function(request) {
if (request.xhr.readyState == 4) {
this.clearTimeout(request);
this.onComplete(request);
this.cleanup(request);
}
},
/**
* Clears the timeout on the request.
* @private
* @param {Object} The request
*/
clearTimeout: function(request) {
clearTimeout(request.timeout);
delete request.timeout;
},
/**
* Cleans up any left over information from the request.
* @private
* @param {Object} The request.
*/
cleanup: function(request) {
request.xhr = null;
delete request.xhr;
},
/**
* To be called when the request has come back from the server.
* @private
* @param {Object} request
* @return {Object} The response.
*/
onComplete : function(request) {
var me = this,
options = request.options,
result,
success,
response;
try {
result = me.parseStatus(request.xhr.status, request.xhr);
if (request.timedout) {
result.success = false;
}
} catch (e) {
// in some browsers we can't access the status if the readyState is not 4, so the request has failed
result = {
success : false,
isException : false
};
}
success = result.success;
if (success) {
response = me.createResponse(request);
me.fireEvent('requestcomplete', me, response, options);
Ext.callback(options.success, options.scope, [response, options]);
} else {
if (result.isException || request.aborted || request.timedout) {
response = me.createException(request);
} else {
response = me.createResponse(request);
}
me.fireEvent('requestexception', me, response, options);
Ext.callback(options.failure, options.scope, [response, options]);
}
Ext.callback(options.callback, options.scope, [options, success, response]);
delete me.requests[request.id];
return response;
},
/**
* Checks if the response status was successful.
* @param {Number} status The status code.
* @param xhr
* @return {Object} An object containing success/status state.
*/
parseStatus: function(status, xhr) {
// see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223
status = status == 1223 ? 204 : status;
var success = (status >= 200 && status < 300) || status == 304 || (status == 0 && xhr.responseText.length > 0),
isException = false;
if (!success) {
switch (status) {
case 12002:
case 12029:
case 12030:
case 12031:
case 12152:
case 13030:
isException = true;
break;
}
}
return {
success: success,
isException: isException
};
},
/**
* Creates the response object.
* @private
* @param {Object} request
*/
createResponse : function(request) {
var xhr = request.xhr,
headers = {},
lines, count, line, index, key, response;
//we need to make this check here because if a request times out an exception is thrown
//when calling getAllResponseHeaders() because the response never came back to populate it
if (request.timedout || request.aborted) {
request.success = false;
lines = [];
} else {
lines = xhr.getAllResponseHeaders().replace(this.lineBreakRe, '\n').split('\n');
}
count = lines.length;
while (count--) {
line = lines[count];
index = line.indexOf(':');
if (index >= 0) {
key = line.substr(0, index).toLowerCase();
if (line.charAt(index + 1) == ' ') {
++index;
}
headers[key] = line.substr(index + 1);
}
}
request.xhr = null;
delete request.xhr;
response = {
request: request,
requestId : request.id,
status : xhr.status,
statusText : xhr.statusText,
getResponseHeader : function(header) {
return headers[header.toLowerCase()];
},
getAllResponseHeaders : function() {
return headers;
},
responseText : xhr.responseText,
responseXML : xhr.responseXML
};
// If we don't explicitly tear down the xhr reference, IE6/IE7 will hold this in the closure of the
// functions created with getResponseHeader/getAllResponseHeaders
xhr = null;
return response;
},
/**
* Creates the exception object.
* @private
* @param {Object} request
*/
createException : function(request) {
return {
request : request,
requestId : request.id,
status : request.aborted ? -1 : 0,
statusText : request.aborted ? 'transaction aborted' : 'communication failure',
aborted: request.aborted,
timedout: request.timedout
};
}
});
/**
* @aside guide ajax
*
* A singleton instance of an {@link Ext.data.Connection}. This class
* is used to communicate with your server side code. It can be used as follows:
*
* Ext.Ajax.request({
* url: 'page.php',
* params: {
* id: 1
* },
* success: function(response){
* var text = response.responseText;
* // process server response here
* }
* });
*
* Default options for all requests can be set by changing a property on the Ext.Ajax class:
*
* Ext.Ajax.setTimeout(60000); // 60 seconds
*
* Any options specified in the request method for the Ajax request will override any
* defaults set on the Ext.Ajax class. In the code sample below, the timeout for the
* request will be 60 seconds.
*
* Ext.Ajax.setTimeout(120000); // 120 seconds
* Ext.Ajax.request({
* url: 'page.aspx',
* timeout: 60000
* });
*
* In general, this class will be used for all Ajax requests in your application.
* The main reason for creating a separate {@link Ext.data.Connection} is for a
* series of requests that share common settings that are different to all other
* requests in the application.
*/
Ext.define('Ext.Ajax', {
extend: 'Ext.data.Connection',
singleton: true,
/**
* @property {Boolean} autoAbort
* Whether a new request should abort any pending requests.
*/
autoAbort : false
});
/**
* Ext.Anim is used to execute 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.define('Ext.Anim', {
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;
this.callSuper([config]);
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;
},
/**
* @ignore
*/
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];
}
}, function() {
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
* Defines different types of animations.
*
* __Note:__ _flip_, _cube_, and _wipe_ animations do not work on Android.
*
* Please refer to {@link Ext.Anim} on how to use animations.
* @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.
* 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.
*/
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
})
};
});
/**
* Provides a base class for audio/visual controls. Should not be used directly.
*
* Please see the {@link Ext.Audio} and {@link Ext.Video} classes for more information.
* @private
*/
Ext.define('Ext.Media', {
extend: 'Ext.Component',
xtype: 'media',
/**
* @event play
* Fires whenever the media is played.
* @param {Ext.Media} this
*/
/**
* @event pause
* Fires whenever the media is paused.
* @param {Ext.Media} this
* @param {Number} time The time at which the media was paused at in seconds.
*/
/**
* @event ended
* Fires whenever the media playback has ended.
* @param {Ext.Media} this
* @param {Number} time The time at which the media ended at in seconds.
*/
/**
* @event stop
* Fires whenever the media is stopped.
* The `pause` event will also fire after the `stop` event if the media is currently playing.
* The `timeupdate` event will also fire after the `stop` event regardless of playing status.
* @param {Ext.Media} this
*/
/**
* @event volumechange
* Fires whenever the volume is changed.
* @param {Ext.Media} this
* @param {Number} volume The volume level from 0 to 1.
*/
/**
* @event mutedchange
* Fires whenever the muted status is changed.
* The volumechange event will also fire after the `mutedchange` event fires.
* @param {Ext.Media} this
* @param {Boolean} muted The muted status.
*/
/**
* @event timeupdate
* Fires when the media is playing every 15 to 250ms.
* @param {Ext.Media} this
* @param {Number} time The current time in seconds.
*/
config: {
/**
* @cfg {String} url
* Location of the media to play.
* @accessor
*/
url: '',
/**
* @cfg {Boolean} enableControls
* Set this to `false` to turn off the native media controls.
* Defaults to `false` when you are on Android, as it doesn't support controls.
* @accessor
*/
enableControls: Ext.os.is.Android ? false : true,
/**
* @cfg {Boolean} autoResume
* Will automatically start playing the media when the container is activated.
* @accessor
*/
autoResume: false,
/**
* @cfg {Boolean} autoPause
* Will automatically pause the media when the container is deactivated.
* @accessor
*/
autoPause: true,
/**
* @cfg {Boolean} preload
* Will begin preloading the media immediately.
* @accessor
*/
preload: true,
/**
* @cfg {Boolean} loop
* Will loop the media forever.
* @accessor
*/
loop: false,
/**
* @cfg {Ext.Element} media
* A reference to the underlying audio/video element.
* @accessor
*/
media: null,
/**
* @cfg {Number} volume
* The volume of the media from 0.0 to 1.0.
* @accessor
*/
volume: 1,
/**
* @cfg {Boolean} muted
* Whether or not the media is muted. This will also set the volume to zero.
* @accessor
*/
muted: false
},
constructor: function() {
this.mediaEvents = {};
this.callSuper(arguments);
},
initialize: function() {
var me = this;
me.callParent();
me.on({
scope: me,
activate : me.onActivate,
deactivate: me.onDeactivate
});
me.addMediaListener({
canplay: 'onCanPlay',
play: 'onPlay',
pause: 'onPause',
ended: 'onEnd',
volumechange: 'onVolumeChange',
timeupdate: 'onTimeUpdate'
});
},
addMediaListener: function(event, fn) {
var me = this,
dom = me.media.dom,
bind = Ext.Function.bind;
Ext.Object.each(event, function(e, fn) {
fn = bind(me[fn], me);
me.mediaEvents[e] = fn;
dom.addEventListener(e, fn);
});
},
onPlay: function() {
this.fireEvent('play', this);
},
onCanPlay: function() {
this.fireEvent('canplay', this);
},
onPause: function() {
this.fireEvent('pause', this, this.getCurrentTime());
},
onEnd: function() {
this.fireEvent('ended', this, this.getCurrentTime());
},
onVolumeChange: function() {
this.fireEvent('volumechange', this, this.media.dom.volume);
},
onTimeUpdate: function() {
this.fireEvent('timeupdate', this, this.getCurrentTime());
},
/**
* Returns if the media is currently playing.
* @return {Boolean} playing `true` if the media is playing.
*/
isPlaying: function() {
return !Boolean(this.media.dom.paused);
},
// @private
onActivate: function() {
var me = this;
if (me.getAutoResume() && !me.isPlaying()) {
me.play();
}
},
// @private
onDeactivate: function() {
var me = this;
if (me.getAutoPause() && me.isPlaying()) {
me.pause();
}
},
/**
* Sets the URL of the media element. If the media element already exists, it is update the src attribute of the
* element. If it is currently playing, it will start the new video.
*/
updateUrl: function(newUrl) {
var dom = this.media.dom;
//when changing the src, we must call load:
//http://developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/ControllingMediaWithJavaScript/ControllingMediaWithJavaScript.html
dom.src = newUrl;
if ('load' in dom) {
dom.load();
}
if (this.isPlaying()) {
this.play();
}
},
/**
* Updates the controls of the video element.
*/
updateEnableControls: function(enableControls) {
this.media.dom.controls = enableControls ? 'controls' : false;
},
/**
* Updates the loop setting of the media element.
*/
updateLoop: function(loop) {
this.media.dom.loop = loop ? 'loop' : false;
},
/**
* Starts or resumes media playback.
*/
play: function() {
var dom = this.media.dom;
if ('play' in dom) {
dom.play();
setTimeout(function() {
dom.play();
}, 10);
}
},
/**
* Pauses media playback.
*/
pause: function() {
var dom = this.media.dom;
if ('pause' in dom) {
dom.pause();
}
},
/**
* Toggles the media playback state.
*/
toggle: function() {
if (this.isPlaying()) {
this.pause();
} else {
this.play();
}
},
/**
* Stops media playback and returns to the beginning.
*/
stop: function() {
var me = this;
me.setCurrentTime(0);
me.fireEvent('stop', me);
me.pause();
},
//@private
updateVolume: function(volume) {
this.media.dom.volume = volume;
},
//@private
updateMuted: function(muted) {
this.fireEvent('mutedchange', this, muted);
this.media.dom.muted = muted;
},
/**
* Returns the current time of the media, in seconds.
* @return {Number}
*/
getCurrentTime: function() {
return this.media.dom.currentTime;
},
/*
* Set the current time of the media.
* @param {Number} time The time, in seconds.
* @return {Number}
*/
setCurrentTime: function(time) {
this.media.dom.currentTime = time;
return time;
},
/**
* Returns the duration of the media, in seconds.
* @return {Number}
*/
getDuration: function() {
return this.media.dom.duration;
},
destroy: function() {
var me = this,
dom = me.media.dom,
mediaEvents = me.mediaEvents;
Ext.Object.each(mediaEvents, function(event, fn) {
dom.removeEventListener(event, fn);
});
this.callSuper();
}
});
/**
* {@link Ext.Audio} is a simple class which provides a container for the [HTML5 Audio element](http://developer.mozilla.org/en-US/docs/Using_HTML5_audio_and_video).
*
* ## Recommended File Types/Compression:
* * Uncompressed WAV and AIF audio
* * MP3 audio
* * AAC-LC
* * HE-AAC audio
*
* ## Notes
* On Android devices, the audio tags controls do not show. You must use the {@link #method-play}, {@link #method-pause} and
* {@link #toggle} methods to control the audio (example below).
*
* ## Examples
*
* Here is an example of the {@link Ext.Audio} component in a fullscreen container:
*
* @example preview
* Ext.create('Ext.Container', {
* fullscreen: true,
* layout: {
* type : 'vbox',
* pack : 'center',
* align: 'stretch'
* },
* items: [
* {
* xtype : 'toolbar',
* docked: 'top',
* title : 'Ext.Audio'
* },
* {
* xtype: 'audio',
* url : 'touch/examples/audio/crash.mp3'
* }
* ]
* });
*
* You can also set the {@link #hidden} configuration of the {@link Ext.Audio} component to true by default,
* and then control the audio by using the {@link #method-play}, {@link #method-pause} and {@link #toggle} methods:
*
* @example preview
* Ext.create('Ext.Container', {
* fullscreen: true,
* layout: {
* type: 'vbox',
* pack: 'center'
* },
* items: [
* {
* xtype : 'toolbar',
* docked: 'top',
* title : 'Ext.Audio'
* },
* {
* xtype: 'toolbar',
* docked: 'bottom',
* defaults: {
* xtype: 'button',
* handler: function() {
* var container = this.getParent().getParent(),
* // use ComponentQuery to get the audio component (using its xtype)
* audio = container.down('audio');
*
* audio.toggle();
* this.setText(audio.isPlaying() ? 'Pause' : 'Play');
* }
* },
* items: [
* { text: 'Play', flex: 1 }
* ]
* },
* {
* html: 'Hidden audio!',
* styleHtmlContent: true
* },
* {
* xtype : 'audio',
* hidden: true,
* url : 'touch/examples/audio/crash.mp3'
* }
* ]
* });
* @aside example audio
*/
Ext.define('Ext.Audio', {
extend: 'Ext.Media',
xtype : 'audio',
config: {
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'audio'
/**
* @cfg {String} url
* The location of the audio to play.
*
* ### Recommended file types are:
* * Uncompressed WAV and AIF audio
* * MP3 audio
* * AAC-LC
* * HE-AAC audio
* @accessor
*/
},
// @private
onActivate: function() {
var me = this;
me.callParent();
if (Ext.os.is.Phone) {
me.element.show();
}
},
// @private
onDeactivate: function() {
var me = this;
me.callParent();
if (Ext.os.is.Phone) {
me.element.hide();
}
},
template: [{
reference: 'media',
preload: 'auto',
tag: 'audio',
cls: Ext.baseCSSPrefix + 'component'
}]
});
/**
* @class Ext.ComponentQuery
* @extends Object
* @singleton
*
* Provides searching of Components within {@link Ext.ComponentManager} (globally) or a specific
* {@link Ext.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>
* @return {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
}
//<debug>
// Exhausted all matches: It's an error
if (i === (length - 1)) {
Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
}
//</debug>
}
}
// Now we are going to check for a mode change. This means a space
// or a > to determine if we are going to select all the children
// of the currently matched items, or a ^ if we are going to use the
// ownerCt axis as the candidate source.
if (modeMatch[1]) { // Assignment, and test for truthiness!
operations.push({
mode: modeMatch[2]||modeMatch[1]
});
selector = selector.replace(modeMatch[0], '');
}
}
// Now that we have all our operations in an array, we are going
// to create a new Query using these operations.
return new cq.Query({
operations: operations
});
}
});
});
/**
* @class Ext.Decorator
* @extends Ext.Component
*
* In a few words, a Decorator is a Component that wraps around another Component. A typical example of a Decorator is a
* {@link Ext.field.Field Field}. A form field is nothing more than a decorator around another component, and gives the
* component a label, as well as extra styling to make it look good in a form.
*
* A Decorator can be thought of as a lightweight Container that has only one child item, and no layout overhead.
* The look and feel of decorators can be styled purely in CSS.
*
* Another powerful feature that Decorator provides is config proxying. For example: all config items of a
* {@link Ext.slider.Slider Slider} also exist in a {@link Ext.field.Slider Slider Field} for API convenience.
* The {@link Ext.field.Slider Slider Field} simply proxies all corresponding getters and setters
* to the actual {@link Ext.slider.Slider Slider} instance. Writing out all the setters and getters to do that is a tedious task
* and a waste of code space. Instead, when you sub-class Ext.Decorator, all you need to do is to specify those config items
* that you want to proxy to the Component using a special 'proxyConfig' class property. Here's how it may look like
* in a Slider Field class:
*
* Ext.define('My.field.Slider', {
* extend: 'Ext.Decorator',
*
* config: {
* component: {
* xtype: 'slider'
* }
* },
*
* proxyConfig: {
* minValue: 0,
* maxValue: 100,
* increment: 1
* }
*
* // ...
* });
*
* Once `My.field.Slider` class is created, it will have all setters and getters methods for all items listed in `proxyConfig`
* automatically generated. These methods all proxy to the same method names that exist within the Component instance.
*/
Ext.define('Ext.Decorator', {
extend: 'Ext.Component',
isDecorator: true,
config: {
/**
* @cfg {Object} component The config object to factory the Component that this Decorator wraps around
*/
component: {}
},
statics: {
generateProxySetter: function(name) {
return function(value) {
var component = this.getComponent();
component[name].call(component, value);
return this;
}
},
generateProxyGetter: function(name) {
return function() {
var component = this.getComponent();
return component[name].call(component);
}
}
},
onClassExtended: function(Class, members) {
if (!members.hasOwnProperty('proxyConfig')) {
return;
}
var ExtClass = Ext.Class,
proxyConfig = members.proxyConfig,
config = members.config;
members.config = (config) ? Ext.applyIf(config, proxyConfig) : proxyConfig;
var name, nameMap, setName, getName;
for (name in proxyConfig) {
if (proxyConfig.hasOwnProperty(name)) {
nameMap = ExtClass.getConfigNameMap(name);
setName = nameMap.set;
getName = nameMap.get;
members[setName] = this.generateProxySetter(setName);
members[getName] = this.generateProxyGetter(getName);
}
}
},
// @private
applyComponent: function(config) {
return Ext.factory(config, Ext.Component);
},
// @private
updateComponent: function(newComponent, oldComponent) {
if (oldComponent) {
if (this.isRendered() && oldComponent.setRendered(false)) {
oldComponent.fireAction('renderedchange', [this, oldComponent, false],
'doUnsetComponent', this, { args: [oldComponent] });
}
else {
this.doUnsetComponent(oldComponent);
}
}
if (newComponent) {
if (this.isRendered() && newComponent.setRendered(true)) {
newComponent.fireAction('renderedchange', [this, newComponent, true],
'doSetComponent', this, { args: [newComponent] });
}
else {
this.doSetComponent(newComponent);
}
}
},
// @private
doUnsetComponent: function(component) {
if (component.renderElement.dom) {
component.setLayoutSizeFlags(0);
this.innerElement.dom.removeChild(component.renderElement.dom);
}
},
// @private
doSetComponent: function(component) {
if (component.renderElement.dom) {
component.setLayoutSizeFlags(this.getSizeFlags());
this.innerElement.dom.appendChild(component.renderElement.dom);
}
},
// @private
setRendered: function(rendered) {
var component;
if (this.callParent(arguments)) {
component = this.getComponent();
if (component) {
component.setRendered(rendered);
}
return true;
}
return false;
},
// @private
setDisabled: function(disabled) {
this.callParent(arguments);
this.getComponent().setDisabled(disabled);
},
destroy: function() {
Ext.destroy(this.getComponent());
this.callParent();
}
});
/**
* This is a simple way to add an image of any size to your application and have it participate in the layout system
* like any other component. This component typically takes between 1 and 3 configurations - a {@link #src}, and
* optionally a {@link #height} and a {@link #width}:
*
* @example miniphone
* var img = Ext.create('Ext.Img', {
* src: 'http://www.sencha.com/assets/images/sencha-avatar-64x64.png',
* height: 64,
* width: 64
* });
* Ext.Viewport.add(img);
*
* It's also easy to add an image into a panel or other container using its xtype:
*
* @example miniphone
* Ext.create('Ext.Panel', {
* fullscreen: true,
* layout: 'hbox',
* items: [
* {
* xtype: 'image',
* src: 'http://www.sencha.com/assets/images/sencha-avatar-64x64.png',
* flex: 1
* },
* {
* xtype: 'panel',
* flex: 2,
* html: 'Sencha Inc.<br/>1700 Seaport Boulevard Suite 120, Redwood City, CA'
* }
* ]
* });
*
* Here we created a panel which contains an image (a profile picture in this case) and a text area to allow the user
* to enter profile information about themselves. In this case we used an {@link Ext.layout.HBox hbox layout} and
* flexed the image to take up one third of the width and the text area to take two thirds of the width. See the
* {@link Ext.layout.HBox hbox docs} for more information on flexing items.
*/
Ext.define('Ext.Img', {
extend: 'Ext.Component',
xtype: ['image', 'img'],
/**
* @event tap
* Fires whenever the component is tapped
* @param {Ext.Img} this The Image instance
* @param {Ext.EventObject} e The event object
*/
/**
* @event load
* Fires when the image is loaded
* @param {Ext.Img} this The Image instance
* @param {Ext.EventObject} e The event object
*/
/**
* @event error
* Fires if an error occured when trying to load the image
* @param {Ext.Img} this The Image instance
* @param {Ext.EventObject} e The event object
*/
config: {
/**
* @cfg {String} src The source of this image
* @accessor
*/
src: null,
/**
* @cfg
* @inheritdoc
*/
baseCls : Ext.baseCSSPrefix + 'img',
/**
* @cfg {String} imageCls The CSS class to be used when {@link #mode} is not set to 'background'
* @accessor
*/
imageCls : Ext.baseCSSPrefix + 'img-image',
/**
* @cfg {String} backgroundCls The CSS class to be used when {@link #mode} is set to 'background'
* @accessor
*/
backgroundCls : Ext.baseCSSPrefix + 'img-background',
/**
* @cfg {String} mode If set to 'background', uses a background-image CSS property instead of an
* `<img>` tag to display the image.
*/
mode: 'background'
},
beforeInitialize: function() {
var me = this;
me.onLoad = Ext.Function.bind(me.onLoad, me);
me.onError = Ext.Function.bind(me.onError, me);
},
initialize: function() {
var me = this;
me.callParent();
me.relayEvents(me.renderElement, '*');
me.element.on({
tap: 'onTap',
scope: me
});
},
hide: function() {
this.callParent();
this.hiddenSrc = this.hiddenSrc || this.getSrc();
this.setSrc(null);
},
show: function() {
this.callParent();
if (this.hiddenSrc) {
this.setSrc(this.hiddenSrc);
delete this.hiddenSrc;
}
},
updateMode: function(mode) {
var me = this,
imageCls = me.getImageCls(),
backgroundCls = me.getBackgroundCls();
if (mode === 'background') {
if (me.imageElement) {
me.imageElement.destroy();
delete me.imageElement;
me.updateSrc(me.getSrc());
}
me.replaceCls(imageCls, backgroundCls);
} else {
me.imageElement = me.element.createChild({ tag: 'img' });
me.replaceCls(backgroundCls, imageCls);
}
},
updateImageCls : function (newCls, oldCls) {
this.replaceCls(oldCls, newCls);
},
updateBackgroundCls : function (newCls, oldCls) {
this.replaceCls(oldCls, newCls);
},
onTap: function(e) {
this.fireEvent('tap', this, e);
},
onAfterRender: function() {
this.updateSrc(this.getSrc());
},
/**
* @private
*/
updateSrc: function(newSrc) {
var me = this,
dom;
if (me.getMode() === 'background') {
dom = this.imageObject || new Image();
}
else {
dom = me.imageElement.dom;
}
this.imageObject = dom;
dom.setAttribute('src', Ext.isString(newSrc) ? newSrc : '');
dom.addEventListener('load', me.onLoad, false);
dom.addEventListener('error', me.onError, false);
},
detachListeners: function() {
var dom = this.imageObject;
if (dom) {
dom.removeEventListener('load', this.onLoad, false);
dom.removeEventListener('error', this.onError, false);
}
},
onLoad : function(e) {
this.detachListeners();
if (this.getMode() === 'background') {
this.element.dom.style.backgroundImage = 'url("' + this.imageObject.src + '")';
}
this.fireEvent('load', this, e);
},
onError : function(e) {
this.detachListeners();
this.fireEvent('error', this, e);
},
doSetWidth: function(width) {
var sizingElement = (this.getMode() === 'background') ? this.element : this.imageElement;
sizingElement.setWidth(width);
this.callParent(arguments);
},
doSetHeight: function(height) {
var sizingElement = (this.getMode() === 'background') ? this.element : this.imageElement;
sizingElement.setHeight(height);
this.callParent(arguments);
},
destroy: function() {
this.detachListeners();
Ext.destroy(this.imageObject, this.imageElement);
delete this.imageObject;
delete this.imageElement;
this.callParent();
}
});
/**
* A simple label component which allows you to insert content using {@link #html} configuration.
*
* @example miniphone
* Ext.Viewport.add({
* xtype: 'label',
* html: 'My label!'
* });
*/
Ext.define('Ext.Label', {
extend: 'Ext.Component',
xtype: 'label',
config: {
baseCls: Ext.baseCSSPrefix + 'label'
/**
* @cfg {String} html
* The label of this component.
*/
}
});
/**
* A simple class used to mask any {@link Ext.Container}.
*
* This should rarely be used directly, instead look at the {@link Ext.Container#masked} configuration.
*
* ## Example
*
* @example miniphone
* Ext.Viewport.add({
* masked: {
* xtype: 'loadmask'
* }
* });
*
* You can customize the loading {@link #message} and whether or not you want to show the {@link #indicator}:
*
* @example miniphone
* Ext.Viewport.add({
* masked: {
* xtype: 'loadmask',
* message: 'A message..',
* indicator: false
* }
* });
*
*/
Ext.define('Ext.LoadMask', {
extend: 'Ext.Mask',
xtype: 'loadmask',
config: {
/**
* @cfg {String} message
* The text to display in a centered loading message box.
* @accessor
*/
message: 'Loading...',
/**
* @cfg {String} messageCls
* The CSS class to apply to the loading message element.
* @accessor
*/
messageCls: Ext.baseCSSPrefix + 'mask-message',
/**
* @cfg {Boolean} indicator
* True to show the loading indicator on this {@link Ext.LoadMask}.
* @accessor
*/
indicator: true
},
getTemplate: function() {
var prefix = Ext.baseCSSPrefix;
return [
{
//it needs an inner so it can be centered within the mask, and have a background
reference: 'innerElement',
cls: prefix + 'mask-inner',
children: [
//the elements required for the CSS loading {@link #indicator}
{
reference: 'indicatorElement',
cls: prefix + 'loading-spinner-outer',
children: [
{
cls: prefix + 'loading-spinner',
children: [
{ tag: 'span', cls: prefix + 'loading-top' },
{ tag: 'span', cls: prefix + 'loading-right' },
{ tag: 'span', cls: prefix + 'loading-bottom' },
{ tag: 'span', cls: prefix + 'loading-left' }
]
}
]
},
//the element used to display the {@link #message}
{
reference: 'messageElement'
}
]
}
];
},
/**
* Updates the message element with the new value of the {@link #message} configuration
* @private
*/
updateMessage: function(newMessage) {
var cls = Ext.baseCSSPrefix + 'has-message';
if (newMessage) {
this.addCls(cls);
} else {
this.removeCls(cls);
}
this.messageElement.setHtml(newMessage);
},
/**
* Replaces the cls of the message element with the value of the {@link #messageCls} configuration.
* @private
*/
updateMessageCls: function(newMessageCls, oldMessageCls) {
this.messageElement.replaceCls(oldMessageCls, newMessageCls);
},
/**
* Shows or hides the loading indicator when the {@link #indicator} configuration is changed.
* @private
*/
updateIndicator: function(newIndicator) {
this[newIndicator ? 'removeCls' : 'addCls'](Ext.baseCSSPrefix + 'indicator-hidden');
}
}, function() {
});
/**
* Provides a cross browser class for retrieving location information.
*
* Based on the [Geolocation API Specification](http://dev.w3.org/geo/api/spec-source.html)
*
* When instantiated, by default this class immediately begins tracking location information,
* firing a {@link #locationupdate} event when new location information is available. To disable this
* location tracking (which may be battery intensive on mobile devices), set {@link #autoUpdate} to `false`.
*
* When this is done, only calls to {@link #updateLocation} will trigger a location retrieval.
*
* A {@link #locationerror} event is raised when an error occurs retrieving the location, either due to a user
* denying the application access to it, or the browser not supporting it.
*
* The below code shows a GeoLocation making a single retrieval of location information.
*
* var geo = Ext.create('Ext.util.Geolocation', {
* autoUpdate: false,
* listeners: {
* locationupdate: function(geo) {
* alert('New latitude: ' + geo.getLatitude());
* },
* locationerror: function(geo, bTimeout, bPermissionDenied, bLocationUnavailable, message) {
* if(bTimeout){
* alert('Timeout occurred.');
* } else {
* alert('Error occurred.');
* }
* }
* }
* });
* geo.updateLocation();
*/
Ext.define('Ext.util.Geolocation', {
extend: 'Ext.Evented',
alternateClassName: ['Ext.util.GeoLocation'],
config: {
/**
* @event locationerror
* Raised when a location retrieval operation failed.
*
* In the case of calling updateLocation, this event will be raised only once.
*
* If {@link #autoUpdate} is set to `true`, this event could be raised repeatedly.
* The first error is relative to the moment {@link #autoUpdate} was set to `true`
* (or this {@link Ext.util.Geolocation} was initialized with the {@link #autoUpdate} config option set to `true`).
* Subsequent errors are relative to the moment when the device determines that it's position has changed.
* @param {Ext.util.Geolocation} this
* @param {Boolean} timeout
* Boolean indicating a timeout occurred
* @param {Boolean} permissionDenied
* Boolean indicating the user denied the location request
* @param {Boolean} locationUnavailable
* Boolean indicating that the location of the device could not be determined.
* For instance, one or more of the location providers used in the location acquisition
* process reported an internal error that caused the process to fail entirely.
* @param {String} message An error message describing the details of the error encountered.
*
* This attribute is primarily intended for debugging and should not be used
* directly in an application user interface.
*/
/**
* @event locationupdate
* Raised when a location retrieval operation has been completed successfully.
* @param {Ext.util.Geolocation} this
* Retrieve the current location information from the GeoLocation object by using the read-only
* properties: {@link #latitude}, {@link #longitude}, {@link #accuracy}, {@link #altitude}, {@link #altitudeAccuracy}, {@link #heading}, and {@link #speed}.
*/
/**
* @cfg {Boolean} autoUpdate
* When set to `true`, continually monitor the location of the device (beginning immediately)
* and fire {@link #locationupdate} and {@link #locationerror} events.
*/
autoUpdate: true,
/**
* @cfg {Number} frequency
* The frequency of each update if {@link #autoUpdate} is set to `true`.
*/
frequency: 10000,
/**
* Read-only property representing the last retrieved
* geographical coordinate specified in degrees.
* @type Number
* @readonly
*/
latitude: null,
/**
* Read-only property representing the last retrieved
* geographical coordinate specified in degrees.
* @type Number
* @readonly
*/
longitude: null,
/**
* Read-only property representing the last retrieved
* accuracy level of the latitude and longitude coordinates,
* specified in meters.
*
* This will always be a non-negative number.
*
* This corresponds to a 95% confidence level.
* @type Number
* @readonly
*/
accuracy: null,
/**
* Read-only property representing the last retrieved
* height of the position, specified in meters above the ellipsoid
* [WGS84](http://dev.w3.org/geo/api/spec-source.html#ref-wgs).
* @type Number
* @readonly
*/
altitude: null,
/**
* Read-only property representing the last retrieved
* accuracy level of the altitude coordinate, specified in meters.
*
* If altitude is not null then this will be a non-negative number.
* Otherwise this returns `null`.
*
* This corresponds to a 95% confidence level.
* @type Number
* @readonly
*/
altitudeAccuracy: null,
/**
* Read-only property representing the last retrieved
* direction of travel of the hosting device,
* specified in non-negative degrees between 0 and 359,
* counting clockwise relative to the true north.
*
* If speed is 0 (device is stationary), then this returns `NaN`.
* @type Number
* @readonly
*/
heading: null,
/**
* Read-only property representing the last retrieved
* current ground speed of the device, specified in meters per second.
*
* If this feature is unsupported by the device, this returns `null`.
*
* If the device is stationary, this returns 0,
* otherwise it returns a non-negative number.
* @type Number
* @readonly
*/
speed: null,
/**
* Read-only property representing when the last retrieved
* positioning information was acquired by the device.
* @type Date
* @readonly
*/
timestamp: null,
//PositionOptions interface
/**
* @cfg {Boolean} allowHighAccuracy
* When set to `true`, provide a hint that the application would like to receive
* the best possible results. This may result in slower response times or increased power consumption.
* The user might also deny this capability, or the device might not be able to provide more accurate
* results than if this option was set to `false`.
*/
allowHighAccuracy: false,
/**
* @cfg {Number} timeout
* The maximum number of milliseconds allowed to elapse between a location update operation
* and the corresponding {@link #locationupdate} event being raised. If a location was not successfully
* acquired before the given timeout elapses (and no other internal errors have occurred in this interval),
* then a {@link #locationerror} event will be raised indicating a timeout as the cause.
*
* Note that the time that is spent obtaining the user permission is **not** included in the period
* covered by the timeout. The `timeout` attribute only applies to the location acquisition operation.
*
* In the case of calling `updateLocation`, the {@link #locationerror} event will be raised only once.
*
* If {@link #autoUpdate} is set to `true`, the {@link #locationerror} event could be raised repeatedly.
* The first timeout is relative to the moment {@link #autoUpdate} was set to `true`
* (or this {@link Ext.util.Geolocation} was initialized with the {@link #autoUpdate} config option set to `true`).
* Subsequent timeouts are relative to the moment when the device determines that it's position has changed.
*/
timeout: Infinity,
/**
* @cfg {Number} maximumAge
* This option indicates that the application is willing to accept cached location information whose age
* is no greater than the specified time in milliseconds. If `maximumAge` is set to 0, an attempt to retrieve
* new location information is made immediately.
*
* Setting the `maximumAge` to Infinity returns a cached position regardless of its age.
*
* If the device does not have cached location information available whose age is no
* greater than the specified `maximumAge`, then it must acquire new location information.
*
* For example, if location information no older than 10 minutes is required, set this property to 600000.
*/
maximumAge: 0,
// @private
provider : undefined
},
updateMaximumAge: function() {
if (this.watchOperation) {
this.updateWatchOperation();
}
},
updateTimeout: function() {
if (this.watchOperation) {
this.updateWatchOperation();
}
},
updateAllowHighAccuracy: function() {
if (this.watchOperation) {
this.updateWatchOperation();
}
},
applyProvider: function(config) {
if (Ext.feature.has.Geolocation) {
if (!config) {
if (navigator && navigator.geolocation) {
config = navigator.geolocation;
}
else if (window.google) {
config = google.gears.factory.create('beta.geolocation');
}
}
}
else {
this.fireEvent('locationerror', this, false, false, true, 'This device does not support Geolocation.');
}
return config;
},
updateAutoUpdate: function(newAutoUpdate, oldAutoUpdate) {
var me = this,
provider = me.getProvider();
if (oldAutoUpdate && provider) {
clearInterval(me.watchOperationId);
me.watchOperationId = null;
}
if (newAutoUpdate) {
if (!provider) {
me.fireEvent('locationerror', me, false, false, true, null);
return;
}
try {
me.updateWatchOperation();
}
catch(e) {
me.fireEvent('locationerror', me, false, false, true, e.message);
}
}
},
// @private
updateWatchOperation: function() {
var me = this,
provider = me.getProvider();
// The native watchPosition method is currently broken in iOS5...
if (me.watchOperationId) {
clearInterval(me.watchOperationId);
}
function pollPosition() {
provider.getCurrentPosition(
Ext.bind(me.fireUpdate, me),
Ext.bind(me.fireError, me),
me.parseOptions()
);
}
pollPosition();
me.watchOperationId = setInterval(pollPosition, this.getFrequency());
},
/**
* Executes a onetime location update operation,
* raising either a {@link #locationupdate} or {@link #locationerror} event.
*
* Does not interfere with or restart ongoing location monitoring.
* @param {Function} callback
* A callback method to be called when the location retrieval has been completed.
*
* Will be called on both success and failure.
*
* The method will be passed one parameter, {@link Ext.util.Geolocation} (**this** reference),
* set to `null` on failure.
*
* geo.updateLocation(function (geo) {
* alert('Latitude: ' + (geo !== null ? geo.latitude : 'failed'));
* });
*
* @param {Object} scope (optional) The scope (**this** reference) in which the handler function is executed.
*
* **If omitted, defaults to the object which fired the event.**
* <!--positonOptions undocumented param, see W3C spec-->
*/
updateLocation: function(callback, scope, positionOptions) {
var me = this,
provider = me.getProvider();
var failFunction = function(message, error) {
if (error) {
me.fireError(error);
}
else {
me.fireEvent('locationerror', me, false, false, true, message);
}
if (callback) {
callback.call(scope || me, null, me); //last parameter for legacy purposes
}
};
if (!provider) {
failFunction(null);
return;
}
try {
provider.getCurrentPosition(
//success callback
function(position) {
me.fireUpdate(position);
if (callback) {
callback.call(scope || me, me, me); //last parameter for legacy purposes
}
},
//error callback
function(error) {
failFunction(null, error);
},
positionOptions || me.parseOptions()
);
}
catch(e) {
failFunction(e.message);
}
},
// @private
fireUpdate: function(position) {
var me = this,
coords = position.coords;
this.position = position;
me.setConfig({
timestamp: position.timestamp,
latitude: coords.latitude,
longitude: coords.longitude,
accuracy: coords.accuracy,
altitude: coords.altitude,
altitudeAccuracy: coords.altitudeAccuracy,
heading: coords.heading,
speed: coords.speed
});
me.fireEvent('locationupdate', me);
},
// @private
fireError: function(error) {
var errorCode = error.code;
this.fireEvent('locationerror', this,
errorCode == error.TIMEOUT,
errorCode == error.PERMISSION_DENIED,
errorCode == error.POSITION_UNAVAILABLE,
error.message == undefined ? null : error.message
);
},
// @private
parseOptions: function() {
var timeout = this.getTimeout(),
ret = {
maximumAge: this.getMaximumAge(),
enableHighAccuracy: this.getAllowHighAccuracy()
};
//Google doesn't like Infinity
if (timeout !== Infinity) {
ret.timeout = timeout;
}
return ret;
},
destroy : function() {
this.setAutoUpdate(false);
}
});
/**
* Wraps a Google Map in an Ext.Component using the [Google Maps API](http://code.google.com/apis/maps/documentation/v3/introduction.html).
*
* To use this component you must include an additional JavaScript file from Google:
*
* <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
*
* ## Example
*
* Ext.Viewport.add({
* xtype: 'map',
* useCurrentLocation: true
* });
*
* @aside example maps
*/
Ext.define('Ext.Map', {
extend: 'Ext.Container',
xtype : 'map',
requires: ['Ext.util.Geolocation'],
isMap: true,
config: {
/**
* @event maprender
* Fired when Map initially rendered.
* @param {Ext.Map} this
* @param {google.maps.Map} map The rendered google.map.Map instance
*/
/**
* @event centerchange
* Fired when map is panned around.
* @param {Ext.Map} this
* @param {google.maps.Map} map The rendered google.map.Map instance
* @param {google.maps.LatLng} center The current LatLng center of the map
*/
/**
* @event typechange
* Fired when display type of the map changes.
* @param {Ext.Map} this
* @param {google.maps.Map} map The rendered google.map.Map instance
* @param {Number} mapType The current display type of the map
*/
/**
* @event zoomchange
* Fired when map is zoomed.
* @param {Ext.Map} this
* @param {google.maps.Map} map The rendered google.map.Map instance
* @param {Number} zoomLevel The current zoom level of the map
*/
/**
* @cfg {String} baseCls
* The base CSS class to apply to the Map's element
* @accessor
*/
baseCls: Ext.baseCSSPrefix + 'map',
/**
* @cfg {Boolean/Ext.util.Geolocation} useCurrentLocation
* Pass in true to center the map based on the geolocation coordinates or pass a
* {@link Ext.util.Geolocation GeoLocation} config to have more control over your GeoLocation options
* @accessor
*/
useCurrentLocation: false,
/**
* @cfg {google.maps.Map} map
* The wrapped map.
* @accessor
*/
map: null,
/**
* @cfg {Ext.util.Geolocation} geo
* Geolocation provider for the map.
* @accessor
*/
geo: null,
/**
* @cfg {Object} mapOptions
* MapOptions as specified by the Google Documentation:
* [http://code.google.com/apis/maps/documentation/v3/reference.html](http://code.google.com/apis/maps/documentation/v3/reference.html)
* @accessor
*/
mapOptions: {}
},
constructor: function() {
this.callParent(arguments);
// this.element.setVisibilityMode(Ext.Element.OFFSETS);
if (!(window.google || {}).maps) {
this.setHtml('Google Maps API is required');
}
},
initialize: function() {
this.callParent();
this.on({
painted: 'doResize',
scope: this
});
this.innerElement.on('touchstart', 'onTouchStart', this);
},
getElementConfig: function() {
return {
reference: 'element',
className: 'x-container',
children: [{
reference: 'innerElement',
className: 'x-inner',
children: [{
reference: 'mapContainer',
className: Ext.baseCSSPrefix + 'map-container'
}]
}]
};
},
onTouchStart: function(e) {
e.makeUnpreventable();
},
applyMapOptions: function(options) {
return Ext.merge({}, this.options, options);
},
updateMapOptions: function(newOptions) {
var me = this,
gm = (window.google || {}).maps,
map = this.getMap();
if (gm && map) {
map.setOptions(newOptions);
}
if (newOptions.center && !me.isPainted()) {
me.un('painted', 'setMapCenter', this);
me.on('painted', 'setMapCenter', this, { delay: 150, single: true, args: [newOptions.center] });
}
},
getMapOptions: function() {
return Ext.merge({}, this.options || this.getInitialConfig('mapOptions'));
},
updateUseCurrentLocation: function(useCurrentLocation) {
this.setGeo(useCurrentLocation);
if (!useCurrentLocation) {
this.renderMap();
}
},
applyGeo: function(config) {
return Ext.factory(config, Ext.util.Geolocation, this.getGeo());
},
updateGeo: function(newGeo, oldGeo) {
var events = {
locationupdate : 'onGeoUpdate',
locationerror : 'onGeoError',
scope : this
};
if (oldGeo) {
oldGeo.un(events);
}
if (newGeo) {
newGeo.on(events);
newGeo.updateLocation();
}
},
doResize: function() {
var gm = (window.google || {}).maps,
map = this.getMap();
if (gm && map) {
gm.event.trigger(map, "resize");
}
},
// @private
renderMap: function() {
var me = this,
gm = (window.google || {}).maps,
element = me.mapContainer,
mapOptions = me.getMapOptions(),
map = me.getMap(),
event;
if (gm) {
if (Ext.os.is.iPad) {
Ext.merge({
navigationControlOptions: {
style: gm.NavigationControlStyle.ZOOM_PAN
}
}, mapOptions);
}
mapOptions = Ext.merge({
zoom: 12,
mapTypeId: gm.MapTypeId.ROADMAP
}, mapOptions);
// This is done separately from the above merge so we don't have to instantiate
// a new LatLng if we don't need to
if (!mapOptions.hasOwnProperty('center')) {
mapOptions.center = new gm.LatLng(37.381592, -122.135672); // Palo Alto
}
if (element.dom.firstChild) {
Ext.fly(element.dom.firstChild).destroy();
}
if (map) {
gm.event.clearInstanceListeners(map);
}
me.setMap(new gm.Map(element.dom, mapOptions));
map = me.getMap();
//Track zoomLevel and mapType changes
event = gm.event;
event.addListener(map, 'zoom_changed', Ext.bind(me.onZoomChange, me));
event.addListener(map, 'maptypeid_changed', Ext.bind(me.onTypeChange, me));
event.addListener(map, 'center_changed', Ext.bind(me.onCenterChange, me));
me.fireEvent('maprender', me, map);
}
},
// @private
onGeoUpdate: function(geo) {
if (geo) {
this.setMapCenter(new google.maps.LatLng(geo.getLatitude(), geo.getLongitude()));
}
},
// @private
onGeoError: Ext.emptyFn,
/**
* Moves the map center to the designated coordinates hash of the form:
*
* { latitude: 37.381592, longitude: -122.135672 }
*
* or a google.maps.LatLng object representing to the target location.
*
* @param {Object/google.maps.LatLng} coordinates Object representing the desired Latitude and
* longitude upon which to center the map.
*/
setMapCenter: function(coordinates) {
var me = this,
map = me.getMap(),
gm = (window.google || {}).maps;
if (gm) {
if (!me.isPainted()) {
me.un('painted', 'setMapCenter', this);
me.on('painted', 'setMapCenter', this, { delay: 150, single: true, args: [coordinates] });
return;
}
coordinates = coordinates || new gm.LatLng(37.381592, -122.135672);
if (coordinates && !(coordinates instanceof gm.LatLng) && 'longitude' in coordinates) {
coordinates = new gm.LatLng(coordinates.latitude, coordinates.longitude);
}
if (!map) {
me.renderMap();
map = me.getMap();
}
if (map && coordinates instanceof gm.LatLng) {
map.panTo(coordinates);
}
else {
this.options = Ext.apply(this.getMapOptions(), {
center: coordinates
});
}
}
},
// @private
onZoomChange : function() {
var mapOptions = this.getMapOptions(),
map = this.getMap(),
zoom;
zoom = (map && map.getZoom) ? map.getZoom() : mapOptions.zoom || 10;
this.options = Ext.apply(mapOptions, {
zoom: zoom
});
this.fireEvent('zoomchange', this, map, zoom);
},
// @private
onTypeChange : function() {
var mapOptions = this.getMapOptions(),
map = this.getMap(),
mapTypeId;
mapTypeId = (map && map.getMapTypeId) ? map.getMapTypeId() : mapOptions.mapTypeId;
this.options = Ext.apply(mapOptions, {
mapTypeId: mapTypeId
});
this.fireEvent('typechange', this, map, mapTypeId);
},
// @private
onCenterChange: function() {
var mapOptions = this.getMapOptions(),
map = this.getMap(),
center;
center = (map && map.getCenter) ? map.getCenter() : mapOptions.center;
this.options = Ext.apply(mapOptions, {
center: center
});
this.fireEvent('centerchange', this, map, center);
},
// @private
destroy: function() {
Ext.destroy(this.getGeo());
var map = this.getMap();
if (map && (window.google || {}).maps) {
google.maps.event.clearInstanceListeners(map);
}
this.callParent();
}
}, function() {
});
/**
* {@link Ext.Title} is used for the {@link Ext.Toolbar#title} configuration in the {@link Ext.Toolbar} component.
* @private
*/
Ext.define('Ext.Title', {
extend: 'Ext.Component',
xtype: 'title',
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: 'x-title',
/**
* @cfg {String} title The title text
*/
title: ''
},
// @private
updateTitle: function(newTitle) {
this.setHtml(newTitle);
}
});
/**
The {@link Ext.Spacer} component is generally used to put space between items in {@link Ext.Toolbar} components.
## Examples
By default the {@link #flex} configuration is set to 1:
@example miniphone preview
Ext.create('Ext.Container', {
fullscreen: true,
items: [
{
xtype : 'toolbar',
docked: 'top',
items: [
{
xtype: 'button',
text : 'Button One'
},
{
xtype: 'spacer'
},
{
xtype: 'button',
text : 'Button Two'
}
]
}
]
});
Alternatively you can just set the {@link #width} configuration which will get the {@link Ext.Spacer} a fixed width:
@example preview
Ext.create('Ext.Container', {
fullscreen: true,
layout: {
type: 'vbox',
pack: 'center',
align: 'stretch'
},
items: [
{
xtype : 'toolbar',
docked: 'top',
items: [
{
xtype: 'button',
text : 'Button One'
},
{
xtype: 'spacer',
width: 50
},
{
xtype: 'button',
text : 'Button Two'
}
]
},
{
xtype: 'container',
items: [
{
xtype: 'button',
text : 'Change Ext.Spacer width',
handler: function() {
//get the spacer using ComponentQuery
var spacer = Ext.ComponentQuery.query('spacer')[0],
from = 10,
to = 250;
//set the width to a random number
spacer.setWidth(Math.floor(Math.random() * (to - from + 1) + from));
}
}
]
}
]
});
You can also insert multiple {@link Ext.Spacer}'s:
@example preview
Ext.create('Ext.Container', {
fullscreen: true,
items: [
{
xtype : 'toolbar',
docked: 'top',
items: [
{
xtype: 'button',
text : 'Button One'
},
{
xtype: 'spacer'
},
{
xtype: 'button',
text : 'Button Two'
},
{
xtype: 'spacer',
width: 20
},
{
xtype: 'button',
text : 'Button Three'
}
]
}
]
});
*/
Ext.define('Ext.Spacer', {
extend: 'Ext.Component',
alias : 'widget.spacer',
config: {
/**
* @cfg {Number} flex
* The flex value of this spacer. This defaults to 1, if no width has been set.
* @accessor
*/
/**
* @cfg {Number} width
* The width of this spacer. If this is set, the value of {@link #flex} will be ignored.
* @accessor
*/
},
// @private
constructor: function(config) {
config = config || {};
if (!config.width) {
config.flex = 1;
}
this.callParent([config]);
}
});
/**
* @aside video tabs-toolbars
*
* {@link Ext.Toolbar}s are most commonly used as docked items as within a {@link Ext.Container}. They can be docked either `top` or `bottom` using the {@link #docked} configuration.
*
* They allow you to insert items (normally {@link Ext.Button buttons}) and also add a {@link #title}.
*
* The {@link #defaultType} of {@link Ext.Toolbar} is {@link Ext.Button}.
*
* You can alternatively use {@link Ext.TitleBar} if you want the title to automatically adjust the size of its items.
*
* ## Examples
*
* @example miniphone preview
* Ext.create('Ext.Container', {
* fullscreen: true,
* layout: {
* type: 'vbox',
* pack: 'center'
* },
* items: [
* {
* xtype : 'toolbar',
* docked: 'top',
* title: 'My Toolbar'
* },
* {
* xtype: 'container',
* defaults: {
* xtype: 'button',
* margin: '10 10 0 10'
* },
* items: [
* {
* text: 'Toggle docked',
* handler: function() {
* var toolbar = Ext.ComponentQuery.query('toolbar')[0],
* newDocked = (toolbar.getDocked() === 'top') ? 'bottom' : 'top';
*
* toolbar.setDocked(newDocked);
* }
* },
* {
* text: 'Toggle UI',
* handler: function() {
* var toolbar = Ext.ComponentQuery.query('toolbar')[0],
* newUi = (toolbar.getUi() === 'light') ? 'dark' : 'light';
*
* toolbar.setUi(newUi);
* }
* },
* {
* text: 'Change title',
* handler: function() {
* var toolbar = Ext.ComponentQuery.query('toolbar')[0],
* titles = ['My Toolbar', 'Ext.Toolbar', 'Configurations are awesome!', 'Beautiful.'],
//internally, the title configuration gets converted into a {@link Ext.Title} component,
//so you must get the title configuration of that component
* title = toolbar.getTitle().getTitle(),
* newTitle = titles[titles.indexOf(title) + 1] || titles[0];
*
* toolbar.setTitle(newTitle);
* }
* }
* ]
* }
* ]
* });
*
* ## Notes
*
* You must use a HTML5 doctype for {@link #docked} `bottom` to work. To do this, simply add the following code to the HTML file:
*
* <!doctype html>
*
* So your index.html file should look a little like this:
*
* <!doctype html>
* <html>
* <head>
* <title>MY application title</title>
* ...
*
*/
Ext.define('Ext.Toolbar', {
extend: 'Ext.Container',
xtype : 'toolbar',
requires: [
'Ext.Button',
'Ext.Title',
'Ext.Spacer',
'Ext.layout.HBox'
],
// @private
isToolbar: true,
config: {
/**
* @cfg baseCls
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'toolbar',
/**
* @cfg {String} ui
* The ui for this {@link Ext.Toolbar}. Either 'light' or 'dark'. You can create more UIs by using using the CSS Mixin {@link #sencha-toolbar-ui}
* @accessor
*/
ui: 'dark',
/**
* @cfg {String/Ext.Title} title
* The title of the toolbar.
* @accessor
*/
title: null,
/**
* @cfg {String} defaultType
* The default xtype to create.
* @accessor
*/
defaultType: 'button',
/**
* @cfg {String} docked
* The docked position for this {@link Ext.Toolbar}.
* If you specify `left` or `right`, the {@link #layout} configuration will automatically change to a `vbox`. It's also
* recommended to adjust the {@link #width} of the toolbar if you do this.
* @accessor
*/
/**
* @cfg {String} minHeight
* The minimum height height of the Toolbar.
* @accessor
*/
minHeight: '2.6em',
/**
* @cfg {Object/String} layout Configuration for this Container's layout. Example:
*
* Ext.create('Ext.Container', {
* layout: {
* type: 'hbox',
* align: 'middle'
* },
* items: [
* {
* xtype: 'panel',
* flex: 1,
* style: 'background-color: red;'
* },
* {
* xtype: 'panel',
* flex: 2,
* style: 'background-color: green'
* }
* ]
* });
*
* See the [layouts guide](#!/guides/layouts) for more information
*
* __Note:__ If you set the {@link #docked} configuration to `left` or `right`, the default layout will change from the
* `hbox` to a `vbox`.
*
* @accessor
*/
layout: {
type: 'hbox',
align: 'center'
}
},
constructor: function(config) {
config = config || {};
if (config.docked == "left" || config.docked == "right") {
config.layout = {
type: 'vbox',
align: 'stretch'
};
}
this.callParent([config]);
},
// @private
applyTitle: function(title) {
if (typeof title == 'string') {
title = {
title: title,
centered: true
};
}
return Ext.factory(title, Ext.Title, this.getTitle());
},
// @private
updateTitle: function(newTitle, oldTitle) {
if (newTitle) {
this.add(newTitle);
}
if (oldTitle) {
oldTitle.destroy();
}
},
/**
* Shows the title, if it exists.
*/
showTitle: function() {
var title = this.getTitle();
if (title) {
title.show();
}
},
/**
* Hides the title, if it exists.
*/
hideTitle: function() {
var title = this.getTitle();
if (title) {
title.hide();
}
}
/**
* Returns an {@link Ext.Title} component.
* @member Ext.Toolbar
* @method getTitle
* @return {Ext.Title}
*/
/**
* Use this to update the {@link #title} configuration.
* @member Ext.Toolbar
* @method setTitle
* @param {String/Ext.Title} title You can either pass a String, or a config/instance of {@link Ext.Title}.
*/
}, function() {
});
/**
* @private
*/
Ext.define('Ext.field.Input', {
extend: 'Ext.Component',
xtype : 'input',
/**
* @event clearicontap
* Fires whenever the clear icon is tapped.
* @param {Ext.field.Input} this
* @param {Ext.EventObject} e The event object
*/
/**
* @event masktap
* @preventable doMaskTap
* Fires whenever a mask is tapped.
* @param {Ext.field.Input} this
* @param {Ext.EventObject} e The event object.
*/
/**
* @event focus
* @preventable doFocus
* Fires whenever the input get focus.
* @param {Ext.EventObject} e The event object.
*/
/**
* @event blur
* @preventable doBlur
* Fires whenever the input loses focus.
* @param {Ext.EventObject} e The event object.
*/
/**
* @event click
* Fires whenever the input is clicked.
* @param {Ext.EventObject} e The event object.
*/
/**
* @event keyup
* Fires whenever keyup is detected.
* @param {Ext.EventObject} e The event object.
*/
/**
* @event paste
* Fires whenever paste is detected.
* @param {Ext.EventObject} e The event object.
*/
/**
* @event mousedown
* Fires whenever the input has a mousedown occur.
* @param {Ext.EventObject} e The event object.
*/
/**
* @property {String} tag The el tag.
* @private
*/
tag: 'input',
cachedConfig: {
/**
* @cfg {String} cls The `className` to be applied to this input.
* @accessor
*/
cls: Ext.baseCSSPrefix + 'form-field',
/**
* @cfg {String} focusCls The CSS class to use when the field receives focus.
* @accessor
*/
focusCls: Ext.baseCSSPrefix + 'field-focus',
// @private
maskCls: Ext.baseCSSPrefix + 'field-mask',
/**
* @cfg {String/Boolean} useMask
* `true` to use a mask on this field, or `auto` to automatically select when you should use it.
* @private
* @accessor
*/
useMask: 'auto',
/**
* @cfg {String} type The type attribute for input fields -- e.g. radio, text, password, file (defaults
* to 'text'). The types 'file' and 'password' must be used to render those field types currently -- there are
* no separate Ext components for those.
* @accessor
*/
type: 'text',
/**
* @cfg {Boolean} checked `true` if the checkbox should render initially checked.
* @accessor
*/
checked: false
},
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'field-input',
/**
* @cfg {String} name The field's HTML name attribute.
* __Note:__ This property must be set if this field is to be automatically included with
* {@link Ext.form.Panel#method-submit form submit()}.
* @accessor
*/
name: null,
/**
* @cfg {Mixed} value A value to initialize this field with.
* @accessor
*/
value: null,
/**
* @property {Boolean} `true` if the field currently has focus.
* @accessor
*/
isFocused: false,
/**
* @cfg {Number} tabIndex The `tabIndex` for this field.
*
* __Note:__ This only applies to fields that are rendered, not those which are built via `applyTo`.
* @accessor
*/
tabIndex: null,
/**
* @cfg {String} placeHolder A string value displayed in the input (if supported) when the control is empty.
* @accessor
*/
placeHolder: null,
/**
* @cfg {Number} [minValue=undefined] The minimum value that this Number field can accept (defaults to `undefined`, e.g. no minimum).
* @accessor
*/
minValue: null,
/**
* @cfg {Number} [maxValue=undefined] The maximum value that this Number field can accept (defaults to `undefined`, e.g. no maximum).
* @accessor
*/
maxValue: null,
/**
* @cfg {Number} [stepValue=undefined] The amount by which the field is incremented or decremented each time the spinner is tapped.
* Defaults to `undefined`, which means that the field goes up or down by 1 each time the spinner is tapped.
* @accessor
*/
stepValue: null,
/**
* @cfg {Number} [maxLength=0] The maximum number of permitted input characters.
* @accessor
*/
maxLength: null,
/**
* @cfg {Boolean} [autoComplete=undefined]
* `true` to set the field's DOM element `autocomplete` attribute to `"on"`, `false` to set to `"off"`. Defaults to `undefined`, leaving the attribute unset.
* @accessor
*/
autoComplete: null,
/**
* @cfg {Boolean} [autoCapitalize=undefined]
* `true` to set the field's DOM element `autocapitalize` attribute to `"on"`, `false` to set to `"off"`. Defaults to `undefined`, leaving the attribute unset
* @accessor
*/
autoCapitalize: null,
/**
* `true` to set the field DOM element `autocorrect` attribute to `"on"`, `false` to set to `"off"`. Defaults to `undefined`, leaving the attribute unset.
* @cfg {Boolean} autoCorrect
* @accessor
*/
autoCorrect: null,
/**
* @cfg {Boolean} [readOnly=undefined]
* `true` to set the field DOM element `readonly` attribute to `"true"`. Defaults to `undefined`, leaving the attribute unset.
* @accessor
*/
readOnly: null,
/**
* @cfg {Number} [maxRows=undefined]
* Sets the field DOM element `maxRows` attribute. Defaults to `undefined`, leaving the attribute unset.
* @accessor
*/
maxRows: null,
/**
* @cfg {String} pattern The value for the HTML5 `pattern` attribute.
* You can use this to change which keyboard layout will be used.
*
* Ext.define('Ux.field.Pattern', {
* extend : 'Ext.field.Text',
* xtype : 'patternfield',
*
* config : {
* component : {
* pattern : '[0-9]*'
* }
* }
* });
*
* Even though it extends {@link Ext.field.Text}, it will display the number keyboard.
*
* @accessor
*/
pattern: null,
/**
* @cfg {Boolean} [disabled=false] `true` to disable the field.
*
* Be aware that conformant with the [HTML specification](http://www.w3.org/TR/html401/interact/forms.html),
* disabled Fields will not be {@link Ext.form.Panel#method-submit submitted}.
* @accessor
*/
/**
* @cfg {Mixed} startValue
* The value that the Field had at the time it was last focused. This is the value that is passed
* to the {@link Ext.field.Text#change} event which is fired if the value has been changed when the Field is blurred.
*
* __This will be `undefined` until the Field has been visited.__ Compare {@link #originalValue}.
* @accessor
*/
startValue: false
},
/**
* @cfg {String/Number} originalValue The original value when the input is rendered.
* @private
*/
// @private
getTemplate: function() {
var items = [
{
reference: 'input',
tag: this.tag
},
{
reference: 'clearIcon',
cls: 'x-clear-icon'
}
];
items.push({
reference: 'mask',
classList: [this.config.maskCls]
});
return items;
},
initElement: function() {
var me = this;
me.callParent();
me.input.on({
scope: me,
keyup: 'onKeyUp',
keydown: 'onKeyDown',
focus: 'onFocus',
blur: 'onBlur',
input: 'onInput',
paste: 'onPaste'
});
me.mask.on({
tap: 'onMaskTap',
scope: me
});
if (me.clearIcon) {
me.clearIcon.on({
tap: 'onClearIconTap',
scope: me
});
}
},
applyUseMask: function(useMask) {
if (useMask === 'auto') {
useMask = Ext.os.is.iOS && Ext.os.version.lt('5');
}
return Boolean(useMask);
},
/**
* Updates the useMask configuration
*/
updateUseMask: function(newUseMask) {
this.mask[newUseMask ? 'show' : 'hide']();
},
updatePattern : function (pattern) {
this.updateFieldAttribute('pattern', pattern);
},
/**
* Helper method to update a specified attribute on the `fieldEl`, or remove the attribute all together.
* @private
*/
updateFieldAttribute: function(attribute, newValue) {
var input = this.input;
if (newValue) {
input.dom.setAttribute(attribute, newValue);
} else {
input.dom.removeAttribute(attribute);
}
},
/**
* Updates the {@link #cls} configuration.
*/
updateCls: function(newCls, oldCls) {
this.input.addCls(Ext.baseCSSPrefix + 'input-el');
this.input.replaceCls(oldCls, newCls);
},
/**
* Updates the type attribute with the {@link #type} configuration.
* @private
*/
updateType: function(newType, oldType) {
var prefix = Ext.baseCSSPrefix + 'input-';
this.input.replaceCls(prefix + oldType, prefix + newType);
this.updateFieldAttribute('type', newType);
},
/**
* Updates the name attribute with the {@link #name} configuration.
* @private
*/
updateName: function(newName) {
this.updateFieldAttribute('name', newName);
},
/**
* Returns the field data value.
* @return {Mixed} value The field value.
*/
getValue: function() {
var input = this.input;
if (input) {
this._value = input.dom.value;
}
return this._value;
},
// @private
applyValue: function(value) {
return (Ext.isEmpty(value)) ? '' : value;
},
/**
* Updates the {@link #value} configuration.
* @private
*/
updateValue: function(newValue) {
var input = this.input;
if (input) {
input.dom.value = newValue;
}
},
setValue: function(newValue) {
var oldValue = this._value;
this.updateValue(this.applyValue(newValue));
newValue = this.getValue();
if (String(newValue) != String(oldValue) && this.initialized) {
this.onChange(this, newValue, oldValue);
}
return this;
},
//<debug>
// @private
applyTabIndex: function(tabIndex) {
if (tabIndex !== null && typeof tabIndex != 'number') {
throw new Error("Ext.field.Field: [applyTabIndex] trying to pass a value which is not a number");
}
return tabIndex;
},
//</debug>
/**
* Updates the tabIndex attribute with the {@link #tabIndex} configuration
* @private
*/
updateTabIndex: function(newTabIndex) {
this.updateFieldAttribute('tabIndex', newTabIndex);
},
// @private
testAutoFn: function(value) {
return [true, 'on'].indexOf(value) !== -1;
},
//<debug>
applyMaxLength: function(maxLength) {
if (maxLength !== null && typeof maxLength != 'number') {
throw new Error("Ext.field.Text: [applyMaxLength] trying to pass a value which is not a number");
}
return maxLength;
},
//</debug>
/**
* Updates the `maxlength` attribute with the {@link #maxLength} configuration.
* @private
*/
updateMaxLength: function(newMaxLength) {
this.updateFieldAttribute('maxlength', newMaxLength);
},
/**
* Updates the `placeholder` attribute with the {@link #placeHolder} configuration.
* @private
*/
updatePlaceHolder: function(newPlaceHolder) {
this.updateFieldAttribute('placeholder', newPlaceHolder);
},
// @private
applyAutoComplete: function(autoComplete) {
return this.testAutoFn(autoComplete);
},
/**
* Updates the `autocomplete` attribute with the {@link #autoComplete} configuration.
* @private
*/
updateAutoComplete: function(newAutoComplete) {
var value = newAutoComplete ? 'on' : 'off';
this.updateFieldAttribute('autocomplete', value);
},
// @private
applyAutoCapitalize: function(autoCapitalize) {
return this.testAutoFn(autoCapitalize);
},
/**
* Updates the `autocapitalize` attribute with the {@link #autoCapitalize} configuration.
* @private
*/
updateAutoCapitalize: function(newAutoCapitalize) {
var value = newAutoCapitalize ? 'on' : 'off';
this.updateFieldAttribute('autocapitalize', value);
},
// @private
applyAutoCorrect: function(autoCorrect) {
return this.testAutoFn(autoCorrect);
},
/**
* Updates the `autocorrect` attribute with the {@link #autoCorrect} configuration.
* @private
*/
updateAutoCorrect: function(newAutoCorrect) {
var value = newAutoCorrect ? 'on' : 'off';
this.updateFieldAttribute('autocorrect', value);
},
/**
* Updates the `min` attribute with the {@link #minValue} configuration.
* @private
*/
updateMinValue: function(newMinValue) {
this.updateFieldAttribute('min', newMinValue);
},
/**
* Updates the `max` attribute with the {@link #maxValue} configuration.
* @private
*/
updateMaxValue: function(newMaxValue) {
this.updateFieldAttribute('max', newMaxValue);
},
/**
* Updates the `step` attribute with the {@link #stepValue} configuration
* @private
*/
updateStepValue: function(newStepValue) {
this.updateFieldAttribute('step', newStepValue);
},
// @private
checkedRe: /^(true|1|on)/i,
/**
* Returns the `checked` value of this field
* @return {Mixed} value The field value
*/
getChecked: function() {
var el = this.input,
checked;
if (el) {
checked = el.dom.checked;
this._checked = checked;
}
return checked;
},
// @private
applyChecked: function(checked) {
return !!this.checkedRe.test(String(checked));
},
setChecked: function(newChecked) {
this.updateChecked(this.applyChecked(newChecked));
this._checked = newChecked;
},
/**
* Updates the `autocorrect` attribute with the {@link #autoCorrect} configuration
* @private
*/
updateChecked: function(newChecked) {
this.input.dom.checked = newChecked;
},
/**
* Updates the `readonly` attribute with the {@link #readOnly} configuration
* @private
*/
updateReadOnly: function(readOnly) {
this.updateFieldAttribute('readonly', readOnly);
},
//<debug>
// @private
applyMaxRows: function(maxRows) {
if (maxRows !== null && typeof maxRows !== 'number') {
throw new Error("Ext.field.Input: [applyMaxRows] trying to pass a value which is not a number");
}
return maxRows;
},
//</debug>
updateMaxRows: function(newRows) {
this.updateFieldAttribute('rows', newRows);
},
doSetDisabled: function(disabled) {
this.callParent(arguments);
this.input.dom.disabled = disabled;
if (!disabled) {
this.blur();
}
},
/**
* Returns `true` if the value of this Field has been changed from its original value.
* Will return `false` if the field is disabled or has not been rendered yet.
* @return {Boolean}
*/
isDirty: function() {
if (this.getDisabled()) {
return false;
}
return String(this.getValue()) !== String(this.originalValue);
},
/**
* Resets the current field value to the original value.
*/
reset: function() {
this.setValue(this.originalValue);
},
// @private
onMaskTap: function(e) {
this.fireAction('masktap', [this, e], 'doMaskTap');
},
// @private
doMaskTap: function(me, e) {
if (me.getDisabled()) {
return false;
}
me.maskCorrectionTimer = Ext.defer(me.showMask, 1000, me);
me.hideMask();
},
// @private
showMask: function(e) {
if (this.mask) {
this.mask.setStyle('display', 'block');
}
},
// @private
hideMask: function(e) {
if (this.mask) {
this.mask.setStyle('display', 'none');
}
},
/**
* Attempts to set the field as the active input focus.
* @return {Ext.field.Input} this
*/
focus: function() {
var me = this,
el = me.input;
if (el && el.dom.focus) {
el.dom.focus();
}
return me;
},
/**
* Attempts to forcefully blur input focus for the field.
* @return {Ext.field.Input} this
* @chainable
*/
blur: function() {
var me = this,
el = this.input;
if (el && el.dom.blur) {
el.dom.blur();
}
return me;
},
/**
* Attempts to forcefully select all the contents of the input field.
* @return {Ext.field.Input} this
* @chainable
*/
select: function() {
var me = this,
el = me.input;
if (el && el.dom.setSelectionRange) {
el.dom.setSelectionRange(0, 9999);
}
return me;
},
onFocus: function(e) {
this.fireAction('focus', [e], 'doFocus');
},
// @private
doFocus: function(e) {
var me = this;
if (me.mask) {
if (me.maskCorrectionTimer) {
clearTimeout(me.maskCorrectionTimer);
}
me.hideMask();
}
if (!me.getIsFocused()) {
me.setIsFocused(true);
me.setStartValue(me.getValue());
}
},
onBlur: function(e) {
this.fireAction('blur', [e], 'doBlur');
},
// @private
doBlur: function(e) {
var me = this,
value = me.getValue(),
startValue = me.getStartValue();
me.setIsFocused(false);
if (String(value) != String(startValue)) {
me.onChange(me, value, startValue);
}
me.showMask();
},
// @private
onClearIconTap: function(e) {
this.fireEvent('clearicontap', this, e);
//focus the field after cleartap happens, but only on android.
//this is to stop the keyboard from hiding. TOUCH-2064
if (Ext.os.is.Android) {
this.focus();
}
},
onClick: function(e) {
this.fireEvent('click', e);
},
onChange: function(me, value, startValue) {
this.fireEvent('change', me, value, startValue);
},
onPaste: function(e) {
this.fireEvent('paste', e);
},
onKeyUp: function(e) {
this.fireEvent('keyup', e);
},
onKeyDown: function() {
// tell the class to ignore the input event. this happens when we want to listen to the field change
// when the input autocompletes
this.ignoreInput = true;
},
onInput: function(e) {
var me = this;
// if we should ignore input, stop now.
if (me.ignoreInput) {
me.ignoreInput = false;
return;
}
// set a timeout for 10ms to check if we want to stop the input event.
// if not, then continue with the event (keyup)
setTimeout(function() {
if (!me.ignoreInput) {
me.fireEvent('keyup', e);
me.ignoreInput = false;
}
}, 10);
},
onMouseDown: function(e) {
this.fireEvent('mousedown', e);
}
});
/**
* @aside guide forms
*
* Field is the base class for all form fields used in Sencha Touch. It provides a lot of shared functionality to all
* field subclasses (for example labels, simple validation, {@link #clearIcon clearing} and tab index management), but
* is rarely used directly. Instead, it is much more common to use one of the field subclasses:
*
* xtype Class
* ---------------------------------------
* textfield {@link Ext.field.Text}
* numberfield {@link Ext.field.Number}
* textareafield {@link Ext.field.TextArea}
* hiddenfield {@link Ext.field.Hidden}
* radiofield {@link Ext.field.Radio}
* checkboxfield {@link Ext.field.Checkbox}
* selectfield {@link Ext.field.Select}
* togglefield {@link Ext.field.Toggle}
* fieldset {@link Ext.form.FieldSet}
*
* Fields are normally used within the context of a form and/or fieldset. See the {@link Ext.form.Panel FormPanel}
* and {@link Ext.form.FieldSet FieldSet} docs for examples on how to put those together, or the list of links above
* for usage of individual field types. If you wish to create your own Field subclasses you can extend this class,
* though it is sometimes more useful to extend {@link Ext.field.Text} as this provides additional text entry
* functionality.
*/
Ext.define('Ext.field.Field', {
extend: 'Ext.Decorator',
alternateClassName: 'Ext.form.Field',
xtype: 'field',
requires: [
'Ext.field.Input'
],
/**
* Set to `true` on all Ext.field.Field subclasses. This is used by {@link Ext.form.Panel#getValues} to determine which
* components inside a form are fields.
* @property isField
* @type Boolean
*/
isField: true,
// @private
isFormField: true,
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'field',
/**
* The label of this field
* @cfg {String} label
* @accessor
*/
label: null,
/**
* @cfg {String} labelAlign The position to render the label relative to the field input.
* Available options are: 'top', 'left', 'bottom' and 'right'
* @accessor
*/
labelAlign: 'left',
/**
* @cfg {Number/String} labelWidth The width to make this field's label.
* @accessor
*/
labelWidth: '30%',
/**
* @cfg {Boolean} labelWrap `true` to allow the label to wrap. If set to `false`, the label will be truncated with
* an ellipsis.
* @accessor
*/
labelWrap: false,
/**
* @cfg {Boolean} clearIcon `true` to use a clear icon in this field.
* @accessor
*/
clearIcon: null,
/**
* @cfg {Boolean} required `true` to make this field required.
*
* __Note:__ this only causes a visual indication.
*
* Doesn't prevent user from submitting the form.
* @accessor
*/
required: false,
/**
* The label Element associated with this Field.
*
* __Note:__ Only available if a {@link #label} is specified.
* @type Ext.Element
* @property labelEl
* @deprecated 2.0
*/
/**
* @cfg {String} [inputType='text'] The type attribute for input fields -- e.g. radio, text, password, file.
* The types 'file' and 'password' must be used to render those field types currently -- there are
* no separate Ext components for those.
* @deprecated 2.0 Please use `input.type` instead.
* @accessor
*/
inputType: null,
/**
* @cfg {String} name The field's HTML name attribute.
*
* __Note:__ this property must be set if this field is to be automatically included with.
* {@link Ext.form.Panel#method-submit form submit()}.
* @accessor
*/
name: null,
/**
* @cfg {Mixed} value A value to initialize this field with.
* @accessor
*/
value: null,
/**
* @cfg {Number} tabIndex The `tabIndex` for this field. Note this only applies to fields that are rendered,
* not those which are built via `applyTo`.
* @accessor
*/
tabIndex: null
/**
* @cfg {Object} component The inner component for this field.
*/
/**
* @cfg {Boolean} fullscreen
* @hide
*/
},
cachedConfig: {
/**
* @cfg {String} labelCls Optional CSS class to add to the Label element.
* @accessor
*/
labelCls: null,
/**
* @cfg {String} requiredCls The `className` to be applied to this Field when the {@link #required} configuration is set to `true`.
* @accessor
*/
requiredCls: Ext.baseCSSPrefix + 'field-required',
/**
* @cfg {String} inputCls CSS class to add to the input element of this fields {@link #component}
*/
inputCls: null
},
/**
* @cfg {Boolean} isFocused
* `true` if this field is currently focused.
* @private
*/
getElementConfig: function() {
var prefix = Ext.baseCSSPrefix;
return {
reference: 'element',
className: 'x-container',
children: [
{
reference: 'label',
cls: prefix + 'form-label',
children: [{
reference: 'labelspan',
tag: 'span'
}]
},
{
reference: 'innerElement',
cls: prefix + 'component-outer'
}
]
};
},
// @private
updateLabel: function(newLabel, oldLabel) {
var renderElement = this.renderElement,
prefix = Ext.baseCSSPrefix;
if (newLabel) {
this.labelspan.setHtml(newLabel);
renderElement.addCls(prefix + 'field-labeled');
} else {
renderElement.removeCls(prefix + 'field-labeled');
}
},
// @private
updateLabelAlign: function(newLabelAlign, oldLabelAlign) {
var renderElement = this.renderElement,
prefix = Ext.baseCSSPrefix;
if (newLabelAlign) {
renderElement.addCls(prefix + 'label-align-' + newLabelAlign);
if (newLabelAlign == "top" || newLabelAlign == "bottom") {
this.label.setWidth('100%');
} else {
this.updateLabelWidth(this.getLabelWidth());
}
}
if (oldLabelAlign) {
renderElement.removeCls(prefix + 'label-align-' + oldLabelAlign);
}
},
// @private
updateLabelCls: function(newLabelCls, oldLabelCls) {
if (newLabelCls) {
this.label.addCls(newLabelCls);
}
if (oldLabelCls) {
this.label.removeCls(oldLabelCls);
}
},
// @private
updateLabelWidth: function(newLabelWidth) {
var labelAlign = this.getLabelAlign();
if (newLabelWidth) {
if (labelAlign == "top" || labelAlign == "bottom") {
this.label.setWidth('100%');
} else {
this.label.setWidth(newLabelWidth);
}
}
},
// @private
updateLabelWrap: function(newLabelWrap, oldLabelWrap) {
var cls = Ext.baseCSSPrefix + 'form-label-nowrap';
if (!newLabelWrap) {
this.addCls(cls);
} else {
this.removeCls(cls);
}
},
/**
* Updates the {@link #required} configuration.
* @private
*/
updateRequired: function(newRequired) {
this.renderElement[newRequired ? 'addCls' : 'removeCls'](this.getRequiredCls());
},
/**
* Updates the {@link #required} configuration
* @private
*/
updateRequiredCls: function(newRequiredCls, oldRequiredCls) {
if (this.getRequired()) {
this.renderElement.replaceCls(oldRequiredCls, newRequiredCls);
}
},
// @private
initialize: function() {
var me = this;
me.callParent();
me.doInitValue();
},
/**
* @private
*/
doInitValue: function() {
/**
* @property {Mixed} originalValue
* The original value of the field as configured in the {@link #value} configuration.
* setting is `true`.
*/
this.originalValue = this.getInitialConfig().value;
},
/**
* Resets the current field value back to the original value on this field when it was created.
*
* // This will create a field with an original value
* var field = Ext.Viewport.add({
* xtype: 'textfield',
* value: 'first value'
* });
*
* // Update the value
* field.setValue('new value');
*
* // Now you can reset it back to the `first value`
* field.reset();
*
* @return {Ext.field.Field} this
*/
reset: function() {
this.setValue(this.originalValue);
return this;
},
/**
* Returns `true` if the value of this Field has been changed from its {@link #originalValue}.
* Will return `false` if the field is disabled or has not been rendered yet.
*
* @return {Boolean} `true` if this field has been changed from its original value (and
* is not disabled), `false` otherwise.
*/
isDirty: function() {
return false;
}
}, function() {
});
/**
* @aside guide forms
*
* The text field is the basis for most of the input fields in Sencha Touch. It provides a baseline of shared
* functionality such as input validation, standard events, state management and look and feel. Typically we create
* text fields inside a form, like this:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'Enter your name',
* items: [
* {
* xtype: 'textfield',
* label: 'First Name',
* name: 'firstName'
* },
* {
* xtype: 'textfield',
* label: 'Last Name',
* name: 'lastName'
* }
* ]
* }
* ]
* });
*
* This creates two text fields inside a form. Text Fields can also be created outside of a Form, like this:
*
* Ext.create('Ext.field.Text', {
* label: 'Your Name',
* value: 'Ed Spencer'
* });
*
* ## Configuring
*
* Text field offers several configuration options, including {@link #placeHolder}, {@link #maxLength},
* {@link #autoComplete}, {@link #autoCapitalize} and {@link #autoCorrect}. For example, here is how we would configure
* a text field to have a maximum length of 10 characters, with placeholder text that disappears when the field is
* focused:
*
* Ext.create('Ext.field.Text', {
* label: 'Username',
* maxLength: 10,
* placeHolder: 'Enter your username'
* });
*
* The autoComplete, autoCapitalize and autoCorrect configs simply set those attributes on the text field and allow the
* native browser to provide those capabilities. For example, to enable auto complete and auto correct, simply
* configure your text field like this:
*
* Ext.create('Ext.field.Text', {
* label: 'Username',
* autoComplete: true,
* autoCorrect: true
* });
*
* These configurations will be picked up by the native browser, which will enable the options at the OS level.
*
* Text field inherits from {@link Ext.field.Field}, which is the base class for all fields in Sencha Touch and provides
* a lot of shared functionality for all fields, including setting values, clearing and basic validation. See the
* {@link Ext.field.Field} documentation to see how to leverage its capabilities.
*/
Ext.define('Ext.field.Text', {
extend: 'Ext.field.Field',
xtype: 'textfield',
alternateClassName: 'Ext.form.Text',
/**
* @event focus
* Fires when this field receives input focus
* @param {Ext.field.Text} this This field
* @param {Ext.event.Event} e
*/
/**
* @event blur
* Fires when this field loses input focus
* @param {Ext.field.Text} this This field
* @param {Ext.event.Event} e
*/
/**
* @event paste
* Fires when this field is pasted.
* @param {Ext.field.Text} this This field
* @param {Ext.event.Event} e
*/
/**
* @event mousedown
* Fires when this field receives a mousedown
* @param {Ext.field.Text} this This field
* @param {Ext.event.Event} e
*/
/**
* @event keyup
* @preventable doKeyUp
* Fires when a key is released on the input element
* @param {Ext.field.Text} this This field
* @param {Ext.event.Event} e
*/
/**
* @event clearicontap
* @preventable doClearIconTap
* Fires when the clear icon is tapped
* @param {Ext.field.Text} this This field
* @param {Ext.event.Event} e
*/
/**
* @event change
* Fires just before the field blurs if the field value has changed
* @param {Ext.field.Text} this This field
* @param {Mixed} newValue The new value
* @param {Mixed} oldValue The original value
*/
/**
* @event action
* @preventable doAction
* Fires whenever the return key or go is pressed. FormPanel listeners
* for this event, and submits itself whenever it fires. Also note
* that this event bubbles up to parent containers.
* @param {Ext.field.Text} this This field
* @param {Mixed} e The key event object
*/
config: {
/**
* @cfg
* @inheritdoc
*/
ui: 'text',
/**
* @cfg
* @inheritdoc
*/
clearIcon: true,
/**
* @cfg {String} placeHolder A string value displayed in the input (if supported) when the control is empty.
* @accessor
*/
placeHolder: null,
/**
* @cfg {Number} maxLength The maximum number of permitted input characters.
* @accessor
*/
maxLength: null,
/**
* True to set the field's DOM element autocomplete attribute to "on", false to set to "off".
* @cfg {Boolean} autoComplete
* @accessor
*/
autoComplete: null,
/**
* True to set the field's DOM element autocapitalize attribute to "on", false to set to "off".
* @cfg {Boolean} autoCapitalize
* @accessor
*/
autoCapitalize: null,
/**
* True to set the field DOM element autocorrect attribute to "on", false to set to "off".
* @cfg {Boolean} autoCorrect
* @accessor
*/
autoCorrect: null,
/**
* True to set the field DOM element readonly attribute to true.
* @cfg {Boolean} readOnly
* @accessor
*/
readOnly: null,
/**
* @cfg {Object} component The inner component for this field, which defaults to an input text.
* @accessor
*/
component: {
xtype: 'input',
type : 'text'
},
bubbleEvents: ['action']
},
// @private
initialize: function() {
var me = this;
me.callParent();
me.getComponent().on({
scope: this,
keyup : 'onKeyUp',
change : 'onChange',
focus : 'onFocus',
blur : 'onBlur',
paste : 'onPaste',
mousedown : 'onMouseDown',
clearicontap: 'onClearIconTap'
});
// set the originalValue of the textfield, if one exists
me.originalValue = me.originalValue || "";
me.getComponent().originalValue = me.originalValue;
me.syncEmptyCls();
},
syncEmptyCls: function() {
var empty = (this._value) ? this._value.length : false,
cls = Ext.baseCSSPrefix + 'empty';
if (empty) {
this.removeCls(cls);
} else {
this.addCls(cls);
}
},
// @private
updateValue: function(newValue) {
var component = this.getComponent(),
// allows newValue to be zero but not undefined, null or an empty string (other falsey values)
valueValid = newValue !== undefined && newValue !== null && newValue !== '';
if (component) {
component.setValue(newValue);
}
this[valueValid ? 'showClearIcon' : 'hideClearIcon']();
this.syncEmptyCls();
},
getValue: function() {
var me = this;
me._value = me.getComponent().getValue();
me.syncEmptyCls();
return me._value;
},
// @private
updatePlaceHolder: function(newPlaceHolder) {
this.getComponent().setPlaceHolder(newPlaceHolder);
},
// @private
updateMaxLength: function(newMaxLength) {
this.getComponent().setMaxLength(newMaxLength);
},
// @private
updateAutoComplete: function(newAutoComplete) {
this.getComponent().setAutoComplete(newAutoComplete);
},
// @private
updateAutoCapitalize: function(newAutoCapitalize) {
this.getComponent().setAutoCapitalize(newAutoCapitalize);
},
// @private
updateAutoCorrect: function(newAutoCorrect) {
this.getComponent().setAutoCorrect(newAutoCorrect);
},
// @private
updateReadOnly: function(newReadOnly) {
if (newReadOnly) {
this.hideClearIcon();
} else {
this.showClearIcon();
}
this.getComponent().setReadOnly(newReadOnly);
},
// @private
updateInputType: function(newInputType) {
var component = this.getComponent();
if (component) {
component.setType(newInputType);
}
},
// @private
updateName: function(newName) {
var component = this.getComponent();
if (component) {
component.setName(newName);
}
},
// @private
updateTabIndex: function(newTabIndex) {
var component = this.getComponent();
if (component) {
component.setTabIndex(newTabIndex);
}
},
/**
* Updates the {@link #inputCls} configuration on this fields {@link #component}
* @private
*/
updateInputCls: function(newInputCls, oldInputCls) {
var component = this.getComponent();
if (component) {
component.replaceCls(oldInputCls, newInputCls);
}
},
doSetDisabled: function(disabled) {
var me = this;
me.callParent(arguments);
var component = me.getComponent();
if (component) {
component.setDisabled(disabled);
}
if (disabled) {
me.hideClearIcon();
} else {
me.showClearIcon();
}
},
// @private
showClearIcon: function() {
var me = this,
value = me.getValue(),
// allows value to be zero but not undefined, null or an empty string (other falsey values)
valueValid = value !== undefined && value !== null && value !== '';
if (me.getClearIcon() && !me.getDisabled() && !me.getReadOnly() && valueValid) {
me.element.addCls(Ext.baseCSSPrefix + 'field-clearable');
}
return me;
},
// @private
hideClearIcon: function() {
if (this.getClearIcon()) {
this.element.removeCls(Ext.baseCSSPrefix + 'field-clearable');
}
},
onKeyUp: function(e) {
this.fireAction('keyup', [this, e], 'doKeyUp');
},
/**
* Called when a key has been pressed in the `<input>`
* @private
*/
doKeyUp: function(me, e) {
// getValue to ensure that we are in sync with the dom
var value = me.getValue(),
// allows value to be zero but not undefined, null or an empty string (other falsey values)
valueValid = value !== undefined && value !== null && value !== '';
this[valueValid ? 'showClearIcon' : 'hideClearIcon']();
if (e.browserEvent.keyCode === 13) {
me.fireAction('action', [me, e], 'doAction');
}
},
doAction: function() {
this.blur();
},
onClearIconTap: function(e) {
this.fireAction('clearicontap', [this, e], 'doClearIconTap');
},
// @private
doClearIconTap: function(me, e) {
me.setValue('');
//sync with the input
me.getValue();
},
onChange: function(me, value, startValue) {
me.fireEvent('change', this, value, startValue);
},
onFocus: function(e) {
this.isFocused = true;
this.fireEvent('focus', this, e);
},
onBlur: function(e) {
var me = this;
this.isFocused = false;
me.fireEvent('blur', me, e);
setTimeout(function() {
me.isFocused = false;
}, 50);
},
onPaste: function(e) {
this.fireEvent('paste', this, e);
},
onMouseDown: function(e) {
this.fireEvent('mousedown', this, e);
},
/**
* Attempts to set the field as the active input focus.
* @return {Ext.field.Text} This field
*/
focus: function() {
this.getComponent().focus();
return this;
},
/**
* Attempts to forcefully blur input focus for the field.
* @return {Ext.field.Text} This field
*/
blur: function() {
this.getComponent().blur();
return this;
},
/**
* Attempts to forcefully select all the contents of the input field.
* @return {Ext.field.Text} this
*/
select: function() {
this.getComponent().select();
return this;
},
reset: function() {
this.getComponent().reset();
//we need to call this to sync the input with this field
this.getValue();
this[this._value ? 'showClearIcon' : 'hideClearIcon']();
},
isDirty: function() {
var component = this.getComponent();
if (component) {
return component.isDirty();
}
return false;
}
});
/**
* @private
*/
Ext.define('Ext.field.TextAreaInput', {
extend: 'Ext.field.Input',
xtype : 'textareainput',
tag: 'textarea'
});
/**
* @aside guide forms
*
* Creates an HTML textarea field on the page. This is useful whenever you need the user to enter large amounts of text
* (i.e. more than a few words). Typically, text entry on mobile devices is not a pleasant experience for the user so
* it's good to limit your use of text areas to only those occasions when free form text is required or alternative
* input methods like select boxes or radio buttons are not possible. Text Areas are usually created inside forms, like
* this:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'About you',
* items: [
* {
* xtype: 'textfield',
* label: 'Name',
* name: 'name'
* },
* {
* xtype: 'textareafield',
* label: 'Bio',
* maxRows: 4,
* name: 'bio'
* }
* ]
* }
* ]
* });
*
* In the example above we're creating a form with a {@link Ext.field.Text text field} for the user's name and a text
* area for their bio. We used the {@link #maxRows} configuration on the text area to tell it to grow to a maximum of 4
* rows of text before it starts using a scroll bar inside the text area to scroll the text.
*
* We can also create a text area outside the context of a form, like this:
*
* This creates two text fields inside a form. Text Fields can also be created outside of a Form, like this:
*
* Ext.create('Ext.field.TextArea', {
* label: 'About You',
* {@link #placeHolder}: 'Tell us about yourself...'
* });
*/
Ext.define('Ext.field.TextArea', {
extend: 'Ext.field.Text',
xtype: 'textareafield',
requires: ['Ext.field.TextAreaInput'],
alternateClassName: 'Ext.form.TextArea',
config: {
/**
* @cfg
* @inheritdoc
*/
ui: 'textarea',
/**
* @cfg
* @inheritdoc
*/
autoCapitalize: false,
/**
* @cfg
* @inheritdoc
*/
component: {
xtype: 'textareainput'
},
/**
* @cfg {Number} maxRows The maximum number of lines made visible by the input.
* @accessor
*/
maxRows: null
},
// @private
updateMaxRows: function(newRows) {
this.getComponent().setMaxRows(newRows);
},
doSetHeight: function(newHeight) {
this.callParent(arguments);
var component = this.getComponent();
component.input.setHeight(newHeight);
},
doSetWidth: function(newWidth) {
this.callParent(arguments);
var component = this.getComponent();
component.input.setWidth(newWidth);
},
/**
* Called when a key has been pressed in the `<input>`
* @private
*/
doKeyUp: function(me) {
// getValue to ensure that we are in sync with the dom
var value = me.getValue();
// show the {@link #clearIcon} if it is being used
me[value ? 'showClearIcon' : 'hideClearIcon']();
}
});
/**
* Utility class for generating different styles of message boxes. The framework provides a global singleton
* {@link Ext.Msg} for common usage which you should use in most cases.
*
* If you want to use {@link Ext.MessageBox} directly, just think of it as a modal {@link Ext.Container}.
*
* Note that the MessageBox is asynchronous. Unlike a regular JavaScript `alert` (which will halt browser execution),
* showing a MessageBox will not cause the code to stop. For this reason, if you have code that should only run _after_
* some user feedback from the MessageBox, you must use a callback function (see the `fn` configuration option parameter
* for the {@link #method-show show} method for more details).
*
* @example preview
* Ext.Msg.alert('Title', 'The quick brown fox jumped over the lazy dog.', Ext.emptyFn);
*
* Checkout {@link Ext.Msg} for more examples.
*
*/
Ext.define('Ext.MessageBox', {
extend : 'Ext.Sheet',
requires: [
'Ext.Toolbar',
'Ext.field.Text',
'Ext.field.TextArea'
],
config: {
/**
* @cfg
* @inheritdoc
*/
ui: 'dark',
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'msgbox',
/**
* @cfg {String} iconCls
* CSS class for the icon. The icon should be 40px x 40px.
* @accessor
*/
iconCls: null,
/**
* @cfg
* @inheritdoc
*/
showAnimation: {
type: 'popIn',
duration: 250,
easing: 'ease-out'
},
/**
* @cfg
* @inheritdoc
*/
hideAnimation: {
type: 'popOut',
duration: 250,
easing: 'ease-out'
},
/**
* Override the default `zIndex` so it is normally always above floating components.
*/
zIndex: 999,
/**
* @cfg {Number} defaultTextHeight
* The default height in pixels of the message box's multiline textarea if displayed.
* @accessor
*/
defaultTextHeight: 75,
/**
* @cfg {String} title
* The title of this {@link Ext.MessageBox}.
* @accessor
*/
title: null,
/**
* @cfg {Array/Object} buttons
* An array of buttons, or an object of a button to be displayed in the toolbar of this {@link Ext.MessageBox}.
*/
buttons: null,
/**
* @cfg {String} message
* The message to be displayed in the {@link Ext.MessageBox}.
* @accessor
*/
message: null,
/**
* @cfg {String} msg
* The message to be displayed in the {@link Ext.MessageBox}.
* @removed 2.0.0 Please use {@link #message} instead.
*/
/**
* @cfg {Object} prompt
* The configuration to be passed if you want an {@link Ext.field.Text} or {@link Ext.field.TextArea} field
* in your {@link Ext.MessageBox}.
*
* Pass an object with the property `multiLine` with a value of `true`, if you want the prompt to use a TextArea.
*
* Alternatively, you can just pass in an object which has an xtype/xclass of another component.
*
* prompt: {
* xtype: 'textareafield',
* value: 'test'
* }
*
* @accessor
*/
prompt: null,
/**
* @private
*/
modal: true,
/**
* @cfg
* @inheritdoc
*/
layout: {
type: 'vbox',
pack: 'center'
}
},
statics: {
OK : {text: 'OK', itemId: 'ok', ui: 'action'},
YES : {text: 'Yes', itemId: 'yes', ui: 'action'},
NO : {text: 'No', itemId: 'no'},
CANCEL: {text: 'Cancel', itemId: 'cancel'},
INFO : Ext.baseCSSPrefix + 'msgbox-info',
WARNING : Ext.baseCSSPrefix + 'msgbox-warning',
QUESTION: Ext.baseCSSPrefix + 'msgbox-question',
ERROR : Ext.baseCSSPrefix + 'msgbox-error',
OKCANCEL: [
{text: 'Cancel', itemId: 'cancel'},
{text: 'OK', itemId: 'ok', ui : 'action'}
],
YESNOCANCEL: [
{text: 'Cancel', itemId: 'cancel'},
{text: 'No', itemId: 'no'},
{text: 'Yes', itemId: 'yes', ui: 'action'}
],
YESNO: [
{text: 'No', itemId: 'no'},
{text: 'Yes', itemId: 'yes', ui: 'action'}
]
},
constructor: function(config) {
config = config || {};
if (config.hasOwnProperty('promptConfig')) {
//<debug warn>
Ext.Logger.deprecate("'promptConfig' config is deprecated, please use 'prompt' config instead", this);
//</debug>
Ext.applyIf(config, {
prompt: config.promptConfig
});
delete config.promptConfig;
}
if (config.hasOwnProperty('multiline') || config.hasOwnProperty('multiLine')) {
config.prompt = config.prompt || {};
Ext.applyIf(config.prompt, {
multiLine: config.multiline || config.multiLine
});
delete config.multiline;
delete config.multiLine;
}
this.defaultAllowedConfig = {};
var allowedConfigs = ['ui', 'showAnimation', 'hideAnimation', 'title', 'message', 'prompt', 'iconCls', 'buttons', 'defaultTextHeight'],
ln = allowedConfigs.length,
i, allowedConfig;
for (i = 0; i < ln; i++) {
allowedConfig = allowedConfigs[i];
this.defaultAllowedConfig[allowedConfig] = this.defaultConfig[allowedConfig];
}
this.callParent([config]);
},
/**
* Creates a new {@link Ext.Toolbar} instance using {@link Ext#factory}.
* @private
*/
applyTitle: function(config) {
if (typeof config == "string") {
config = {
title: config
};
}
Ext.applyIf(config, {
docked: 'top',
minHeight: '1.3em',
cls : this.getBaseCls() + '-title'
});
return Ext.factory(config, Ext.Toolbar, this.getTitle());
},
/**
* Adds the new {@link Ext.Toolbar} instance into this container.
* @private
*/
updateTitle: function(newTitle) {
if (newTitle) {
this.add(newTitle);
}
},
/**
* Adds the new {@link Ext.Toolbar} instance into this container.
* @private
*/
updateButtons: function(newButtons) {
var me = this;
if (newButtons) {
if (me.buttonsToolbar) {
me.buttonsToolbar.removeAll();
me.buttonsToolbar.setItems(newButtons);
} else {
me.buttonsToolbar = Ext.create('Ext.Toolbar', {
docked : 'bottom',
defaultType: 'button',
layout : {
type: 'hbox',
pack: 'center'
},
ui : me.getUi(),
cls : me.getBaseCls() + '-buttons',
items : newButtons
});
me.add(me.buttonsToolbar);
}
}
},
/**
* @private
*/
applyMessage: function(config) {
config = {
html : config,
cls : this.getBaseCls() + '-text'
};
return Ext.factory(config, Ext.Component, this._message);
},
/**
* @private
*/
updateMessage: function(newMessage) {
if (newMessage) {
this.add(newMessage);
}
},
getMessage: function() {
if (this._message) {
return this._message.getHtml();
}
return null;
},
/**
* @private
*/
applyIconCls: function(config) {
config = {
xtype : 'component',
docked: 'left',
width : 40,
height: 40,
baseCls: Ext.baseCSSPrefix + 'icon',
hidden: (config) ? false : true,
cls: config
};
return Ext.factory(config, Ext.Component, this._iconCls);
},
/**
* @private
*/
updateIconCls: function(newIconCls, oldIconCls) {
var me = this;
//ensure the title and button elements are added first
this.getTitle();
this.getButtons();
if (newIconCls) {
this.add(newIconCls);
} else {
this.remove(oldIconCls);
}
},
getIconCls: function() {
var icon = this._iconCls,
iconCls;
if (icon) {
iconCls = icon.getCls();
return (iconCls) ? iconCls[0] : null;
}
return null;
},
/**
* @private
*/
applyPrompt: function(prompt) {
if (prompt) {
var config = {
label: false
};
if (Ext.isObject(prompt)) {
Ext.apply(config, prompt);
}
if (config.multiLine) {
config.height = Ext.isNumber(config.multiLine) ? parseFloat(config.multiLine) : this.getDefaultTextHeight();
return Ext.factory(config, Ext.field.TextArea, this.getPrompt());
} else {
return Ext.factory(config, Ext.field.Text, this.getPrompt());
}
}
return prompt;
},
/**
* @private
*/
updatePrompt: function(newPrompt, oldPrompt) {
if (newPrompt) {
this.add(newPrompt);
}
if (oldPrompt) {
this.remove(oldPrompt);
}
},
// @private
// pass `fn` config to show method instead
onClick: function(button) {
if (button) {
var config = button.config.userConfig || {},
initialConfig = button.getInitialConfig(),
prompt = this.getPrompt();
if (typeof config.fn == 'function') {
this.on({
hiddenchange: function() {
config.fn.call(
config.scope || null,
initialConfig.itemId || initialConfig.text,
prompt ? prompt.getValue() : null,
config
);
},
single: true,
scope: this
});
}
if (config.input) {
config.input.dom.blur();
}
}
this.hide();
},
/**
* Displays the {@link Ext.MessageBox} with a specified configuration. All
* display functions (e.g. {@link #method-prompt}, {@link #alert}, {@link #confirm})
* on MessageBox call this function internally, although those calls
* are basic shortcuts and do not support all of the config options allowed here.
*
* Example usage:
*
* @example
* Ext.Msg.show({
* title: 'Address',
* message: 'Please enter your address:',
* width: 300,
* buttons: Ext.MessageBox.OKCANCEL,
* multiLine: true,
* prompt : { maxlength : 180, autocapitalize : true },
* fn: function(buttonId) {
* alert('You pressed the "' + buttonId + '" button.');
* }
* });
*
* @param {Object} config An object with the following config options:
*
* @param {Object/Array} [config.buttons=false]
* A button config object or Array of the same(e.g., `Ext.MessageBox.OKCANCEL` or `{text:'Foo', itemId:'cancel'}`),
* or false to not show any buttons.
*
* @param {String} config.cls
* A custom CSS class to apply to the message box's container element.
*
* @param {Function} config.fn
* A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
*
* @param {String} config.fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
* @param {String} config.fn.value Value of the input field if either `prompt` or `multiline` option is `true`.
* @param {Object} config.fn.opt The config object passed to show.
*
* @param {Number} [config.width=auto]
* A fixed width for the MessageBox.
*
* @param {Number} [config.height=auto]
* A fixed height for the MessageBox.
*
* @param {Object} config.scope
* The scope of the callback function
*
* @param {String} config.icon
* A CSS class that provides a background image to be used as the body icon for the dialog
* (e.g. Ext.MessageBox.WARNING or 'custom-class').
*
* @param {Boolean} [config.modal=true]
* `false` to allow user interaction with the page while the message box is displayed.
*
* @param {String} [config.message=&#160;]
* A string that will replace the existing message box body text.
* Defaults to the XHTML-compliant non-breaking space character `&#160;`.
*
* @param {Number} [config.defaultTextHeight=75]
* The default height in pixels of the message box's multiline textarea if displayed.
*
* @param {Boolean} [config.prompt=false]
* `true` to prompt the user to enter single-line text. Please view the {@link Ext.MessageBox#method-prompt} documentation in {@link Ext.MessageBox}.
* for more information.
*
* @param {Boolean} [config.multiline=false]
* `true` to prompt the user to enter multi-line text.
*
* @param {String} config.title
* The title text.
*
* @param {String} config.value
* The string value to set into the active textbox element if displayed.
*
* @return {Ext.MessageBox} this
*/
show: function(initialConfig) {
//if it has not been added to a container, add it to the Viewport.
if (!this.getParent() && Ext.Viewport) {
Ext.Viewport.add(this);
}
if (!initialConfig) {
return this.callParent();
}
var config = Ext.Object.merge({}, {
value: ''
}, initialConfig);
var buttons = initialConfig.buttons || Ext.MessageBox.OK || [],
buttonBarItems = [],
userConfig = initialConfig;
Ext.each(buttons, function(buttonConfig) {
if (!buttonConfig) {
return;
}
buttonBarItems.push(Ext.apply({
userConfig: userConfig,
scope : this,
handler : 'onClick'
}, buttonConfig));
}, this);
config.buttons = buttonBarItems;
if (config.promptConfig) {
//<debug warn>
Ext.Logger.deprecate("'promptConfig' config is deprecated, please use 'prompt' config instead", this);
//</debug>
}
config.prompt = (config.promptConfig || config.prompt) || null;
if (config.multiLine) {
config.prompt = config.prompt || {};
config.prompt.multiLine = config.multiLine;
delete config.multiLine;
}
config = Ext.merge({}, this.defaultAllowedConfig, config);
this.setConfig(config);
var prompt = this.getPrompt();
if (prompt) {
prompt.setValue(initialConfig.value || '');
}
this.callParent();
return this;
},
/**
* Displays a standard read-only message box with an OK button (comparable to the basic JavaScript alert prompt). If
* a callback function is passed it will be called after the user clicks the button, and the `itemId` of the button
* that was clicked will be passed as the only parameter to the callback.
*
* @param {String} title The title bar text.
* @param {String} message The message box body text.
* @param {Function} fn A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
* @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
* @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
* @param {Object} fn.opt The config object passed to show.
* @param {Object} scope The scope (`this` reference) in which the callback is executed.
* Defaults to: the browser window
*
* @return {Ext.MessageBox} this
*/
alert: function(title, message, fn, scope) {
return this.show({
title: title || null,
message: message || null,
buttons: Ext.MessageBox.OK,
promptConfig: false,
fn: function() {
if (fn) {
fn.apply(scope, arguments);
}
},
scope: scope
});
},
/**
* Displays a confirmation message box with Yes and No buttons (comparable to JavaScript's confirm). If a callback
* function is passed it will be called after the user clicks either button, and the id of the button that was
* clicked will be passed as the only parameter to the callback (could also be the top-right close button).
*
* @param {String} title The title bar text.
* @param {String} message The message box body text.
* @param {Function} fn A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
* @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
* @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
* @param {Object} fn.opt The config object passed to show.
* @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
*
* Defaults to: the browser window
*
* @return {Ext.MessageBox} this
*/
confirm: function(title, message, fn, scope) {
return this.show({
title : title || null,
message : message || null,
buttons : Ext.MessageBox.YESNO,
promptConfig: false,
scope : scope,
fn: function() {
if (fn) {
fn.apply(scope, arguments);
}
}
});
},
/**
* Displays a message box with OK and Cancel buttons prompting the user to enter some text (comparable to
* JavaScript's prompt). The prompt can be a single-line or multi-line textbox. If a callback function is passed it
* will be called after the user clicks either button, and the id of the button that was clicked (could also be the
* top-right close button) and the text that was entered will be passed as the two parameters to the callback.
*
* Example usage:
*
* @example
* Ext.Msg.prompt(
* 'Welcome!',
* 'What\'s your name going to be today?',
* function (buttonId, value) {
* console.log(value);
* },
* null,
* false,
* null,
* {
* autoCapitalize: true,
* placeHolder: 'First-name please...'
* }
* );
*
* @param {String} title The title bar text.
* @param {String} message The message box body text.
* @param {Function} fn A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
* @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
* @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
* @param {Object} fn.opt The config object passed to show.
* @param {Object} scope The scope (`this` reference) in which the callback is executed.
*
* Defaults to: the browser window.
*
* @param {Boolean/Number} [multiLine=false] `true` to create a multiline textbox using the `defaultTextHeight` property,
* or the height in pixels to create the textbox.
*
* @param {String} [value] Default value of the text input element.
*
* @param {Object} [prompt=true]
* The configuration for the prompt. See the {@link Ext.MessageBox#cfg-prompt prompt} documentation in {@link Ext.MessageBox}
* for more information.
*
* @return {Ext.MessageBox} this
*/
prompt: function(title, message, fn, scope, multiLine, value, prompt) {
return this.show({
title : title || null,
message : message || null,
buttons : Ext.MessageBox.OKCANCEL,
scope : scope,
prompt : prompt || true,
multiLine: multiLine,
value : value,
fn: function() {
if (fn) {
fn.apply(scope, arguments);
}
}
});
}
}, function(MessageBox) {
Ext.onSetup(function() {
/**
* @class Ext.Msg
* @extends Ext.MessageBox
* @singleton
*
* A global shared singleton instance of the {@link Ext.MessageBox} class.
*
* Allows for simple creation of various different alerts and notifications.
*
* To change any configurations on this singleton instance, you must change the
* `defaultAllowedConfig` object. For example to remove all animations on `Msg`:
*
* Ext.Msg.defaultAllowedConfig.showAnimation = false;
* Ext.Msg.defaultAllowedConfig.hideAnimation = false;
*
* ## Examples
*
* ### Alert
* Use the {@link #alert} method to show a basic alert:
*
* @example preview
* Ext.Msg.alert('Title', 'The quick brown fox jumped over the lazy dog.', Ext.emptyFn);
*
* ### Prompt
* Use the {@link #method-prompt} method to show an alert which has a textfield:
*
* @example preview
* Ext.Msg.prompt('Name', 'Please enter your name:', function(text) {
* // process text value and close...
* });
*
* ### Confirm
* Use the {@link #confirm} method to show a confirmation alert (shows yes and no buttons).
*
* @example preview
* Ext.Msg.confirm("Confirmation", "Are you sure you want to do that?", Ext.emptyFn);
*/
Ext.Msg = new MessageBox;
});
});
/**
* SegmentedButton is a container for a group of {@link Ext.Button}s. Generally a SegmentedButton would be
* a child of a {@link Ext.Toolbar} and would be used to switch between different views.
*
* ## Example usage:
*
* @example
* var segmentedButton = Ext.create('Ext.SegmentedButton', {
* allowMultiple: true,
* items: [
* {
* text: 'Option 1'
* },
* {
* text: 'Option 2',
* pressed: true
* },
* {
* text: 'Option 3'
* }
* ],
* listeners: {
* toggle: function(container, button, pressed){
* alert("User toggled the '" + button.getText() + "' button: " + (pressed ? 'on' : 'off'));
* }
* }
* });
* Ext.Viewport.add({ xtype: 'container', padding: 10, items: [segmentedButton] });
*/
Ext.define('Ext.SegmentedButton', {
extend: 'Ext.Container',
xtype : 'segmentedbutton',
requires: ['Ext.Button'],
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'segmentedbutton',
/**
* @cfg {String} pressedCls
* CSS class when a button is in pressed state.
* @accessor
*/
pressedCls: Ext.baseCSSPrefix + 'button-pressed',
/**
* @cfg {Boolean} allowMultiple
* Allow multiple pressed buttons.
* @accessor
*/
allowMultiple: false,
/**
* @cfg {Boolean} allowDepress
* Allow toggling the pressed state of each button.
* Defaults to `true` when {@link #allowMultiple} is `true`.
* @accessor
*/
allowDepress: false,
/**
* @cfg {Boolean} allowToggle Allow child buttons to be pressed when tapped on. Set to `false` to allow tapping but not toggling of the buttons.
* @accessor
*/
allowToggle: true,
/**
* @cfg {Array} pressedButtons
* The pressed buttons for this segmented button.
*
* You can remove all pressed buttons by calling {@link #setPressedButtons} with an empty array.
* @accessor
*/
pressedButtons: [],
/**
* @cfg
* @inheritdoc
*/
layout: {
type : 'hbox',
align: 'stretch'
},
/**
* @cfg
* @inheritdoc
*/
defaultType: 'button'
},
/**
* @event toggle
* Fires when any child button's pressed state has changed.
* @param {Ext.SegmentedButton} this
* @param {Ext.Button} button The toggled button.
* @param {Boolean} isPressed Boolean to indicate if the button was pressed or not.
*/
initialize: function() {
var me = this;
me.callParent();
me.on({
delegate: '> button',
scope : me,
tap: 'onButtonRelease'
});
me.onAfter({
delegate: '> button',
scope : me,
hiddenchange: 'onButtonHiddenChange'
});
},
updateAllowMultiple: function(allowMultiple) {
if (!this.initialized && !this.getInitialConfig().hasOwnProperty('allowDepress') && allowMultiple) {
this.setAllowDepress(true);
}
},
/**
* We override `initItems` so we can check for the pressed config.
*/
applyItems: function() {
var me = this,
pressedButtons = [],
ln, i, item, items;
//call the parent first so the items get converted into a MixedCollection
me.callParent(arguments);
items = this.getItems();
ln = items.length;
for (i = 0; i < ln; i++) {
item = items.items[i];
if (item.getInitialConfig('pressed')) {
pressedButtons.push(items.items[i]);
}
}
me.updateFirstAndLastCls(items);
me.setPressedButtons(pressedButtons);
},
/**
* Button sets a timeout of 10ms to remove the {@link #pressedCls} on the release event.
* We don't want this to happen, so lets return `false` and cancel the event.
* @private
*/
onButtonRelease: function(button) {
if (!this.getAllowToggle()) {
return;
}
var me = this,
pressedButtons = me.getPressedButtons() || [],
buttons = [],
alreadyPressed;
if (!me.getDisabled() && !button.getDisabled()) {
//if we allow for multiple pressed buttons, use the existing pressed buttons
if (me.getAllowMultiple()) {
buttons = pressedButtons.concat(buttons);
}
alreadyPressed = (buttons.indexOf(button) !== -1) || (pressedButtons.indexOf(button) !== -1);
//if we allow for depressing buttons, and the new pressed button is currently pressed, remove it
if (alreadyPressed && me.getAllowDepress()) {
Ext.Array.remove(buttons, button);
} else if (!alreadyPressed || !me.getAllowDepress()) {
buttons.push(button);
}
me.setPressedButtons(buttons);
}
},
onItemAdd: function() {
this.callParent(arguments);
this.updateFirstAndLastCls(this.getItems());
},
onItemRemove: function() {
this.callParent(arguments);
this.updateFirstAndLastCls(this.getItems());
},
// @private
onButtonHiddenChange: function() {
this.updateFirstAndLastCls(this.getItems());
},
// @private
updateFirstAndLastCls: function(items) {
var ln = items.length,
basePrefix = Ext.baseCSSPrefix,
firstCls = basePrefix + 'first',
lastCls = basePrefix + 'last',
item, i;
//remove all existing classes
for (i = 0; i < ln; i++) {
item = items.items[i];
item.removeCls(firstCls);
item.removeCls(lastCls);
}
//add a first cls to the first non-hidden button
for (i = 0; i < ln; i++) {
item = items.items[i];
if (!item.isHidden()) {
item.addCls(firstCls);
break;
}
}
//add a last cls to the last non-hidden button
for (i = ln - 1; i >= 0; i--) {
item = items.items[i];
if (!item.isHidden()) {
item.addCls(lastCls);
break;
}
}
},
/**
* @private
*/
applyPressedButtons: function(newButtons) {
var me = this,
array = [],
button, ln, i;
if (me.getAllowToggle()) {
if (Ext.isArray(newButtons)) {
ln = newButtons.length;
for (i = 0; i< ln; i++) {
button = me.getComponent(newButtons[i]);
if (button && array.indexOf(button) === -1) {
array.push(button);
}
}
} else {
button = me.getComponent(newButtons);
if (button && array.indexOf(button) === -1) {
array.push(button);
}
}
}
return array;
},
/**
* Updates the pressed buttons.
* @private
*/
updatePressedButtons: function(newButtons, oldButtons) {
var me = this,
items = me.getItems(),
pressedCls = me.getPressedCls(),
events = [],
item, button, ln, i, e;
//loop through existing items and remove the pressed cls from them
ln = items.length;
if (oldButtons && oldButtons.length) {
for (i = 0; i < ln; i++) {
item = items.items[i];
if (oldButtons.indexOf(item) != -1 && newButtons.indexOf(item) == -1) {
item.removeCls([pressedCls, item.getPressedCls()]);
events.push({
item: item,
toggle: false
});
}
}
}
//loop through the new pressed buttons and add the pressed cls to them
ln = newButtons.length;
for (i = 0; i < ln; i++) {
button = newButtons[i];
if (!oldButtons || oldButtons.indexOf(button) == -1) {
button.addCls(pressedCls);
events.push({
item: button,
toggle: true
});
}
}
//loop through each of the events and fire them after a delay
ln = events.length;
if (ln && oldButtons !== undefined) {
Ext.defer(function() {
for (i = 0; i < ln; i++) {
e = events[i];
me.fireEvent('toggle', me, e.item, e.toggle);
}
}, 50);
}
},
/**
* Returns `true` if a specified {@link Ext.Button} is pressed.
* @param {Ext.Button} button The button to check if pressed.
* @return {Boolean} pressed
*/
isPressed: function(button) {
var pressedButtons = this.getPressedButtons();
return pressedButtons.indexOf(button) != -1;
},
/**
* @private
*/
doSetDisabled: function(disabled) {
var me = this;
me.items.each(function(item) {
item.setDisabled(disabled);
}, me);
me.callParent(arguments);
}
}, function() {
});
/**
* A mixin which allows a data component to be sorted
* @ignore
*/
Ext.define('Ext.Sortable', {
mixins: {
observable: 'Ext.mixin.Observable'
},
requires: ['Ext.util.Draggable'],
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'sortable',
/**
* @cfg {Number} delay
* How many milliseconds a user must hold the draggable before starting a
* drag operation.
* @private
* @accessor
*/
delay: 0
},
/**
* @cfg {String} direction
* Possible values: 'vertical', 'horizontal'.
*/
direction: 'vertical',
/**
* @cfg {String} cancelSelector
* A simple CSS selector that represents elements within the draggable
* that should NOT initiate a drag.
*/
cancelSelector: null,
// not yet implemented
//indicator: true,
//proxy: true,
//tolerance: null,
/**
* @cfg {HTMLElement/Boolean} constrain
* An Element to constrain the Sortable dragging to.
* If `true` is specified, the dragging will be constrained to the element
* of the sortable.
*/
constrain: window,
/**
* @cfg {String} group
* Draggable and Droppable objects can participate in a group which are
* capable of interacting.
*/
group: 'base',
/**
* @cfg {Boolean} revert
* This should NOT be changed.
* @private
*/
revert: true,
/**
* @cfg {String} itemSelector
* A simple CSS selector that represents individual items within the Sortable.
*/
itemSelector: null,
/**
* @cfg {String} handleSelector
* A simple CSS selector to indicate what is the handle to drag the Sortable.
*/
handleSelector: null,
/**
* @cfg {Boolean} disabled
* Passing in `true` will disable this Sortable.
*/
disabled: false,
// Properties
/**
* Read-only property that indicates whether a Sortable is currently sorting.
* @type Boolean
* @private
* @readonly
*/
sorting: false,
/**
* Read-only value representing whether the Draggable can be moved vertically.
* This is automatically calculated by Draggable by the direction configuration.
* @type Boolean
* @private
* @readonly
*/
vertical: false,
/**
* Creates new Sortable.
* @param {Mixed} el
* @param {Object} config
*/
constructor : function(el, config) {
config = config || {};
Ext.apply(this, config);
this.addEvents(
/**
* @event sortstart
* @param {Ext.Sortable} this
* @param {Ext.event.Event} e
*/
'sortstart',
/**
* @event sortend
* @param {Ext.Sortable} this
* @param {Ext.event.Event} e
*/
'sortend',
/**
* @event sortchange
* @param {Ext.Sortable} this
* @param {Ext.Element} el The Element being dragged.
* @param {Number} index The index of the element after the sort change.
*/
'sortchange'
// not yet implemented.
// 'sortupdate',
// 'sortreceive',
// 'sortremove',
// 'sortenter',
// 'sortleave',
// 'sortactivate',
// 'sortdeactivate'
);
this.el = Ext.get(el);
this.callParent();
this.mixins.observable.constructor.call(this);
if (this.direction == 'horizontal') {
this.horizontal = true;
}
else if (this.direction == 'vertical') {
this.vertical = true;
}
else {
this.horizontal = this.vertical = true;
}
this.el.addCls(this.baseCls);
this.startEventName = (this.getDelay() > 0) ? 'taphold' : 'tapstart';
if (!this.disabled) {
this.enable();
}
},
// @private
onStart : function(e, t) {
if (this.cancelSelector && e.getTarget(this.cancelSelector)) {
return;
}
if (this.handleSelector && !e.getTarget(this.handleSelector)) {
return;
}
if (!this.sorting) {
this.onSortStart(e, t);
}
},
// @private
onSortStart : function(e, t) {
this.sorting = true;
var draggable = Ext.create('Ext.util.Draggable', t, {
threshold: 0,
revert: this.revert,
direction: this.direction,
constrain: this.constrain === true ? this.el : this.constrain,
animationDuration: 100
});
draggable.on({
drag: this.onDrag,
dragend: this.onDragEnd,
scope: this
});
this.dragEl = t;
this.calculateBoxes();
if (!draggable.dragging) {
draggable.onStart(e);
}
this.fireEvent('sortstart', this, e);
},
// @private
calculateBoxes : function() {
this.items = [];
var els = this.el.select(this.itemSelector, false),
ln = els.length, i, item, el, box;
for (i = 0; i < ln; i++) {
el = els[i];
if (el != this.dragEl) {
item = Ext.fly(el).getPageBox(true);
item.el = el;
this.items.push(item);
}
}
},
// @private
onDrag : function(draggable, e) {
var items = this.items,
ln = items.length,
region = draggable.region,
sortChange = false,
i, intersect, overlap, item;
for (i = 0; i < ln; i++) {
item = items[i];
intersect = region.intersect(item);
if (intersect) {
if (this.vertical && Math.abs(intersect.top - intersect.bottom) > (region.bottom - region.top) / 2) {
if (region.bottom > item.top && item.top > region.top) {
draggable.el.insertAfter(item.el);
}
else {
draggable.el.insertBefore(item.el);
}
sortChange = true;
}
else if (this.horizontal && Math.abs(intersect.left - intersect.right) > (region.right - region.left) / 2) {
if (region.right > item.left && item.left > region.left) {
draggable.el.insertAfter(item.el);
}
else {
draggable.el.insertBefore(item.el);
}
sortChange = true;
}
if (sortChange) {
// We reset the draggable (initializes all the new start values)
draggable.reset();
// Move the draggable to its current location (since the transform is now
// different)
draggable.moveTo(region.left, region.top);
// Finally lets recalculate all the items boxes
this.calculateBoxes();
this.fireEvent('sortchange', this, draggable.el, this.el.select(this.itemSelector, false).indexOf(draggable.el.dom));
return;
}
}
}
},
// @private
onDragEnd : function(draggable, e) {
draggable.destroy();
this.sorting = false;
this.fireEvent('sortend', this, draggable, e);
},
/**
* Enables sorting for this Sortable.
* This method is invoked immediately after construction of a Sortable unless
* the disabled configuration is set to `true`.
*/
enable : function() {
this.el.on(this.startEventName, this.onStart, this, {delegate: this.itemSelector, holdThreshold: this.getDelay()});
this.disabled = false;
},
/**
* Disables sorting for this Sortable.
*/
disable : function() {
this.el.un(this.startEventName, this.onStart, this);
this.disabled = true;
},
/**
* Method to determine whether this Sortable is currently disabled.
* @return {Boolean} The disabled state of this Sortable.
*/
isDisabled: function() {
return this.disabled;
},
/**
* Method to determine whether this Sortable is currently sorting.
* @return {Boolean} The sorting state of this Sortable.
*/
isSorting : function() {
return this.sorting;
},
/**
* Method to determine whether this Sortable is currently disabled.
* @return {Boolean} The disabled state of this Sortable.
*/
isVertical : function() {
return this.vertical;
},
/**
* Method to determine whether this Sortable is currently sorting.
* @return {Boolean} The sorting state of this Sortable.
*/
isHorizontal : function() {
return this.horizontal;
}
});
(function() {
var lastTime = 0,
vendors = ['ms', 'moz', 'webkit', 'o'],
ln = vendors.length,
i, vendor;
for (i = 0; i < ln && !window.requestAnimationFrame; ++i) {
vendor = vendors[i];
window.requestAnimationFrame = window[vendor + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendor + 'CancelAnimationFrame'] || window[vendor + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime(),
timeToCall = Math.max(0, 16 - (currTime - lastTime)),
id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
}());
/**
* @private
* Handle batch read / write of DOMs, currently used in SizeMonitor + PaintMonitor
*/
Ext.define('Ext.TaskQueue', {
singleton: true,
pending: false,
mode: true,
constructor: function() {
this.readQueue = [];
this.writeQueue = [];
this.run = Ext.Function.bind(this.run, this);
},
requestRead: function(fn, scope, args) {
this.request(true);
this.readQueue.push(arguments);
},
requestWrite: function(fn, scope, args) {
this.request(false);
this.writeQueue.push(arguments);
},
request: function(mode) {
if (!this.pending) {
this.pending = true;
this.mode = mode;
requestAnimationFrame(this.run);
}
},
run: function() {
this.pending = false;
var readQueue = this.readQueue,
writeQueue = this.writeQueue,
request = null,
queue;
if (this.mode) {
queue = readQueue;
if (writeQueue.length > 0) {
request = false;
}
}
else {
queue = writeQueue;
if (readQueue.length > 0) {
request = true;
}
}
var tasks = queue.slice(),
i, ln, task, fn, scope;
queue.length = 0;
for (i = 0, ln = tasks.length; i < ln; i++) {
task = tasks[i];
fn = task[0];
scope = task[1];
if (typeof fn == 'string') {
fn = scope[fn];
}
if (task.length > 2) {
fn.apply(scope, task[2]);
}
else {
fn.call(scope);
}
}
tasks.length = 0;
if (request !== null) {
this.request(request);
}
}
});
/**
* {@link Ext.TitleBar}'s are most commonly used as a docked item within an {@link Ext.Container}.
*
* The main difference between a {@link Ext.TitleBar} and an {@link Ext.Toolbar} is that
* the {@link #title} configuration is **always** centered horizontally in a {@link Ext.TitleBar} between
* any items aligned left or right.
*
* You can also give items of a {@link Ext.TitleBar} an `align` configuration of `left` or `right`
* which will dock them to the `left` or `right` of the bar.
*
* ## Examples
*
* @example preview
* Ext.Viewport.add({
* xtype: 'titlebar',
* docked: 'top',
* title: 'Navigation',
* items: [
* {
* iconCls: 'add',
* iconMask: true,
* align: 'left'
* },
* {
* iconCls: 'home',
* iconMask: true,
* align: 'right'
* }
* ]
* });
*
* Ext.Viewport.setStyleHtmlContent(true);
* Ext.Viewport.setHtml('This shows the title being centered and buttons using align <i>left</i> and <i>right</i>.');
*
* <br />
*
* @example preview
* Ext.Viewport.add({
* xtype: 'titlebar',
* docked: 'top',
* title: 'Navigation',
* items: [
* {
* align: 'left',
* text: 'This button has a super long title'
* },
* {
* iconCls: 'home',
* iconMask: true,
* align: 'right'
* }
* ]
* });
*
* Ext.Viewport.setStyleHtmlContent(true);
* Ext.Viewport.setHtml('This shows how the title is automatically moved to the right when one of the aligned buttons is very wide.');
*
* <br />
*
* @example preview
* Ext.Viewport.add({
* xtype: 'titlebar',
* docked: 'top',
* title: 'A very long title',
* items: [
* {
* align: 'left',
* text: 'This button has a super long title'
* },
* {
* align: 'right',
* text: 'Another button'
* }
* ]
* });
*
* Ext.Viewport.setStyleHtmlContent(true);
* Ext.Viewport.setHtml('This shows how the title and buttons will automatically adjust their size when the width of the items are too wide..');
*
* The {@link #defaultType} of Toolbar's is {@link Ext.Button button}.
*/
Ext.define('Ext.TitleBar', {
extend: 'Ext.Container',
xtype: 'titlebar',
requires: [
'Ext.Button',
'Ext.Title',
'Ext.Spacer'
],
// @private
isToolbar: true,
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'toolbar',
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'navigation-bar',
/**
* @cfg {String} ui
* Style options for Toolbar. Either 'light' or 'dark'.
* @accessor
*/
ui: 'dark',
/**
* @cfg {String} title
* The title of the toolbar.
* @accessor
*/
title: null,
/**
* @cfg {String} defaultType
* The default xtype to create.
* @accessor
*/
defaultType: 'button',
height: '2.6em',
/**
* @cfg
* @hide
*/
layout: {
type: 'hbox'
},
/**
* @cfg {Array/Object} items The child items to add to this TitleBar. The {@link #defaultType} of
* a TitleBar is {@link Ext.Button}, so you do not need to specify an `xtype` if you are adding
* buttons.
*
* You can also give items a `align` configuration which will align the item to the `left` or `right` of
* the TitleBar.
* @accessor
*/
items: []
},
/**
* The max button width in this toolbar
* @private
*/
maxButtonWidth: '40%',
constructor: function() {
this.refreshTitlePosition = Ext.Function.createThrottled(this.refreshTitlePosition, 50, this);
this.callParent(arguments);
},
beforeInitialize: function() {
this.applyItems = this.applyInitialItems;
},
initialize: function() {
delete this.applyItems;
this.add(this.initialItems);
delete this.initialItems;
this.on({
painted: 'refreshTitlePosition',
single: true
});
},
applyInitialItems: function(items) {
var me = this,
defaults = me.getDefaults() || {};
me.initialItems = items;
me.leftBox = me.add({
xtype: 'container',
style: 'position: relative',
layout: {
type: 'hbox',
align: 'center'
},
listeners: {
resize: 'refreshTitlePosition',
scope: me
}
});
me.spacer = me.add({
xtype: 'component',
style: 'position: relative',
flex: 1,
listeners: {
resize: 'refreshTitlePosition',
scope: me
}
});
me.rightBox = me.add({
xtype: 'container',
style: 'position: relative',
layout: {
type: 'hbox',
align: 'center'
},
listeners: {
resize: 'refreshTitlePosition',
scope: me
}
});
me.titleComponent = me.add({
xtype: 'title',
hidden: defaults.hidden,
centered: true
});
me.doAdd = me.doBoxAdd;
me.remove = me.doBoxRemove;
me.doInsert = me.doBoxInsert;
},
doBoxAdd: function(item) {
if (item.config.align == 'right') {
this.rightBox.add(item);
}
else {
this.leftBox.add(item);
}
},
doBoxRemove: function(item) {
if (item.config.align == 'right') {
this.rightBox.remove(item);
}
else {
this.leftBox.remove(item);
}
},
doBoxInsert: function(index, item) {
if (item.config.align == 'right') {
this.rightBox.add(item);
}
else {
this.leftBox.add(item);
}
},
getMaxButtonWidth: function() {
var value = this.maxButtonWidth;
//check if it is a percentage
if (Ext.isString(this.maxButtonWidth)) {
value = parseInt(value.replace('%', ''), 10);
value = Math.round((this.element.getWidth() / 100) * value);
}
return value;
},
refreshTitlePosition: function() {
var titleElement = this.titleComponent.renderElement;
titleElement.setWidth(null);
titleElement.setLeft(null);
//set the min/max width of the left button
var leftBox = this.leftBox,
leftButton = leftBox.down('button'),
singleButton = leftBox.getItems().getCount() == 1,
leftBoxWidth, maxButtonWidth;
if (leftButton && singleButton) {
if (leftButton.getWidth() == null) {
leftButton.renderElement.setWidth('auto');
}
leftBoxWidth = leftBox.renderElement.getWidth();
maxButtonWidth = this.getMaxButtonWidth();
if (leftBoxWidth > maxButtonWidth) {
leftButton.renderElement.setWidth(maxButtonWidth);
}
}
var spacerBox = this.spacer.renderElement.getPageBox(),
titleBox = titleElement.getPageBox(),
widthDiff = titleBox.width - spacerBox.width,
titleLeft = titleBox.left,
titleRight = titleBox.right,
halfWidthDiff, leftDiff, rightDiff;
if (widthDiff > 0) {
titleElement.setWidth(spacerBox.width);
halfWidthDiff = widthDiff / 2;
titleLeft += halfWidthDiff;
titleRight -= halfWidthDiff;
}
leftDiff = spacerBox.left - titleLeft;
rightDiff = titleRight - spacerBox.right;
if (leftDiff > 0) {
titleElement.setLeft(leftDiff);
}
else if (rightDiff > 0) {
titleElement.setLeft(-rightDiff);
}
titleElement.repaint();
},
// @private
updateTitle: function(newTitle) {
this.titleComponent.setTitle(newTitle);
if (this.isPainted()) {
this.refreshTitlePosition();
}
}
});
/**
* @aside example video
* Provides a simple Container for HTML5 Video.
*
* ## Notes
*
* - There are quite a few issues with the `<video>` tag on Android devices. On Android 2+, the video will
* appear and play on first attempt, but any attempt afterwards will not work.
*
* ## Useful Properties
*
* - {@link #url}
* - {@link #autoPause}
* - {@link #autoResume}
*
* ## Useful Methods
*
* - {@link #method-pause}
* - {@link #method-play}
* - {@link #toggle}
*
* ## Example
*
* var panel = Ext.create('Ext.Panel', {
* fullscreen: true,
* items: [
* {
* xtype : 'video',
* x : 600,
* y : 300,
* width : 175,
* height : 98,
* url : "porsche911.mov",
* posterUrl: 'porsche.png'
* }
* ]
* });
*/
Ext.define('Ext.Video', {
extend: 'Ext.Media',
xtype: 'video',
config: {
/**
* @cfg {String/Array} urls
* Location of the video to play. This should be in H.264 format and in a .mov file format.
* @accessor
*/
/**
* @cfg {String} posterUrl
* Location of a poster image to be shown before showing the video.
* @accessor
*/
posterUrl: null,
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'video'
},
template: [{
/**
* @property {Ext.dom.Element} ghost
* @private
*/
reference: 'ghost',
classList: [Ext.baseCSSPrefix + 'video-ghost']
}, {
tag: 'video',
reference: 'media',
classList: [Ext.baseCSSPrefix + 'media']
}],
initialize: function() {
var me = this;
me.callParent();
me.media.hide();
me.onBefore({
erased: 'onErased',
scope: me
});
me.ghost.on({
tap: 'onGhostTap',
scope: me
});
me.media.on({
pause: 'onPause',
scope: me
});
if (Ext.os.is.Android4 || Ext.os.is.iPad) {
this.isInlineVideo = true;
}
},
applyUrl: function(url) {
return [].concat(url);
},
updateUrl: function(newUrl) {
var me = this,
media = me.media,
newLn = newUrl.length,
existingSources = media.query('source'),
oldLn = existingSources.length,
i;
for (i = 0; i < oldLn; i++) {
Ext.fly(existingSources[i]).destroy();
}
for (i = 0; i < newLn; i++) {
media.appendChild(Ext.Element.create({
tag: 'source',
src: newUrl[i]
}));
}
if (me.isPlaying()) {
me.play();
}
},
onErased: function() {
this.pause();
this.media.setTop(-2000);
this.ghost.show();
},
/**
* @private
* Called when the {@link #ghost} element is tapped.
*/
onGhostTap: function() {
var me = this,
media = this.media,
ghost = this.ghost;
media.show();
if (Ext.os.is.Android2) {
setTimeout(function() {
me.play();
setTimeout(function() {
media.hide();
}, 10);
}, 10);
} else {
// Browsers which support native video tag display only, move the media down so
// we can control the Viewport
ghost.hide();
me.play();
}
},
/**
* @private
* native video tag display only, move the media down so we can control the Viewport
*/
onPause: function() {
this.callParent(arguments);
if (!this.isInlineVideo) {
this.media.setTop(-2000);
this.ghost.show();
}
},
/**
* @private
* native video tag display only, move the media down so we can control the Viewport
*/
onPlay: function() {
this.callParent(arguments);
this.media.setTop(0);
},
/**
* Updates the URL to the poster, even if it is rendered.
* @param {Object} newUrl
*/
updatePosterUrl: function(newUrl) {
var ghost = this.ghost;
if (ghost) {
ghost.setStyle('background-image', 'url(' + newUrl + ')');
}
}
});
/**
* @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 actual 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
*
* @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 [the Controllers guide](#!/guide/controllers) 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 {String[]} 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 {String[]} 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.
* @return {Object}
*/
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) {
//<debug>
if (Ext.isArray(refs)) {
Ext.Logger.deprecate("In Sencha Touch 2 the refs config accepts an object but you have passed it an array.");
}
//</debug>
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 me = this,
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] = function(refName, info) {
var args = [refName, info];
return function() {
return me.getRef.apply(me, args.concat.apply(args, arguments));
};
}(refName, info);
}
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 occurred 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 {
this.setToken(window.location.hash.substr(1));
setInterval(Ext.bind(this.detectStateChange, this), 100);
}
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);
},
/**
* Navigate to the previous active action. This changes the page url.
*/
back: function() {
var actions = this.getActions(),
previousAction = actions[actions.length - 2],
app = previousAction.getController().getApplication();
actions.pop();
app.redirectTo(previousAction.getUrl());
},
/**
* @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 [device profiles guide](#!/guide/profiles).
*
* @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.
* @accessor
* @readonly
*/
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
* @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:
*
* - [Using History Support](#!/guide/history_support)
* - [Intro to Applications](#!/guide/apps_intro)
* - [Using Controllers](#!/guide/controllers)
*
*/
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 (required) The url regex to match against.
*/
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/Boolean} 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 recognize segments starting with a colon,
* e.g. on 'namespace/:controller/:action', :controller and :action will be recognized
*/
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.
* @readonly
*/
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:
*
* Ext.Router.draw(function(map) {
* map.connect('activate/:token', {controller: 'users', action: 'activate'});
* map.connect('home', {controller: 'index', action: 'home'});
* });
*
* @param {Function} fn The fn to call
*/
draw: function(fn) {
fn.call(this, this);
},
/**
* @private
*/
clear: function() {
this.setRoutes([]);
}
}, function() {
});
/**
* @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 everything 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 dependency files.
*
* Note that to enable the loading of external dependencies we just have to tell the Loader where to find those files,
* which is what we do with the Ext.Loader.setPath call above. In this case we're telling the Loader to find any class
* starting with the 'Auth' namespace inside our 'Auth' folder. This means we can drop our common Auth code into our
* application alongside the app folder and the framework will be able to figure out how to load everything.
*
* ## Launching
*
* Each Application can define a {@link Ext.app.Application#launch launch} function, which is called as soon as all of
* your app's classes have been loaded and the app is ready to be launched. This is usually the best place to put any
* application startup logic, typically creating the main view structure for your app.
*
* In addition to the Application launch function, there are two other places you can put app startup logic. Firstly,
* each Controller is able to define an {@link Ext.app.Controller#init init} function, which is called before the
* Application launch function. Secondly, if you are using Device Profiles, each Profile can define a
* {@link Ext.app.Profile#launch launch} function, which is called after the Controller init functions but before the
* Application launch function.
*
* Note that only the active Profile has its launch function called - for example if you define profiles for Phone and
* Tablet and then launch the app on a tablet, only the Tablet Profile's launch function is called.
*
* 1. Controller#init functions called
* 2. Profile#launch function called
* 3. Application#launch function called
* 4. Controller#launch functions called
*
* When using Profiles it is common to place most of the bootup logic inside the Profile launch function because each
* Profile has a different set of views that need to be constructed at startup.
*
* ## Adding to Home Screen
*
* iOS devices allow your users to add your app to their home screen for easy access. iOS allows you to customize
* several aspects of this, including the icon that will appear on the home screen and the startup image. These can be
* specified in the Ext.application setup block:
*
* Ext.application({
* name: 'MyApp',
*
* {@link #icon}: 'resources/img/icon.png',
* {@link #isIconPrecomposed}: false,
* {@link #startupImage}: {
* '320x460': 'resources/startup/320x460.jpg',
* '640x920': 'resources/startup/640x920.png',
* '640x1096': 'resources/startup/640x1096.png',
* '768x1004': 'resources/startup/768x1004.png',
* '748x1024': 'resources/startup/748x1024.png',
* '1536x2008': 'resources/startup/1536x2008.png',
* '1496x2048': 'resources/startup/1496x2048.png'
* }
* });
*
* When the user adds your app to the home screen, your resources/img/icon.png file will be used as the application
* {@link #icon}. We also used the {@link #isIconPrecomposed} configuration to turn off the gloss effect that is automatically added
* to icons in iOS. Finally we used the {@link #startupImage} configuration to provide the images that will be displayed
* while your application is starting up. See also {@link #statusBarStyle}.
*
* ## Find out more
*
* If you are not already familiar with writing applications with Sencha Touch 2 we recommend reading the
* [intro to applications guide](#!/guide/apps_intro), which lays out the core principles of writing apps
* with Sencha Touch 2.
*/
Ext.define('Ext.app.Application', {
extend: 'Ext.app.Controller',
requires: [
'Ext.app.History',
'Ext.app.Profile',
'Ext.app.Router',
'Ext.app.Action'
],
config: {
/**
* @cfg {String/Object} icon
* Specifies a set of URLs to the application icon for different device form factors. This icon is displayed
* when the application is added to the device's Home Screen.
*
* Ext.setup({
* icon: {
* 57: 'resources/icons/Icon.png',
* 72: 'resources/icons/Icon~ipad.png',
* 114: 'resources/icons/Icon@2x.png',
* 144: 'resources/icons/Icon~ipad@2x.png'
* },
* onReady: function() {
* // ...
* }
* });
*
* Each key represents the dimension of the icon as a square shape. For example: '57' is the key for a 57 x 57
* icon image. Here is the breakdown of each dimension and its device target:
*
* - 57: Non-retina iPhone, iPod touch, and all Android devices
* - 72: Retina iPhone and iPod touch
* - 114: Non-retina iPad (first and second generation)
* - 144: Retina iPad (third generation)
*
* Note that the dimensions of the icon images must be exactly 57x57, 72x72, 114x114 and 144x144 respectively.
*
* It is highly recommended that you provide all these different sizes to accommodate a full range of
* devices currently available. However if you only have one icon in one size, make it 57x57 in size and
* specify it as a string value. This same icon will be used on all supported devices.
*
* Ext.application({
* icon: 'resources/icons/Icon.png',
* launch: function() {
* // ...
* }
* });
*/
/**
* @cfg {Object} startupImage
* Specifies a set of URLs to the application startup images for different device form factors. This image is
* displayed when the application is being launched from the Home Screen icon. Note that this currently only applies
* to iOS devices.
*
* Ext.application({
* startupImage: {
* '320x460': 'resources/startup/320x460.jpg',
* '640x920': 'resources/startup/640x920.png',
* '640x1096': 'resources/startup/640x1096.png',
* '768x1004': 'resources/startup/768x1004.png',
* '748x1024': 'resources/startup/748x1024.png',
* '1536x2008': 'resources/startup/1536x2008.png',
* '1496x2048': 'resources/startup/1496x2048.png'
* },
* launch: function() {
* // ...
* }
* });
*
* Each key represents the dimension of the image. For example: '320x460' is the key for a 320px x 460px image.
* Here is the breakdown of each dimension and its device target:
*
* - 320x460: Non-retina iPhone, iPod touch, and all Android devices
* - 640x920: Retina iPhone and iPod touch
* - 640x1096: iPhone 5 and iPod touch (fifth generation)
* - 768x1004: Non-retina iPad (first and second generation) in portrait orientation
* - 748x1024: Non-retina iPad (first and second generation) in landscape orientation
* - 1536x2008: Retina iPad (third generation) in portrait orientation
* - 1496x2048: Retina iPad (third generation) in landscape orientation
*
* Please note that there's no automatic fallback mechanism for the startup images. In other words, if you don't specify
* a valid image for a certain device, nothing will be displayed while the application is being launched on that device.
*/
/**
* @cfg {Boolean} isIconPrecomposed
* `true` to not having a glossy effect added to the icon by the OS, which will preserve its exact look. This currently
* only applies to iOS devices.
*/
/**
* @cfg {String} [statusBarStyle='black'] Allows you to set the style of the status bar when your app is added to the
* home screen on iOS devices. Alternative is to set to 'black-translucent', which turns
* the status bar semi-transparent and overlaps the app content. This is usually not a good option for web apps
*/
/**
* @cfg {String} tabletIcon Path to the _.png_ image file to use when your app is added to the home screen on an
* iOS **tablet** device (iPad).
* @deprecated 2.0.0 Please use the {@link #icon} configuration instead.
*/
/**
* @cfg {String} phoneIcon Path to the _.png_ image file to use when your app is added to the home screen on an
* iOS **phone** device (iPhone or iPod).
* @deprecated 2.0.0 Please use the {@link #icon} configuration instead.
*/
/**
* @cfg {Boolean} glossOnIcon If set to `false`, the 'gloss' effect added to home screen {@link #icon icons} on
* iOS devices will be removed.
* @deprecated 2.0.0 Please use the {@link #isIconPrecomposed} configuration instead.
*/
/**
* @cfg {String} phoneStartupScreen Path to the _.png_ image file that will be displayed while the app is
* starting up once it has been added to the home screen of an iOS phone device (iPhone or iPod). This _.png_
* file should be 320px wide and 460px high.
* @deprecated 2.0.0 Please use the {@link #startupImage} configuration instead.
*/
/**
* @cfg {String} tabletStartupScreen Path to the _.png_ image file that will be displayed while the app is
* starting up once it has been added to the home screen of an iOS tablet device (iPad). This _.png_ file should
* be 768px wide and 1004px high.
* @deprecated 2.0.0 Please use the {@link #startupImage} configuration instead.
*/
/**
* @cfg {Array} profiles The set of profiles to load for this Application. Each profile is expected to
* exist inside the *app/profile* directory and define a class following the convention
* AppName.profile.ProfileName. For example, in the code below, the classes *AppName.profile.Phone*
* and *AppName.profile.Tablet* will be loaded. Note that we are able to specify
* either the full class name (as with *AppName.profile.Tablet*) or just the final part of the class name
* and leave Application to automatically prepend *AppName.profile.'* to each:
*
* profiles: [
* 'Phone',
* 'AppName.profile.Tablet',
* 'SomeCustomNamespace.profile.Desktop'
* ]
* @accessor
*/
profiles: [],
/**
* @cfg {Array} controllers The set of controllers to load for this Application. Each controller is expected to
* exist inside the *app/controller* directory and define a class following the convention
* AppName.controller.ControllerName. For example, in the code below, the classes *AppName.controller.Users*,
* *AppName.controller.Groups* and *AppName.controller.Products* will be loaded. Note that we are able to specify
* either the full class name (as with *AppName.controller.Products*) or just the final part of the class name
* and leave Application to automatically prepend *AppName.controller.'* to each:
*
* controllers: [
* 'Users',
* 'Groups',
* 'AppName.controller.Products',
* 'SomeCustomNamespace.controller.Orders'
* ]
* @accessor
*/
controllers: [],
/**
* @cfg {Ext.app.History} history The global {@link Ext.app.History History} instance attached to this
* Application.
* @accessor
* @readonly
*/
history: {},
/**
* @cfg {String} name The name of the Application. This should be a single word without spaces or periods
* because it is used as the Application's global namespace. All classes in your application should be
* namespaced undef the Application's name - for example if your application name is 'MyApp', your classes
* should be named 'MyApp.model.User', 'MyApp.controller.Users', 'MyApp.view.Main' etc
* @accessor
*/
name: null,
/**
* @cfg {String} appFolder The path to the directory which contains all application's classes.
* This path will be registered via {@link Ext.Loader#setPath} for the namespace specified in the {@link #name name} config.
* @accessor
*/
appFolder : 'app',
/**
* @cfg {Ext.app.Router} router The global {@link Ext.app.Router Router} instance attached to this Application.
* @accessor
* @readonly
*/
router: {},
/**
* @cfg {Array} controllerInstances Used internally as the collection of instantiated controllers. Use {@link #getController} instead.
* @private
* @accessor
*/
controllerInstances: [],
/**
* @cfg {Array} profileInstances Used internally as the collection of instantiated profiles.
* @private
* @accessor
*/
profileInstances: [],
/**
* @cfg {Ext.app.Profile} currentProfile The {@link Ext.app.Profile Profile} that is currently active for the
* Application. This is set once, automatically by the Application before launch.
* @accessor
* @readonly
*/
currentProfile: null,
/**
* @cfg {Function} launch An optional function that will be called when the Application is ready to be
* launched. This is normally used to render any initial UI required by your application
* @accessor
*/
launch: Ext.emptyFn,
/**
* @private
* @cfg {Boolean} enableLoader Private config to disable loading of Profiles at application construct time.
* This is used by Sencha's unit test suite to test _Application.js_ in isolation and is likely to be removed
* in favor of a more pleasing solution by the time you use it.
* @accessor
*/
enableLoader: true,
/**
* @private
* @cfg {String[]} requires An array of extra dependencies, to be required after this application's {@link #name} config
* has been processed properly, but before anything else to ensure overrides get executed first.
* @accessor
*/
requires: []
},
/**
* Constructs a new Application instance.
*/
constructor: function(config) {
config = config || {};
Ext.applyIf(config, {
application: this
});
this.initConfig(config);
//it's common to pass in functions to an application but because they are not predictable config names they
//aren't ordinarily placed onto this so we need to do it manually
for (var key in config) {
this[key] = config[key];
}
//<debug>
Ext.Loader.setConfig({ enabled: true });
//</debug>
Ext.require(this.getRequires(), function() {
if (this.getEnableLoader() !== false) {
Ext.require(this.getProfiles(), this.onProfilesLoaded, this);
}
}, this);
},
/**
* Dispatches a given {@link Ext.app.Action} to the relevant Controller instance. This is not usually called
* directly by the developer, instead Sencha Touch's History support picks up on changes to the browser's url
* and calls dispatch automatically.
* @param {Ext.app.Action} action The action to dispatch.
* @param {Boolean} [addToHistory=true] Sets the browser's url to the action's url.
*/
dispatch: function(action, addToHistory) {
action = action || {};
Ext.applyIf(action, {
application: this
});
action = Ext.factory(action, Ext.app.Action);
if (action) {
var profile = this.getCurrentProfile(),
profileNS = profile ? profile.getNamespace() : undefined,
controller = this.getController(action.getController(), profileNS);
if (controller) {
if (addToHistory !== false) {
this.getHistory().add(action, true);
}
controller.execute(action);
}
}
},
/**
* Redirects the browser to the given url. This only affects the url after the '#'. You can pass in either a String
* or a Model instance - if a Model instance is defined its {@link Ext.data.Model#toUrl toUrl} function is called,
* which returns a string representing the url for that model. Internally, this uses your application's
* {@link Ext.app.Router Router} to decode the url into a matching controller action and then calls
* {@link #dispatch}.
* @param {String/Ext.data.Model} url The String url to redirect to.
*/
redirectTo: function(url) {
if (Ext.data && Ext.data.Model && url instanceof Ext.data.Model) {
var record = url;
url = record.toUrl();
}
var decoded = this.getRouter().recognize(url);
if (decoded) {
decoded.url = url;
if (record) {
decoded.data = {};
decoded.data.record = record;
}
return this.dispatch(decoded);
}
},
/**
* @private
* (documented on Controller's control config)
*/
control: function(selectors, controller) {
//if the controller is not defined, use this instead (the application instance)
controller = controller || this;
var dispatcher = this.getEventDispatcher(),
refs = (controller) ? controller.getRefs() : {},
selector, eventName, listener, listeners, ref;
for (selector in selectors) {
if (selectors.hasOwnProperty(selector)) {
listeners = selectors[selector];
ref = refs[selector];
//refs can be used in place of selectors
if (ref) {
selector = ref.selector || ref;
}
for (eventName in listeners) {
listener = listeners[eventName];
if (Ext.isString(listener)) {
listener = controller[listener];
}
dispatcher.addListener('component', selector, eventName, listener, controller);
}
}
}
},
/**
* Returns the Controller instance for the given controller name.
* @param {String} name The name of the Controller.
* @param {String} [profileName] Optional profile name. If passed, this is the same as calling
* `getController('profileName.controllerName')`.
*/
getController: function(name, profileName) {
var instances = this.getControllerInstances(),
appName = this.getName(),
format = Ext.String.format,
topLevelName;
if (name instanceof Ext.app.Controller) {
return name;
}
if (instances[name]) {
return instances[name];
} else {
topLevelName = format("{0}.controller.{1}", appName, name);
profileName = format("{0}.controller.{1}.{2}", appName, profileName, name);
return instances[profileName] || instances[topLevelName];
}
},
/**
* @private
* Callback that is invoked when all of the configured Profiles have been loaded. Detects the current profile and
* gathers any additional dependencies from that profile, then loads all of those dependencies.
*/
onProfilesLoaded: function() {
var profiles = this.getProfiles(),
length = profiles.length,
instances = [],
requires = this.gatherDependencies(),
current, i, profileDeps;
for (i = 0; i < length; i++) {
instances[i] = Ext.create(profiles[i], {
application: this
});
/*
* Note that we actually require all of the dependencies for all Profiles - this is so that we can produce
* a single build file that will work on all defined Profiles. Although the other classes will be loaded,
* the correct Profile will still be identified and the other classes ignored. While this feels somewhat
* inefficient, the majority of the bulk of an application is likely to be the framework itself. The bigger
* the app though, the bigger the effect of this inefficiency so ideally we will create a way to create and
* load Profile-specific builds in a future release.
*/
profileDeps = instances[i].getDependencies();
requires = requires.concat(profileDeps.all);
if (instances[i].isActive() && !current) {
current = instances[i];
this.setCurrentProfile(current);
this.setControllers(this.getControllers().concat(profileDeps.controller));
this.setModels(this.getModels().concat(profileDeps.model));
this.setViews(this.getViews().concat(profileDeps.view));
this.setStores(this.getStores().concat(profileDeps.store));
}
}
this.setProfileInstances(instances);
Ext.require(requires, this.loadControllerDependencies, this);
},
/**
* @private
* Controllers can also specify dependencies, so we grab them all here and require them.
*/
loadControllerDependencies: function() {
this.instantiateControllers();
var controllers = this.getControllerInstances(),
classes = [],
stores = [],
i, controller, controllerStores, name;
for (name in controllers) {
controller = controllers[name];
controllerStores = controller.getStores();
stores = stores.concat(controllerStores);
classes = classes.concat(controller.getModels().concat(controller.getViews()).concat(controllerStores));
}
this.setStores(this.getStores().concat(stores));
Ext.require(classes, this.onDependenciesLoaded, this);
},
/**
* @private
* Callback that is invoked when all of the Application, Controller and Profile dependencies have been loaded.
* Launches the controllers, then the profile and application.
*/
onDependenciesLoaded: function() {
var me = this,
profile = this.getCurrentProfile(),
launcher = this.getLaunch(),
controllers, name;
this.instantiateStores();
controllers = this.getControllerInstances();
for (name in controllers) {
controllers[name].init(this);
}
if (profile) {
profile.launch();
}
launcher.call(me);
for (name in controllers) {
//<debug warn>
if (controllers[name] && !(controllers[name] instanceof Ext.app.Controller)) {
Ext.Logger.warn("The controller '" + name + "' doesn't have a launch method. Are you sure it extends from Ext.app.Controller?");
} else {
//</debug>
controllers[name].launch(this);
//<debug warn>
}
//</debug>
}
me.redirectTo(window.location.hash.substr(1));
},
/**
* @private
* Gathers up all of the previously computed MVCS dependencies into a single array that we can pass to {@link Ext#require}.
*/
gatherDependencies: function() {
var classes = this.getModels().concat(this.getViews()).concat(this.getControllers());
Ext.each(this.getStores(), function(storeName) {
if (Ext.isString(storeName)) {
classes.push(storeName);
}
}, this);
return classes;
},
/**
* @private
* Should be called after dependencies are loaded, instantiates all of the Stores specified in the {@link #stores}
* config. For each item in the stores array we make sure the Store is instantiated. When strings are specified,
* the corresponding _app/store/StoreName.js_ was loaded so we now instantiate a `MyApp.store.StoreName`, giving it the
* id `StoreName`.
*/
instantiateStores: function() {
var stores = this.getStores(),
length = stores.length,
store, storeClass, storeName, splits, i;
for (i = 0; i < length; i++) {
store = stores[i];
if (Ext.data && Ext.data.Store && !(store instanceof Ext.data.Store)) {
if (Ext.isString(store)) {
storeName = store;
storeClass = Ext.ClassManager.classes[store];
store = {
xclass: store
};
//we don't want to wipe out a configured storeId in the app's Store subclass so need
//to check for this first
if (storeClass.prototype.defaultConfig.storeId === undefined) {
splits = storeName.split('.');
store.id = splits[splits.length - 1];
}
}
stores[i] = Ext.factory(store, Ext.data.Store);
}
}
this.setStores(stores);
},
/**
* @private
* Called once all of our controllers have been loaded
*/
instantiateControllers: function() {
var controllerNames = this.getControllers(),
instances = {},
length = controllerNames.length,
name, i;
for (i = 0; i < length; i++) {
name = controllerNames[i];
instances[name] = Ext.create(name, {
application: this
});
}
return this.setControllerInstances(instances);
},
/**
* @private
* As a convenience developers can locally qualify controller names (e.g. 'MyController' vs
* 'MyApp.controller.MyController'). This just makes sure everything ends up fully qualified
*/
applyControllers: function(controllers) {
return this.getFullyQualified(controllers, 'controller');
},
/**
* @private
* As a convenience developers can locally qualify profile names (e.g. 'MyProfile' vs
* 'MyApp.profile.MyProfile'). This just makes sure everything ends up fully qualified
*/
applyProfiles: function(profiles) {
return this.getFullyQualified(profiles, 'profile');
},
/**
* @private
* Checks that the name configuration has any whitespace, and trims them if found.
*/
applyName: function(name) {
var oldName;
if (name && name.match(/ /g)) {
oldName = name;
name = name.replace(/ /g, "");
// <debug>
Ext.Logger.warn('Attempting to create an application with a name which contains whitespace ("' + oldName + '"). Renamed to "' + name + '".');
// </debug>
}
return name;
},
/**
* @private
* Makes sure the app namespace exists, sets the `app` property of the namespace to this application and sets its
* loading path (checks to make sure the path hadn't already been set via Ext.Loader.setPath)
*/
updateName: function(newName) {
Ext.ClassManager.setNamespace(newName + '.app', this);
if (!Ext.Loader.config.paths[newName]) {
Ext.Loader.setPath(newName, this.getAppFolder());
}
},
/**
* @private
*/
applyRouter: function(config) {
return Ext.factory(config, Ext.app.Router, this.getRouter());
},
/**
* @private
*/
applyHistory: function(config) {
var history = Ext.factory(config, Ext.app.History, this.getHistory());
history.on('change', this.onHistoryChange, this);
return history;
},
/**
* @private
*/
onHistoryChange: function(url) {
this.dispatch(this.getRouter().recognize(url), false);
}
}, function() {
});
/**
* A class to replicate the behavior of the Contextual menu in BlackBerry 10.
*
* More information: http://docs.blackberry.com/en/developers/deliverables/41577/contextual_menus.jsp
*
* var menu = Ext.create('Ext.bb.CrossCut', {
* items: [
* {
* text: 'New',
* iconMask: true,
* iconCls: 'compose'
* },
* {
* text: 'Reply',
* iconMask: true,
* iconCls: 'reply'
* },
* {
* text: 'Settings',
* iconMask: true,
* iconCls: 'settings'
* }
* ]
* });
*/
Ext.define('Ext.bb.CrossCut', {
extend: 'Ext.Sheet',
xtype: 'crosscut',
requires: [
'Ext.Button'
],
config: {
/**
* @hide
*/
top: 0,
/**
* @hide
*/
right: 0,
/**
* @hide
*/
bottom: 0,
/**
* @hide
*/
left: null,
/**
* @hide
*/
enter: 'right',
/**
* @hide
*/
exit: 'right',
/**
* @hide
*/
hideOnMaskTap: true,
/**
* @hide
*/
baseCls: 'bb-crosscut',
/**
* @hide
*/
layout: {
type: 'vbox',
pack: 'middle'
},
/**
* @hide
*/
defaultType: 'button',
/**
* @hide
*/
showAnimation: {
preserveEndState: true,
to: {
width: 275
}
},
/**
* @hide
*/
hideAnimation: {
preserveEndState: true,
to: {
width: 68
}
},
defaults: {
baseCls: 'bb-crosscut-item'
}
}
});
/**
* @private
*/
Ext.define('Ext.carousel.Item', {
extend: 'Ext.Decorator',
config: {
baseCls: 'x-carousel-item',
component: null,
translatable: true
}
});
/**
* A private utility class used by Ext.Carousel to create indicators.
* @private
*/
Ext.define('Ext.carousel.Indicator', {
extend: 'Ext.Component',
xtype : 'carouselindicator',
alternateClassName: 'Ext.Carousel.Indicator',
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'carousel-indicator',
direction: 'horizontal'
},
/**
* @event previous
* Fires when this indicator is tapped on the left half
* @param {Ext.carousel.Indicator} this
*/
/**
* @event next
* Fires when this indicator is tapped on the right half
* @param {Ext.carousel.Indicator} this
*/
initialize: function() {
this.callParent();
this.indicators = [];
this.element.on({
tap: 'onTap',
scope: this
});
},
updateDirection: function(newDirection, oldDirection) {
var baseCls = this.getBaseCls();
this.element.replaceCls(oldDirection, newDirection, baseCls);
if (newDirection === 'horizontal') {
this.setBottom(0);
this.setRight(null);
}
else {
this.setRight(0);
this.setBottom(null);
}
},
addIndicator: function() {
this.indicators.push(this.element.createChild({
tag: 'span'
}));
},
removeIndicator: function() {
var indicators = this.indicators;
if (indicators.length > 0) {
indicators.pop().destroy();
}
},
setActiveIndex: function(index) {
var indicators = this.indicators,
currentActiveIndex = this.activeIndex,
currentActiveItem = indicators[currentActiveIndex],
activeItem = indicators[index],
baseCls = this.getBaseCls();
if (currentActiveItem) {
currentActiveItem.removeCls(baseCls, null, 'active');
}
if (activeItem) {
activeItem.addCls(baseCls, null, 'active');
}
this.activeIndex = index;
return this;
},
// @private
onTap: function(e) {
var touch = e.touch,
box = this.element.getPageBox(),
centerX = box.left + (box.width / 2),
centerY = box.top + (box.height / 2),
direction = this.getDirection();
if ((direction === 'horizontal' && touch.pageX >= centerX) || (direction === 'vertical' && touch.pageY >= centerY)) {
this.fireEvent('next', this);
}
else {
this.fireEvent('previous', this);
}
},
destroy: function() {
var indicators = this.indicators,
i, ln, indicator;
for (i = 0,ln = indicators.length; i < ln; i++) {
indicator = indicators[i];
indicator.destroy();
}
indicators.length = 0;
this.callParent();
}
});
/**
* @private
*/
Ext.define('Ext.util.TranslatableGroup', {
extend: 'Ext.util.translatable.Abstract',
config: {
items: [],
activeIndex: 0,
itemLength: {
x: 0,
y: 0
}
},
applyItems: function(items) {
return Ext.Array.from(items);
},
doTranslate: function(x, y) {
var items = this.getItems(),
activeIndex = this.getActiveIndex(),
itemLength = this.getItemLength(),
itemLengthX = itemLength.x,
itemLengthY = itemLength.y,
useX = typeof x == 'number',
useY = typeof y == 'number',
offset, i, ln, item, translateX, translateY;
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
if (item) {
offset = (i - activeIndex);
if (useX) {
translateX = x + offset * itemLengthX;
}
if (useY) {
translateY = y + offset * itemLengthY;
}
item.translate(translateX, translateY);
}
}
}
});
/**
* @class Ext.carousel.Carousel
* @author Jacky Nguyen <jacky@sencha.com>
*
* Carousels, like [tabs](#!/guide/tabs), are a great way to allow the user to swipe through multiple full-screen pages.
* A Carousel shows only one of its pages at a time but allows you to swipe through with your finger.
*
* Carousels can be oriented either horizontally or vertically and are easy to configure - they just work like any other
* Container. Here's how to set up a simple horizontal Carousel:
*
* @example
* Ext.create('Ext.Carousel', {
* fullscreen: true,
*
* defaults: {
* styleHtmlContent: true
* },
*
* items: [
* {
* html : 'Item 1',
* style: 'background-color: #5E99CC'
* },
* {
* html : 'Item 2',
* style: 'background-color: #759E60'
* },
* {
* html : 'Item 3'
* }
* ]
* });
*
* We can also make Carousels orient themselves vertically:
*
* @example preview
* Ext.create('Ext.Carousel', {
* fullscreen: true,
* direction: 'vertical',
*
* defaults: {
* styleHtmlContent: true
* },
*
* items: [
* {
* html : 'Item 1',
* style: 'background-color: #759E60'
* },
* {
* html : 'Item 2',
* style: 'background-color: #5E99CC'
* }
* ]
* });
*
* ### Common Configurations
* * {@link #ui} defines the style of the carousel
* * {@link #direction} defines the direction of the carousel
* * {@link #indicator} defines if the indicator show be shown
*
* ### Useful Methods
* * {@link #next} moves to the next card
* * {@link #previous} moves to the previous card
* * {@link #setActiveItem} moves to the passed card
*
* ## Further Reading
*
* For more information about Carousels see the [Carousel guide](#!/guide/carousel).
*
* @aside guide carousel
* @aside example carousel
*/
Ext.define('Ext.carousel.Carousel', {
extend: 'Ext.Container',
alternateClassName: 'Ext.Carousel',
xtype: 'carousel',
requires: [
'Ext.fx.easing.EaseOut',
'Ext.carousel.Item',
'Ext.carousel.Indicator',
'Ext.util.TranslatableGroup'
],
config: {
/**
* @cfg layout
* Hide layout config in Carousel. It only causes confusion.
* @accessor
* @private
*/
/**
* @cfg
* @inheritdoc
*/
baseCls: 'x-carousel',
/**
* @cfg {String} direction
* The direction of the Carousel, either 'horizontal' or 'vertical'.
* @accessor
*/
direction: 'horizontal',
directionLock: false,
animation: {
duration: 250,
easing: {
type: 'ease-out'
}
},
/**
* @cfg {Boolean} indicator
* Provides an indicator while toggling between child items to let the user
* know where they are in the card stack.
* @accessor
*/
indicator: true,
/**
* @cfg {String} ui
* Style options for Carousel. Default is 'dark'. 'light' is also available.
* @accessor
*/
ui: 'dark',
itemConfig: {},
bufferSize: 1,
itemLength: null
},
itemLength: 0,
offset: 0,
flickStartOffset: 0,
flickStartTime: 0,
dragDirection: 0,
count: 0,
painted: false,
activeIndex: -1,
beforeInitialize: function() {
this.element.on({
dragstart: 'onDragStart',
drag: 'onDrag',
dragend: 'onDragEnd',
scope: this
});
this.element.on('resize', 'onSizeChange', this);
this.carouselItems = [];
this.orderedCarouselItems = [];
this.inactiveCarouselItems = [];
this.hiddenTranslation = 0;
},
updateBufferSize: function(size) {
var ItemClass = Ext.carousel.Item,
total = size * 2 + 1,
isRendered = this.isRendered(),
innerElement = this.innerElement,
items = this.carouselItems,
ln = items.length,
itemConfig = this.getItemConfig(),
itemLength = this.getItemLength(),
direction = this.getDirection(),
setterName = direction === 'horizontal' ? 'setWidth' : 'setHeight',
i, item;
for (i = ln; i < total; i++) {
item = Ext.factory(itemConfig, ItemClass);
if (itemLength) {
item[setterName].call(item, itemLength);
}
item.setLayoutSizeFlags(this.LAYOUT_BOTH);
items.push(item);
innerElement.append(item.renderElement);
if (isRendered && item.setRendered(true)) {
item.fireEvent('renderedchange', this, item, true);
}
}
this.getTranslatable().setActiveIndex(size);
},
setRendered: function(rendered) {
var wasRendered = this.rendered;
if (rendered !== wasRendered) {
this.rendered = rendered;
var items = this.items.items,
carouselItems = this.carouselItems,
i, ln, item;
for (i = 0,ln = items.length; i < ln; i++) {
item = items[i];
if (!item.isInnerItem()) {
item.setRendered(rendered);
}
}
for (i = 0,ln = carouselItems.length; i < ln; i++) {
carouselItems[i].setRendered(rendered);
}
return true;
}
return false;
},
onSizeChange: function() {
this.refreshSizing();
this.refreshCarouselItems();
this.refreshActiveItem();
},
onItemAdd: function(item, index) {
this.callParent(arguments);
var innerIndex = this.getInnerItems().indexOf(item),
indicator = this.getIndicator();
if (indicator && item.isInnerItem()) {
indicator.addIndicator();
}
if (innerIndex <= this.getActiveIndex()) {
this.refreshActiveIndex();
}
if (this.isIndexDirty(innerIndex) && !this.isItemsInitializing) {
this.refreshActiveItem();
}
},
doItemLayoutAdd: function(item) {
if (item.isInnerItem()) {
return;
}
this.callParent(arguments);
},
onItemRemove: function(item, index) {
this.callParent(arguments);
var innerIndex = this.getInnerItems().indexOf(item),
indicator = this.getIndicator(),
carouselItems = this.carouselItems,
i, ln, carouselItem;
if (item.isInnerItem() && indicator) {
indicator.removeIndicator();
}
if (innerIndex <= this.getActiveIndex()) {
this.refreshActiveIndex();
}
if (this.isIndexDirty(innerIndex)) {
for (i = 0,ln = carouselItems.length; i < ln; i++) {
carouselItem = carouselItems[i];
if (carouselItem.getComponent() === item) {
carouselItem.setComponent(null);
}
}
this.refreshActiveItem();
}
},
doItemLayoutRemove: function(item) {
if (item.isInnerItem()) {
return;
}
this.callParent(arguments);
},
onInnerItemMove: function(item, toIndex, fromIndex) {
if ((this.isIndexDirty(toIndex) || this.isIndexDirty(fromIndex))) {
this.refreshActiveItem();
}
},
doItemLayoutMove: function(item) {
if (item.isInnerItem()) {
return;
}
this.callParent(arguments);
},
isIndexDirty: function(index) {
var activeIndex = this.getActiveIndex(),
bufferSize = this.getBufferSize();
return (index >= activeIndex - bufferSize && index <= activeIndex + bufferSize);
},
getTranslatable: function() {
var translatable = this.translatable;
if (!translatable) {
this.translatable = translatable = new Ext.util.TranslatableGroup;
translatable.setItems(this.orderedCarouselItems);
translatable.on('animationend', 'onAnimationEnd', this);
}
return translatable;
},
onDragStart: function(e) {
var direction = this.getDirection(),
absDeltaX = e.absDeltaX,
absDeltaY = e.absDeltaY,
directionLock = this.getDirectionLock();
this.isDragging = true;
if (directionLock) {
if ((direction === 'horizontal' && absDeltaX > absDeltaY)
|| (direction === 'vertical' && absDeltaY > absDeltaX)) {
e.stopPropagation();
}
else {
this.isDragging = false;
return;
}
}
this.getTranslatable().stopAnimation();
this.dragStartOffset = this.offset;
this.dragDirection = 0;
},
onDrag: function(e) {
if (!this.isDragging) {
return;
}
var startOffset = this.dragStartOffset,
direction = this.getDirection(),
delta = direction === 'horizontal' ? e.deltaX : e.deltaY,
lastOffset = this.offset,
flickStartTime = this.flickStartTime,
dragDirection = this.dragDirection,
now = Ext.Date.now(),
currentActiveIndex = this.getActiveIndex(),
maxIndex = this.getMaxItemIndex(),
lastDragDirection = dragDirection,
offset;
if ((currentActiveIndex === 0 && delta > 0) || (currentActiveIndex === maxIndex && delta < 0)) {
delta *= 0.5;
}
offset = startOffset + delta;
if (offset > lastOffset) {
dragDirection = 1;
}
else if (offset < lastOffset) {
dragDirection = -1;
}
if (dragDirection !== lastDragDirection || (now - flickStartTime) > 300) {
this.flickStartOffset = lastOffset;
this.flickStartTime = now;
}
this.dragDirection = dragDirection;
this.setOffset(offset);
},
onDragEnd: function(e) {
if (!this.isDragging) {
return;
}
this.onDrag(e);
this.isDragging = false;
var now = Ext.Date.now(),
itemLength = this.itemLength,
threshold = itemLength / 2,
offset = this.offset,
activeIndex = this.getActiveIndex(),
maxIndex = this.getMaxItemIndex(),
animationDirection = 0,
flickDistance = offset - this.flickStartOffset,
flickDuration = now - this.flickStartTime,
indicator = this.getIndicator(),
velocity;
if (flickDuration > 0 && Math.abs(flickDistance) >= 10) {
velocity = flickDistance / flickDuration;
if (Math.abs(velocity) >= 1) {
if (velocity < 0 && activeIndex < maxIndex) {
animationDirection = -1;
}
else if (velocity > 0 && activeIndex > 0) {
animationDirection = 1;
}
}
}
if (animationDirection === 0) {
if (activeIndex < maxIndex && offset < -threshold) {
animationDirection = -1;
}
else if (activeIndex > 0 && offset > threshold) {
animationDirection = 1;
}
}
if (indicator) {
indicator.setActiveIndex(activeIndex - animationDirection);
}
this.animationDirection = animationDirection;
this.setOffsetAnimated(animationDirection * itemLength);
},
applyAnimation: function(animation) {
animation.easing = Ext.factory(animation.easing, Ext.fx.easing.EaseOut);
return animation;
},
updateDirection: function(direction) {
var indicator = this.getIndicator();
this.currentAxis = (direction === 'horizontal') ? 'x' : 'y';
if (indicator) {
indicator.setDirection(direction);
}
},
/**
* @private
* @chainable
*/
setOffset: function(offset) {
this.offset = offset;
this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset);
return this;
},
/**
* @private
* @return {Ext.carousel.Carousel} this
* @chainable
*/
setOffsetAnimated: function(offset) {
var indicator = this.getIndicator();
if (indicator) {
indicator.setActiveIndex(this.getActiveIndex() - this.animationDirection);
}
this.offset = offset;
this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset, this.getAnimation());
return this;
},
onAnimationEnd: function(translatable) {
var currentActiveIndex = this.getActiveIndex(),
animationDirection = this.animationDirection,
axis = this.currentAxis,
currentOffset = translatable[axis],
itemLength = this.itemLength,
offset;
if (animationDirection === -1) {
offset = itemLength + currentOffset;
}
else if (animationDirection === 1) {
offset = currentOffset - itemLength;
}
else {
offset = currentOffset;
}
offset -= this.itemOffset;
this.offset = offset;
this.setActiveItem(currentActiveIndex - animationDirection);
},
refresh: function() {
this.refreshSizing();
this.refreshActiveItem();
},
refreshSizing: function() {
var element = this.element,
itemLength = this.getItemLength(),
translatableItemLength = {
x: 0,
y: 0
},
itemOffset, containerSize;
if (this.getDirection() === 'horizontal') {
containerSize = element.getWidth();
}
else {
containerSize = element.getHeight();
}
this.hiddenTranslation = -containerSize;
if (itemLength === null) {
itemLength = containerSize;
itemOffset = 0;
}
else {
itemOffset = (containerSize - itemLength) / 2;
}
this.itemLength = itemLength;
this.itemOffset = itemOffset;
translatableItemLength[this.currentAxis] = itemLength;
this.getTranslatable().setItemLength(translatableItemLength);
},
refreshOffset: function() {
this.setOffset(this.offset);
},
refreshActiveItem: function() {
this.doSetActiveItem(this.getActiveItem());
},
/**
* Returns the index of the currently active card.
* @return {Number} The index of the currently active card.
*/
getActiveIndex: function() {
return this.activeIndex;
},
refreshActiveIndex: function() {
this.activeIndex = this.getInnerItemIndex(this.getActiveItem());
},
refreshCarouselItems: function() {
var items = this.carouselItems,
i, ln, item;
for (i = 0,ln = items.length; i < ln; i++) {
item = items[i];
item.getTranslatable().refresh();
}
this.refreshInactiveCarouselItems();
},
refreshInactiveCarouselItems: function() {
var items = this.inactiveCarouselItems,
hiddenTranslation = this.hiddenTranslation,
axis = this.currentAxis,
i, ln, item;
for (i = 0,ln = items.length; i < ln; i++) {
item = items[i];
item.translateAxis(axis, hiddenTranslation);
}
},
/**
* @private
* @return {Number}
*/
getMaxItemIndex: function() {
return this.innerItems.length - 1;
},
/**
* @private
* @return {Number}
*/
getInnerItemIndex: function(item) {
return this.innerItems.indexOf(item);
},
/**
* @private
* @return {Object}
*/
getInnerItemAt: function(index) {
return this.innerItems[index];
},
/**
* @private
* @return {Object}
*/
applyActiveItem: function() {
var activeItem = this.callParent(arguments),
activeIndex;
if (activeItem) {
activeIndex = this.getInnerItemIndex(activeItem);
if (activeIndex !== -1) {
this.activeIndex = activeIndex;
return activeItem;
}
}
},
doSetActiveItem: function(activeItem) {
var activeIndex = this.getActiveIndex(),
maxIndex = this.getMaxItemIndex(),
indicator = this.getIndicator(),
bufferSize = this.getBufferSize(),
carouselItems = this.carouselItems.slice(),
orderedCarouselItems = this.orderedCarouselItems,
visibleIndexes = {},
visibleItems = {},
visibleItem, component, id, i, index, ln, carouselItem;
if (carouselItems.length === 0) {
return;
}
this.callParent(arguments);
orderedCarouselItems.length = 0;
if (activeItem) {
id = activeItem.getId();
visibleItems[id] = activeItem;
visibleIndexes[id] = bufferSize;
if (activeIndex > 0) {
for (i = 1; i <= bufferSize; i++) {
index = activeIndex - i;
if (index >= 0) {
visibleItem = this.getInnerItemAt(index);
id = visibleItem.getId();
visibleItems[id] = visibleItem;
visibleIndexes[id] = bufferSize - i;
}
else {
break;
}
}
}
if (activeIndex < maxIndex) {
for (i = 1; i <= bufferSize; i++) {
index = activeIndex + i;
if (index <= maxIndex) {
visibleItem = this.getInnerItemAt(index);
id = visibleItem.getId();
visibleItems[id] = visibleItem;
visibleIndexes[id] = bufferSize + i;
}
else {
break;
}
}
}
for (i = 0,ln = carouselItems.length; i < ln; i++) {
carouselItem = carouselItems[i];
component = carouselItem.getComponent();
if (component) {
id = component.getId();
if (visibleIndexes.hasOwnProperty(id)) {
carouselItems.splice(i, 1);
i--;
ln--;
delete visibleItems[id];
orderedCarouselItems[visibleIndexes[id]] = carouselItem;
}
}
}
for (id in visibleItems) {
if (visibleItems.hasOwnProperty(id)) {
visibleItem = visibleItems[id];
carouselItem = carouselItems.pop();
carouselItem.setComponent(visibleItem);
orderedCarouselItems[visibleIndexes[id]] = carouselItem;
}
}
}
this.inactiveCarouselItems.length = 0;
this.inactiveCarouselItems = carouselItems;
this.refreshOffset();
this.refreshInactiveCarouselItems();
if (indicator) {
indicator.setActiveIndex(activeIndex);
}
},
/**
* Switches to the next card.
* @return {Ext.carousel.Carousel} this
* @chainable
*/
next: function() {
this.setOffset(0);
if (this.activeIndex === this.getMaxItemIndex()) {
return this;
}
this.animationDirection = -1;
this.setOffsetAnimated(-this.itemLength);
return this;
},
/**
* Switches to the previous card.
* @return {Ext.carousel.Carousel} this
* @chainable
*/
previous: function() {
this.setOffset(0);
if (this.activeIndex === 0) {
return this;
}
this.animationDirection = 1;
this.setOffsetAnimated(this.itemLength);
return this;
},
// @private
applyIndicator: function(indicator, currentIndicator) {
return Ext.factory(indicator, Ext.carousel.Indicator, currentIndicator);
},
// @private
updateIndicator: function(indicator) {
if (indicator) {
this.insertFirst(indicator);
indicator.setUi(this.getUi());
indicator.on({
next: 'next',
previous: 'previous',
scope: this
});
}
},
destroy: function() {
var carouselItems = this.carouselItems.slice();
this.carouselItems.length = 0;
Ext.destroy(carouselItems, this.getIndicator(), this.translatable);
this.callParent();
delete this.carouselItems;
}
}, function() {
});
/**
* @class Ext.carousel.Infinite
* @author Jacky Nguyen <jacky@sencha.com>
* @private
*
* The true infinite implementation of Carousel, private for now until it's stable to be public
*/
Ext.define('Ext.carousel.Infinite', {
extend: 'Ext.carousel.Carousel',
config: {
indicator: null,
maxItemIndex: Infinity,
innerItemConfig: {}
},
applyIndicator: function(indicator) {
//<debug error>
if (indicator) {
Ext.Logger.error("'indicator' in Infinite Carousel implementation is not currently supported", this);
}
//</debug>
return;
},
updateBufferSize: function(size) {
this.callParent(arguments);
var total = size * 2 + 1,
ln = this.innerItems.length,
innerItemConfig = this.getInnerItemConfig(),
i;
this.isItemsInitializing = true;
for (i = ln; i < total; i++) {
this.doAdd(this.factoryItem(innerItemConfig));
}
this.isItemsInitializing = false;
this.rebuildInnerIndexes();
this.refreshActiveItem();
},
updateMaxItemIndex: function(maxIndex, oldMaxIndex) {
if (oldMaxIndex !== undefined) {
var activeIndex = this.getActiveIndex();
if (activeIndex > maxIndex) {
this.setActiveItem(maxIndex);
}
else {
this.rebuildInnerIndexes(activeIndex);
this.refreshActiveItem();
}
}
},
rebuildInnerIndexes: function(activeIndex) {
var indexToItem = this.innerIndexToItem,
idToIndex = this.innerIdToIndex,
items = this.innerItems.slice(),
ln = items.length,
bufferSize = this.getBufferSize(),
maxIndex = this.getMaxItemIndex(),
changedIndexes = [],
i, oldIndex, index, id, item;
if (activeIndex === undefined) {
this.innerIndexToItem = indexToItem = {};
this.innerIdToIndex = idToIndex = {};
for (i = 0; i < ln; i++) {
item = items[i];
id = item.getId();
idToIndex[id] = i;
indexToItem[i] = item;
this.fireEvent('itemindexchange', this, item, i, -1);
}
}
else {
for (i = activeIndex - bufferSize; i <= activeIndex + bufferSize; i++) {
if (i >= 0 && i <= maxIndex) {
if (indexToItem.hasOwnProperty(i)) {
Ext.Array.remove(items, indexToItem[i]);
continue;
}
changedIndexes.push(i);
}
}
for (i = 0,ln = changedIndexes.length; i < ln; i++) {
item = items[i];
id = item.getId();
index = changedIndexes[i];
oldIndex = idToIndex[id];
delete indexToItem[oldIndex];
idToIndex[id] = index;
indexToItem[index] = item;
this.fireEvent('itemindexchange', this, item, index, oldIndex);
}
}
},
reset: function() {
this.rebuildInnerIndexes();
this.setActiveItem(0);
},
refreshItems: function() {
var items = this.innerItems,
idToIndex = this.innerIdToIndex,
index, item, i, ln;
for (i = 0,ln = items.length; i < ln; i++) {
item = items[i];
index = idToIndex[item.getId()];
this.fireEvent('itemindexchange', this, item, index, -1);
}
},
getInnerItemIndex: function(item) {
var index = this.innerIdToIndex[item.getId()];
return (typeof index == 'number') ? index : -1;
},
getInnerItemAt: function(index) {
return this.innerIndexToItem[index];
},
applyActiveItem: function(activeItem) {
this.getItems();
this.getBufferSize();
var maxIndex = this.getMaxItemIndex(),
currentActiveIndex = this.getActiveIndex();
if (typeof activeItem == 'number') {
activeItem = Math.max(0, Math.min(activeItem, maxIndex));
if (activeItem === currentActiveIndex) {
return;
}
this.activeIndex = activeItem;
this.rebuildInnerIndexes(activeItem);
activeItem = this.getInnerItemAt(activeItem);
}
if (activeItem) {
return this.callParent([activeItem]);
}
}
});
(function () {
if (!Ext.global.Float32Array) {
// Typed Array polyfill
var Float32Array = function (array) {
if (typeof array === 'number') {
this.length = array;
} else if ('length' in array) {
this.length = array.length;
for (var i = 0, len = array.length; i < len; i++) {
this[i] = +array[i];
}
}
};
Float32Array.prototype = [];
Ext.global.Float32Array = Float32Array;
}
})();
/**
* Utility class providing mathematics functionalities through all the draw package.
*/
Ext.define('Ext.draw.Draw', {
singleton: true,
radian: Math.PI / 180,
pi2: Math.PI * 2,
/**
* Function that returns its first element.
* @param {Mixed} a
* @return {Mixed}
*/
reflectFn: function (a) {
return a;
},
/**
* Converting degrees to radians.
* @param {Number} degrees
* @return {Number}
*/
rad: function (degrees) {
return degrees % 360 * Math.PI / 180;
},
/**
* Converting radians to degrees.
* @param {Number} radian
* @return {Number}
*/
degrees: function (radian) {
return radian * 180 / Math.PI % 360;
},
/**
*
* @param bbox1
* @param bbox2
* @param [padding]
* @return {Boolean}
*/
isBBoxIntersect: function (bbox1, bbox2, padding) {
padding = padding || 0;
return (Math.max(bbox1.x, bbox2.x) - padding > Math.min(bbox1.x + bbox1.width, bbox2.x + bbox2.width)) ||
(Math.max(bbox1.y, bbox2.y) - padding > Math.min(bbox1.y + bbox1.height, bbox2.y + bbox2.height));
},
/**
* Natural cubic spline interpolation.
* This algorithm runs in linear time.
*
* @param {Array} points Array of numbers.
*/
spline: function (points) {
var i, j, ln = points.length,
nd, d, y, ny,
r = 0,
zs = new Float32Array(points.length),
result = new Float32Array(points.length * 3 - 2);
zs[0] = 0;
zs[ln - 1] = 0;
for (i = 1; i < ln - 1; i++) {
zs[i] = (points[i + 1] + points[i - 1] - 2 * points[i]) - zs[i - 1];
r = 1 / (4 - r);
zs[i] *= r;
}
for (i = ln - 2; i > 0; i--) {
r = 3.732050807568877 + 48.248711305964385 / (-13.928203230275537 + Math.pow(0.07179676972449123, i));
zs[i] -= zs[i + 1] * r;
}
ny = points[0];
nd = ny - zs[0];
for (i = 0, j = 0; i < ln - 1; j += 3) {
y = ny;
d = nd;
i++;
ny = points[i];
nd = ny - zs[i];
result[j] = y;
result[j + 1] = (nd + 2 * d) / 3;
result[j + 2] = (nd * 2 + d) / 3;
}
result[j] = ny;
return result;
},
/**
* @method
* @private
* Work around for iOS.
* Nested 3d-transforms seems to prevent the redraw inside it until some event is fired.
*/
updateIOS: Ext.os.is.iOS ? function () {
Ext.getBody().createChild({id: 'frame-workaround', style: 'position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px; background: rgba(0,0,0,0.001); z-index: 100000'});
Ext.draw.Animator.schedule(function () {Ext.get('frame-workaround').destroy();});
} : Ext.emptyFn
});
/**
* Limited cache is a size limited cache container that stores limited number of objects.
*
* When {@link #get} is called, the container will try to find the object in the list.
* If failed it will call the {@link #feeder} to create that object. If there are too many
* objects in the container, the old ones are removed.
*
* __Note:__ This is not using a Least Recently Used policy due to simplicity and performance consideration.
*/
Ext.define("Ext.draw.LimitedCache", {
config: {
/**
* @cfg {Number}
* The amount limit of the cache.
*/
limit: 40,
/**
* @cfg {Function}
* Function that generates the object when look-up failed.
* @return {Number}
*/
feeder: function () {
return 0;
},
/**
* @cfg {Object}
* The scope for {@link #feeder}
*/
scope: null
},
cache: null,
constructor: function (config) {
this.cache = {};
this.cache.list = [];
this.cache.tail = 0;
this.initConfig(config);
},
/**
* Get a cached object.
* @param {String} id
* @param {Mixed...} args Arguments appended to feeder.
* @return {Object}
*/
get: function (id) {
// TODO: Implement cache hit optimization
var cache = this.cache,
limit = this.getLimit(),
feeder = this.getFeeder(),
scope = this.getScope() || this;
if (cache[id]) {
return cache[id].value;
}
if (cache.list[cache.tail]) {
delete cache[cache.list[cache.tail].cacheId];
}
cache[id] = cache.list[cache.tail] = {
value: feeder.apply(scope, Array.prototype.slice.call(arguments, 1)),
cacheId: id
};
cache.tail++;
if (cache.tail === limit) {
cache.tail = 0;
}
return cache[id].value;
},
/**
* Clear all the objects.
*/
clear: function () {
this.cache = {};
this.cache.list = [];
this.cache.tail = 0;
}
});
/**
* @class Ext.draw.gradient.Gradient
*
* Creates a gradient.
*/
Ext.define("Ext.draw.gradient.Gradient", {
requires: ["Ext.draw.LimitedCache"],
mixins: {
identifiable: 'Ext.mixin.Identifiable'
},
identifiablePrefix: 'ext-gradient-',
isGradient: true,
statics: {
gradientCache: null
},
config: {
/**
* @cfg {Array/Object} Defines the stops of the gradient.
*/
stops: []
},
applyStops: function (newStops) {
var stops = [],
ln = newStops.length,
i, stop, color;
for (i = 0; i < ln; i++) {
stop = newStops[i];
color = Ext.draw.Color.fly(stop.color || 'none');
stops.push({
offset: Math.min(1, Math.max(0, 'offset' in stop ? stop.offset : stop.position || 0)),
color: color.toString()
});
}
stops.sort(function (a, b) {
return a.offset - b.offset;
});
return stops;
},
onClassExtended: function (subClass, member) {
if (!member.alias && member.type) {
member.alias = 'gradient.' + member.type;
}
},
constructor: function (config) {
config = config || {};
this.gradientCache = new Ext.draw.LimitedCache({
feeder: function (gradient, ctx, bbox) {
return gradient.generateGradient(ctx, bbox);
},
scope: this
});
this.initConfig(config);
this.id = config.id;
this.getId();
},
/**
* @protected
* Generates the gradient for the given context.
* @param ctx The context.
* @param bbox
* @return {Object}
*/
generateGradient: Ext.emptyFn,
/**
* @private
* @param ctx
* @param bbox
* @return {*}
*/
getGradient: function (ctx, bbox) {
return this.gradientCache.get(this.id + ',' + bbox.x + ',' + bbox.y + ',' + bbox.width + ',' + bbox.height, this, ctx, bbox);
},
/**
* @private
*/
clearCache: function () {
this.gradientCache.clear();
}
});
(function () {
/**
* Represents an RGB color and provides helper functions on it e.g. to get
* color components in HSL color space.
*/
Ext.define('Ext.draw.Color', {
statics: {
colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
rgbToHexRe: /\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
rgbaToHexRe: /\s*rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\.\d]+)\)/,
hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/
},
isColor: true,
/**
* @cfg {Number} lightnessFactor
*
* The default factor to compute the lighter or darker color.
*/
lightnessFactor: 0.2,
/**
* @constructor
* @param {Number} red Red component (0..255)
* @param {Number} green Green component (0..255)
* @param {Number} blue Blue component (0..255)
* @param {Number} [alpha=1] (optional) Alpha component (0..1)
*/
constructor: function (red, green, blue, alpha) {
this.setRGB(red, green, blue, alpha);
},
setRGB: function (red, green, blue, alpha) {
var me = this;
me.r = Math.min(255, Math.max(0, red));
me.g = Math.min(255, Math.max(0, green));
me.b = Math.min(255, Math.max(0, blue));
if (alpha === undefined) {
me.a = 1;
} else {
me.a = Math.min(1, Math.max(0, alpha));
}
},
/**
* Returns the gray value (0 to 255) of the color.
*
* The gray value is calculated using the formula r*0.3 + g*0.59 + b*0.11.
*
* @return {Number}
*/
getGrayscale: function () {
// http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
return this.r * 0.3 + this.g * 0.59 + this.b * 0.11;
},
/**
* Get the equivalent HSL components of the color.
* @param {Array} [target] Optional array to receive the values.
* @return {Array}
*/
getHSL: function (target) {
var me = this,
r = me.r / 255,
g = me.g / 255,
b = me.b / 255,
max = Math.max(r, g, b),
min = Math.min(r, g, b),
delta = max - min,
h,
s = 0,
l = 0.5 * (max + min);
// min==max means achromatic (hue is undefined)
if (min !== max) {
s = (l < 0.5) ? delta / (max + min) : delta / (2 - max - min);
if (r === max) {
h = 60 * (g - b) / delta;
} else if (g === max) {
h = 120 + 60 * (b - r) / delta;
} else {
h = 240 + 60 * (r - g) / delta;
}
if (h < 0) {
h += 360;
}
if (h >= 360) {
h -= 360;
}
}
if (target) {
target[0] = h;
target[1] = s;
target[2] = l;
} else {
target = [h, s, l];
}
return target;
},
/**
* Set current color based on the specified HSL values.
*
* @param {Number} h Hue component (0..359)
* @param {Number} s Saturation component (0..1)
* @param {Number} l Lightness component (0..1)
* @return this
*/
setHSL: function (h, s, l) {
var c, x, m,
abs = Math.abs,
floor = Math.floor;
h = (h % 360 + 360 ) % 360;
s = s > 1 ? 1 : s < 0 ? 0 : s;
l = l > 1 ? 1 : l < 0 ? 0 : l;
if (s === 0 || h === null) {
l *= 255;
this.setRGB(l, l, l);
}
else {
// http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
// C is the chroma
// X is the second largest component
// m is the lightness adjustment
h /= 60;
c = s * (1 - abs(2 * l - 1));
x = c * (1 - abs(h - 2 * floor(h / 2) - 1));
m = l - c / 2;
m *= 255;
c *= 255;
x *= 255;
switch (floor(h)) {
case 0:
this.setRGB(c + m, x + m, m);
break;
case 1:
this.setRGB(x + m, c + m, m);
break;
case 2:
this.setRGB(m, c + m, x + m);
break;
case 3:
this.setRGB(m, x + m, c + m);
break;
case 4:
this.setRGB(x + m, m, c + m);
break;
case 5:
this.setRGB(c + m, m, x + m);
break;
}
}
return this;
},
/**
* Return a new color that is lighter than this color.
* @param {Number} [factor=0.2] Lighter factor (0..1).
* @return {Ext.draw.Color}
*/
createLighter: function (factor) {
var hsl = this.getHSL();
factor = factor || this.lightnessFactor;
// COMPAT Ext.util.Numbers -> Ext.Number
hsl[2] = hsl[2] + factor;
if (hsl[2] > 1) {
hsl[2] = 1;
} else if (hsl[2] < 0) {
hsl[2] = 0;
}
return Ext.draw.Color.fromHSL(hsl[0], hsl[1], hsl[2]);
},
/**
* Return a new color that is darker than this color.
* @param {Number} [factor=0.2] Darker factor (0..1).
* @return {Ext.draw.Color}
*/
createDarker: function (factor) {
factor = factor || this.lightnessFactor;
return this.createLighter(-factor);
},
/**
* Return the color in the hex format, i.e. '#rrggbb'.
* @return {String}
*/
toString: function () {
if (this.a === 1) {
var me = this,
round = Math.round,
r = round(me.r).toString(16),
g = round(me.g).toString(16),
b = round(me.b).toString(16);
r = (r.length === 1) ? '0' + r : r;
g = (g.length === 1) ? '0' + g : g;
b = (b.length === 1) ? '0' + b : b;
return ['#', r, g, b].join('');
} else {
return 'rgba(' + [Math.round(this.r), Math.round(this.g), Math.round(this.b), this.a].join(',') + ')';
}
},
/**
* Convert a color to hexadecimal format.
*
* @param {String/Array} color The color value (i.e 'rgb(255, 255, 255)', 'color: #ffffff').
* Can also be an Array, in this case the function handles the first member.
* @return {String} The color in hexadecimal format.
*/
toHex: function (color) {
if (Ext.isArray(color)) {
color = color[0];
}
if (!Ext.isString(color)) {
return '';
}
if (color.substr(0, 1) === '#') {
return color;
}
var digits = Ext.draw.Color.colorToHexRe.exec(color);
if (Ext.isArray(digits)) {
var red = parseInt(digits[2], 10),
green = parseInt(digits[3], 10),
blue = parseInt(digits[4], 10),
rgb = blue | (green << 8) | (red << 16);
return digits[1] + '#' + ("000000" + rgb.toString(16)).slice(-6);
}
else {
return '';
}
},
/**
* Parse the string and set current color.
*
* Supported formats: '#rrggbb', '#rgb', and 'rgb(r,g,b)'.
*
* If the string is not recognized, an `undefined` will be returned instead.
*
* @param {String} str Color in string.
* @return this
*/
setFromString: function (str) {
var values, r, g, b, a = 1,
parse = parseInt;
if (str === 'none') {
this.r = this.g = this.b = this.a = 0;
return this;
}
if ((str.length === 4 || str.length === 7) && str.substr(0, 1) === '#') {
values = str.match(Ext.draw.Color.hexRe);
if (values) {
r = parse(values[1], 16) >> 0;
g = parse(values[2], 16) >> 0;
b = parse(values[3], 16) >> 0;
if (str.length === 4) {
r += (r * 16);
g += (g * 16);
b += (b * 16);
}
}
}
else if ((values = str.match(Ext.draw.Color.rgbToHexRe))) {
r = +values[1];
g = +values[2];
b = +values[3];
} else if ((values = str.match(Ext.draw.Color.rgbaToHexRe))) {
r = +values[1];
g = +values[2];
b = +values[3];
a = +values[4];
} else {
if (Ext.draw.Color.ColorList.hasOwnProperty(str.toLowerCase())) {
return this.setFromString(Ext.draw.Color.ColorList[str.toLowerCase()]);
}
}
if (typeof r === 'undefined') {
return this;
}
this.r = r;
this.g = g;
this.b = b;
this.a = a;
return this;
}
}, function () {
var flyColor = new this();
// TODO(zhangbei): do we have a better way to convert color names to rgb?
this.addStatics({
/**
* Returns a flyweight instance of Ext.draw.Color.
*
* Can be called with either a CSS color string or with separate
* arguments for red, green, blue, alpha.
*
* @param {Number/String} red Red component (0..255) or CSS color string.
* @param {Number} [green] Green component (0..255)
* @param {Number} [blue] Blue component (0..255)
* @param {Number} [alpha=1] Alpha component (0..1)
* @return {Ext.draw.Color}
* @static
*/
fly: function (r, g, b, a) {
switch (arguments.length) {
case 1:
flyColor.setFromString(r);
break;
case 3:
case 4:
flyColor.setRGB(r, g, b, a);
break;
default:
return null;
}
return flyColor;
},
ColorList: {
"aliceblue": "#f0f8ff", "antiquewhite": "#faebd7", "aqua": "#00ffff", "aquamarine": "#7fffd4", "azure": "#f0ffff",
"beige": "#f5f5dc", "bisque": "#ffe4c4", "black": "#000000", "blanchedalmond": "#ffebcd", "blue": "#0000ff", "blueviolet": "#8a2be2", "brown": "#a52a2a", "burlywood": "#deb887",
"cadetblue": "#5f9ea0", "chartreuse": "#7fff00", "chocolate": "#d2691e", "coral": "#ff7f50", "cornflowerblue": "#6495ed", "cornsilk": "#fff8dc", "crimson": "#dc143c", "cyan": "#00ffff",
"darkblue": "#00008b", "darkcyan": "#008b8b", "darkgoldenrod": "#b8860b", "darkgray": "#a9a9a9", "darkgreen": "#006400", "darkkhaki": "#bdb76b", "darkmagenta": "#8b008b", "darkolivegreen": "#556b2f",
"darkorange": "#ff8c00", "darkorchid": "#9932cc", "darkred": "#8b0000", "darksalmon": "#e9967a", "darkseagreen": "#8fbc8f", "darkslateblue": "#483d8b", "darkslategray": "#2f4f4f", "darkturquoise": "#00ced1",
"darkviolet": "#9400d3", "deeppink": "#ff1493", "deepskyblue": "#00bfff", "dimgray": "#696969", "dodgerblue": "#1e90ff",
"firebrick": "#b22222", "floralwhite": "#fffaf0", "forestgreen": "#228b22", "fuchsia": "#ff00ff",
"gainsboro": "#dcdcdc", "ghostwhite": "#f8f8ff", "gold": "#ffd700", "goldenrod": "#daa520", "gray": "#808080", "green": "#008000", "greenyellow": "#adff2f",
"honeydew": "#f0fff0", "hotpink": "#ff69b4",
"indianred ": "#cd5c5c", "indigo ": "#4b0082", "ivory": "#fffff0", "khaki": "#f0e68c",
"lavender": "#e6e6fa", "lavenderblush": "#fff0f5", "lawngreen": "#7cfc00", "lemonchiffon": "#fffacd", "lightblue": "#add8e6", "lightcoral": "#f08080", "lightcyan": "#e0ffff", "lightgoldenrodyellow": "#fafad2",
"lightgray": "#d3d3d3", "lightgrey": "#d3d3d3", "lightgreen": "#90ee90", "lightpink": "#ffb6c1", "lightsalmon": "#ffa07a", "lightseagreen": "#20b2aa", "lightskyblue": "#87cefa", "lightslategray": "#778899", "lightsteelblue": "#b0c4de",
"lightyellow": "#ffffe0", "lime": "#00ff00", "limegreen": "#32cd32", "linen": "#faf0e6",
"magenta": "#ff00ff", "maroon": "#800000", "mediumaquamarine": "#66cdaa", "mediumblue": "#0000cd", "mediumorchid": "#ba55d3", "mediumpurple": "#9370d8", "mediumseagreen": "#3cb371", "mediumslateblue": "#7b68ee",
"mediumspringgreen": "#00fa9a", "mediumturquoise": "#48d1cc", "mediumvioletred": "#c71585", "midnightblue": "#191970", "mintcream": "#f5fffa", "mistyrose": "#ffe4e1", "moccasin": "#ffe4b5",
"navajowhite": "#ffdead", "navy": "#000080",
"oldlace": "#fdf5e6", "olive": "#808000", "olivedrab": "#6b8e23", "orange": "#ffa500", "orangered": "#ff4500", "orchid": "#da70d6",
"palegoldenrod": "#eee8aa", "palegreen": "#98fb98", "paleturquoise": "#afeeee", "palevioletred": "#d87093", "papayawhip": "#ffefd5", "peachpuff": "#ffdab9", "peru": "#cd853f", "pink": "#ffc0cb", "plum": "#dda0dd", "powderblue": "#b0e0e6", "purple": "#800080",
"red": "#ff0000", "rosybrown": "#bc8f8f", "royalblue": "#4169e1",
"saddlebrown": "#8b4513", "salmon": "#fa8072", "sandybrown": "#f4a460", "seagreen": "#2e8b57", "seashell": "#fff5ee", "sienna": "#a0522d", "silver": "#c0c0c0", "skyblue": "#87ceeb", "slateblue": "#6a5acd", "slategray": "#708090", "snow": "#fffafa", "springgreen": "#00ff7f", "steelblue": "#4682b4",
"tan": "#d2b48c", "teal": "#008080", "thistle": "#d8bfd8", "tomato": "#ff6347", "turquoise": "#40e0d0",
"violet": "#ee82ee",
"wheat": "#f5deb3", "white": "#ffffff", "whitesmoke": "#f5f5f5",
"yellow": "#ffff00", "yellowgreen": "#9acd32"
},
/**
* Create a new color based on the specified HSL values.
*
* @param {Number} h Hue component (0..359)
* @param {Number} s Saturation component (0..1)
* @param {Number} l Lightness component (0..1)
* @return {Ext.draw.Color}
* @static
*/
fromHSL: function (h, s, l) {
return (new this(0, 0, 0, 0)).setHSL(h, s, l);
},
/**
* Parse the string and create a new color.
*
* Supported formats: '#rrggbb', '#rgb', and 'rgb(r,g,b)'.
*
* If the string is not recognized, an undefined will be returned instead.
*
* @param {String} string Color in string.
* @returns {Ext.draw.Color}
* @static
*/
fromString: function (string) {
return (new this(0, 0, 0, 0)).setFromString(string);
},
/**
* Convenience method for creating a color.
*
* Can be called with several different combinations of arguments:
*
* // Ext.draw.Color is returned unchanged.
* Ext.draw.Color.create(new Ext.draw.color(255, 0, 0, 0));
*
* // CSS color string.
* Ext.draw.Color.create("red");
*
* // Array of red, green, blue, alpha
* Ext.draw.Color.create([255, 0, 0, 0]);
*
* // Separate arguments of red, green, blue, alpha
* Ext.draw.Color.create(255, 0, 0, 0);
*
* // Returns black when no arguments given.
* Ext.draw.Color.create();
*
* @param {Ext.draw.Color/String/Number[]/Number} [red] Red component (0..255),
* CSS color string or array of all components.
* @param {Number} [green] Green component (0..255)
* @param {Number} [blue] Blue component (0..255)
* @param {Number} [alpha=1] Alpha component (0..1)
* @return {Ext.draw.Color}
* @static
*/
create: function (arg) {
if (arg instanceof this) {
return arg;
} else if (Ext.isArray(arg)) {
return new Ext.draw.Color(arg[0], arg[1], arg[2], arg[3]);
} else if (Ext.isString(arg)) {
return Ext.draw.Color.fromString(arg);
} else if (arguments.length > 2) {
return new Ext.draw.Color(arguments[0], arguments[1], arguments[2], arguments[3]);
} else {
return new Ext.draw.Color(0, 0, 0, 0);
}
}
});
});
})();
/**
* @private
* @class Ext.draw.sprite.AttributeParser
*
* Parsers used for sprite attributes.
*/
Ext.define("Ext.draw.sprite.AttributeParser", {
singleton: true,
attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
requires: ['Ext.draw.Color'],
"default": function (n) {
return n;
},
string: function (n) {
return String(n);
},
number: function (n) {
if (!isNaN(n)) {
return n;
}
},
angle: function (n) {
if (!isNaN(n)) {
n %= Math.PI * 2;
if (n < -Math.PI) {
n += Math.PI * 2;
}
if (n > Math.PI) {
n -= Math.PI * 2;
}
return n;
}
},
data: function (n) {
if (Ext.isArray(n)) {
return n.slice();
} else if (n instanceof Float32Array) {
return new Float32Array(n);
}
},
bool: function (n) {
return !!n;
},
color: function (n) {
if (n instanceof Ext.draw.Color) {
return n.toString();
} else if (n instanceof Ext.draw.gradient.Gradient) {
return n;
} else if (!n) {
return 'none';
} else if (Ext.isString(n)) {
return n;
} else if (n.type === 'linear') {
return Ext.create('Ext.draw.gradient.Linear', n);
} else if (n.type === 'radial') {
return Ext.create('Ext.draw.gradient.Radial', n);
} else if (n.type === 'pattern') {
return Ext.create('Ext.draw.gradient.Pattern', n);
}
},
limited: function (low, hi) {
return (function (n) {
return isNaN(n) ? undefined : Math.min(Math.max(+n, low), hi);
});
},
limited01: function (n) {
return isNaN(n) ? undefined : Math.min(Math.max(+n, 0), 1);
},
enums: function () {
var enums = {},
args = Array.prototype.slice.call(arguments, 0),
i, ln;
for (i = 0, ln = args.length; i < ln; i++) {
enums[args[i]] = true;
}
return (function (n) {
return n in enums ? n : undefined;
});
}
});
(function () {
function compute(from, to, delta) {
return from + (to - from) * delta;
}
/**
* @private
* @class Ext.draw.sprite.AnimationParser
*
* Parsers for sprite attributes used in animations.
*/
Ext.define("Ext.draw.sprite.AnimationParser", {
singleton: true,
attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
requires: ['Ext.draw.Color'],
color: {
parseInitial: function (color1, color2) {
if (Ext.isString(color1)) {
color1 = Ext.draw.Color.create(color1);
}
if (Ext.isString(color2)) {
color2 = Ext.draw.Color.create(color2);
}
if ((color1 instanceof Ext.draw.Color) && (color2 instanceof Ext.draw.Color)) {
return [
[color1.r, color1.g, color1.b, color1.a],
[color2.r, color2.g, color2.b, color2.a]
];
} else {
return [color1 || color2, color2 || color1];
}
},
compute: function (from, to, delta) {
if (!Ext.isArray(from) || !Ext.isArray(to)) {
return to || from;
} else {
return [compute(from[0], to[0], delta), compute(from[1], to[1], delta), compute(from[2], to[2], delta), compute(from[3], to[3], delta)];
}
},
serve: function (array) {
var color = Ext.draw.Color.fly(array[0], array[1], array[2], array[3]);
return color.toString();
}
},
number: {
parse: function (n) {
return n === null ? null : +n;
},
compute: function (from, to, delta) {
if (!Ext.isNumber(from) || !Ext.isNumber(to)) {
return to || from;
} else {
return compute(from, to, delta);
}
}
},
angle: {
parseInitial: function (from, to) {
if (to - from > Math.PI) {
to -= Math.PI * 2;
} else if (to - from < -Math.PI) {
to += Math.PI * 2;
}
return [from, to];
},
compute: function (from, to, delta) {
if (!Ext.isNumber(from) || !Ext.isNumber(to)) {
return to || from;
} else {
return compute(from, to, delta);
}
}
},
path: {
parseInitial: function (from, to) {
var fromStripes = from.toStripes(),
toStripes = to.toStripes(),
i, j,
fromLength = fromStripes.length, toLength = toStripes.length,
fromStripe, toStripe,
length,
lastStripe = toStripes[toLength - 1],
endPoint = [lastStripe[lastStripe.length - 2], lastStripe[lastStripe.length - 1]];
for (i = fromLength; i < toLength; i++) {
fromStripes.push(fromStripes[fromLength - 1].slice(0));
}
for (i = toLength; i < fromLength; i++) {
toStripes.push(endPoint.slice(0));
}
length = fromStripes.length;
toStripes.path = to;
toStripes.temp = new Ext.draw.Path();
for (i = 0; i < length; i++) {
fromStripe = fromStripes[i];
toStripe = toStripes[i];
fromLength = fromStripe.length;
toLength = toStripe.length;
toStripes.temp.types.push('M');
for (j = toLength; j < fromLength; j += 6) {
toStripe.push(endPoint[0], endPoint[1], endPoint[0], endPoint[1], endPoint[0], endPoint[1]);
}
lastStripe = toStripes[toStripes.length - 1];
endPoint = [lastStripe[lastStripe.length - 2], lastStripe[lastStripe.length - 1]];
for (j = fromLength; j < toLength; j += 6) {
fromStripe.push(endPoint[0], endPoint[1], endPoint[0], endPoint[1], endPoint[0], endPoint[1]);
}
for (i = 0; i < toStripe.length; i++) {
toStripe[i] -= fromStripe[i];
}
for (i = 2; i < toStripe.length; i += 6) {
toStripes.temp.types.push('C');
}
}
return [fromStripes, toStripes];
},
compute: function (fromStripes, toStripes, delta) {
if (delta >= 1) {
return toStripes.path;
}
var i = 0, ln = fromStripes.length,
j = 0, ln2, from, to,
temp = toStripes.temp.coords, pos = 0;
for (; i < ln; i++) {
from = fromStripes[i];
to = toStripes[i];
ln2 = from.length;
for (j = 0; j < ln2; j++) {
temp[pos++] = to[j] * delta + from[j];
}
}
return toStripes.temp;
}
},
data: {
compute: function (from, to, delta, target) {
var lf = from.length - 1,
lt = to.length - 1,
len = Math.max(lf, lt),
f, t, i;
if (!target || target === from) {
target = [];
}
target.length = len + 1;
for (i = 0; i <= len; i++) {
f = from[Math.min(i, lf)];
t = to[Math.min(i, lt)];
if (isNaN(f)) {
target[i] = t;
} else {
target[i] = (t - f) * delta + f;
}
}
return target;
}
},
text: {
compute: function (from, to, delta) {
return from.substr(0, Math.round(from.length * (1 - delta))) + to.substr(Math.round(to.length * (1 - delta)));
}
},
limited: "number",
limited01: "number"
});
})();
/**
* @private
* Flyweight object to process the attribute of a sprite.
*/
Ext.define("Ext.draw.sprite.AttributeDefinition", {
requires: [
'Ext.draw.sprite.AttributeParser',
'Ext.draw.sprite.AnimationParser'
],
config: {
/**
* @cfg {Object} defaults Defines the default values of attributes.
*/
defaults: {
},
/**
* @cfg {Object} aliases Defines the aletrnative names for attributes.
*/
aliases: {
},
/**
* @cfg {Object} animationProcessors Defines the process used to animate between attributes.
*/
animationProcessors: {
},
/**
* @cfg {Object} processors Defines the preprocessing used on the attribute.
*/
processors: {
},
/**
* @cfg {Object} dirty Defines what other attributes need to be updated when an attribute is changed.
*/
dirtyTriggers: {
},
/**
* @cfg {Object} updaters Defines the postprocessing used by the attribute.
*/
updaters: {
}
},
inheritableStatics: {
processorRe: /^(\w+)\(([\w\-,]*)\)$/
},
constructor: function (config) {
var me = this;
me.initConfig(config);
},
applyDefaults: function (defaults, oldDefaults) {
oldDefaults = Ext.apply(oldDefaults || {}, this.normalize(defaults));
return oldDefaults;
},
applyAliases: function (aliases, oldAliases) {
return Ext.apply(oldAliases || {}, aliases);
},
applyProcessors: function (processors, oldProcessors) {
this.getAnimationProcessors();
var name,
result = oldProcessors || {},
defaultProcessor = Ext.draw.sprite.AttributeParser,
processorRe = this.self.processorRe,
animationProcessors = {}, anyAnimationProcessors,
match, fn;
for (name in processors) {
fn = processors[name];
if (!Ext.isFunction(fn)) {
if (Ext.isString(fn)) {
match = fn.match(processorRe);
if (match) {
fn = defaultProcessor[match[1]].apply(defaultProcessor, match[2].split(','));
} else {
animationProcessors[name] = fn;
anyAnimationProcessors = true;
fn = defaultProcessor[fn];
}
} else {
continue;
}
}
result[name] = fn;
}
if (anyAnimationProcessors) {
this.setAnimationProcessors(animationProcessors);
}
return result;
},
applyAnimationProcessors: function (animationProcessors, oldAnimationProcessors) {
var parser = Ext.draw.sprite.AnimationParser,
item;
if (!oldAnimationProcessors) {
oldAnimationProcessors = {};
}
for (var name in animationProcessors) {
item = animationProcessors[name];
if (item === 'none') {
oldAnimationProcessors[name] = null;
} else if (Ext.isString(item) && !(name in oldAnimationProcessors)) {
if (item in parser) {
while (Ext.isString(parser[item])) {
item = parser[item];
}
oldAnimationProcessors[name] = parser[item];
}
} else if (Ext.isObject(item)) {
oldAnimationProcessors[name] = item;
}
}
return oldAnimationProcessors;
},
applyDirtyTriggers: function (dirtyTriggers, oldDirtyTrigger) {
if (!oldDirtyTrigger) {
oldDirtyTrigger = {};
}
for (var name in dirtyTriggers) {
oldDirtyTrigger[name] = dirtyTriggers[name].split(',');
}
return oldDirtyTrigger;
},
applyUpdaters: function (updaters, oldUpdaters) {
return Ext.apply(oldUpdaters || {}, updaters);
},
batchedNormalize: function (batchedChanges, reserveUnrecognized) {
if (!batchedChanges) {
return {};
}
var definition = this,
processors = definition.getProcessors(),
aliases = definition.getAliases(),
normalized = {}, i, ln,
undef, name, val,
translation, rotation, scaling,
matrix, subVal, split;
if ('rotation' in batchedChanges) {
rotation = batchedChanges.rotation;
}
else {
rotation = ('rotate' in batchedChanges) ? batchedChanges.rotate : undef;
}
if ('scaling' in batchedChanges) {
scaling = batchedChanges.scaling;
}
else {
scaling = ('scale' in batchedChanges) ? batchedChanges.scale : undef;
}
if ('translation' in batchedChanges) {
translation = batchedChanges.translation;
} else {
translation = ('translate' in batchedChanges) ? batchedChanges.translate : undef;
}
if (typeof scaling !== 'undefined') {
if (Ext.isNumber(scaling)) {
normalized.scalingX = scaling;
normalized.scalingY = scaling;
} else {
if ('x' in scaling) {
normalized.scalingX = scaling.x;
}
if ('y' in scaling) {
normalized.scalingY = scaling.y;
}
if ('centerX' in scaling) {
normalized.scalingCenterX = scaling.centerX;
}
if ('centerY' in scaling) {
normalized.scalingCenterY = scaling.centerY;
}
}
}
if (typeof rotation !== 'undefined') {
if (Ext.isNumber(rotation)) {
rotation = Ext.draw.Draw.rad(rotation);
normalized.rotationRads = rotation;
} else {
if ('rads' in rotation) {
normalized.rotationRads = rotation.rads;
} else if ('degrees' in rotation) {
if (Ext.isArray(rotation.degrees)) {
normalized.rotationRads = rotation.degrees.map(function (deg) {
return Ext.draw.Draw.rad(deg);
});
} else {
normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
}
}
if ('centerX' in rotation) {
normalized.rotationCenterX = rotation.centerX;
}
if ('centerY' in rotation) {
normalized.rotationCenterY = rotation.centerY;
}
}
}
if (typeof translation !== 'undefined') {
if ('x' in translation) {
normalized.translationX = translation.x;
}
if ('y' in translation) {
normalized.translationY = translation.y;
}
}
if ('matrix' in batchedChanges) {
matrix = Ext.draw.Matrix.create(batchedChanges.matrix);
split = matrix.split();
normalized.matrix = matrix;
normalized.rotationRads = split.rotation;
normalized.rotationCenterX = 0;
normalized.rotationCenterY = 0;
normalized.scalingX = split.scaleX;
normalized.scalingY = split.scaleY;
normalized.scalingCenterX = 0;
normalized.scalingCenterY = 0;
normalized.translationX = split.translateX;
normalized.translationY = split.translateY;
}
for (name in batchedChanges) {
val = batchedChanges[name];
if (typeof val === 'undefined') {
continue;
} else if (Ext.isArray(val)) {
if (name in aliases) {
name = aliases[name];
}
if (name in processors) {
normalized[name] = [];
for (i = 0, ln = val.length; i < ln; i++) {
subVal = processors[name].call(this, val[i]);
if (typeof val !== 'undefined') {
normalized[name][i] = subVal;
}
}
} else if (reserveUnrecognized){
normalized[name] = val;
}
} else {
if (name in aliases) {
name = aliases[name];
}
if (name in processors) {
val = processors[name].call(this, val);
if (typeof val !== 'undefined') {
normalized[name] = val;
}
} else if (reserveUnrecognized){
normalized[name] = val;
}
}
}
return normalized;
},
/**
* Normalizes the changes given via their processors before they are applied as attributes.
*
* @param changes The changes given.
* @return {Object} The normalized values.
*/
normalize: function (changes, reserveUnrecognized) {
if (!changes) {
return {};
}
var definition = this,
processors = definition.getProcessors(),
aliases = definition.getAliases(),
translation = changes.translation || changes.translate,
normalized = {},
name, val, rotation, scaling, matrix, split;
if ('rotation' in changes) {
rotation = changes.rotation;
}
else {
rotation = ('rotate' in changes) ? changes.rotate : undefined;
}
if ('scaling' in changes) {
scaling = changes.scaling;
}
else {
scaling = ('scale' in changes) ? changes.scale : undefined;
}
if (translation) {
if ('x' in translation) {
normalized.translationX = translation.x;
}
if ('y' in translation) {
normalized.translationY = translation.y;
}
}
if (typeof scaling !== 'undefined') {
if (Ext.isNumber(scaling)) {
normalized.scalingX = scaling;
normalized.scalingY = scaling;
} else {
if ('x' in scaling) {
normalized.scalingX = scaling.x;
}
if ('y' in scaling) {
normalized.scalingY = scaling.y;
}
if ('centerX' in scaling) {
normalized.scalingCenterX = scaling.centerX;
}
if ('centerY' in scaling) {
normalized.scalingCenterY = scaling.centerY;
}
}
}
if (typeof rotation !== 'undefined') {
if (Ext.isNumber(rotation)) {
rotation = Ext.draw.Draw.rad(rotation);
normalized.rotationRads = rotation;
} else {
if ('rads' in rotation) {
normalized.rotationRads = rotation.rads;
} else if ('degrees' in rotation) {
normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
}
if ('centerX' in rotation) {
normalized.rotationCenterX = rotation.centerX;
}
if ('centerY' in rotation) {
normalized.rotationCenterY = rotation.centerY;
}
}
}
if ('matrix' in changes) {
matrix = Ext.draw.Matrix.create(changes.matrix);
split = matrix.split();
normalized.matrix = matrix;
normalized.rotationRads = split.rotation;
normalized.rotationCenterX = 0;
normalized.rotationCenterY = 0;
normalized.scalingX = split.scaleX;
normalized.scalingY = split.scaleY;
normalized.scalingCenterX = 0;
normalized.scalingCenterY = 0;
normalized.translationX = split.translateX;
normalized.translationY = split.translateY;
}
for (name in changes) {
val = changes[name];
if (typeof val === 'undefined') {
continue;
}
if (name in aliases) {
name = aliases[name];
}
if (name in processors) {
val = processors[name].call(this, val);
if (typeof val !== 'undefined') {
normalized[name] = val;
}
} else if (reserveUnrecognized){
normalized[name] = val;
}
}
return normalized;
},
setBypassingNormalization: function (attr, modifierStack, changes) {
return modifierStack.pushDown(attr, changes);
},
set: function (attr, modifierStack, changes) {
changes = this.normalize(changes);
return this.setBypassingNormalization(attr, modifierStack, changes);
}
});
/**
* @class Ext.draw.modifier.Modifier
*
* Each sprite has a stack of modifiers. The resulting attributes of sprite is
* the content of the stack top. When setting attributes to a sprite,
* changes will be pushed-down though the stack of modifiers and pop-back the
* additive changes; When modifier is triggered to change the attribute of a
* sprite, it will pop-up the changes to the top.
*/
Ext.define("Ext.draw.modifier.Modifier", {
config: {
/**
* @cfg {Ext.draw.modifier.Modifier} previous Previous modifier that receives
* the push-down changes.
*/
previous: null,
/**
* @cfg {Ext.draw.modifier.Modifier} next Next modifier that receives the
* pop-up changes.
*/
next: null,
/**
* @cfg {Ext.draw.sprite.Sprite} sprite The sprite that the modifier is bound.
*/
sprite: null
},
constructor: function (config) {
this.initConfig(config);
},
updateNext: function (next) {
if (next) {
next.setPrevious(this);
}
},
updatePrev: function (prev) {
if (prev) {
prev.setNext (this);
}
},
/**
* Validate attribute set before use.
*
* @param {Object} attr The attribute to be validated. Note that it may be already initialized, so do
* not override properties that has already be used.
*/
prepareAttributes: function (attr) {
if (this._previous) {
this._previous.prepareAttributes(attr);
}
},
/**
* Invoked when changes need to be popped up to the top.
* @param attributes The source attributes.
* @param changes The changes to be popped up.
*/
popUp: function (attributes, changes) {
if (this._next) {
this._next.popUp(attributes, changes);
} else {
Ext.apply(attributes, changes);
}
},
/**
* Invoked when changes need to pushed down to the sprite.
* @param attr The source attributes.
* @param {Object} changes The changes to make. This object might be changed unexpectedly inside the method.
* @return {Mixed}
*/
pushDown: function (attr, changes) {
if (this._previous) {
return this._previous.pushDown(attr, changes);
} else {
for (var name in changes) {
if (changes[name] === attr[name]) {
delete changes[name];
}
}
return changes;
}
}
});
/**
* @class Ext.draw.modifier.Target
* @extends Ext.draw.modifier.Modifier
*
* This is the destination modifier that has to be put at
* the top of the modifier stack.
*
*/
Ext.define("Ext.draw.modifier.Target", {
extend: "Ext.draw.modifier.Modifier",
alias: 'modifier.target',
statics: {
uniqueId: 0
},
/**
* @inheritdoc
*/
prepareAttributes: function (attr) {
if (this._previous) {
this._previous.prepareAttributes(attr);
}
// TODO: Investigate the performance hit for introducing an id
attr.attributeId = 'attribute-' + Ext.draw.modifier.Target.uniqueId++;
if (!attr.hasOwnProperty('canvasAttributes')) {
attr.bbox = {
plain: {dirty: true},
transform: {dirty: true}
};
attr.dirty = true;
attr.dirtyFlags = {};
attr.canvasAttributes = {};
attr.matrix = new Ext.draw.Matrix();
attr.inverseMatrix = new Ext.draw.Matrix();
}
},
/**
* @private
* Applies the appropriate dirty flags from the modifier changes.
* @param attr The source attributes.
* @param changes The modifier changes.
*/
setDirtyFlags: function (attr, changes) {
Ext.apply(attr, changes);
var sprite = this._sprite,
dirtyTriggers = sprite.self.def._dirtyTriggers,
name, dirtyFlags = attr.dirtyFlags, flags, any = false,
triggers, trigger, i, ln, canvasNames;
for (name in changes) {
if ((triggers = dirtyTriggers[name])) {
i = 0;
while ((trigger = triggers[i++])) {
if (!(flags = dirtyFlags[trigger])) {
flags = dirtyFlags[trigger] = [];
}
flags.push(name);
}
}
}
for (name in changes) {
any = true;
break;
}
if (!any) {
return;
}
// This can prevent sub objects to set duplicated attributes to
// context.
if (dirtyFlags.canvas) {
canvasNames = dirtyFlags.canvas;
delete dirtyFlags.canvas;
for (i = 0, ln = canvasNames.length; i < ln; i++) {
name = canvasNames[i];
attr.canvasAttributes[name] = attr[name];
}
}
// Spreading dirty flags to children
if (attr.hasOwnProperty('children')) {
for (i = 0, ln = attr.children.length; i < ln; i++) {
Ext.apply(attr.children[i].dirtyFlags, dirtyFlags);
sprite.updateDirtyFlags(attr.children[i]);
}
}
sprite.setDirty(true);
},
/**
* @inheritdoc
*/
popUp: function (attributes, changes) {
this.setDirtyFlags(attributes, changes);
this._sprite.updateDirtyFlags(attributes);
},
/**
* @inheritdoc
*/
pushDown: function (attr, changes) {
if (this._previous) {
changes = this._previous.pushDown(attr, changes);
}
this.setDirtyFlags(attr, changes);
this._sprite.updateDirtyFlags(attr);
return changes;
}
});
(function () {
var pow = Math.pow,
sin = Math.sin,
cos = Math.cos,
sqrt = Math.sqrt,
pi = Math.PI,
easings, addEasing, poly, createPoly, easing, i, l;
//create polynomial easing equations
poly = ['quad', 'cubic', 'quart', 'quint'];
//create other easing equations
easings = {
pow: function (p, x) {
return pow(p, x[0] || 6);
},
expo: function (p) {
return pow(2, 8 * (p - 1));
},
circ: function (p) {
return 1 - sqrt(1 - p * p);
},
sine: function (p) {
return 1 - sin((1 - p) * pi / 2);
},
back: function (p, n) {
n = n || 1.616;
return p * p * ((n + 1) * p - n);
},
bounce: function (p) {
var value;
for (var a = 0, b = 1; 1; a += b, b /= 2) {
if (p >= (7 - 4 * a) / 11) {
value = b * b - pow((11 - 6 * a - 11 * p) / 4, 2);
break;
}
}
return value;
},
elastic: function (p, x) {
return pow(2, 10 * --p) * cos(20 * p * pi * (x || 1) / 3);
}
};
//Add easeIn, easeOut, easeInOut options to all easing equations.
addEasing = function (easing, params) {
params = params && params.length ? params : [ params ];
return Ext.apply(easing, {
easeIn: function (pos) {
return easing(pos, params);
},
easeOut: function (pos) {
return 1 - easing(1 - pos, params);
},
easeInOut: function (pos) {
return (pos <= 0.5) ? easing(2 * pos, params) / 2
: (2 - easing(2 * (1 - pos), params)) / 2;
}
});
};
//Append the polynomial equations with easing support to the EasingPrototype.
createPoly = function (times) {
return function (p) {
return pow(p, times);
};
};
for (i = 0, l = poly.length; i < l; ++i) {
easings[poly[i]] = createPoly(i + 2);
}
//Add linear interpolator
easings.linear = function (x) {
return x;
};
for (easing in easings) {
if (easings.hasOwnProperty(easing)) {
addEasing(easings[easing]);
}
}
/**
* @class
* Contains transition equations such as `Quad`, `Cubic`, `Quart`, `Quint`,
* `Expo`, `Circ`, `Pow`, `Sine`, `Back`, `Bounce`, `Elastic`, etc.
*
* Contains transition equations such as `Quad`, `Cubic`, `Quart`, `Quint`, `Expo`, `Circ`, `Pow`, `Sine`, `Back`, `Bounce`, `Elastic`, etc.
* Each transition also contains methods for applying this function as ease in, ease out or ease in and out accelerations.
*
* var fx = Ext.create('Ext.draw.fx.Sprite', {
* sprite: sprite,
* duration: 1000,
* easing: 'backOut'
* });
*/
Ext.define('Ext.draw.TimingFunctions', {
singleton: true,
easingMap: {
linear: easings.linear,
easeIn: easings.quad.easeIn,
easeOut: easings.quad.easeOut,
easeInOut: easings.quad.easeInOut,
backIn: easings.back,
backOut: function (x, n) {
return 1 - easings.back(1 - x, n);
},
backInOut: function (x, n) {
if (x < 0.5) {
return easings.back(x * 2, n) * 0.5;
} else {
return 1 - easings.back((1 - x) * 2, n) * 0.5;
}
},
elasticIn: function (x, n) {
return 1 - easings.elastic(1 - x, n);
},
elasticOut: easings.elastic,
bounceIn: easings.bounce,
bounceOut: function (x) {
return 1 - easings.bounce(1 - x);
}
}
}, function () {
Ext.apply(this, easings);
});
})();
/**
* @class Ext.draw.Animator
*
* Singleton class that manages the animation pool.
*/
Ext.define('Ext.draw.Animator', {
uses: ['Ext.draw.Draw'],
singleton: true,
frameCallbacks: {},
frameCallbackId: 0,
scheduled: 0,
frameStartTimeOffset: Ext.frameStartTime,
animations: [],
/**
* Cross platform `animationTime` implementation.
* @return {Number}
*/
animationTime: function () {
return Ext.frameStartTime - this.frameStartTimeOffset;
},
/**
* Adds an animated object to the animation pool.
*
* @param {Object} animation The animation descriptor to add to the pool.
*/
add: function (animation) {
if (!this.contains(animation)) {
this.animations.push(animation);
Ext.draw.Animator.ignite();
if ('fireEvent' in animation) {
animation.fireEvent('animationstart', animation);
}
}
},
/**
* Removes an animation from the pool.
* TODO: This is broken when called within `step` method.
* @param {Object} animation The animation to remove from the pool.
*/
remove: function (animation) {
var me = this,
animations = me.animations,
i = 0,
l = animations.length;
for (; i < l; ++i) {
if (animations[i] === animation) {
animations.splice(i, 1);
if ('fireEvent' in animation) {
animation.fireEvent('animationend', animation);
}
return;
}
}
},
/**
* Returns `true` or `false` whether it contains the given animation or not.
*
* @param {Object} animation The animation to check for.
* @return {Boolean}
*/
contains: function (animation) {
return this.animations.indexOf(animation) > -1;
},
/**
* Returns `true` or `false` whether the pool is empty or not.
* @return {Boolean}
*/
empty: function () {
return this.animations.length === 0;
},
/**
* Given a frame time it will filter out finished animations from the pool.
*
* @param {Number} frameTime The frame's start time, in milliseconds.
*/
step: function (frameTime) {
var me = this,
// TODO: Try to find a way to get rid of this copy
animations = me.animations.slice(),
animation,
i = 0, j = 0,
l = animations.length;
for (; i < l; ++i) {
animation = animations[i];
animation.step(frameTime);
if (animation.animating) {
animations[j++] = animation;
} else {
me.animations.splice(j, 1);
if (animation.fireEvent) {
animation.fireEvent('animationend');
}
}
}
},
/**
* Register an one-time callback that will be called at the next frame.
* @param callback
* @param scope
* @return {String}
*/
schedule: function (callback, scope) {
scope = scope || this;
var id = 'frameCallback' + (this.frameCallbackId++);
if (Ext.isString(callback)) {
callback = scope[callback];
}
Ext.draw.Animator.frameCallbacks[id] = {fn: callback, scope: scope, once: true};
this.scheduled++;
Ext.draw.Animator.ignite();
return id;
},
/**
* Cancel a registered one-time callback
* @param id
*/
cancel: function (id) {
if (Ext.draw.Animator.frameCallbacks[id] && Ext.draw.Animator.frameCallbacks[id].once) {
this.scheduled--;
delete Ext.draw.Animator.frameCallbacks[id];
}
},
/**
* Register a recursive callback that will be called at every frame.
*
* @param callback
* @param scope
* @return {String}
*/
addFrameCallback: function (callback, scope) {
scope = scope || this;
if (Ext.isString(callback)) {
callback = scope[callback];
}
var id = 'frameCallback' + (this.frameCallbackId++);
Ext.draw.Animator.frameCallbacks[id] = {fn: callback, scope: scope};
return id;
},
/**
* Unregister a recursive callback.
* @param id
*/
removeFrameCallback: function (id) {
delete Ext.draw.Animator.frameCallbacks[id];
},
/**
* @private
*/
fireFrameCallbacks: function () {
var callbacks = this.frameCallbacks,
once = [],
id, i, ln, fn, cb;
for (id in callbacks) {
cb = callbacks[id];
fn = cb.fn;
if (Ext.isString(fn)) {
fn = cb.scope[fn];
}
fn.call(cb.scope);
if (cb.once) {
once.push(id);
}
}
for (i = 0, ln = once.length; i < ln; i++) {
this.scheduled--;
delete callbacks[once[i]];
}
}
}, function () {
//Initialize the endless animation loop.
var looping = false,
frame = Ext.draw.Animator,
requestAnimationFramePolyfill = (function (global) {
return global.requestAnimationFrame ||
global.webkitRequestAnimationFrame ||
global.mozAnimationFrame ||
global.oAnimationFrame ||
global.msAnimationFrame ||
function (callback) { setTimeout(callback, 1); };
})(Ext.global),
animationStartTimePolyfill = (function (global) {
return (global.animationStartTime ? function () { return global.animationStartTime; } : null) ||
(global.webkitAnimationStartTime ? function () { return global.webkitAnimationStartTime; } : null) ||
(global.mozAnimationStartTime ? function () { return global.mozAnimationStartTime; } : null) ||
(global.oAnimationStartTime ? function () { return global.oAnimationStartTime; } : null) ||
(global.msAnimationStartTime ? function () { return global.msAnimationStartTime; } : null) ||
(Date.now ? function () { return Date.now(); } :
function () { return +new Date(); });
})(Ext.global);
// <debug>
var startLooping, frames;
// </debug>
function animationLoop() {
Ext.frameStartTime = animationStartTimePolyfill();
// <debug>
if (startLooping === undefined) {
startLooping = Ext.frameStartTime;
}
// </debug>
frame.step(frame.animationTime());
frame.fireFrameCallbacks();
if (frame.scheduled || !frame.empty()) {
requestAnimationFramePolyfill(animationLoop);
// <debug>
frames++;
// </debug>
} else {
looping = false;
// <debug>
startLooping = undefined;
// </debug>
}
// <debug>
frame.framerate = frames * 1000 / (frame.animationTime() - startLooping);
// </debug>
}
// <debug>
frame.clearCounter = function () {
startLooping = frame.animationTime();
frames = 0;
};
// </debug>
frame.ignite = function () {
if (!looping) {
// <debug>
frames = 0;
// </debug>
looping = true;
requestAnimationFramePolyfill(animationLoop);
Ext.draw.Draw.updateIOS();
}
};
});
/**
* @class Ext.draw.modifier.Animation
* @extends Ext.draw.modifier.Modifier
*
* The Animation modifier.
*
* Sencha Touch allows users to use transitional animation on sprites. Simply set the duration
* and easing in the animation modifier, then all the changes to the sprites will be animated.
*
* Also, you can use different durations and easing functions on different attributes by using
* {@link customDuration} and {@link customEasings}.
*
* By default, an animation modifier will be created during the initialization of a sprite.
* You can get the modifier of `sprite` by `sprite.fx`.
*
*/
Ext.define("Ext.draw.modifier.Animation", {
mixins: {
observable: 'Ext.mixin.Observable'
},
requires: [
'Ext.draw.TimingFunctions',
'Ext.draw.Animator'
],
extend: 'Ext.draw.modifier.Modifier',
alias: 'modifier.animation',
config: {
/**
* @cfg {Function} easing
* Default easing function.
*/
easing: function (x) {
return x;
},
/**
* @cfg {Number} duration
* Default duration time (ms).
*/
duration: 0,
/**
* @cfg {Object} customEasings Overrides the default easing function for defined attributes.
*/
customEasings: {},
/**
* @cfg {Object} customDuration Overrides the default duration for defined attributes.
*/
customDuration: {}
},
constructor: function () {
this.anyAnimation = false;
this.anySpecialAnimations = false;
this.animating = 0;
this.animatingPool = [];
this.callSuper(arguments);
},
/**
* @inheritdoc
*/
prepareAttributes: function (attr) {
if (!attr.hasOwnProperty('timers')) {
attr.animating = false;
attr.timers = {};
attr.animationOriginal = Ext.Object.chain(attr);
attr.animationOriginal.upperLevel = attr;
}
if (this._previous) {
this._previous.prepareAttributes(attr.animationOriginal);
}
},
updateSprite: function (sprite) {
// Apply the config that was configured in the sprite.
this.setConfig(sprite.config.fx);
},
updateDuration: function (duration) {
this.anyAnimation = duration > 0;
},
applyEasing: function (easing) {
if (typeof easing === 'string') {
return Ext.draw.TimingFunctions.easingMap[easing];
} else {
return easing;
}
},
applyCustomEasings: function (newCustomEasing, oldCustomEasing) {
oldCustomEasing = oldCustomEasing || {};
var attr, attrs, easing, i, ln;
for (attr in newCustomEasing) {
easing = newCustomEasing[attr];
attrs = attr.split(',');
if (typeof easing === 'string') {
easing = Ext.draw.TimingFunctions.easingMap[easing];
}
for (i = 0, ln = attrs.length; i < ln; i++) {
oldCustomEasing[attrs[i]] = easing;
}
}
return oldCustomEasing;
},
/**
* Set special easings on the given attributes.
* @param attrs The source attributes.
* @param easing The special easings.
*/
setEasingOn: function (attrs, easing) {
attrs = Ext.Array.from(attrs).slice();
var customEasing = {},
i = 0,
ln = attrs.length;
for (; i < ln; i++) {
customEasing[attrs[i]] = easing;
}
this.setDurationEasings(customEasing);
},
/**
* Remove special easings on the given attributes.
* @param attrs The source attributes.
*/
clearEasingOn: function (attrs) {
attrs = Ext.Array.from(attrs, true);
var i = 0, ln = attrs.length;
for (; i < ln; i++) {
delete this._customEasing[attrs[i]];
}
},
applyCustomDuration: function (newCustomDuration, oldCustomDuration) {
oldCustomDuration = oldCustomDuration || {};
var attr, duration, attrs, i, ln, anySpecialAnimations = this.anySpecialAnimations;
for (attr in newCustomDuration) {
duration = newCustomDuration[attr];
attrs = attr.split(',');
anySpecialAnimations = true;
for (i = 0, ln = attrs.length; i < ln; i++) {
oldCustomDuration[attrs[i]] = duration;
}
}
this.anySpecialAnimations = anySpecialAnimations;
return oldCustomDuration;
},
/**
* Set special duration on the given attributes.
* @param attrs The source attributes.
* @param duration The special duration.
*/
setDurationOn: function (attrs, duration) {
attrs = Ext.Array.from(attrs).slice();
var customDurations = {},
i = 0,
ln = attrs.length;
for (; i < ln; i++) {
customDurations[attrs[i]] = duration;
}
this.setCustomDuration(customDurations);
},
/**
* Remove special easings on the given attributes.
* @param attrs The source attributes.
*/
clearDurationOn: function (attrs) {
attrs = Ext.Array.from(attrs, true);
var i = 0, ln = attrs.length;
for (; i < ln; i++) {
delete this._customDuration[attrs[i]];
}
},
/**
* @private
* Initializes Animator for the animation.
* @param attributes The source attributes.
* @param animating The animating flag.
*/
setAnimating: function (attributes, animating) {
var me = this,
i, j;
if (attributes.animating !== animating) {
attributes.animating = animating;
if (animating) {
me.animatingPool.push(attributes);
if (me.animating === 0) {
Ext.draw.Animator.add(me);
}
me.animating++;
} else {
for (i = 0, j = 0; i < me.animatingPool.length; i++) {
if (me.animatingPool[i] !== attributes) {
me.animatingPool[j++] = me.animatingPool[i];
}
}
me.animating = me.animatingPool.length = j;
}
}
},
/**
* @private
* Set the attr with given easing and duration.
* @param {Object} attr The attributes collection.
* @param {Object} changes The changes that popped up from lower modifier.
* @return {Object} The changes to pop up.
*/
setAttrs: function (attr, changes) {
var timers = attr.timers,
parsers = this._sprite.self.def._animationProcessors,
defaultEasing = this._easing,
defaultDuration = this._duration,
customDuration = this._customDuration,
customEasings = this._customEasings,
anySpecial = this.anySpecialAnimations,
any = this.anyAnimation || anySpecial,
original = attr.animationOriginal,
ignite = false,
timer, name, newValue, startValue, parser, easing, duration;
if (!any) {
// If there is no animation enabled
// When applying changes to attributes, simply stop current animation
// and set the value.
for (name in changes) {
if (attr[name] === changes[name]) {
delete changes[name];
} else {
attr[name] = changes[name];
}
delete original[name];
delete timers[name];
}
return changes;
} else {
// If any animation
for (name in changes) {
newValue = changes[name];
startValue = attr[name];
if (newValue !== startValue && any && startValue !== undefined && startValue !== null && (parser = parsers[name])) {
// If this property is animating.
// Figure out the desired duration and easing.
easing = defaultEasing;
duration = defaultDuration;
if (anySpecial) {
// Deducing the easing function and duration
if (name in customEasings) {
easing = customEasings[name];
}
if (name in customDuration) {
duration = customDuration[name];
}
}
// If the property is animating
if (duration) {
if (!timers[name]) {
timers[name] = {};
}
timer = timers[name];
timer.start = 0;
timer.easing = easing;
timer.duration = duration;
timer.compute = parser.compute;
timer.serve = parser.serve || Ext.draw.Draw.reflectFn;
if (parser.parseInitial) {
var initial = parser.parseInitial(startValue, newValue);
timer.source = initial[0];
timer.target = initial[1];
} else if (parser.parse) {
timer.source = parser.parse(startValue);
timer.target = parser.parse(newValue);
} else {
timer.source = startValue;
timer.target = newValue;
}
// The animation started. Change to originalVal.
timers[name] = timer;
original[name] = newValue;
delete changes[name];
ignite = true;
continue;
} else {
delete original[name];
}
} else {
delete original[name];
}
// If the property is not animating.
delete timers[name];
}
}
if (ignite && !attr.animating) {
this.setAnimating(attr, true);
}
return changes;
},
/**
* @private
*
* Update attributes to current value according to current animation time.
* This method will not effect the values of lower layers, but may delete a
* value from it.
* @param attr The source attributes.
* @return {Object} the changes to popup.
*/
updateAttributes: function (attr) {
if (!attr.animating) {
return {};
}
var changes = {}, change,
any = false,
original = attr.animationOriginal,
timers = attr.timers,
now = Ext.draw.Animator.animationTime(),
name, timer, delta;
// If updated in the same frame, return.
if (attr.lastUpdate === now) {
return {};
}
for (name in timers) {
timer = timers[name];
if (!timer.start) {
timer.start = now;
delta = 0;
} else {
delta = (now - timer.start) / timer.duration;
}
if (delta >= 1) {
changes[name] = original[name];
delete original[name];
delete timers[name];
} else {
changes[name] = timer.serve(timer.compute(timer.source, timer.target, timer.easing(delta), attr[name]));
any = true;
}
}
attr.lastUpdate = now;
this.setAnimating(attr, any);
return changes;
},
/**
* @inheritdoc
*/
pushDown: function (attr, changes) {
changes = Ext.draw.modifier.Modifier.prototype.pushDown.call(this, attr.animationOriginal, changes);
return this.setAttrs(attr, changes);
},
/**
* @inheritdoc
*/
popUp: function (attr, changes) {
attr = attr.upperLevel;
changes = this.setAttrs(attr, changes);
if (this._next) {
return this._next.popUp(attr, changes);
} else {
return Ext.apply(attr, changes);
}
},
// This is called as an animated object in `Ext.draw.Animator`.
step: function () {
var me = this,
pool = me.animatingPool.slice(),
attributes,
i, ln;
for (i = 0, ln = pool.length; i < ln; i++) {
attributes = pool[i];
var changes = this.updateAttributes(attributes),
name;
// Looking for anything in changes
//noinspection LoopStatementThatDoesntLoopJS
for (name in changes) {
if (this._next) {
this._next.popUp(attributes, changes);
}
break;
}
}
},
/**
* Stop all animations effected by this modifier
*/
stop: function () {
this.step();
var me = this,
pool = me.animatingPool,
i, ln;
for (i = 0, ln = pool.length; i < ln; i++) {
pool[i].animating = false;
}
me.animatingPool.length = 0;
me.animating = 0;
Ext.draw.Animator.remove(me);
},
destroy: function () {
var me = this;
me.animatingPool.length = 0;
me.animating = 0;
}
});
/**
* @class Ext.draw.modifier.Highlight
* @extends Ext.draw.modifier.Modifier
*
* Highlight is a modifier that will override the attributes
* with its `highlightStyle` attributes when `highlighted` is true.
*/
Ext.define("Ext.draw.modifier.Highlight", {
extend: 'Ext.draw.modifier.Modifier',
alias: 'modifier.highlight',
config: {
/**
* @cfg {Boolean} enabled 'true' if the highlight is applied.
*/
enabled: false,
/**
* @cfg {Object} highlightStyle The style attributes of the highlight modifier.
*/
highlightStyle: null
},
preFx: true,
applyHighlightStyle: function (style, oldStyle) {
oldStyle = oldStyle || {};
if (this.getSprite()) {
Ext.apply(oldStyle, this.getSprite().self.def.normalize(style));
} else {
Ext.apply(oldStyle, style);
}
return oldStyle;
},
/**
* @inheritdoc
*/
prepareAttributes: function (attr) {
if (!attr.hasOwnProperty('highlightOriginal')) {
attr.highlighted = false;
attr.highlightOriginal = Ext.Object.chain(attr);
}
if (this._previous) {
this._previous.prepareAttributes(attr.highlightOriginal);
}
},
updateSprite: function (sprite, oldSprite) {
if (sprite) {
if (this.getHighlightStyle()) {
this._highlightStyle = sprite.self.def.normalize(this.getHighlightStyle());
}
this.setHighlightStyle(sprite.config.highlightCfg);
}
// Before attaching to a sprite, register the highlight related
// attributes to its definition.
//
// TODO(zhangbei): Unfortunately this will effect all the sprites of the same type.
// As the redundant attributes would not effect performance, it is not yet a big problem.
var def = sprite.self.def;
this.setSprite(sprite);
def.setConfig({
defaults: {
highlighted: false
},
processors: {
highlighted: 'bool'
},
aliases: {
"highlight": "highlighted",
"highlighting": "highlighted"
},
dirtyFlags: {
},
updaters: {
}
});
},
/**
* Filter modifier changes if overriding source attributes.
* @param attr The source attributes.
* @param changes The modifier changes.
* @return {*} The filtered changes.
*/
filterChanges: function (attr, changes) {
var me = this,
name,
original = attr.highlightOriginal,
style = me.getHighlightStyle();
if (attr.highlighted) {
for (name in changes) {
if (style.hasOwnProperty(name)) {
// If it's highlighted, then save the changes to lower level
// on overridden attributes.
original[name] = changes[name];
delete changes[name];
}
}
}
for (name in changes) {
if (name !== 'highlighted' && original[name] === changes[name]) {
// If it's highlighted, then save the changes to lower level
// on overridden attributes.
delete changes[name];
}
}
return changes;
},
/**
* @inheritdoc
*/
pushDown: function (attr, changes) {
var style = this.getHighlightStyle(),
original = attr.highlightOriginal,
oldHighlighted, name;
if (changes.hasOwnProperty('highlighted')) {
oldHighlighted = changes.highlighted;
// Hide `highlighted` and `highlightStyle` to underlying modifiers.
delete changes.highlighted;
if (this._previous) {
changes = this._previous.pushDown(original, changes);
}
changes = this.filterChanges(attr, changes);
if (oldHighlighted !== attr.highlighted) {
if (oldHighlighted) {
// switching on
// At this time, original should be empty.
for (name in style) {
// If changes[name] just changed the value in lower levels,
if (name in changes) {
original[name] = changes[name];
} else {
original[name] = attr[name];
}
if (original[name] !== style[name]) {
changes[name] = style[name];
}
}
} else {
// switching off
for (name in style) {
if (!(name in changes)) {
changes[name] = original[name];
}
delete original[name]; // TODO: Need deletion API?
}
}
changes.highlighted = oldHighlighted;
}
} else {
if (this._previous) {
changes = this._previous.pushDown(original, changes);
}
changes = this.filterChanges(attr, changes);
}
return changes;
},
/**
* @inheritdoc
*/
popUp: function (attr, changes) {
changes = this.filterChanges(attr, changes);
Ext.draw.modifier.Modifier.prototype.popUp.call(this, attr, changes);
}
});
/**
* A Sprite is an object rendered in a Drawing surface. There are different options and types of sprites.
* The configuration of a Sprite is an object with the following properties:
*
* Additionally there are three transform objects that can be set with `setAttributes` which are `translate`, `rotate` and
* `scale`.
*
* For translate, the configuration object contains `x` and `y` attributes that indicate where to
* translate the object. For example:
*
* sprite.setAttributes({
* translate: {
* x: 10,
* y: 10
* }
* }, true);
*
* For rotation, the configuration object contains `x` and `y` attributes for the center of the rotation (which are optional),
* and a `degrees` attribute that specifies the rotation in degrees. For example:
*
* sprite.setAttributes({
* rotate: {
* degrees: 90
* }
* }, true);
*
* For scaling, the configuration object contains `x` and `y` attributes for the x-axis and y-axis scaling. For example:
*
* sprite.setAttributes({
* scale: {
* x: 10,
* y: 3
* }
* }, true);
*
* Sprites can be created with a reference to a {@link Ext.draw.Surface}
*
* var drawComponent = Ext.create('Ext.draw.Component', {
* // ...
* });
*
* var sprite = Ext.create('Ext.draw.sprite.Sprite', {
* type: 'circle',
* fill: '#ff0',
* surface: drawComponent.surface,
* radius: 5
* });
*
* Sprites can also be added to the surface as a configuration object:
*
* var sprite = drawComponent.surface.add({
* type: 'circle',
* fill: '#ff0',
* radius: 5
* });
*
* In order to properly apply properties and render the sprite we have to
* `show` the sprite setting the option `redraw` to `true`:
*
* sprite.show(true);
*
* The constructor configuration object of the Sprite can also be used and passed into the {@link Ext.draw.Surface}
* `add` method to append a new sprite to the canvas. For example:
*
* drawComponent.surface.add({
* type: 'circle',
* fill: '#ffc',
* radius: 100,
* x: 100,
* y: 100
* });
*/
Ext.define('Ext.draw.sprite.Sprite', {
alias: 'sprite.sprite',
mixins: {
observable: 'Ext.mixin.Observable'
},
requires: [
'Ext.draw.Draw',
'Ext.draw.gradient.Gradient',
'Ext.draw.sprite.AttributeDefinition',
'Ext.draw.sprite.AttributeParser',
'Ext.draw.modifier.Target',
'Ext.draw.modifier.Animation',
'Ext.draw.modifier.Highlight'
],
isSprite: true,
inheritableStatics: {
def: {
processors: {
/**
* @cfg {String} [strokeStyle="none"] The color of the stroke (a CSS color value).
*/
strokeStyle: "color",
/**
* @cfg {String} [fillStyle="none"] The color of the shadow (a CSS color value).
*/
fillStyle: "color",
/**
* @cfg {Number} [strokeOpacity=1] The opacity of the stroke. Limited from 0 to 1.
*/
strokeOpacity: "limited01",
/**
* @cfg {Number} [fillOpacity=1] The opacity of the fill. Limited from 0 to 1.
*/
fillOpacity: "limited01",
/**
* @cfg {Number} [lineWidth=1] The width of the line stroke.
*/
lineWidth: "number",
/**
* @cfg {String} [lineCap="butt"] The style of the line caps.
*/
lineCap: "enums(butt,round,square)",
/**
* @cfg {String} [lineJoin="miter"] The style of the line join.
*/
lineJoin: "enums(round,bevel,miter)",
/**
* @cfg {Number} [miterLimit=1] Sets the distance between the inner corner and the outer corner where two lines meet.
*/
miterLimit: "number",
/**
* @cfg {String} [shadowColor="none"] The color of the shadow (a CSS color value).
*/
shadowColor: "color",
/**
* @cfg {Number} [shadowOffsetX=0] The offset of the sprite's shadow on the x-axis.
*/
shadowOffsetX: "number",
/**
* @cfg {Number} [shadowOffsetY=0] The offset of the sprite's shadow on the y-axis.
*/
shadowOffsetY: "number",
/**
* @cfg {Number} [shadowBlur=0] The amount blur used on the shadow.
*/
shadowBlur: "number",
/**
* @cfg {Number} [globalAlpha=1] The opacity of the sprite. Limited from 0 to 1.
*/
globalAlpha: "limited01",
globalCompositeOperation: "enums(source-over,destination-over,source-in,destination-in,source-out,destination-out,source-atop,destination-atop,lighter,xor,copy)",
/**
* @cfg {Boolean} [hidden=false] Determines whether or not the sprite is hidden.
*/
hidden: "bool",
/**
* @cfg {Boolean} [transformFillStroke=false] Determines whether the fill and stroke are affected by sprite transformations.
*/
transformFillStroke: "bool",
/**
* @cfg {Number} [zIndex=0] The stacking order of the sprite.
*/
zIndex: "number",
/**
* @cfg {Number} [translationX=0] The translation of the sprite on the x-axis.
*/
translationX: "number",
/**
* @cfg {Number} [translationY=0] The translation of the sprite on the y-axis.
*/
translationY: "number",
/**
* @cfg {Number} [rotationRads=0] The degree of rotation of the sprite.
*/
rotationRads: "number",
/**
* @cfg {Number} [rotationCenterX=null] The central coordinate of the sprite's scale operation on the x-axis.
*/
rotationCenterX: "number",
/**
* @cfg {Number} [rotationCenterY=null] The central coordinate of the sprite's rotate operation on the y-axis.
*/
rotationCenterY: "number",
/**
* @cfg {Number} [scalingX=1] The scaling of the sprite on the x-axis.
*/
scalingX: "number",
/**
* @cfg {Number} [scalingY=1] The scaling of the sprite on the y-axis.
*/
scalingY: "number",
/**
* @cfg {Number} [scalingCenterX=null] The central coordinate of the sprite's scale operation on the x-axis.
*/
scalingCenterX: "number",
/**
* @cfg {Number} [scalingCenterY=null] The central coordinate of the sprite's scale operation on the y-axis.
*/
scalingCenterY: "number"
},
aliases: {
"stroke": "strokeStyle",
"fill": "fillStyle",
"color": "fillStyle",
"stroke-width": "lineWidth",
"stroke-linecap": "lineCap",
"stroke-linejoin": "lineJoin",
"stroke-miterlimit": "miterLimit",
"text-anchor": "textAlign",
"opacity": "globalAlpha",
translateX: "translationX",
translateY: "translationY",
rotateRads: "rotationRads",
rotateCenterX: "rotationCenterX",
rotateCenterY: "rotationCenterY",
scaleX: "scalingX",
scaleY: "scalingY",
scaleCenterX: "scalingCenterX",
scaleCenterY: "scalingCenterY"
},
defaults: {
hidden: false,
zIndex: 0,
strokeStyle: "none",
fillStyle: "none",
lineWidth: 1,
lineCap: "butt",
lineJoin: "miter",
miterLimit: 1,
shadowColor: "none",
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowBlur: 0,
globalAlpha: 1,
strokeOpacity: 1,
fillOpacity: 1,
transformFillStroke: false,
translationX: 0,
translationY: 0,
rotationRads: 0,
rotationCenterX: null,
rotationCenterY: null,
scalingX: 1,
scalingY: 1,
scalingCenterX: null,
scalingCenterY: null
},
dirtyTriggers: {
hidden: "canvas",
zIndex: "zIndex",
globalAlpha: "canvas",
globalCompositeOperation: "canvas",
transformFillStroke: "canvas",
strokeStyle: "canvas",
fillStyle: "canvas",
strokeOpacity: "canvas",
fillOpacity: "canvas",
lineWidth: "canvas",
lineCap: "canvas",
lineJoin: "canvas",
miterLimit: "canvas",
shadowColor: "canvas",
shadowOffsetX: "canvas",
shadowOffsetY: "canvas",
shadowBlur: "canvas",
translationX: "transform",
translationY: "transform",
rotationRads: "transform",
rotationCenterX: "transform",
rotationCenterY: "transform",
scalingX: "transform",
scalingY: "transform",
scalingCenterX: "transform",
scalingCenterY: "transform"
},
updaters: {
"bbox": function (attrs) {
attrs.bbox.plain.dirty = true;
attrs.bbox.transform.dirty = true;
if (
attrs.rotationRads !== 0 && (attrs.rotationCenterX === null || attrs.rotationCenterY === null) ||
((attrs.scalingX !== 1 || attrs.scalingY !== 1) &&
(attrs.scalingCenterX === null || attrs.scalingCenterY === null)
)
) {
if (!attrs.dirtyFlags.transform) {
attrs.dirtyFlags.transform = [];
}
}
},
"zIndex": function (attrs) {
attrs.dirtyZIndex = true;
},
"transform": function (attrs) {
attrs.dirtyTransform = true;
attrs.bbox.transform.dirty = true;
}
}
}
},
config: {
parent: null
},
onClassExtended: function (Class, member) {
var initCfg = Class.superclass.self.def.initialConfig,
cfg;
if (member.inheritableStatics && member.inheritableStatics.def) {
cfg = Ext.merge({}, initCfg, member.inheritableStatics.def);
Class.def = Ext.create("Ext.draw.sprite.AttributeDefinition", cfg);
delete member.inheritableStatics.def;
} else {
Class.def = Ext.create("Ext.draw.sprite.AttributeDefinition", initCfg);
}
},
constructor: function (config) {
if (this.$className === 'Ext.draw.sprite.Sprite') {
throw 'Ext.draw.sprite.Sprite is an abstract class';
}
config = config || {};
var me = this,
groups = [].concat(config.group || []),
i, ln;
me.id = config.id || Ext.id(null, 'ext-sprite-');
me.group = new Array(groups.length);
for (i = 0, ln = groups.length; i < ln; i++) {
me.group[i] = groups[i].id || groups[i].toString();
}
me.attr = {};
me.initConfig(config);
var modifiers = Ext.Array.from(config.modifiers, true);
me.prepareModifiers(modifiers);
me.initializeAttributes();
me.setAttributes(me.self.def.getDefaults(), true);
me.setAttributes(config);
},
getDirty: function () {
return this.attr.dirty;
},
setDirty: function (dirty) {
if ((this.attr.dirty = dirty)) {
if (this._parent) {
this._parent.setDirty(true);
}
}
},
addModifier: function (modifier, reinitializeAttributes) {
var me = this;
if (!(modifier instanceof Ext.draw.modifier.Modifier)) {
modifier = Ext.factory(modifier, null, null, 'modifier');
}
modifier.setSprite(this);
if (modifier.preFx || modifier.config && modifier.config.preFx) {
if (me.fx.getPrevious()) {
me.fx.getPrevious().setNext(modifier);
}
modifier.setNext(me.fx);
} else {
me.topModifier.getPrevious().setNext(modifier);
modifier.setNext(me.topModifier);
}
if (reinitializeAttributes) {
me.initializeAttributes();
}
return modifier;
},
prepareModifiers: function (additionalModifiers) {
// Set defaults
var me = this,
modifier, i, ln;
me.topModifier = new Ext.draw.modifier.Target({sprite: me});
// Link modifiers
me.fx = new Ext.draw.modifier.Animation({sprite: me});
me.fx.setNext(me.topModifier);
for (i = 0, ln = additionalModifiers.length; i < ln; i++) {
me.addModifier(additionalModifiers[i], false);
}
},
initializeAttributes: function () {
var me = this;
me.topModifier.prepareAttributes(me.attr);
},
updateDirtyFlags: function (attrs) {
var me = this,
dirtyFlags = attrs.dirtyFlags, flags,
updaters = me.self.def._updaters,
any = false,
dirty = false,
flag;
do {
any = false;
for (flag in dirtyFlags) {
me.updateDirtyFlags = Ext.emptyFn;
flags = dirtyFlags[flag];
delete dirtyFlags[flag];
if (updaters[flag]) {
updaters[flag].call(me, attrs, flags);
}
any = true;
delete me.updateDirtyFlags;
}
dirty = dirty || any;
} while (any);
if (dirty) {
me.setDirty(true);
}
},
/**
* Set attributes of the sprite.
*
* @param {Object} changes The content of the change.
* @param {Boolean} [bypassNormalization] `true` to avoid normalization of the given changes.
* @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
* The content of object may be destroyed.
*/
setAttributes: function (changes, bypassNormalization, avoidCopy) {
var attributes = this.attr;
if (bypassNormalization) {
if (avoidCopy) {
this.topModifier.pushDown(attributes, changes);
} else {
this.topModifier.pushDown(attributes, Ext.apply({}, changes));
}
} else {
this.topModifier.pushDown(attributes, this.self.def.normalize(changes));
}
},
/**
* Set attributes of the sprite, assuming the names and values have already been
* normalized.
*
* @deprecated Use setAttributes directy with bypassNormalization argument being `true`.
* @param {Object} changes The content of the change.
* @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
* The content of object may be destroyed.
*/
setAttributesBypassingNormalization: function (changes, avoidCopy) {
return this.setAttributes(changes, true, avoidCopy);
},
/**
* Returns the bounding box for the given Sprite as calculated with the Canvas engine.
*
* @param {Boolean} [isWithoutTransform] Whether to calculate the bounding box with the current transforms or not.
*/
getBBox: function (isWithoutTransform) {
var me = this,
attr = me.attr,
bbox = attr.bbox,
plain = bbox.plain,
transform = bbox.transform;
if (plain.dirty) {
me.updatePlainBBox(plain);
plain.dirty = false;
}
if (isWithoutTransform) {
return plain;
} else {
me.applyTransformations();
if (transform.dirty) {
me.updateTransformedBBox(transform, plain);
transform.dirty = false;
}
return transform;
}
},
/**
* @protected
* @function
* Subclass will fill the plain object with `x`, `y`, `width`, `height` information of the plain bounding box of
* this sprite.
*
* @param {Object} plain Target object.
*/
updatePlainBBox: Ext.emptyFn,
/**
* @protected
* Subclass will fill the plain object with `x`, `y`, `width`, `height` information of the transformed
* bounding box of this sprite.
*
* @param {Object} transform Target object.
* @param {Object} plain Auxilary object providing information of plain object.
*/
updateTransformedBBox: function (transform, plain) {
this.attr.matrix.transformBBox(plain, 0, transform);
},
/**
* Subclass can rewrite this function to gain better performance.
* @param {Boolean} isWithoutTransform
* @return {Array}
*/
getBBoxCenter: function (isWithoutTransform) {
var bbox = this.getBBox(isWithoutTransform);
if (bbox) {
return [
bbox.x + bbox.width * 0.5,
bbox.y + bbox.height * 0.5
];
} else {
return [0, 0];
}
},
/**
* Hide the sprite.
* @return {Ext.draw.sprite.Sprite} this
* @chainable
*/
hide: function () {
this.attr.hidden = true;
this.setDirty(true);
return this;
},
/**
* Show the sprite.
* @return {Ext.draw.sprite.Sprite} this
* @chainable
*/
show: function () {
this.attr.hidden = false;
this.setDirty(true);
return this;
},
useAttributes: function (ctx) {
this.applyTransformations();
var attrs = this.attr,
canvasAttributes = attrs.canvasAttributes,
strokeStyle = canvasAttributes.strokeStyle,
fillStyle = canvasAttributes.fillStyle,
id;
if (strokeStyle) {
if (strokeStyle.isGradient) {
ctx.strokeStyle = 'black';
ctx.strokeGradient = strokeStyle;
} else {
ctx.strokeGradient = false;
}
}
if (fillStyle) {
if (fillStyle.isGradient) {
ctx.fillStyle = 'black';
ctx.fillGradient = fillStyle;
} else {
ctx.fillGradient = false;
}
}
for (id in canvasAttributes) {
if (canvasAttributes[id] !== undefined && canvasAttributes[id] !== ctx[id]) {
ctx[id] = canvasAttributes[id];
}
}
ctx.setGradientBBox(this.getBBox(this.attr.transformFillStroke));
},
// @private
applyTransformations: function (force) {
if (!force && !this.attr.dirtyTransform) {
return;
}
var me = this,
attr = me.attr,
center = me.getBBoxCenter(true),
centerX = center[0],
centerY = center[1],
x = attr.translationX,
y = attr.translationY,
sx = attr.scalingX,
sy = attr.scalingY === null ? attr.scalingX : attr.scalingY,
scx = attr.scalingCenterX === null ? centerX : attr.scalingCenterX,
scy = attr.scalingCenterY === null ? centerY : attr.scalingCenterY,
rad = attr.rotationRads,
rcx = attr.rotationCenterX === null ? centerX : attr.rotationCenterX,
rcy = attr.rotationCenterY === null ? centerY : attr.rotationCenterY,
cos = Math.cos(rad),
sin = Math.sin(rad);
if (sx === 1 && sy === 1) {
scx = 0;
scy = 0;
}
if (rad === 0) {
rcx = 0;
rcy = 0;
}
attr.matrix.elements = [
cos * sx, sin * sy,
-sin * sx, cos * sy,
scx + (rcx - cos * rcx - scx + rcy * sin) * sx + x,
scy + (rcy - cos * rcy - scy + rcx * -sin) * sy + y
];
attr.matrix.inverse(attr.inverseMatrix);
attr.dirtyTransform = false;
attr.bbox.transform.dirty = true;
},
/**
* Called before rendering.
*/
preRender: Ext.emptyFn,
/**
* Render method.
* @param {Ext.draw.Surface} surface The surface.
* @param {Object} ctx A context object compatible with CanvasRenderingContext2D.
* @param {Array} region The clip region (or called dirty rect) of the current rendering. Not be confused
* with `surface.getRegion()`.
*
* @return {*} returns `false` to stop rendering in this frame. All the sprite haven't been rendered
* will have their dirty flag untouched.
*/
render: Ext.emptyFn,
repaint: function () {
var parent = this.getParent();
while (parent && !(parent instanceof Ext.draw.Surface)) {
parent = parent.getParent();
}
if (parent) {
parent.renderFrame();
}
},
/**
* Removes the sprite and clears all listeners.
*/
destroy: function () {
var me = this, modifier = me.topModifier, curr;
while (modifier) {
curr = modifier;
modifier = modifier.getPrevious();
curr.destroy();
}
delete me.attr;
me.destroy = Ext.emptyFn;
if (me.fireEvent('beforedestroy', me) !== false) {
me.fireEvent('destroy', me);
}
this.callSuper();
}
}, function () {
this.def = Ext.create("Ext.draw.sprite.AttributeDefinition", this.def);
});
(function () {
var PI2_3 = 2.0943951023931953/* 120 Deg */,
abs = Math.abs,
sin = Math.cos,
cos = Math.cos,
acos = Math.acos,
sqrt = Math.sqrt,
exp = Math.exp,
log = Math.log;
/**
* @private
* Singleton Class that provides methods to solve cubic equation.
*/
Ext.define("Ext.draw.Solver", {
singleton: true,
/**
* Cubic root of number
* @param number {Number}
*/
cubicRoot: function (number) {
if (number > 0) {
return exp(log(number) / 3);
} else if (number < 0) {
return -exp(log(-number) / 3);
} else {
return 0;
}
},
/**
* Returns the function f(x) = a * x + b and solver for f(x) = y
* @param a
* @param b
*/
linearFunction: function (a, b) {
var result;
if (a === 0) {
result = function (t) {
return b;
};
result.solve = function (y) {
// if y == d there should be a real root
// but we can ignore it for geometry calculations.
return [];
};
} else {
result = function (t) {
return a * t + b;
};
result.solve = function (y) {
return [(y - b) / a];
};
}
return result;
},
/**
* Returns the function f(x) = a * x ^ 2 + b * x + c and solver for f(x) = y
*
* @param a
* @param b
* @param c
*/
quadraticFunction: function (a, b, c) {
var result;
if (a === 0) {
return this.linearFunction(b, c);
} else {
// Quadratic equation.
result = function (t) {
return (a * t + b) * t + c;
};
var delta0temp = b * b - 4 * a * c,
delta = function (y) {
return delta0temp + 4 * a * y;
}, solveTemp0 = 1 / a * 0.5,
solveTemp1 = -solveTemp0 * b;
solveTemp0 = abs(solveTemp0);
result.solve = function (y) {
var deltaTemp = delta(y);
if (deltaTemp < 0) {
return [];
}
deltaTemp = sqrt(deltaTemp);
// have to distinct roots here.
return [solveTemp1 - deltaTemp * solveTemp0, solveTemp1 + deltaTemp * solveTemp0];
};
}
return result;
},
/**
* Returns the function f(x) = a * x^3 + b * x^2 + c * x + d and solver for f(x) = y
* @param a
* @param b
* @param c
* @param d
*/
cubicFunction: function (a, b, c, d) {
var result;
if (a === 0) {
return this.quadraticFunction(b, c, d);
} else {
result = function (t) {
return ((a * t + b) * t + c) * t + d;
};
var b_a_3 = b / a / 3,
c_a = c / a,
d_a = d / a,
b2 = b_a_3 * b_a_3,
deltaTemp0 = (b_a_3 * c_a - d_a) * 0.5 - b_a_3 * b2,
deltaTemp1 = b2 - c_a / 3,
deltaTemp13 = deltaTemp1 * deltaTemp1 * deltaTemp1;
if (deltaTemp1 === 0) {
result.solve = function (y) {
return [-b_a_3 + this.cubicRoot(deltaTemp0 * 2 + y / a)];
};
} else {
if (deltaTemp1 > 0) {
var deltaTemp1_2 = sqrt(deltaTemp1),
deltaTemp13_2 = deltaTemp1_2 * deltaTemp1_2 * deltaTemp1_2;
deltaTemp1_2 += deltaTemp1_2;
}
result.solve = function (y) {
y /= a;
var d0 = deltaTemp0 + y * 0.5,
deltaTemp = d0 * d0 - deltaTemp13;
if (deltaTemp > 0) {
deltaTemp = sqrt(deltaTemp);
return [-b_a_3 + this.cubicRoot(d0 + deltaTemp) + this.cubicRoot(d0 - deltaTemp)];
} else if (deltaTemp === 0) {
var cr = this.cubicRoot(d0),
root0 = -b_a_3 - cr;
if (d0 >= 0) {
return [root0, root0, -b_a_3 + 2 * cr];
} else {
return [-b_a_3 + 2 * cr, root0, root0];
}
} else {
var theta = acos(d0 / deltaTemp13_2) / 3,
ra = deltaTemp1_2 * cos(theta) - b_a_3,
rb = deltaTemp1_2 * cos(theta + PI2_3) - b_a_3,
rc = deltaTemp1_2 * cos(theta - PI2_3) - b_a_3;
if (ra < rb) {
if (rb < rc) {
return [ra, rb, rc];
} else if (ra < rc) {
return[ra, rc, rb];
} else {
return [rc, ra, rb];
}
} else {
if (ra < rc) {
return [rb, ra, rc];
} else if (rb < rc) {
return [rb, rc, ra];
} else {
return [rc, rb, ra];
}
}
}
};
}
}
return result;
},
createBezierSolver: function (a, b, c, d) {
return this.cubicFunction(3 * (b - c) + d - a, 3 * (a - 2 * b + c), 3 * (b - a), a);
}
});
})();
/**
* Class representing a path.
* Designed to be compatible with [CanvasPathMethods](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#canvaspathmethods)
* and will hopefully be replaced by the browsers' implementation of the Path object.
*/
Ext.define("Ext.draw.Path", {
requires: ['Ext.draw.Draw', 'Ext.draw.Solver'],
statics: {
pathRe: /,?([achlmqrstvxz]),?/gi,
pathRe2: /-/gi,
pathSplitRe: /\s|,/g
},
svgString: '',
/**
* Create a path from pathString
* @constructor
* @param pathString
*/
constructor: function (pathString) {
var me = this;
me.coords = [];
me.types = [];
me.cursor = null;
me.startX = 0;
me.startY = 0;
me.solvers = {};
if (pathString) {
me.fromSvgString(pathString);
}
},
/**
* Clear the path.
*/
clear: function () {
var me = this;
me.coords.length = 0;
me.types.length = 0;
me.cursor = null;
me.startX = 0;
me.startY = 0;
me.solvers = {};
me.dirt();
},
/**
* @private
*/
dirt: function () {
this.svgString = '';
},
/**
* Move to a position.
* @param {Number} x
* @param {Number} y
*/
moveTo: function (x, y) {
var me = this;
if (!me.cursor) {
me.cursor = [x, y];
}
me.coords.push(x, y);
me.types.push('M');
me.startX = x;
me.startY = y;
me.cursor[0] = x;
me.cursor[1] = y;
me.dirt();
},
/**
* A straight line to a position.
* @param {Number} x
* @param {Number} y
*/
lineTo: function (x, y) {
var me = this;
if (!me.cursor) {
me.cursor = [x, y];
me.coords.push(x, y);
me.types.push('M');
} else {
me.coords.push(x, y);
me.types.push('L');
}
me.cursor[0] = x;
me.cursor[1] = y;
me.dirt();
},
/**
* A cubic bezier curve to a position.
* @param {Number} cx1
* @param {Number} cy1
* @param {Number} cx2
* @param {Number} cy2
* @param {Number} x
* @param {Number} y
*/
bezierCurveTo: function (cx1, cy1, cx2, cy2, x, y) {
var me = this;
if (!me.cursor) {
me.moveTo(cx1, cy1);
}
me.coords.push(cx1, cy1, cx2, cy2, x, y);
me.types.push('C');
me.cursor[0] = x;
me.cursor[1] = y;
me.dirt();
},
/**
* A quadratic bezier curve to a position.
* @param {Number} cx
* @param {Number} cy
* @param {Number} x
* @param {Number} y
*/
quadraticCurveTo: function (cx, cy, x, y) {
var me = this;
if (!me.cursor) {
me.moveTo(cx, cy);
}
me.bezierCurveTo(
(me.cursor[0] * 2 + cx) / 3, (me.cursor[1] * 2 + cy) / 3,
(x * 2 + cx) / 3, (y * 2 + cy) / 3,
x, y
);
},
/**
* Close this path with a straight line.
*/
closePath: function () {
var me = this;
if (me.cursor) {
me.types.push('Z');
me.dirt();
}
},
/**
* Create a elliptic arc curve compatible with SVG's arc to instruction.
*
* The curve start from (`x1`, `y1`) and ends at (`x2`, `y2`). The ellipse
* has radius `rx` and `ry` and a rotation of `rotation`.
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @param {Number} [rx]
* @param {Number} [ry]
* @param {Number} [rotation]
*/
arcTo: function (x1, y1, x2, y2, rx, ry, rotation) {
var me = this;
if (ry === undefined) {
ry = rx;
}
if (rotation === undefined) {
rotation = 0;
}
if (!me.cursor) {
me.moveTo(x1, y1);
return;
}
if (rx === 0 || ry === 0) {
me.lineTo(x1, y1);
return;
}
x2 -= x1;
y2 -= y1;
var x0 = me.cursor[0] - x1,
y0 = me.cursor[1] - y1,
area = x2 * y0 - y2 * x0,
cos, sin, xx, yx, xy, yy,
l0 = Math.sqrt(x0 * x0 + y0 * y0),
l2 = Math.sqrt(x2 * x2 + y2 * y2),
dist, cx, cy;
// cos rx, -sin ry , x1 - cos rx x1 + ry sin y1
// sin rx, cos ry, -rx sin x1 + y1 - cos ry y1
if (area === 0) {
me.lineTo(x1, y1);
return;
}
if (ry !== rx) {
cos = Math.cos(rotation);
sin = Math.sin(rotation);
xx = cos / rx;
yx = sin / ry;
xy = -sin / rx;
yy = cos / ry;
var temp = xx * x0 + yx * y0;
y0 = xy * x0 + yy * y0;
x0 = temp;
temp = xx * x2 + yx * y2;
y2 = xy * x2 + yy * y2;
x2 = temp;
} else {
x0 /= rx;
y0 /= ry;
x2 /= rx;
y2 /= ry;
}
cx = x0 * l2 + x2 * l0;
cy = y0 * l2 + y2 * l0;
dist = 1 / (Math.sin(Math.asin(Math.abs(area) / (l0 * l2)) * 0.5) * Math.sqrt(cx * cx + cy * cy));
cx *= dist;
cy *= dist;
var k0 = (cx * x0 + cy * y0) / (x0 * x0 + y0 * y0),
k2 = (cx * x2 + cy * y2) / (x2 * x2 + y2 * y2);
var cosStart = x0 * k0 - cx,
sinStart = y0 * k0 - cy,
cosEnd = x2 * k2 - cx,
sinEnd = y2 * k2 - cy,
startAngle = Math.atan2(sinStart, cosStart),
endAngle = Math.atan2(sinEnd, cosEnd);
if (area > 0) {
if (endAngle < startAngle) {
endAngle += Math.PI * 2;
}
} else {
if (startAngle < endAngle) {
startAngle += Math.PI * 2;
}
}
if (ry !== rx) {
cx = cos * cx * rx - sin * cy * ry + x1;
cy = sin * cy * ry + cos * cy * ry + y1;
me.lineTo(cos * rx * cosStart - sin * ry * sinStart + cx,
sin * rx * cosStart + cos * ry * sinStart + cy);
me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
} else {
cx = cx * rx + x1;
cy = cy * ry + y1;
me.lineTo(rx * cosStart + cx, ry * sinStart + cy);
me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
}
},
/**
* Create an elliptic arc.
*
* See [the whatwg reference of ellipse](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-ellipse).
*
* @param cx
* @param cy
* @param radiusX
* @param radiusY
* @param rotation
* @param startAngle
* @param endAngle
* @param anticlockwise
*/
ellipse: function (cx, cy, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
var me = this,
coords = me.coords,
start = coords.length, count,
i, j;
if (endAngle - startAngle >= Math.PI * 2) {
me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle, startAngle + Math.PI, anticlockwise);
me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle + Math.PI, endAngle, anticlockwise);
return;
}
if (!anticlockwise) {
if (endAngle < startAngle) {
endAngle += Math.PI * 2;
}
count = me.approximateArc(coords, cx, cy, radiusX, radiusY, rotation, startAngle, endAngle);
} else {
if (startAngle < endAngle) {
startAngle += Math.PI * 2;
}
count = me.approximateArc(coords, cx, cy, radiusX, radiusY, rotation, endAngle, startAngle);
for (i = start, j = coords.length - 2; i < j; i += 2, j -= 2) {
var temp = coords[i];
coords[i] = coords[j];
coords[j] = temp;
temp = coords[i + 1];
coords[i + 1] = coords[j + 1];
coords[j + 1] = temp;
}
}
if (!me.cursor) {
me.cursor = [coords[coords.length - 2], coords[coords.length - 1]];
me.types.push('M');
} else {
me.cursor[0] = coords[coords.length - 2];
me.cursor[1] = coords[coords.length - 1];
me.types.push('L');
}
for (i = 2; i < count; i += 6) {
me.types.push('C');
}
me.dirt();
},
/**
* Create an circular arc.
*
* @param x
* @param y
* @param radius
* @param startAngle
* @param endAngle
* @param anticlockwise
*/
arc: function (x, y, radius, startAngle, endAngle, anticlockwise) {
this.ellipse(x, y, radius, radius, 0, startAngle, endAngle, anticlockwise);
},
/**
* Draw a rectangle and close it.
*
* @param x
* @param y
* @param width
* @param height
*/
rect: function (x, y, width, height) {
var me = this;
me.moveTo(x, y);
me.lineTo(x + width, y);
me.lineTo(x + width, y + height);
me.lineTo(x, y + height);
me.closePath();
},
/**
* @private
* @param result
* @param cx
* @param cy
* @param rx
* @param ry
* @param phi
* @param theta1
* @param theta2
* @return {Number}
*/
approximateArc: function (result, cx, cy, rx, ry, phi, theta1, theta2) {
var cosPhi = Math.cos(phi),
sinPhi = Math.sin(phi),
cosTheta1 = Math.cos(theta1),
sinTheta1 = Math.sin(theta1),
xx = cosPhi * cosTheta1 * rx - sinPhi * sinTheta1 * ry,
yx = -cosPhi * sinTheta1 * rx - sinPhi * cosTheta1 * ry,
xy = sinPhi * cosTheta1 * rx + cosPhi * sinTheta1 * ry,
yy = -sinPhi * sinTheta1 * rx + cosPhi * cosTheta1 * ry,
rightAngle = Math.PI / 2,
count = 2,
exx = xx,
eyx = yx,
exy = xy,
eyy = yy,
rho = 0.547443256150549,
temp, y1, x3, y3, x2, y2;
theta2 -= theta1;
if (theta2 < 0) {
theta2 += Math.PI * 2;
}
result.push(xx + cx, xy + cy);
while (theta2 >= rightAngle) {
result.push(
exx + eyx * rho + cx, exy + eyy * rho + cy,
exx * rho + eyx + cx, exy * rho + eyy + cy,
eyx + cx, eyy + cy
);
count += 6;
theta2 -= rightAngle;
temp = exx;
exx = eyx;
eyx = -temp;
temp = exy;
exy = eyy;
eyy = -temp;
}
if (theta2) {
y1 = (0.3294738052815987 + 0.012120855841304373 * theta2) * theta2;
x3 = Math.cos(theta2);
y3 = Math.sin(theta2);
x2 = x3 + y1 * y3;
y2 = y3 - y1 * x3;
result.push(
exx + eyx * y1 + cx, exy + eyy * y1 + cy,
exx * x2 + eyx * y2 + cx, exy * x2 + eyy * y2 + cy,
exx * x3 + eyx * y3 + cx, exy * x3 + eyy * y3 + cy
);
count += 6;
}
return count;
},
/**
* [http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes](http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes)
* @param rx
* @param ry
* @param rotation Differ from svg spec, this is radian.
* @param fA
* @param fS
* @param x2
* @param y2
*/
arcSvg: function (rx, ry, rotation, fA, fS, x2, y2) {
if (rx < 0) {
rx = -rx;
}
if (ry < 0) {
ry = -ry;
}
var me = this,
x1 = me.cursor[0],
y1 = me.cursor[1],
hdx = (x1 - x2) / 2,
hdy = (y1 - y2) / 2,
cosPhi = Math.cos(rotation),
sinPhi = Math.sin(rotation),
xp = hdx * cosPhi + hdy * sinPhi,
yp = -hdx * sinPhi + hdy * cosPhi,
ratX = xp / rx,
ratY = yp / ry,
lambda = ratX * ratX + ratY * ratY,
cx = (x1 + x2) * 0.5, cy = (y1 + y2) * 0.5,
cpx = 0, cpy = 0;
if (lambda >= 1) {
lambda = Math.sqrt(lambda);
rx *= lambda;
ry *= lambda;
// me gives lambda == cpx == cpy == 0;
} else {
lambda = Math.sqrt(1 / lambda - 1);
if (fA === fS) {
lambda = -lambda;
}
cpx = lambda * rx * ratY;
cpy = -lambda * ry * ratX;
cx += cosPhi * cpx - sinPhi * cpy;
cy += sinPhi * cpx + cosPhi * cpy;
}
var theta1 = Math.atan2((yp - cpy) / ry, (xp - cpx) / rx),
deltaTheta = Math.atan2((-yp - cpy) / ry, (-xp - cpx) / rx) - theta1;
if (fS) {
if (deltaTheta <= 0) {
deltaTheta += Math.PI * 2;
}
} else {
if (deltaTheta >= 0) {
deltaTheta -= Math.PI * 2;
}
}
me.ellipse(cx, cy, rx, ry, rotation, theta1, theta1 + deltaTheta, 1 - fS);
},
/**
* Feed the path from svg path string.
* @param pathString
*/
fromSvgString: function (pathString) {
if (!pathString) {
return;
}
var me = this,
parts,
paramCounts = {
a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0,
A: 7, C: 6, H: 1, L: 2, M: 2, Q: 4, S: 4, T: 2, V: 1, Z: 0
},
lastCommand = '',
lastControlX, lastControlY,
lastX = 0, lastY = 0,
part = false, i, partLength, relative;
// Split the string to items.
if (Ext.isString(pathString)) {
parts = pathString.replace(Ext.draw.Path.pathRe, " $1 ").replace(Ext.draw.Path.pathRe2, " -").split(Ext.draw.Path.pathSplitRe);
} else if (Ext.isArray(pathString)) {
parts = pathString.join(',').split(Ext.draw.Path.pathSplitRe);
}
// Remove empty entries
for (i = 0, partLength = 0; i < parts.length; i++) {
if (parts[i] !== '') {
parts[partLength++] = parts[i];
}
}
parts.length = partLength;
me.clear();
for (i = 0; i < parts.length;) {
lastCommand = part;
part = parts[i];
relative = (part.toUpperCase() !== part);
i++;
switch (part) {
case 'M':
me.moveTo(lastX = +parts[i], lastY = +parts[i + 1]);
i += 2;
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
i += 2;
}
break;
case 'L':
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
i += 2;
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
i += 2;
}
break;
case 'A':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.arcSvg(
+parts[i], +parts[i + 1],
+parts[i + 2] * Math.PI / 180,
+parts[i + 3], +parts[i + 4],
lastX = +parts[i + 5], lastY = +parts[i + 6]);
i += 7;
}
break;
case 'C':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.bezierCurveTo(
+parts[i ], +parts[i + 1],
lastControlX = +parts[i + 2], lastControlY = +parts[i + 3],
lastX = +parts[i + 4], lastY = +parts[i + 5]);
i += 6;
}
break;
case 'Z':
me.closePath();
break;
case 'm':
me.moveTo(lastX += +parts[i], lastY += +parts[i + 1]);
i += 2;
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
i += 2;
}
break;
case 'l':
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
i += 2;
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
i += 2;
}
break;
case 'a':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.arcSvg(
+parts[i], +parts[i + 1],
+parts[i + 2] * Math.PI / 180,
+parts[i + 3], +parts[i + 4],
lastX += +parts[i + 5], lastY += +parts[i + 6]);
i += 7;
}
break;
case 'c':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.bezierCurveTo(lastX + (+parts[i]), lastY + (+parts[i + 1]),
lastControlX = lastX + (+parts[i + 2]), lastControlY = lastY + (+parts[i + 3]),
lastX += +parts[i + 4], lastY += +parts[i + 5]);
i += 6;
}
break;
case 'z':
me.closePath();
break;
case 's':
if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
lastControlX = lastX;
lastControlY = lastY;
}
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.bezierCurveTo(
lastX + lastX - lastControlX, lastY + lastY - lastControlY,
lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]),
lastX += +parts[i + 2], lastY += +parts[i + 3]);
i += 4;
}
break;
case 'S':
if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
lastControlX = lastX;
lastControlY = lastY;
}
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.bezierCurveTo(
lastX + lastX - lastControlX, lastY + lastY - lastControlY,
lastControlX = +parts[i], lastControlY = +parts[i + 1],
lastX = (+parts[i + 2]), lastY = (+parts[i + 3]));
i += 4;
}
break;
case 'q':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.quadraticCurveTo(
lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]),
lastX += +parts[i + 2], lastY += +parts[i + 3]);
i += 4;
}
break;
case 'Q':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.quadraticCurveTo(
lastControlX = +parts[i], lastControlY = +parts[i + 1],
lastX = +parts[i + 2], lastY = +parts[i + 3]);
i += 4;
}
break;
case 't':
if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
lastControlX = lastX;
lastControlY = lastY;
}
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.quadraticCurveTo(
lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY,
lastX += +parts[i + 1], lastY += +parts[i + 2]);
i += 2;
}
break;
case 'T':
if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
lastControlX = lastX;
lastControlY = lastY;
}
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.quadraticCurveTo(
lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY,
lastX = (+parts[i + 1]), lastY = (+parts[i + 2]));
i += 2;
}
break;
case 'h':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX += +parts[i], lastY);
i++;
}
break;
case 'H':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX = +parts[i], lastY);
i++;
}
break;
case 'v':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX, lastY += +parts[i]);
i++;
}
break;
case 'V':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX, lastY = +parts[i]);
i++;
}
break;
}
}
},
/**
* @private
* @param x1
* @param y1
* @param x2
* @param y2
* @param x
* @param y
* @return {Number}
*/
rayTestLine: function (x1, y1, x2, y2, x, y) {
var cx;
if (y1 === y2) {
if (y === y1) {
if (Math.min(x1, x2) <= x && x <= Math.max(x1, x2)) {
return -1;
}
} else {
return 0;
}
}
if (y1 < y && y < y2 || y2 < y && y < y1) {
cx = (y - y1) * (x2 - x1) / (y2 - y1) + x1;
if (cx === x) {
return -1;
} else if (cx < x) {
return 0;
} else {
return 1;
}
} else {
return 0;
}
},
/**
* @private
* @param x1
* @param y1
* @param x2
* @param y2
* @param x3
* @param y3
* @param x4
* @param y4
* @param x
* @param y
* @return {*}
*/
rayTestCubicBezier: function (x1, y1, x2, y2, x3, y3, x4, y4, x, y, idx) {
if (Math.min(x1, x2, x3, x4) <= x && x <= Math.max(x1, x2, x3, x4)) {
if (Math.min(y1, y2, y3, y4) <= y && y <= Math.max(y1, y2, y3, y4)) {
var me = this,
solver = me.solvers[idx] || (me.solvers[idx] = Ext.draw.Solver.createBezierSolver(x1, x2, x3, x4)),
result = solver.solve(y);
return (+(x <= result[0] && 0 <= result[0] && result[0] <= 1)) +
(+(x <= result[1] && 0 <= result[1] && result[1] <= 1)) +
(+(x <= result[2] && 0 <= result[2] && result[2] <= 1));
}
}
return 0;
},
/**
* Test wether the given point is on or inside the path.
* @param x
* @param y
* @return {Boolean}
*/
isPointInPath: function (x, y) {
var me = this,
i, j, count = 0, test = 0,
types = me.types,
coords = me.coords,
ln = types.length, firstX = null, firstY = null, lastX = 0, lastY = 0;
for (i = 0, j = 0; i < ln; i++) {
switch (types[i]) {
case 'M':
if (firstX !== null) {
test = me.rayTestLine(firstX, firstY, lastX, lastY, x, y);
if (test < 0) {
count += 1;
} else {
count += test;
}
}
firstX = lastX = coords[j];
firstY = lastY = coords[j + 1];
j += 2;
break;
case 'L':
test = me.rayTestLine(lastX, lastY, coords[j], coords[j + 1], x, y);
if (test < 0) {
return true;
}
count += test;
lastX = coords[j];
lastY = coords[j + 1];
j += 2;
break;
case 'C':
test = me.rayTestCubicBezier(
lastX, lastY,
coords[j], coords[j + 1],
coords[j + 2], coords[j + 3],
coords[j + 4], coords[j + 5],
x, y, i);
if (test < 0) {
return true;
}
count += test;
lastX = coords[j + 4];
lastY = coords[j + 5];
j += 6;
break;
case 'Z':
break;
}
}
return count % 2 === 1;
},
/**
* Clone this path.
* @return {Ext.draw.Path}
*/
clone: function () {
var me = this,
path = new Ext.draw.Path();
path.coords = me.coords.slice(0);
path.types = me.types.slice(0);
path.cursor = me.cursor ? me.cursor.slice(0) : null;
path.startX = me.startX;
path.startY = me.startY;
path.svgString = me.svgString;
return path;
},
/**
* Transform the current path by a matrix.
* @param {Ext.draw.Matrix} matrix
*/
transform: function (matrix) {
if (matrix.isIdentity()) {
return;
}
var xx = matrix.getXX(), yx = matrix.getYX(), dx = matrix.getDX(),
xy = matrix.getXY(), yy = matrix.getYY(), dy = matrix.getDY(),
coords = this.coords,
i = 0, ln = coords.length,
x, y;
for (; i < ln; i += 2) {
x = coords[i];
y = coords[i + 1];
coords[i] = x * xx + y * yx + dx;
coords[i + 1] = x * xy + y * yy + dy;
}
this.dirt();
},
/**
* Get the bounding box of this matrix.
* @param {Object} [target] Optional object to receive the result.
*
* @return {Object} Object with x, y, width and height
*/
getDimension: function (target) {
if (!target) {
target = {};
}
if (!this.types || !this.types.length) {
target.x = 0;
target.y = 0;
target.width = 0;
target.height = 0;
return target;
}
target.left = Infinity;
target.top = Infinity;
target.right = -Infinity;
target.bottom = -Infinity;
var i = 0, j = 0,
types = this.types,
coords = this.coords,
ln = types.length, x, y;
for (; i < ln; i++) {
switch (types[i]) {
case 'M':
case 'L':
x = coords[j];
y = coords[j + 1];
target.left = Math.min(x, target.left);
target.top = Math.min(y, target.top);
target.right = Math.max(x, target.right);
target.bottom = Math.max(y, target.bottom);
j += 2;
break;
case 'C':
this.expandDimension(target, x, y,
coords[j], coords[j + 1],
coords[j + 2], coords[j + 3],
x = coords[j + 4], y = coords[j + 5]);
j += 6;
break;
}
}
target.x = target.left;
target.y = target.top;
target.width = target.right - target.left;
target.height = target.bottom - target.top;
return target;
},
/**
* Get the bounding box as if the path is transformed by a matrix.
*
* @param {Ext.draw.Matrix} matrix
* @param {Object} [target] Optional object to receive the result.
*
* @return {Object} An object with x, y, width and height.
*/
getDimensionWithTransform: function (matrix, target) {
if (!this.types || !this.types.length) {
if (!target) {
target = {};
}
target.x = 0;
target.y = 0;
target.width = 0;
target.height = 0;
return target;
}
target.left = Infinity;
target.top = Infinity;
target.right = -Infinity;
target.bottom = -Infinity;
var xx = matrix.getXX(), yx = matrix.getYX(), dx = matrix.getDX(),
xy = matrix.getXY(), yy = matrix.getYY(), dy = matrix.getDY(),
i = 0, j = 0,
types = this.types,
coords = this.coords,
ln = types.length, x, y;
for (; i < ln; i++) {
switch (types[i]) {
case 'M':
case 'L':
x = coords[j] * xx + coords[j + 1] * yx + dx;
y = coords[j] * xy + coords[j + 1] * yy + dy;
target.left = Math.min(x, target.left);
target.top = Math.min(y, target.top);
target.right = Math.max(x, target.right);
target.bottom = Math.max(y, target.bottom);
j += 2;
break;
case 'C':
this.expandDimension(target,
x, y,
coords[j] * xx + coords[j + 1] * yx + dx,
coords[j] * xy + coords[j + 1] * yy + dy,
coords[j + 2] * xx + coords[j + 3] * yx + dx,
coords[j + 2] * xy + coords[j + 3] * yy + dy,
x = coords[j + 4] * xx + coords[j + 5] * yx + dx,
y = coords[j + 4] * xy + coords[j + 5] * yy + dy);
j += 6;
break;
}
}
if (!target) {
target = {};
}
target.x = target.left;
target.y = target.top;
target.width = target.right - target.left;
target.height = target.bottom - target.top;
return target;
},
/**
* @private
* Expand the rect by the bbox of a bezier curve.
*
* @param target
* @param x1
* @param y1
* @param cx1
* @param cy1
* @param cx2
* @param cy2
* @param x2
* @param y2
*/
expandDimension: function (target, x1, y1, cx1, cy1, cx2, cy2, x2, y2) {
var me = this,
l = target.left, r = target.right, t = target.top, b = target.bottom,
dim = me.dim || (me.dim = []);
me.curveDimension(x1, cx1, cx2, x2, dim);
l = Math.min(l, dim[0]);
r = Math.max(r, dim[1]);
me.curveDimension(y1, cy1, cy2, y2, dim);
t = Math.min(t, dim[0]);
b = Math.max(b, dim[1]);
target.left = l;
target.right = r;
target.top = t;
target.bottom = b;
},
/**
* @private
* Determin the
* @param a
* @param b
* @param c
* @param d
* @param dim
*/
curveDimension: function (a, b, c, d, dim) {
var qa = 3 * (-a + 3 * (b - c) + d),
qb = 6 * (a - 2 * b + c),
qc = -3 * (a - b), x, y,
min = Math.min(a, d),
max = Math.max(a, d), delta;
if (qa === 0) {
if (qb === 0) {
dim[0] = min;
dim[1] = max;
return;
} else {
x = -qc / qb;
if (0 < x && x < 1) {
y = this.interpolate(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
}
} else {
delta = qb * qb - 4 * qa * qc;
if (delta >= 0) {
delta = Math.sqrt(delta);
x = (delta - qb) / 2 / qa;
if (0 < x && x < 1) {
y = this.interpolate(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
if (delta > 0) {
x -= delta / qa;
if (0 < x && x < 1) {
y = this.interpolate(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
}
}
}
dim[0] = min;
dim[1] = max;
},
/**
* @private
*
* Returns `a * (1 - t) ^ 3 + 3 * b (1 - t) ^ 2 * t + 3 * c (1 - t) * t ^ 3 + d * t ^ 3`.
*
* @param a
* @param b
* @param c
* @param d
* @param t
* @return {Number}
*/
interpolate: function (a, b, c, d, t) {
if (t === 0) {
return a;
}
if (t === 1) {
return d;
}
var rate = (1 - t) / t;
return t * t * t * (d + rate * (3 * c + rate * (3 * b + rate * a)));
},
/**
* Reconstruct path from cubic bezier curve stripes.
* @param {Array} stripes
*/
fromStripes: function (stripes) {
var me = this,
i = 0, ln = stripes.length,
j, ln2, stripe;
me.clear();
for (; i < ln; i++) {
stripe = stripes[i];
me.coords.push.apply(me.coords, stripe);
me.types.push('M');
for (j = 2, ln2 = stripe.length; j < ln2; j += 6) {
me.types.push('C');
}
}
if (!me.cursor) {
me.cursor = [];
}
me.cursor[0] = me.coords[me.coords.length - 2];
me.cursor[1] = me.coords[me.coords.length - 1];
me.dirt();
},
/**
* Convert path to bezier curve stripes.
* @param {Array} [target] The optional array to receive the result.
* @return {Array}
*/
toStripes: function (target) {
var stripes = target || [], curr,
x, y, lastX, lastY, startX, startY,
i, j,
types = this.types,
coords = this.coords,
ln = types.length;
for (i = 0, j = 0; i < ln; i++) {
switch (types[i]) {
case 'M':
curr = [startX = lastX = coords[j++], startY = lastY = coords[j++]];
stripes.push(curr);
break;
case 'L':
x = coords[j++];
y = coords[j++];
curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
break;
case 'C':
curr.push(coords[j++], coords[j++], coords[j++], coords[j++], lastX = coords[j++], lastY = coords[j++]);
break;
case 'Z':
x = startX;
y = startY;
curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
break;
}
}
return stripes;
},
/**
* @private
* Update cache for svg string of this path.
*/
updateSvgString: function () {
var result = [],
types = this.types,
coords = this.coords,
ln = types.length,
i = 0, j = 0;
for (; i < ln; i++) {
switch (types[i]) {
case 'M':
result.push('M' + coords[j] + ',' + coords[j + 1]);
j += 2;
break;
case 'L':
result.push('L' + coords[j] + ',' + coords[j + 1]);
j += 2;
break;
case 'C':
result.push('C' + coords[j] + ',' + coords[j + 1] + ' ' +
coords[j + 2] + ',' + coords[j + 3] + ' ' +
coords[j + 4] + ',' + coords[j + 5]);
j += 6;
break;
case 'Z':
result.push('Z');
break;
}
}
this.svgString = result.join('');
},
/**
* Return an svg path string for this path.
* @return {String}
*/
toString: function () {
if (!this.svgString) {
this.updateSvgString();
}
return this.svgString;
}
});
/**
* @class Ext.draw.sprite.Path
* @extends Ext.draw.sprite.Sprite
*
* A sprite that represents a path.
*
* @example preview miniphone
* var component = new Ext.draw.Component({
* items: [{
* type: 'path',
* path: 'M75,75 c0,-25 50,25 50,0 c0,-25 -50,25 -50,0',
* fillStyle: 'blue'
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(component);
*/
Ext.define("Ext.draw.sprite.Path", {
extend: "Ext.draw.sprite.Sprite",
requires: ['Ext.draw.Draw', 'Ext.draw.Path'],
alias: 'sprite.path',
type: 'path',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {String} path The SVG based path string used by the sprite.
*/
path: function (n, o) {
if (!(n instanceof Ext.draw.Path)) {
n = new Ext.draw.Path(n);
}
return n;
}
},
aliases: {
"d": "path"
},
dirtyTriggers: {
path: 'bbox'
},
updaters: {
"path": function (attr) {
var path = attr.path;
if (!path || path.bindAttr !== attr) {
path = new Ext.draw.Path();
path.bindAttr = attr;
attr.path = path;
}
path.clear();
this.updatePath(path, attr);
attr.dirtyFlags.bbox = ['path'];
}
}
}
},
updatePlainBBox: function (plain) {
if (this.attr.path) {
this.attr.path.getDimension(plain);
}
},
updateTransformedBBox: function (transform) {
if (this.attr.path) {
this.attr.path.getDimensionWithTransform(this.attr.matrix, transform);
}
},
render: function (surface, ctx) {
var mat = this.attr.matrix,
attr = this.attr;
if (!attr.path || attr.path.coords.length === 0) {
return;
}
mat.toContext(ctx);
ctx.appendPath(attr.path);
ctx.fillStroke(attr);
},
/**
* Update the path.
* @param {Ext.draw.Path} path An empty path to draw on using path API.
* @param {Object} attr The attribute object. Note: DO NOT use the `sprite.attr` instead of this
* if you want to work with instancing.
*/
updatePath: function (path, attr) {}
});
/**
* @class Ext.draw.sprite.Circle
* @extends Ext.draw.sprite.Path
*
* A sprite that represents a circle.
*
* @example preview miniphone
* new Ext.draw.Component({
* fullscreen: true,
* items: [{
* type: 'circle',
* cx: 100,
* cy: 100,
* r: 25,
* fillStyle: 'blue'
* }]
* });
*
*/
Ext.define("Ext.draw.sprite.Circle", {
extend: "Ext.draw.sprite.Path",
alias: 'sprite.circle',
type: 'circle',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [cx=0] The center coordinate of the sprite on the x-axis.
*/
cx: "number",
/**
* @cfg {Number} [cy=0] The center coordinate of the sprite on the y-axis.
*/
cy: "number",
/**
* @cfg {Number} [r=0] The radius of the sprite.
*/
r: "number"
},
aliases: {
radius: "r",
x: "cx",
y: "cy",
centerX: "cx",
centerY: "cy"
},
defaults: {
cx: 0,
cy: 0,
r: 0
},
dirtyTriggers: {
cx: 'path',
cy: 'path',
r: 'path'
}
}
},
updatePlainBBox: function (plain) {
var attr = this.attr,
cx = attr.cx,
cy = attr.cy,
r = attr.r;
plain.x = cx - r;
plain.y = cy - r;
plain.width = r + r;
plain.height = r + r;
},
updateTransformedBBox: function (transform) {
var attr = this.attr,
cx = attr.cx,
cy = attr.cy,
r = attr.r,
matrix = attr.matrix,
scalesX = matrix.getScaleX(),
scalesY = matrix.getScaleY(),
w, h;
w = scalesX * r;
h = scalesY * r;
transform.x = matrix.x(cx, cy) - w;
transform.y = matrix.y(cx, cy) - h;
transform.width = w + w;
transform.height = h + h;
},
updatePath: function (path, attr) {
path.arc(attr.cx, attr.cy, attr.r, 0, Math.PI * 2, false);
}
});
/**
* @class Ext.draw.sprite.Ellipse
* @extends Ext.draw.sprite.Path
*
* A sprite that represents an ellipse.
*
* @example preview miniphone
* var component = new Ext.draw.Component({
* items: [{
* type: 'ellipse',
* cx: 100,
* cy: 100,
* rx: 40,
* ry: 25,
* fillStyle: 'blue'
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(component);
*/
Ext.define("Ext.draw.sprite.Ellipse", {
extend: "Ext.draw.sprite.Path",
alias: 'sprite.ellipse',
type: 'circle',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [cx=0] The center coordinate of the sprite on the x-axis.
*/
cx: "number",
/**
* @cfg {Number} [cy=0] The center coordinate of the sprite on the y-axis.
*/
cy: "number",
/**
* @cfg {Number} [rx=1] The radius of the sprite on the x-axis.
*/
rx: "number",
/**
* @cfg {Number} [ry=1] The radius of the sprite on the y-axis.
*/
ry: "number",
/**
* @cfg {Number} [axisRotation=0] The rotation of the sprite about its axis.
*/
axisRotation: "number"
},
aliases: {
radius: "r",
x: "cx",
y: "cy",
centerX: "cx",
centerY: "cy",
radiusX: "rx",
radiusY: "ry"
},
defaults: {
cx: 0,
cy: 0,
rx: 1,
ry: 1,
axisRotation: 0
},
dirtyTriggers: {
cx: 'path',
cy: 'path',
rx: 'path',
ry: 'path',
axisRotation: 'path'
}
}
},
updatePlainBBox: function (plain) {
var attr = this.attr,
cx = attr.cx,
cy = attr.cy,
rx = attr.rx,
ry = attr.ry;
plain.x = cx - rx;
plain.y = cy - ry;
plain.width = rx + rx;
plain.height = ry + ry;
},
updateTransformedBBox: function (transform) {
var attr = this.attr,
cx = attr.cx,
cy = attr.cy,
rx = attr.rx,
ry = attr.ry,
rxy = ry / rx,
matrix = attr.matrix.clone(),
xx, xy, yx, yy, dx, dy, w, h;
matrix.append(1, 0, 0, rxy, 0, cy * (1 - rxy));
xx = matrix.getXX();
yx = matrix.getYX();
dx = matrix.getDX();
xy = matrix.getXY();
yy = matrix.getYY();
dy = matrix.getDY();
w = Math.sqrt(xx * xx + yx * yx) * rx;
h = Math.sqrt(xy * xy + yy * yy) * rx;
transform.x = cx * xx + cy * yx + dx - w;
transform.y = cx * xy + cy * yy + dy - h;
transform.width = w + w;
transform.height = h + h;
},
updatePath: function (path, attr) {
path.ellipse(attr.cx, attr.cy, attr.rx, attr.ry, attr.axisRotation, 0, Math.PI * 2, false);
}
});
/**
* Utility class to provide a way to *approximately* measure the dimension of texts without a drawing context.
*/
Ext.define("Ext.draw.TextMeasurer", {
singleton: true,
uses: ['Ext.draw.engine.Canvas'],
measureDiv: null,
measureCache: {},
/**
* @private Measure the size of a text with specific font by using DOM to measure it.
* Could be very expensive therefore should be used lazily.
* @param {String} text
* @param {String} font
* @return {Object} An object with `width` and `height` properties.
* @return {Number} return.width
* @return {Number} return.height
*/
actualMeasureText: function (text, font) {
var me = Ext.draw.TextMeasurer,
measureDiv = me.measureDiv,
FARAWAY = 100000,
size;
if (!measureDiv) {
var parent = Ext.Element.create({
style: {
"overflow": "hidden",
"position": "relative",
"float": "left", // DO NOT REMOVE THE QUOTE OR IT WILL BREAK COMPRESSOR
"width": 0,
"height": 0
}
});
me.measureDiv = measureDiv = Ext.Element.create({});
measureDiv.setStyle({
"position": 'absolute',
"x": FARAWAY,
"y": FARAWAY,
"z-index": -FARAWAY,
"white-space": "nowrap",
"display": 'block',
"padding": 0,
"margin": 0
});
Ext.getBody().appendChild(parent);
parent.appendChild(measureDiv);
}
if (font) {
measureDiv.setStyle({
font: font,
lineHeight: 'normal'
});
}
measureDiv.setText('(' + text + ')');
size = measureDiv.getSize();
measureDiv.setText('()');
size.width -= measureDiv.getSize().width;
return size;
},
/**
* Measure a single-line text with specific font.
* This will split the text to characters and add up their size.
* That may *not* be the exact size of the text as it is displayed.
* @param {String} text
* @param {String} font
* @return {Object} An object with `width` and `height` properties.
* @return {Number} return.width
* @return {Number} return.height
*/
measureTextSingleLine: function (text, font) {
text = text.toString();
var cache = this.measureCache,
chars = text.split(''),
width = 0,
height = 0,
cachedItem, charactor, i, ln, size;
if (!cache[font]) {
cache[font] = {};
}
cache = cache[font];
if (cache[text]) {
return cache[text];
}
for (i = 0, ln = chars.length; i < ln; i++) {
charactor = chars[i];
if (!(cachedItem = cache[charactor])) {
size = this.actualMeasureText(charactor, font);
cachedItem = cache[charactor] = size;
}
width += cachedItem.width;
height = Math.max(height, cachedItem.height);
}
return cache[text] = {
width: width,
height: height
};
},
/**
* Measure a text with specific font.
* This will split the text to lines and add up their size.
* That may *not* be the exact size of the text as it is displayed.
* @param {String} text
* @param {String} font
* @return {Object} An object with `width` and `height` properties.
* @return {Number} return.width
* @return {Number} return.height
*/
measureText: function (text, font) {
var lines = text.split('\n'),
ln = lines.length,
height = 0,
width = 0,
line, i;
if (ln === 1) {
return this.measureTextSingleLine(text, font);
}
for (i = 0; i < ln; i++) {
line = this.measureTextSingleLine(lines[i], font);
height += line.height;
width = Math.max(width, line.width);
}
return {
width: width,
height: height
};
}
});
/**
* @class Ext.draw.sprite.Text
* @extends Ext.draw.sprite.Sprite
*
* A sprite that represents text.
*
* @example preview miniphone
* var component = new Ext.draw.Component({
* items: [{
* type: 'text',
* x: 50,
* y: 50,
* text: 'Sencha',
* fontSize: 18,
* fillStyle: 'blue'
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(component);
*/
Ext.define("Ext.draw.sprite.Text", {
extend: "Ext.draw.sprite.Sprite",
requires: ['Ext.draw.TextMeasurer'],
alias: 'sprite.text',
type: 'text',
lineBreakRe: /\n/g,
inheritableStatics: {
shortHand1Re: /'(.*)'/g,
shortHand2Re: / /g,
shortHand3Re: /\s*,\s*/g,
shortHand4Re: /\$\$\$\$/g,
def: {
processors: {
/**
* @cfg {Number} [x=0] The position of the sprite on the x-axis.
*/
x: "number",
/**
* @cfg {Number} [y=0] The position of the sprite on the y-axis.
*/
y: "number",
/**
* @cfg {String} [text=''] The text represented in the sprite.
*/
text: "string",
/**
* @cfg {String/Number} [fontSize='10px'] The size of the font displayed.
*/
fontSize: function (n) {
if (!isNaN(n)) {
return +n + 'px';
} else if (n.match(Ext.dom.Element.unitRe)) {
return n;
}
},
/**
* @cfg {String} [fontStyle=''] The style of the font displayed. {normal, italic, oblique}
*/
fontStyle: "enums(,italic,oblique)",
/**
* @cfg {String} [fontVariant=''] The variant of the font displayed. {normal, small-caps}
*/
fontVariant: "enums(,small-caps)",
/**
* @cfg {String} [fontWeight=''] The weight of the font displayed. {normal, bold, bolder, lighter}
*/
fontWeight: (function (fontWeights) {
return function (n) {
if (!n) {
return "";
} else if (n === 'normal') {
return '';
} else if (!isNaN(n)) {
n = +n;
if (100 <= n && n <= 900) {
return n;
}
} else if (n in fontWeights) {
return n;
}
};
})({"normal": true, "bold": true, "bolder": true, "lighter": true}),
/**
* @cfg {String} [fontFamily='sans-serif'] The family of the font displayed.
*/
fontFamily: "string",
/**
* @cfg {String} [textAlign='start'] The alignment of the text displayed. {left, right, center, start, end}
*/
textAlign: (function (textAligns) {
return function (n) {
if (n === 'middle') {
return 'center';
} else if (!n) {
return "center";
} else if (!Ext.isString(n)) {
return undefined;
} else if (n in textAligns) {
return n;
}
};
})({"left": true, "right": true, "center": true, "start": true, "end": true}),
/**
* @cfg {String} [textBaseline="alphabetic"] The baseline of the text displayed. {top, hanging, middle, alphabetic, ideographic, bottom}
*/
textBaseline: (function (textBaselines) {
return function (n) {
if (n === false) {
return "alphabetic";
} else if (n in textBaselines) {
return n;
} else if (n === 'center') {
return 'middle';
}
};
})({"top": true, "hanging": true, "middle": true, "alphabetic": true, "ideographic": true, "bottom": true}),
/**
* @cfg {String} [font='10px sans-serif'] The font displayed.
*/
font: "string"
},
aliases: {
"font-size": "fontSize",
"font-family": "fontFamily",
"font-weight": "fontWeight",
"font-variant": "fontVariant",
"text-anchor": "textAlign"
},
defaults: {
fontStyle: '',
fontVariant: '',
fontWeight: '',
fontSize: '10px',
fontFamily: 'sans-serif',
font: '10px sans-serif',
textBaseline: "alphabetic",
textAlign: "start",
strokeStyle: 'none',
divBased: true,
fillStyle: '#000',
x: 0,
y: 0,
text: ''
},
dirtyTriggers: {
fontStyle: 'font,bbox',
fontVariant: 'font,bbox',
fontWeight: 'font,bbox',
fontSize: 'font,bbox',
fontFamily: 'font,bbox',
font: 'font-short-hand,bbox,canvas',
textBaseline: 'bbox',
textAlign: 'bbox',
x: "bbox",
y: "bbox",
text: "bbox"
},
updaters: {
"font-short-hand": (function (dispatcher) {
return function (attrs) {
// TODO: Do this according to http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
var value = attrs.font,
parts, part, i, ln, dispKey;
value = value.replace(Ext.draw.sprite.Text.shortHand1Re, function (a, arg1) {
return arg1.replace(Ext.draw.sprite.Text.shortHand2Re, '$$$$');
});
value = value.replace(Ext.draw.sprite.Text.shortHand3Re, ',');
parts = value.split(' ');
attrs = {};
for (i = 0, ln = parts.length; i < ln; i++) {
part = parts[i];
dispKey = dispatcher[part];
if (dispKey) {
attrs[dispKey] = part;
} else if (part.match(Ext.dom.Element.unitRe)) {
attrs.fontSize = part;
} else {
attrs.fontFamily = part.replace(Ext.draw.sprite.Text.shortHand4Re, ' ');
}
}
this.setAttributesBypassingNormalization(attrs);
};
})({
"italic": "fontStyles",
"oblique": "fontStyles",
"bold": "fontWeights",
"bolder": "fontWeights",
"lighter": "fontWeights",
"100": "fontWeights",
"200": "fontWeights",
"300": "fontWeights",
"400": "fontWeights",
"500": "fontWeights",
"600": "fontWeights",
"700": "fontWeights",
"800": "fontWeights",
"900": "fontWeights",
"small-caps": "fontVariant"
}),
"font": function (attrs) {
var font = '';
if (attrs.fontWeight) {
font += attrs.fontWeight + ' ';
}
if (attrs.fontVariant) {
font += attrs.fontVariant + ' ';
}
if (attrs.fontSize) {
font += attrs.fontSize + ' ';
}
if (attrs.fontFamily) {
font += attrs.fontFamily + ' ';
}
this.setAttributesBypassingNormalization({
font: font.substr(0, font.length - 1)
});
}
}
}
},
constructor: function (config) {
Ext.draw.sprite.Sprite.prototype.constructor.call(this, config);
},
updatePlainBBox: function (plain) {
var me = this,
attr = me.attr,
x = attr.x,
y = attr.y,
font = attr.font,
text = attr.text,
baseline = attr.textBaseline,
alignment = attr.textAlign,
size = Ext.draw.TextMeasurer.measureText(text, font),
height = size.height,
width = size.width;
switch (baseline) {
case 'hanging' :
case 'top':
break;
case 'ideographic' :
case 'bottom' :
y -= height;
break;
case 'alphabetic' :
y -= height * 0.8;
break;
case 'middle' :
case 'center' :
y -= height * 0.5;
break;
}
switch (alignment) {
case 'end' :
case 'right' :
x -= width;
break;
case 'middle' :
case 'center' :
x -= width * 0.5;
break;
}
plain.x = x;
plain.y = y;
plain.width = width;
plain.height = height;
},
setText: function (text) {
this.setAttributesBypassingNormalization({text: text});
},
setElementStyles: function (element, styles) {
var stylesCache = element.stylesCache || (element.stylesCache = {}),
style = element.dom.style,
name;
for (name in styles) {
if (stylesCache[name] !== styles[name]) {
stylesCache[name] = style[name] = styles[name];
}
}
},
render: function (surface, ctx) {
var attr = this.attr,
mat = Ext.draw.Matrix.fly(attr.matrix.elements.slice(0)),
bbox = this.getBBox(true),
x, y, i, lines;
if (attr.text.length === 0) {
return;
}
lines = attr.text.split('\n');
// Simulate textBaseline and textAlign.
x = attr.bbox.plain.x;
y = attr.bbox.plain.y;
mat.toContext(ctx);
for (i = 0; i < lines.length; i++) {
if (ctx.fillStyle !== 'rgba(0, 0, 0, 0)') {
ctx.fillText(lines[i], x, y + bbox.height / lines.length * i);
}
if (ctx.strokeStyle !== 'rgba(0, 0, 0, 0)') {
ctx.strokeText(lines[i], x, y + bbox.height / lines.length * i);
}
}
}
});
/**
* @class Ext.draw.sprite.EllipticalArc
* @extends Ext.draw.sprite.Ellipse
*
* A sprite that represents an elliptical arc.
*
* @example preview miniphone
* var component = new Ext.draw.Component({
* items: [{
* type: 'ellipticalArc',
* cx: 100,
* cy: 100,
* rx: 40,
* ry: 25,
* fillStyle: 'blue',
* startAngle: 0,
* endAngle: Math.PI,
* anticlockwise: true,
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(component);
*/
Ext.define("Ext.draw.sprite.EllipticalArc", {
extend: "Ext.draw.sprite.Ellipse",
alias: 'sprite.ellipticalArc',
type: 'ellipticalArc',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [startAngle=0] The beginning angle of the arc.
*/
startAngle: "number",
/**
* @cfg {Number} [endAngle=Math.PI*2] The ending angle of the arc.
*/
endAngle: "number",
/**
* @cfg {Boolean} [anticlockwise=false] Determines whether or not the arc is drawn clockwise.
*/
anticlockwise: "bool"
},
aliases: {
from: "startAngle",
to: "endAngle",
start: "startAngle",
end: "endAngle"
},
defaults: {
startAngle: 0,
endAngle: Math.PI * 2,
anticlockwise: false
},
dirtyTriggers: {
startAngle: 'path',
endAngle: 'path',
anticlockwise: 'path'
}
}
},
updatePath: function (path, attr) {
path.ellipse(attr.cx, attr.cy, attr.rx, attr.ry, attr.axisRotation, attr.startAngle, attr.endAngle, attr.anticlockwise);
}
});
/**
* @class Ext.draw.sprite.Sector
* @extends Ext.draw.sprite.Path
*
* A sprite representing a pie slice.
*/
Ext.define("Ext.draw.sprite.Sector", {
extend: "Ext.draw.sprite.Path",
alias: 'sprite.sector',
type: 'sector',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [centerX=0] The center coordinate of the sprite on the x-axis.
*/
centerX: "number",
/**
* @cfg {Number} [centerY=0] The center coordinate of the sprite on the y-axis.
*/
centerY: "number",
/**
* @cfg {Number} [startAngle=0] The starting angle of the sprite.
*/
startAngle: "number",
/**
* @cfg {Number} [endAngle=0] The ending angle of the sprite.
*/
endAngle: "number",
/**
* @cfg {Number} [startRho=0] The starting point of the radius of the sprite.
*/
startRho: "number",
/**
* @cfg {Number} [endRho=150] The ending point of the radius of the sprite.
*/
endRho: "number",
/**
* @cfg {Number} [margin=0] The margin of the sprite from the center of pie.
*/
margin: "number"
},
aliases: {
rho: 'endRho'
},
dirtyTriggers: {
centerX: "path,bbox",
centerY: "path,bbox",
startAngle: "path,bbox",
endAngle: "path,bbox",
startRho: "path,bbox",
endRho: "path,bbox",
margin: "path,bbox"
},
defaults: {
centerX: 0,
centerY: 0,
startAngle: 0,
endAngle: 0,
startRho: 0,
endRho: 150,
margin: 0,
path: 'M 0,0'
}
}
},
updatePath: function (path, attr) {
var startAngle = Math.min(attr.startAngle, attr.endAngle),
endAngle = Math.max(attr.startAngle, attr.endAngle),
midAngle = (startAngle + endAngle) * 0.5,
margin = attr.margin,
centerX = attr.centerX,
centerY = attr.centerY,
startRho = Math.min(attr.startRho, attr.endRho),
endRho = Math.max(attr.startRho, attr.endRho);
if (margin) {
centerX += margin * Math.cos(midAngle);
centerY += margin * Math.sin(midAngle);
}
path.moveTo(centerX + startRho * Math.cos(startAngle), centerY + startRho * Math.sin(startAngle));
path.lineTo(centerX + endRho * Math.cos(startAngle), centerY + endRho * Math.sin(startAngle));
path.arc(centerX, centerY, endRho, startAngle, endAngle, false);
path.lineTo(centerX + startRho * Math.cos(endAngle), centerY + startRho * Math.sin(endAngle));
path.arc(centerX, centerY, startRho, endAngle, startAngle, true);
}
});
/**
* @class Ext.draw.sprite.Composite
* @extends Ext.draw.sprite.Sprite
*
* Represents a group of sprites.
*/
Ext.define("Ext.draw.sprite.Composite", {
extend: "Ext.draw.sprite.Sprite",
alias: 'sprite.composite',
type: 'composite',
constructor: function () {
this.callSuper(arguments);
this.sprites = [];
this.sprites.map = {};
},
/**
* Adds a sprite to the composite.
*/
add: function (sprite) {
if (!(sprite instanceof Ext.draw.sprite.Sprite)) {
sprite = Ext.create('sprite.' + sprite.type, sprite);
sprite.setParent(this);
}
var oldTransformations = sprite.applyTransformations,
me = this,
attr = me.attr;
sprite.applyTransformations = function () {
if (sprite.attr.dirtyTransform) {
attr.dirtyTransform = true;
attr.bbox.plain.dirty = true;
attr.bbox.transform.dirty = true;
}
oldTransformations.call(sprite);
};
this.sprites.push(sprite);
this.sprites.map[sprite.id] = sprite.getId();
attr.bbox.plain.dirty = true;
attr.bbox.transform.dirty = true;
},
/**
* Updates the bounding box of the composite, which contains the bounding box of all sprites in the composite.
*/
updatePlainBBox: function (plain) {
var me = this,
left = Infinity,
right = -Infinity,
top = Infinity,
bottom = -Infinity,
sprite, bbox, i, ln;
for (i = 0, ln = me.sprites.length; i < ln; i++) {
sprite = me.sprites[i];
sprite.applyTransformations();
bbox = sprite.getBBox();
if (left > bbox.x) {
left = bbox.x;
}
if (right < bbox.x + bbox.width) {
right = bbox.x + bbox.width;
}
if (top > bbox.y) {
top = bbox.y;
}
if (bottom < bbox.y + bbox.height) {
bottom = bbox.y + bbox.height;
}
}
plain.x = left;
plain.y = top;
plain.width = right - left;
plain.height = bottom - top;
},
/**
* Renders all sprites contained in the composite to the surface.
*/
render: function (surface, ctx, region) {
var mat = this.attr.matrix,
i, ln;
mat.toContext(ctx);
for (i = 0, ln = this.sprites.length; i < ln; i++) {
surface.renderSprite(this.sprites[i], region);
}
}
});
/**
* @class Ext.draw.sprite.Arc
* @extend Ext.draw.sprite.Circle
*
* A sprite that represents a circular arc.
*
* @example preview miniphone
* var component = new Ext.draw.Component({
* items: [{
* type: 'arc',
* cx: 100,
* cy: 100,
* r: 25,
* fillStyle: 'blue',
* startAngle: 0,
* endAngle: Math.PI,
* anticlockwise: true
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(component);
*/
Ext.define("Ext.draw.sprite.Arc", {
extend: "Ext.draw.sprite.Circle",
alias: 'sprite.arc',
type: 'arc',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [startAngle=0] The beginning angle of the arc.
*/
startAngle: "number",
/**
* @cfg {Number} [endAngle=Math.PI*2] The ending angle of the arc.
*/
endAngle: "number",
/**
* @cfg {Boolean} [anticlockwise=false] Determines whether or not the arc is drawn clockwise.
*/
anticlockwise: "bool"
},
aliases: {
from: "startAngle",
to: "endAngle",
start: "startAngle",
end: "endAngle"
},
defaults: {
startAngle: 0,
endAngle: Math.PI * 2,
anticlockwise: false
},
dirtyTriggers: {
startAngle: 'path',
endAngle: 'path',
anticlockwise: 'path'
}
}
},
updatePath: function (path, attr) {
path.arc(attr.cx, attr.cy, attr.r, attr.startAngle, attr.endAngle, attr.anticlockwise);
}
});
/**
* @class Ext.draw.sprite.Rect
* @extends Ext.draw.sprite.Path
*
* A sprite that represents a rectangle.
*
* @example preview miniphone
* var component = new Ext.draw.Component({
* items: [{
* type: 'rect',
* x: 50,
* y: 50,
* width: 50,
* height: 50,
* fillStyle: 'blue'
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(component);
*/
Ext.define("Ext.draw.sprite.Rect", {
extend: "Ext.draw.sprite.Path",
alias: 'sprite.rect',
type: 'rect',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [x=0] The position of the sprite on the x-axis.
*/
x: 'number',
/**
* @cfg {Number} [y=0] The position of the sprite on the y-axis.
*/
y: 'number',
/**
* @cfg {Number} [width=1] The width of the sprite.
*/
width: 'number',
/**
* @cfg {Number} [height=1] The height of the sprite.
*/
height: 'number',
/**
* @cfg {Number} [radius=0] The radius of the rounded corners.
*/
radius: 'number'
},
aliases: {
},
dirtyTriggers: {
x: 'path',
y: 'path',
width: 'path',
height: 'path',
radius: 'path'
},
defaults: {
x: 0,
y: 0,
width: 1,
height: 1,
radius: 0
}
}
},
updatePlainBBox: function (plain) {
var attr = this.attr;
plain.x = attr.x;
plain.y = attr.y;
plain.width = attr.width;
plain.height = attr.height;
},
updateTransformedBBox: function (transform, plain) {
this.attr.matrix.transformBBox(plain, this.attr.radius, transform);
},
updatePath: function (path, attr) {
var x = attr.x,
y = attr.y,
width = attr.width,
height = attr.height,
radius = Math.min(attr.radius, Math.abs(attr.height) * 0.5, Math.abs(attr.width) * 0.5);
if (radius === 0) {
path.rect(x, y, width, height);
} else {
path.moveTo(x + radius, y);
path.arcTo(x + width, y, x + width, y + height, radius);
path.arcTo(x + width, y + height, x, y + height, radius);
path.arcTo(x, y + height, x, y, radius);
path.arcTo(x, y, x + radius, y, radius);
}
}
});
/**
* @class Ext.draw.sprite.Image
* @extends Ext.draw.sprite.Rect
*
* A sprite that represents an image.
*/
Ext.define("Ext.draw.sprite.Image", {
extend: "Ext.draw.sprite.Rect",
alias: 'sprite.image',
type: 'image',
statics: {
imageLoaders: {}
},
inheritableStatics: {
def: {
processors: {
/**
* @cfg {String} [src=''] The image source of the sprite.
*/
src: 'string'
},
defaults: {
src: '',
width: null,
height: null
}
}
},
render: function (surface, ctx) {
var me = this,
attr = me.attr,
mat = attr.matrix,
src = attr.src,
x = attr.x,
y = attr.y,
width = attr.width,
height = attr.height,
loadingStub = Ext.draw.sprite.Image.imageLoaders[src],
imageLoader,
i;
if (loadingStub && loadingStub.done) {
mat.toContext(ctx);
ctx.drawImage(loadingStub.image, x, y, width || loadingStub.width, height || loadingStub.width);
} else if (!loadingStub) {
imageLoader = new Image();
loadingStub = Ext.draw.sprite.Image.imageLoaders[src] = {
image: imageLoader,
done: false,
pendingSprites: [me],
pendingSurfaces: [surface]
};
imageLoader.width = width;
imageLoader.height = height;
imageLoader.onload = function () {
if (!loadingStub.done) {
loadingStub.done = true;
for (i = 0; i < loadingStub.pendingSprites.length; i++) {
loadingStub.pendingSprites[i].setDirty(true);
}
for (i in loadingStub.pendingSurfaces) {
loadingStub.pendingSurfaces[i].renderFrame();
}
}
};
imageLoader.src = src;
} else {
Ext.Array.include(loadingStub.pendingSprites, me);
Ext.Array.include(loadingStub.pendingSurfaces, surface);
}
}
});
/**
* @class Ext.draw.sprite.Instancing
* @extends Ext.draw.sprite.Sprite
*
* Sprite that represents multiple instances based on the given template.
*/
Ext.define("Ext.draw.sprite.Instancing", {
extend: "Ext.draw.sprite.Sprite",
alias: 'sprite.instancing',
type: 'instancing',
config: {
/**
* @cfg {Object} [template=null] The sprite template used by all instances.
*/
template: null
},
instances: null,
constructor: function (config) {
this.instances = [];
this.callSuper([config]);
if (config && config.template) {
this.setTemplate(config.template);
}
},
applyTemplate: function (template) {
if (!(template instanceof Ext.draw.sprite.Sprite)) {
template = Ext.create(template.xclass || "sprite." + template.type, template);
}
template.setParent(this);
template.attr.children = [];
this.instances = [];
this.position = 0;
return template;
},
/**
* Creates a new sprite instance.
*
* @param {Object} config The configuration of the instance.
* @param {Object} [data]
* @param {Boolean} [bypassNormalization] 'true' to bypass attribute normalization.
* @param {Boolean} [avoidCopy] 'true' to avoid copying.
* @return {Object} The attributes of the instance.
*/
createInstance: function (config, data, bypassNormalization, avoidCopy) {
var template = this.getTemplate(),
originalAttr = template.attr,
attr = Ext.Object.chain(originalAttr);
template.topModifier.prepareAttributes(attr);
template.attr = attr;
template.setAttributes(config, bypassNormalization, avoidCopy);
attr.data = data;
this.instances.push(attr);
template.attr = originalAttr;
this.position++;
originalAttr.children.push(attr);
return attr;
},
/**
* Not supported.
*
* @return {null}
*/
getBBox: function () { return null; },
/**
* Returns the bounding box for the instance at the given index.
*
* @param {Number} index The index of the instance.
* @param {Boolean} [isWithoutTransform] 'true' to not apply sprite transforms to the bounding box.
* @return {Object} The bounding box for the instance.
*/
getBBoxFor: function (index, isWithoutTransform) {
var template = this.getTemplate(),
originalAttr = template.attr,
bbox;
template.attr = this.instances[index];
bbox = template.getBBox(isWithoutTransform);
template.attr = originalAttr;
return bbox;
},
render: function (surface, ctx, clipRegion) {
var me = this,
mat = me.attr.matrix,
template = me.getTemplate(),
originalAttr = template.attr,
instances = me.instances,
i, ln = me.position;
mat.toContext(ctx);
template.preRender(surface, ctx, clipRegion);
template.useAttributes(ctx);
for (i = 0; i < ln; i++) {
if (instances[i].dirtyZIndex) {
break;
}
}
for (i = 0; i < ln; i++) {
if (instances[i].hidden) {
continue;
}
ctx.save();
template.attr = instances[i];
template.applyTransformations();
template.useAttributes(ctx);
template.render(surface, ctx, clipRegion);
ctx.restore();
}
template.attr = originalAttr;
},
/**
* Sets the attributes for the instance at the given index.
*
* @param {Number} index the index of the instance
* @param {Object} changes the attributes to change
* @param {Boolean} [bypassNormalization] 'true' to avoid attribute normalization
*/
setAttributesFor: function (index, changes, bypassNormalization) {
var template = this.getTemplate(),
originalAttr = template.attr,
attr = this.instances[index];
template.attr = attr;
try {
if (bypassNormalization) {
changes = Ext.apply({}, changes);
} else {
changes = template.self.def.normalize(changes);
}
template.topModifier.pushDown(attr, changes);
template.updateDirtyFlags(attr);
} finally {
template.attr = originalAttr;
}
},
destroy: function () {
this.callSuper();
this.instances.length = 0;
this.instances = null;
if (this.getTemplate()) {
this.getTemplate().destroy();
}
}
});
/**
* Linear gradient.
*
* @example preview miniphone
* new Ext.draw.Component({
* fullscreen: true,
* items: [
* {
* type: 'circle',
* cx: 100,
* cy: 100,
* r: 25,
* fillStyle: {
* type: 'linear',
* degrees: 90,
* stops: [
* {
* offset: 0,
* color: 'green'
* },
* {
* offset: 1,
* color: 'blue'
* }
* ]
* }
* }
* ]
* });
*/
Ext.define("Ext.draw.gradient.Linear", {
extend: 'Ext.draw.gradient.Gradient',
type: 'linear',
config: {
/**
* @cfg {Number} The degree of rotation of the gradient.
*/
degrees: 0
},
setAngle: function (angle) {
this.setDegrees(angle);
},
updateDegrees: function () {
this.clearCache();
},
updateStops: function () {
this.clearCache();
},
/**
* @inheritdoc
*/
generateGradient: function (ctx, bbox) {
var angle = Ext.draw.Draw.rad(this.getDegrees()),
cos = Math.cos(angle),
sin = Math.sin(angle),
w = bbox.width,
h = bbox.height,
cx = bbox.x + w * 0.5,
cy = bbox.y + h * 0.5,
stops = this.getStops(),
ln = stops.length,
gradient,
l, i;
if (!isNaN(cx) && !isNaN(cy) && h > 0 && w > 0) {
l = (Math.sqrt(h * h + w * w) * Math.abs(Math.cos(angle - Math.atan(h / w)))) / 2;
gradient = ctx.createLinearGradient(
cx + cos * l, cy + sin * l,
cx - cos * l, cy - sin * l
);
for (i = 0; i < ln; i++) {
gradient.addColorStop(stops[i].offset, stops[i].color);
}
return gradient;
}
return 'none';
}
});
/**
* Radial gradient.
*
* @example preview miniphone
* var component = new Ext.draw.Component({
* items: [{
* type: 'circle',
* cx: 100,
* cy: 100,
* r: 25,
* fillStyle: {
* type: 'radial',
* start: {
* x: 0,
* y: 0,
* r: 0
* },
* end: {
* x: 0,
* y: 0,
* r: 1
* },
* stops: [
* {
* offset: 0,
* color: 'white'
* },
* {
* offset: 1,
* color: 'blue'
* }
* ]
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(component);
*/
Ext.define("Ext.draw.gradient.Radial", {
extend: 'Ext.draw.gradient.Gradient',
type: 'radial',
config: {
/**
* @cfg {Object} start The starting circle of the gradient.
*/
start: {
x: 0,
y: 0,
r: 0
},
/**
* @cfg {Object} end The ending circle of the gradient.
*/
end: {
x: 0,
y: 0,
r: 1
}
},
applyStart: function (newStart, oldStart) {
if (!oldStart) {
return newStart;
}
var circle = {
x: oldStart.x,
y: oldStart.y,
r: oldStart.r
};
if ('x' in newStart) {
circle.x = newStart.x;
} else if ('centerX' in newStart) {
circle.x = newStart.centerX;
}
if ('y' in newStart) {
circle.y = newStart.y;
} else if ('centerY' in newStart) {
circle.y = newStart.centerY;
}
if ('r' in newStart) {
circle.r = newStart.r;
} else if ('radius' in newStart) {
circle.r = newStart.radius;
}
return circle;
},
applyEnd: function (newEnd, oldEnd) {
if (!oldEnd) {
return newEnd;
}
var circle = {
x: oldEnd.x,
y: oldEnd.y,
r: oldEnd.r
};
if ('x' in newEnd) {
circle.x = newEnd.x;
} else if ('centerX' in newEnd) {
circle.x = newEnd.centerX;
}
if ('y' in newEnd) {
circle.y = newEnd.y;
} else if ('centerY' in newEnd) {
circle.y = newEnd.centerY;
}
if ('r' in newEnd) {
circle.r = newEnd.r;
} else if ('radius' in newEnd) {
circle.r = newEnd.radius;
}
return circle;
},
/**
* @inheritdoc
*/
generateGradient: function (ctx, bbox) {
var start = this.getStart(),
end = this.getEnd(),
w = bbox.width * 0.5,
h = bbox.height * 0.5,
x = bbox.x + w,
y = bbox.y + h,
gradient = ctx.createRadialGradient(
x + start.x * w, y + start.y * h, start.r * Math.max(w, h),
x + end.x * w, y + end.y * h, end.r * Math.max(w, h)
),
stops = this.getStops(),
ln = stops.length,
i;
for (i = 0; i < ln; i++) {
gradient.addColorStop(stops[i].offset, stops[i].color);
}
return gradient;
}
});
/**
* Utility class to calculate [affine transformation](http://en.wikipedia.org/wiki/Affine_transformation) matrix.
*
* This class is compatible with SVGMatrix except:
*
* 1. Ext.draw.Matrix is not read only.
* 2. Using Number as its components rather than floats.
*
* Using this class to reduce the severe numeric problem with HTML Canvas and SVG transformation.
*
*/
Ext.define('Ext.draw.Matrix', {
statics: {
/**
* @static
* Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p) and (x1p, y1p)
* @param x0
* @param y0
* @param x1
* @param y1
* @param x0p
* @param y0p
* @param x1p
* @param y1p
*/
createAffineMatrixFromTwoPair: function (x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
var dx = x1 - x0,
dy = y1 - y0,
dxp = x1p - x0p,
dyp = y1p - y0p,
r = 1 / (dx * dx + dy * dy),
a = dx * dxp + dy * dyp,
b = dxp * dy - dx * dyp,
c = -a * x0 - b * y0,
f = b * x0 - a * y0;
return new this(a * r, -b * r, b * r, a * r, c * r + x0p, f * r + y0p);
},
/**
* @static
* Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p) and (x1p, y1p)
* @param x0
* @param y0
* @param x1
* @param y1
* @param x0p
* @param y0p
* @param x1p
* @param y1p
*/
createPanZoomFromTwoPair: function (x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
if (arguments.length === 2) {
return this.createPanZoomFromTwoPair.apply(this, x0.concat(y0));
}
var dx = x1 - x0,
dy = y1 - y0,
cx = (x0 + x1) * 0.5,
cy = (y0 + y1) * 0.5,
dxp = x1p - x0p,
dyp = y1p - y0p,
cxp = (x0p + x1p) * 0.5,
cyp = (y0p + y1p) * 0.5,
r = dx * dx + dy * dy,
rp = dxp * dxp + dyp * dyp,
scale = Math.sqrt(rp / r);
return new this(scale, 0, 0, scale, cxp - scale * cx, cyp - scale * cy);
},
/**
* @static
* Create a flyweight to wrap the given array.
* The flyweight will directly refer the object and the elements can be changed by other methods.
*
* Do not hold the instance of flyweight matrix.
*
* @param {Array} elements
* @return {Ext.draw.Matrix}
*/
fly: (function () {
var flyMatrix = null,
simplefly = function (elements) {
flyMatrix.elements = elements;
return flyMatrix;
};
return function (elements) {
if (!flyMatrix) {
flyMatrix = new Ext.draw.Matrix();
}
flyMatrix.elements = elements;
Ext.draw.Matrix.fly = simplefly;
return flyMatrix;
};
})(),
/**
* @static
* Create a matrix from `mat`. If `mat` is already a matrix, returns it.
* @param {Mixed} mat
* @return {Ext.draw.Matrix}
*/
create: function (mat) {
if (mat instanceof this) {
return mat;
}
return new this(mat);
}
},
/**
* Create an affine transform matrix.
*
* @param xx Coefficient from x to x
* @param xy Coefficient from x to y
* @param yx Coefficient from y to x
* @param yy Coefficient from y to y
* @param dx Offset of x
* @param dy Offset of y
*/
constructor: function (xx, xy, yx, yy, dx, dy) {
if (xx && xx.length === 6) {
this.elements = xx.slice();
} else if (xx !== undefined) {
this.elements = [xx, xy, yx, yy, dx, dy];
} else {
this.elements = [1, 0, 0, 1, 0, 0];
}
},
/**
* Prepend a matrix onto the current.
*
* __Note:__ The given transform will come after the current one.
*
* @param xx Coefficient from x to x.
* @param xy Coefficient from x to y.
* @param yx Coefficient from y to x.
* @param yy Coefficient from y to y.
* @param dx Offset of x.
* @param dy Offset of y.
* @return {Ext.draw.Matrix} this
*/
prepend: function (xx, xy, yx, yy, dx, dy) {
var elements = this.elements,
xx0 = elements[0],
xy0 = elements[1],
yx0 = elements[2],
yy0 = elements[3],
dx0 = elements[4],
dy0 = elements[5];
elements[0] = xx * xx0 + yx * xy0;
elements[1] = xy * xx0 + yy * xy0;
elements[2] = xx * yx0 + yx * yy0;
elements[3] = xy * yx0 + yy * yy0;
elements[4] = xx * dx0 + yx * dy0 + dx;
elements[5] = xy * dx0 + yy * dy0 + dy;
return this;
},
/**
* Prepend a matrix onto the current.
*
* __Note:__ The given transform will come after the current one.
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
prependMatrix: function (matrix) {
return this.prepend.apply(this, matrix.elements);
},
/**
* Postpend a matrix onto the current.
*
* __Note:__ The given transform will come before the current one.
*
* @param xx Coefficient from x to x.
* @param xy Coefficient from x to y.
* @param yx Coefficient from y to x.
* @param yy Coefficient from y to y.
* @param dx Offset of x.
* @param dy Offset of y.
* @return {Ext.draw.Matrix} this
*/
append: function (xx, xy, yx, yy, dx, dy) {
var elements = this.elements,
xx0 = elements[0],
xy0 = elements[1],
yx0 = elements[2],
yy0 = elements[3],
dx0 = elements[4],
dy0 = elements[5];
elements[0] = xx * xx0 + xy * yx0;
elements[1] = xx * xy0 + xy * yy0;
elements[2] = yx * xx0 + yy * yx0;
elements[3] = yx * xy0 + yy * yy0;
elements[4] = dx * xx0 + dy * yx0 + dx0;
elements[5] = dx * xy0 + dy * yy0 + dy0;
return this;
},
/**
* Postpend a matrix onto the current.
*
* __Note:__ The given transform will come before the current one.
*
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
appendMatrix: function (matrix) {
return this.append.apply(this, matrix.elements);
},
/**
* Set the elements of a Matrix
* @param {Number} xx
* @param {Number} xy
* @param {Number} yx
* @param {Number} yy
* @param {Number} dx
* @param {Number} dy
* @return {Ext.draw.Matrix} this
*/
set: function (xx, xy, yx, yy, dx, dy) {
var elements = this.elements;
elements[0] = xx;
elements[1] = xy;
elements[2] = yx;
elements[3] = yy;
elements[4] = dx;
elements[5] = dy;
return this;
},
/**
* Return a new matrix represents the opposite transformation of the current one.
*
* @param {Ext.draw.Matrix} [target] A target matrix. If present, it will receive
* the result of inversion to avoid creating a new object.
*
* @return {Ext.draw.Matrix}
*/
inverse: function (target) {
var elements = this.elements,
a = elements[0],
b = elements[1],
c = elements[2],
d = elements[3],
e = elements[4],
f = elements[5],
rDim = 1 / (a * d - b * c);
a *= rDim;
b *= rDim;
c *= rDim;
d *= rDim;
if (target) {
target.set(d, -b, -c, a, c * f - d * e, b * e - a * f);
return target;
} else {
return new Ext.draw.Matrix(d, -b, -c, a, c * f - d * e, b * e - a * f);
}
},
/**
* Translate the matrix.
*
* @param {Number} x
* @param {Number} y
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
translate: function (x, y, prepend) {
if (prepend) {
return this.prepend(1, 0, 0, 1, x, y);
} else {
return this.append(1, 0, 0, 1, x, y);
}
},
/**
* Scale the matrix.
*
* @param {Number} sx
* @param {Number} sy
* @param {Number} scx
* @param {Number} scy
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
scale: function (sx, sy, scx, scy, prepend) {
var me = this;
// null or undefined
if (sy == null) {
sy = sx;
}
if (scx === undefined) {
scx = 0;
}
if (scy === undefined) {
scy = 0;
}
if (prepend) {
return me.prepend(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
} else {
return me.append(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
}
},
/**
* Rotate the matrix.
*
* @param {Number} angle Radians to rotate
* @param {Number|null} rcx Center of rotation.
* @param {Number|null} rcy Center of rotation.
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
rotate: function (angle, rcx, rcy, prepend) {
var me = this,
cos = Math.cos(angle),
sin = Math.sin(angle);
rcx = rcx || 0;
rcy = rcy || 0;
if (prepend) {
return me.prepend(
cos, sin,
-sin, cos,
rcx - cos * rcx + rcy * sin,
rcy - cos * rcy - rcx * sin
);
} else {
return me.append(
cos, sin,
-sin, cos,
rcx - cos * rcx + rcy * sin,
rcy - cos * rcy - rcx * sin
);
}
},
/**
* Rotate the matrix by the angle of a vector.
*
* @param {Number} x
* @param {Number} y
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
rotateFromVector: function (x, y, prepend) {
var me = this,
d = Math.sqrt(x * x + y * y),
cos = x / d,
sin = y / d;
if (prepend) {
return me.prepend(cos, sin, -sin, cos, 0, 0);
} else {
return me.append(cos, sin, -sin, cos, 0, 0);
}
},
/**
* Clone this matrix.
* @return {Ext.draw.Matrix}
*/
clone: function () {
return new Ext.draw.Matrix(this.elements);
},
/**
* Horizontally flip the matrix
* @return {Ext.draw.Matrix} this
*/
flipX: function () {
return this.append(-1, 0, 0, 1, 0, 0);
},
/**
* Vertically flip the matrix
* @return {Ext.draw.Matrix} this
*/
flipY: function () {
return this.append(1, 0, 0, -1, 0, 0);
},
/**
* Skew the matrix
* @param {Number} angle
* @return {Ext.draw.Matrix} this
*/
skewX: function (angle) {
return this.append(1, Math.tan(angle), 0, -1, 0, 0);
},
/**
* Skew the matrix
* @param {Number} angle
* @return {Ext.draw.Matrix} this
*/
skewY: function (angle) {
return this.append(1, 0, Math.tan(angle), -1, 0, 0);
},
/**
* Reset the matrix to identical.
* @return {Ext.draw.Matrix} this
*/
reset: function () {
return this.set(1, 0, 0, 1, 0, 0);
},
/**
* @private
* Split Matrix to `{{devicePixelRatio,c,0},{b,devicePixelRatio,0},{0,0,1}}.{{xx,0,dx},{0,yy,dy},{0,0,1}}`
* @return {Object} Object with b,c,d=devicePixelRatio,xx,yy,dx,dy
*/
precisionCompensate: function (devicePixelRatio, comp) {
var elements = this.elements,
x2x = elements[0],
x2y = elements[1],
y2x = elements[2],
y2y = elements[3],
newDx = elements[4],
newDy = elements[5],
r = x2y * y2x - x2x * y2y;
comp.b = devicePixelRatio * x2y / x2x;
comp.c = devicePixelRatio * y2x / y2y;
comp.d = devicePixelRatio;
comp.xx = x2x / devicePixelRatio;
comp.yy = y2y / devicePixelRatio;
comp.dx = (newDy * x2x * y2x - newDx * x2x * y2y) / r / devicePixelRatio;
comp.dy = (newDx * x2y * y2y - newDy * x2x * y2y) / r / devicePixelRatio;
},
/**
* @private
* Split Matrix to `{{1,c,0},{b,d,0},{0,0,1}}.{{xx,0,dx},{0,xx,dy},{0,0,1}}`
* @return {Object} Object with b,c,d,xx,yy=xx,dx,dy
*/
precisionCompensateRect: function (devicePixelRatio, comp) {
var elements = this.elements,
x2x = elements[0],
x2y = elements[1],
y2x = elements[2],
y2y = elements[3],
newDx = elements[4],
newDy = elements[5],
yxOnXx = y2x / x2x;
comp.b = devicePixelRatio * x2y / x2x;
comp.c = devicePixelRatio * yxOnXx;
comp.d = devicePixelRatio * y2y / x2x;
comp.xx = x2x / devicePixelRatio;
comp.yy = x2x / devicePixelRatio;
comp.dx = (newDy * y2x - newDx * y2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
comp.dy = -(newDy * x2x - newDx * x2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
},
/**
* Transform point returning the x component of the result.
* @param {Number} x
* @param {Number} y
* @return {Number} x component of the result.
*/
x: function (x, y) {
var elements = this.elements;
return x * elements[0] + y * elements[2] + elements[4];
},
/**
* Transform point returning the y component of the result.
* @param {Number} x
* @param {Number} y
* @return {Number} y component of the result.
*/
y: function (x, y) {
var elements = this.elements;
return x * elements[1] + y * elements[3] + elements[5];
},
/**
* @private
* @param i
* @param j
* @return {String}
*/
get: function (i, j) {
return +this.elements[i + j * 2].toFixed(4);
},
/**
* Transform a point to a new array.
* @param {Array} point
* @return {Array}
*/
transformPoint: function (point) {
var elements = this.elements;
return [
point[0] * elements[0] + point[1] * elements[2] + elements[4],
point[0] * elements[1] + point[1] * elements[3] + elements[5]
];
},
/**
*
* @param {Object} bbox Given as `{x: Number, y: Number, width: Number, height: Number}`.
* @param {Number} [radius]
* @param {Object} [target] Optional target object to recieve the result.
* Recommanded to use it for better gc.
*
* @return {Object} Object with x, y, width and height.
*/
transformBBox: function (bbox, radius, target) {
var elements = this.elements,
l = bbox.x,
t = bbox.y,
w0 = bbox.width * 0.5,
h0 = bbox.height * 0.5,
xx = elements[0],
xy = elements[1],
yx = elements[2],
yy = elements[3],
cx = l + w0,
cy = t + h0,
w, h, scales;
if (radius) {
w0 -= radius;
h0 -= radius;
scales = [
Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]),
Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3])
];
w = Math.abs(w0 * xx) + Math.abs(h0 * yx) + Math.abs(scales[0] * radius);
h = Math.abs(w0 * xy) + Math.abs(h0 * yy) + Math.abs(scales[1] * radius);
} else {
w = Math.abs(w0 * xx) + Math.abs(h0 * yx);
h = Math.abs(w0 * xy) + Math.abs(h0 * yy);
}
if (!target) {
target = {};
}
target.x = cx * xx + cy * yx + elements[4] - w;
target.y = cx * xy + cy * yy + elements[5] - h;
target.width = w + w;
target.height = h + h;
return target;
},
/**
* Transform a list for points.
*
* __Note:__ will change the original list but not points inside it.
* @param {Array} list
* @return {Array} list
*/
transformList: function (list) {
var elements = this.elements,
xx = elements[0], yx = elements[2], dx = elements[4],
xy = elements[1], yy = elements[3], dy = elements[5],
ln = list.length,
p, i;
for (i = 0; i < ln; i++) {
p = list[i];
list[i] = [
p[0] * xx + p[1] * yx + dx,
p[0] * xy + p[1] * yy + dy
];
}
return list;
},
/**
* Determines whether this matrix is an identity matrix (no transform).
* @return {Boolean}
*/
isIdentity: function () {
var elements = this.elements;
return elements[0] === 1 &&
elements[1] === 0 &&
elements[2] === 0 &&
elements[3] === 1 &&
elements[4] === 0 &&
elements[5] === 0;
},
/**
* Determines if this matrix has the same values as another matrix.
* @param {Ext.draw.Matrix} matrix
* @return {Boolean}
*/
equals: function (matrix) {
var elements = this.elements,
elements2 = matrix.elements;
return elements[0] === elements2[0] &&
elements[1] === elements2[1] &&
elements[2] === elements2[2] &&
elements[3] === elements2[3] &&
elements[4] === elements2[4] &&
elements[5] === elements2[5];
},
/**
* Create an array of elements by horizontal order (xx,yx,dx,yx,yy,dy).
* @return {Array}
*/
toArray: function () {
var elements = this.elements;
return [elements[0], elements[2], elements[4], elements[1], elements[3], elements[5]];
},
/**
* Create an array of elements by vertical order (xx,xy,yx,yy,dx,dy).
* @return {Array|String}
*/
toVerticalArray: function () {
return this.elements.slice();
},
/**
* Get an array of elements.
* The numbers are rounded to keep only 4 decimals.
* @return {Array}
*/
toString: function () {
var me = this;
return [me.get(0, 0), me.get(0, 1), me.get(1, 0), me.get(1, 1), me.get(2, 0), me.get(2, 1)].join(',');
},
/**
* Apply the matrix to a drawing context.
* @param {Object} ctx
* @return {Ext.draw.Matrix} this
*/
toContext: function (ctx) {
ctx.transform.apply(ctx, this.elements);
return this;
},
/**
* Return a string that can be used as transform attribute in SVG.
* @return {String}
*/
toSvg: function () {
var elements = this.elements;
// The reason why we cannot use `.join` is the `1e5` form is not accepted in svg.
return "matrix(" +
elements[0].toFixed(9) + ',' +
elements[1].toFixed(9) + ',' +
elements[2].toFixed(9) + ',' +
elements[3].toFixed(9) + ',' +
elements[4].toFixed(9) + ',' +
elements[5].toFixed(9) +
")";
},
/**
* Get the x scale of the matrix.
* @return {Number}
*/
getScaleX: function () {
var elements = this.elements;
return Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]);
},
/**
* Get the y scale of the matrix.
* @return {Number}
*/
getScaleY: function () {
var elements = this.elements;
return Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3]);
},
/**
* Get x-to-x component of the matrix
* @return {Number}
*/
getXX: function () {
return this.elements[0];
},
/**
* Get x-to-y component of the matrix.
* @return {Number}
*/
getXY: function () {
return this.elements[1];
},
/**
* Get y-to-x component of the matrix.
* @return {Number}
*/
getYX: function () {
return this.elements[2];
},
/**
* Get y-to-y component of the matrix.
* @return {Number}
*/
getYY: function () {
return this.elements[3];
},
/**
* Get offset x component of the matrix.
* @return {Number}
*/
getDX: function () {
return this.elements[4];
},
/**
* Get offset y component of the matrix.
* @return {Number}
*/
getDY: function () {
return this.elements[5];
},
/**
* Split matrix into Translate Scale, Shear, and Rotate.
* @return {Object}
*/
split: function () {
var el = this.elements,
xx = el[0],
xy = el[1],
yx = el[2],
yy = el[3],
out = {
translateX: el[4],
translateY: el[5]
};
out.scaleX = Math.sqrt(xx * xx + yx * yx);
out.shear = (xx * xy + yx * yy) / out.scaleX;
xy -= out.shear * xx;
yy -= out.shear * yx;
out.scaleY = Math.sqrt(xy * xy + yy * yy);
out.shear /= out.scaleY;
out.rotation = -Math.atan2(yx / out.scaleX, xy / out.scaleY);
out.isSimple = Math.abs(out.shear) < 1e-9 && (!out.rotation || Math.abs(out.scaleX - out.scaleY) < 1e-9);
return out;
}
}, function () {
function registerName(properties, name, i) {
properties[name] = {
get: function () {
return this.elements[i];
},
set: function (val) {
this.elements[i] = val;
}
};
}
// Compatibility with SVGMatrix
// https://developer.mozilla.org/en/DOM/SVGMatrix
if (Object.defineProperties) {
var properties = {};
/**
* @property {Number} a Get x-to-x component of the matrix. Avoid using it for performance consideration.
* Use {@link #getXX} instead.
*/
registerName(properties, 'a', 0);
// TODO: Help me finish this.
registerName(properties, 'b', 1);
registerName(properties, 'c', 2);
registerName(properties, 'd', 3);
registerName(properties, 'e', 4);
registerName(properties, 'f', 5);
Object.defineProperties(this.prototype, properties);
}
/**
* Postpend a matrix onto the current.
*
* __Note:__ The given transform will come before the current one.
*
* @method
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
this.prototype.multiply = this.prototype.appendMatrix;
});
/**
* @deprecated
* A collection of sprites that delegates sprite functions to its elements.
*
* Avoid using this multiple groups in a surface as it is error prone.
* The group notion may be remove in future releases.
*
*/
Ext.define("Ext.draw.Group", {
mixins: {
observable: 'Ext.mixin.Observable'
},
config: {
surface: null
},
statics: {
/**
* @private
* @param name
* @return {Function}
*/
createRelayEvent: function (name) {
return (function (e) {
this.fireEvent(name, e);
});
},
/**
* @private
* @param name
* @return {Function}
*/
createDispatcherMethod: function (name) {
return function () {
var args = Array.prototype.slice.call(arguments, 0), items = this.items, i = 0, ln = items.length, item;
while (i < ln) {
item = items[i++];
item[name].apply(item, args);
}
};
}
},
autoDestroy: false,
constructor: function (config) {
this.initConfig(config);
this.map = {};
this.items = [];
this.length = 0;
},
/**
* Add sprite to group.
* @param {Ext.draw.sprite.Sprite} sprite
*/
add: function (sprite) {
var id = sprite.getId(),
oldSprite = this.map[id];
if (!oldSprite) {
sprite.group.push(this.id);
this.map[id] = sprite;
this.items.push(sprite);
this.length++;
} else if (sprite !== oldSprite) {
Ext.Logger.error('Sprite with duplicated id.');
}
},
/**
* Remote sprite from group.
* @param {Ext.draw.sprite.Sprite} sprite
* @param {Boolean} [destroySprite]
*/
remove: function (sprite, destroySprite) {
var id = sprite.getId(),
oldSprite = this.map[id];
destroySprite = destroySprite || this.autoDestroy;
if (oldSprite) {
if (oldSprite === sprite) {
delete this.map[id];
this.length--;
Ext.Array.remove(this.items, sprite);
if (destroySprite) {
oldSprite.destroy();
} else {
Ext.Array.remove(sprite.group, this);
}
} else if (sprite !== oldSprite) {
Ext.Logger.error('Sprite with duplicated id.');
}
}
},
/**
* Add a list of sprites to group.
* @param {Array|Ext.draw.sprite.Sprite} sprites
*/
addAll: function (sprites) {
if (sprites.isSprite) {
this.add(sprites);
} else if (Ext.isArray(sprites)) {
var i = 0;
while (i < sprites.length) {
this.add(sprites[i++]);
}
}
},
/**
* Iterate all sprites with specific function.
* __Note:__ Avoid using this for performance consideration.
* @param {Function} fn Function to iterate.
*/
each: function (fn) {
var i = 0,
items = this.items,
ln = items.length;
while (i < ln) {
if (false === fn(items[i])) {
return;
}
}
},
/**
* Clear the group
* @param {Boolean} [destroySprite]
*/
clear: function (destroySprite) {
var i, ln, sprite, items;
if (destroySprite || this.autoDestroy) {
items = this.items.slice(0);
for (i = 0, ln = items.length; i < ln; i++) {
items[i].destroy();
}
} else {
items = this.items.slice(0);
for (i = 0, ln = items.length; i < ln; i++) {
sprite = items[i];
Ext.Array.remove(sprite.group, this);
}
}
this.length = 0;
this.map = {};
this.items.length = 0;
},
/**
* Get the i-th sprite of the group.
* __Note:__ Do not reply on the order of the sprite. It could be changed by {@link Ext.draw.Surface#stableSort}.
* @param {Number} index
* @return {Ext.draw.sprite.Sprite}
*/
getAt: function (index) {
return this.items[index];
},
/**
* Get the sprite with id or index.
* It will first find sprite with given id, otherwise will try to use the id as an index.
* @param {String|Number} id
* @return {Ext.draw.sprite.Sprite}
*/
get: function (id) {
return this.map[id] || this.items[id];
},
/**
* Destroy the group and remove it from surface.
*/
destroy: function () {
this.clear();
this.getSurface().getGroups().remove(this);
}
}, function () {
this.addMembers({
/**
* Set attributes to all sprites in the group.
*
* @param {Object} o Sprite attribute options just like in {@link Ext.draw.sprite.Sprite}.
* @method
*/
setAttributes: this.createDispatcherMethod('setAttributes'),
/**
* Display all sprites in the group.
*
* @param {Boolean} o Whether to re-render the frame.
* @method
*/
show: this.createDispatcherMethod('show'),
/**
* Hide all sprites in the group.
*
* @param {Boolean} o Whether to re-render the frame.
* @method
*/
hide: this.createDispatcherMethod('hide'),
/**
* Set dirty flag for all sprites in the group
* @method
*/
setDirty: this.createDispatcherMethod('setDirty'),
/**
* Return the minimal bounding box that contains all the sprites bounding boxes in this group.
*
* Bad performance. Avoid using it.
*/
getBBox: function (isWithTransform) {
if (this.length === 0) {
return {x: 0, y: 0, width: 0, height: 0};
}
var i, ln, l = Infinity, r = -Infinity, t = Infinity, b = -Infinity, bbox;
for (i = 0, ln = this.items.length; i < ln; i++) {
bbox = this.items[i].getBBox(isWithTransform);
if (!bbox) {
continue;
}
if (bbox.x + bbox.width > r) {
r = bbox.x + bbox.width;
}
if (bbox.x < l) {
l = bbox.x;
}
if (bbox.y + bbox.height > b) {
b = bbox.y + bbox.height;
}
if (bbox.y < t) {
t = bbox.y;
}
}
return {
x: l,
y: t,
height: b - t,
width: r - l
};
}
});
});
/**
* A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}.
* A Surface contains methods to render sprites, get bounding boxes of sprites, add
* sprites to the canvas, initialize other graphic components, etc. One of the most used
* methods for this class is the `add` method, to add Sprites to the surface.
*
* Most of the Surface methods are abstract and they have a concrete implementation
* in VML or SVG engines.
*
* A Surface instance can be accessed as a property of a draw component. For example:
*
* drawComponent.getSurface('main').add({
* type: 'circle',
* fill: '#ffc',
* radius: 100,
* x: 100,
* y: 100
* });
*
* The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.sprite.Sprite}
* class documentation.
*
* ## Example
*
* drawComponent.getSurface('main').add([
* {
* type: 'circle',
* radius: 10,
* fill: '#f00',
* x: 10,
* y: 10,
* group: 'circles'
* },
* {
* type: 'circle',
* radius: 10,
* fill: '#0f0',
* x: 50,
* y: 50,
* group: 'circles'
* },
* {
* type: 'circle',
* radius: 10,
* fill: '#00f',
* x: 100,
* y: 100,
* group: 'circles'
* },
* {
* type: 'rect',
* radius: 10,
* x: 10,
* y: 10,
* group: 'rectangles'
* },
* {
* type: 'rect',
* radius: 10,
* x: 50,
* y: 50,
* group: 'rectangles'
* },
* {
* type: 'rect',
* radius: 10,
* x: 100,
* y: 100,
* group: 'rectangles'
* }
* ]);
*
*/
Ext.define('Ext.draw.Surface', {
extend: 'Ext.Component',
xtype: 'surface',
requires: [
'Ext.draw.sprite.*',
'Ext.draw.gradient.*',
'Ext.draw.sprite.AttributeDefinition',
'Ext.draw.Matrix',
'Ext.draw.Draw',
'Ext.draw.Group'
],
uses: [
"Ext.draw.engine.Canvas"
],
defaultIdPrefix: 'ext-surface-',
/**
* The reported device pixel density.
*/
devicePixelRatio: window.devicePixelRatio,
statics: {
/**
* Stably sort the list of sprites by their zIndex.
* TODO: Improve the performance. Reduce gc impact.
* @param list
*/
stableSort: function (list) {
if (list.length < 2) {
return;
}
var keys = {}, sortedKeys, result = [], i, ln, zIndex;
for (i = 0, ln = list.length; i < ln; i++) {
zIndex = list[i].attr.zIndex;
if (!keys[zIndex]) {
keys[zIndex] = [list[i]];
} else {
keys[zIndex].push(list[i]);
}
}
sortedKeys = Object.keys(keys).sort(function (a, b) {return a - b;});
for (i = 0, ln = sortedKeys.length; i < ln; i++) {
result.push.apply(result, keys[sortedKeys[i]]);
}
for (i = 0, ln = list.length; i < ln; i++) {
list[i] = result[i];
}
}
},
config: {
/**
* @cfg {Array}
* The region of the surface related to its component.
*/
region: null,
/**
* @cfg {Object}
* The config of a background sprite of current surface
*/
background: null,
/**
* @cfg {Ext.draw.Group}
* The default group of the surfaces.
*/
items: [],
/**
* @cfg {Array}
* An array of groups.
*/
groups: [],
/**
* @cfg {Boolean}
* Indicates whether the surface needs redraw.
*/
dirty: false
},
dirtyPredecessor: 0,
constructor: function (config) {
var me = this;
me.predecessors = [];
me.successors = [];
me.pendingRenderFrame = false;
me.callSuper([config]);
me.matrix = new Ext.draw.Matrix();
me.inverseMatrix = me.matrix.inverse(me.inverseMatrix);
me.resetTransform();
},
/**
* Round the number to align to the pixels on device.
* @param num The number to align.
* @return {Number} The resultant alignment.
*/
roundPixel: function (num) {
return Math.round(this.devicePixelRatio * num) / this.devicePixelRatio;
},
/**
* Mark the surface to render after another surface is updated.
* @param surface The surface to wait for.
*/
waitFor: function (surface) {
var me = this,
predecessors = me.predecessors;
if (!Ext.Array.contains(predecessors, surface)) {
predecessors.push(surface);
surface.successors.push(me);
if (surface._dirty) {
me.dirtyPredecessor++;
}
}
},
setDirty: function (dirty) {
if (this._dirty !== dirty) {
var successors = this.successors, successor,
i, ln = successors.length;
for (i = 0; i < ln; i++) {
successor = successors[i];
if (dirty) {
successor.dirtyPredecessor++;
successor.setDirty(true);
} else {
successor.dirtyPredecessor--;
if (successor.dirtyPredecessor === 0 && successor.pendingRenderFrame) {
successor.renderFrame();
}
}
}
this._dirty = dirty;
}
},
applyElement: function (newElement, oldElement) {
if (oldElement) {
oldElement.set(newElement);
} else {
oldElement = Ext.Element.create(newElement);
}
this.setDirty(true);
return oldElement;
},
applyBackground: function (background, oldBackground) {
this.setDirty(true);
if (Ext.isString(background)) {
background = { fillStyle: background };
}
return Ext.factory(background, Ext.draw.sprite.Rect, oldBackground);
},
applyRegion: function (region, oldRegion) {
if (oldRegion && region[0] === oldRegion[0] && region[1] === oldRegion[1] && region[2] === oldRegion[2] && region[3] === oldRegion[3]) {
return;
}
if (Ext.isArray(region)) {
return [region[0], region[1], region[2], region[3]];
} else if (Ext.isObject(region)) {
return [
region.x || region.left,
region.y || region.top,
region.width || (region.right - region.left),
region.height || (region.bottom - region.top)
];
}
},
updateRegion: function (region) {
var me = this,
l = region[0],
t = region[1],
r = l + region[2],
b = t + region[3],
background = this.getBackground(),
element = me.element;
element.setBox({
top: Math.floor(t),
left: Math.floor(l),
width: Math.ceil(r - Math.floor(l)),
height: Math.ceil(b - Math.floor(t))
});
if (background) {
background.setAttributes({
x: 0,
y: 0,
width: Math.ceil(r - Math.floor(l)),
height: Math.ceil(b - Math.floor(t))
});
}
me.setDirty(true);
},
/**
* Reset the matrix of the surface.
*/
resetTransform: function () {
this.matrix.set(1, 0, 0, 1, 0, 0);
this.inverseMatrix.set(1, 0, 0, 1, 0, 0);
this.setDirty(true);
},
updateComponent: function (component, oldComponent) {
if (component) {
component.element.dom.appendChild(this.element.dom);
}
},
/**
* Add a Sprite to the surface.
* You can put any number of object as parameter.
* See {@link Ext.draw.sprite.Sprite} for the configuration object to be passed into this method.
*
* For example:
*
* drawComponent.surface.add({
* type: 'circle',
* fill: '#ffc',
* radius: 100,
* x: 100,
* y: 100
* });
*
*/
add: function () {
var me = this,
args = Array.prototype.slice.call(arguments),
argIsArray = Ext.isArray(args[0]),
sprite, items, i, ln, results, group, groups;
items = argIsArray ? args[0] : args;
results = [];
for (i = 0, ln = items.length; i < ln; i++) {
sprite = items[i];
if (!items[i]) {
continue;
}
sprite = me.prepareItems(args[0])[0];
groups = sprite.group;
if (groups.length) {
for (i = 0, ln = groups.length; i < ln; i++) {
group = groups[i];
me.getGroup(group).add(sprite);
}
}
me.getItems().add(sprite);
results.push(sprite);
sprite.setParent(this);
me.onAdd(sprite);
}
me.dirtyZIndex = true;
me.setDirty(true);
if (!argIsArray && results.length === 1) {
return results[0];
} else {
return results;
}
},
/**
* @protected
* Invoked when a sprite is adding to the surface.
* @param {Ext.draw.sprite.Sprite} sprite The sprite to be added.
*/
onAdd: Ext.emptyFn,
/**
* Remove a given sprite from the surface, optionally destroying the sprite in the process.
* You can also call the sprite own `remove` method.
*
* For example:
*
* drawComponent.surface.remove(sprite);
* // or...
* sprite.remove();
*
* @param {Ext.draw.sprite.Sprite} sprite
* @param {Boolean} destroySprite
*/
remove: function (sprite, destroySprite) {
if (sprite) {
if (destroySprite === true) {
sprite.destroy();
} else {
this.getGroups().each(function (item) {
item.remove(sprite);
});
this.getItems().remove(sprite);
}
this.dirtyZIndex = true;
this.setDirty(true);
}
},
/**
* Remove all sprites from the surface, optionally destroying the sprites in the process.
*
* For example:
*
* drawComponent.surface.removeAll();
*
*/
removeAll: function () {
this.getItems().clear();
this.dirtyZIndex = true;
},
// @private
applyItems: function (items, oldItems) {
var result;
if (items instanceof Ext.draw.Group) {
result = items;
} else {
result = new Ext.draw.Group({surface: this});
result.autoDestroy = true;
result.addAll(this.prepareItems(items));
}
this.setDirty(true);
return result;
},
/**
* @private
* Initialize and apply defaults to surface items.
*/
prepareItems: function (items) {
items = [].concat(items);
// Make sure defaults are applied and item is initialized
var me = this,
item, i, ln, j,
removeSprite = function (sprite) {
this.remove(sprite, false);
};
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
if (!(item instanceof Ext.draw.sprite.Sprite)) {
// Temporary, just take in configs...
item = items[i] = me.createItem(item);
}
for (j = 0; j < item.group.length; j++) {
me.getGroup(item.group[i]).add(item);
}
item.on('beforedestroy', removeSprite, me);
}
return items;
},
applyGroups: function (groups, oldGroups) {
var result;
if (groups instanceof Ext.util.MixedCollection) {
result = groups;
} else {
result = new Ext.util.MixedCollection();
result.addAll(groups);
}
if (oldGroups) {
oldGroups.each(function (group) {
if (!result.contains()) {
group.destroy();
}
});
oldGroups.destroy();
}
this.setDirty(true);
return result;
},
/**
* @deprecated Do not use groups directly
* Returns a new group or an existent group associated with the current surface.
* The group returned is a {@link Ext.draw.Group} group.
*
* For example:
*
* var spriteGroup = drawComponent.surface.getGroup('someGroupId');
*
* @param {String} id The unique identifier of the group.
* @return {Ext.draw.Group} The group.
*/
getGroup: function (id) {
var group;
if (typeof id === "string") {
group = this.getGroups().get(id);
if (!group) {
group = this.createGroup(id);
}
} else {
group = id;
}
return group;
},
/**
* @private
* @deprecated Do not use groups directly
* @param id
* @return {Ext.draw.Group} The group.
*/
createGroup: function (id) {
var group = this.getGroups().get(id);
if (!group) {
group = new Ext.draw.Group({surface: this});
group.id = id || Ext.id(null, 'ext-surface-group-');
this.getGroups().add(group);
}
this.setDirty(true);
return group;
},
/**
* @private
* @deprecated Do not use groups directly
* @param group
*/
removeGroup: function (group) {
if (Ext.isString(group)) {
group = this.getGroups().get(group);
}
if (group) {
this.getGroups().remove(group);
group.destroy();
}
this.setDirty(true);
},
/**
* @private Creates an item and appends it to the surface. Called
* as an internal method when calling `add`.
*/
createItem: function (config) {
var sprite = Ext.create(config.xclass || 'sprite.' + config.type, config);
return sprite;
},
/**
* @deprecated Use the `sprite.getBBox(isWithoutTransform)` directly.
* @param sprite
* @param isWithoutTransform
* @return {Object}
*/
getBBox: function (sprite, isWithoutTransform) {
return sprite.getBBox(isWithoutTransform);
},
/**
* Empty the surface content (without touching the sprites.)
*/
clear: Ext.emptyFn,
/**
* @private
* Order the items by their z-index if any of that has been changed since last sort.
*/
orderByZIndex: function () {
var me = this,
items = me.getItems().items,
dirtyZIndex = false,
i, ln;
if (me.getDirty()) {
for (i = 0, ln = items.length; i < ln; i++) {
if (items[i].attr.dirtyZIndex) {
dirtyZIndex = true;
break;
}
}
if (dirtyZIndex) {
// sort by zIndex
Ext.draw.Surface.stableSort(items);
this.setDirty(true);
}
for (i = 0, ln = items.length; i < ln; i++) {
items[i].attr.dirtyZIndex = false;
}
}
},
/**
* Force the element to redraw.
*/
repaint: function () {
var me = this;
me.repaint = Ext.emptyFn;
setTimeout(function () {
delete me.repaint;
me.element.repaint();
}, 1);
},
/**
* Triggers the re-rendering of the canvas.
*/
renderFrame: function () {
if (!this.element) {
return;
}
if (this.dirtyPredecessor > 0) {
this.pendingRenderFrame = true;
}
var me = this,
region = this.getRegion(),
background = me.getBackground(),
items = me.getItems().items,
item, i, ln;
// Cannot render before the surface is placed.
if (!region) {
return;
}
// This will also check the dirty flags of the sprites.
me.orderByZIndex();
if (me.getDirty()) {
me.clear();
me.clearTransform();
if (background) {
me.renderSprite(background);
}
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
item.applyTransformations();
if (false === me.renderSprite(item)) {
return;
}
item.attr.textPositionCount = me.textPosition;
}
me.setDirty(false);
}
},
/**
* @private
* Renders a single sprite into the surface.
* Do not call it from outside `renderFrame` method.
*
* @param {Ext.draw.sprite.Sprite} sprite The Sprite to be rendered.
* @return {Boolean} returns `false` to stop the rendering to continue.
*/
renderSprite: Ext.emptyFn,
/**
* @private
* Clears the current transformation state on the surface.
*/
clearTransform: Ext.emptyFn,
/**
* Returns 'true' if the surface is dirty.
* @return {Boolean} 'true' if the surface is dirty
*/
getDirty: function () {
return this._dirty;
},
/**
* Destroys the surface. This is done by removing all components from it and
* also removing its reference to a DOM element.
*
* For example:
*
* drawComponent.surface.destroy();
*/
destroy: function () {
var me = this;
me.removeAll();
me.setBackground(null);
me.setGroups([]);
me.getGroups().destroy();
me.predecessors = null;
me.successors = null;
me.callSuper();
}
});
/**
* @class Ext.draw.engine.SvgContext
*
* A class that imitates a canvas context but generates svg elements instead.
*/
Ext.define('Ext.draw.engine.SvgContext', {
/**
* @private
* Properties to be saved/restored in `save` and `restore` method.
*/
toSave: ["strokeOpacity", "strokeStyle", "fillOpacity", "fillStyle", "globalAlpha", "lineWidth", "lineCap",
"lineJoin", "miterLimit", "shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor",
"globalCompositeOperation", "position"],
"strokeOpacity": 1,
"strokeStyle": "none",
"fillOpacity": 1,
"fillStyle": "none",
"globalAlpha": 1,
"lineWidth": 1,
"lineCap": "butt",
"lineJoin": "miter",
"miterLimit": 10,
"shadowOffsetX": 0,
"shadowOffsetY": 0,
"shadowBlur": 0,
"shadowColor": "none",
"globalCompositeOperation": "src",
constructor: function (SvgSurface) {
this.surface = SvgSurface;
this.status = [];
this.matrix = new Ext.draw.Matrix();
this.path = null;
this.clear();
},
/**
* Clears the context.
*/
clear: function () {
this.group = this.surface.mainGroup;
this.position = 0;
this.path = null;
},
/**
* @private
* @param tag
* @return {*}
*/
getElement: function (tag) {
return this.surface.getSvgElement(this.group, tag, this.position++);
},
/**
* Pushes the context state to the state stack.
*/
save: function () {
var toSave = this.toSave,
obj = {},
group = this.getElement('g');
for (var i = 0; i < toSave.length; i++) {
if (toSave[i] in this) {
obj[toSave[i]] = this[toSave[i]];
}
}
this.position = 0;
obj.matrix = this.matrix.clone();
this.status.push(obj);
this.group = group;
},
/**
* Pops the state stack and restores the state.
*/
restore: function () {
var toSave = this.toSave,
obj = this.status.pop(),
children = this.group.dom.childNodes;
while (children.length > this.position) {
Ext.fly(children[children.length - 1]).destroy();
}
for (var i = 0; i < toSave.length; i++) {
if (toSave[i] in obj) {
this[toSave[i]] = obj[toSave[i]];
} else {
delete this[toSave[i]];
}
}
this.setTransform.apply(this, obj.matrix.elements);
this.group = this.group.getParent();
},
/**
* Changes the transformation matrix to apply the matrix given by the arguments as described below.
* @param xx
* @param yx
* @param xy
* @param yy
* @param dx
* @param dy
*/
transform: function (xx, yx, xy, yy, dx, dy) {
if (this.path) {
var inv = Ext.draw.Matrix.fly([xx, yx, xy, yy, dx, dy]).inverse();
this.path.transform(inv);
}
this.matrix.append(xx, yx, xy, yy, dx, dy);
},
/**
* Changes the transformation matrix to the matrix given by the arguments as described below.
* @param xx
* @param yx
* @param xy
* @param yy
* @param dx
* @param dy
*/
setTransform: function (xx, yx, xy, yy, dx, dy) {
if (this.path) {
this.path.transform(this.matrix);
}
this.matrix.reset();
this.transform(xx, yx, xy, yy, dx, dy);
},
setGradientBBox: function (bbox) {
this.bbox = bbox;
},
/**
* Resets the current default path.
*/
beginPath: function () {
this.path = new Ext.draw.Path();
},
/**
* Creates a new subpath with the given point.
* @param x
* @param y
*/
moveTo: function (x, y) {
if (!this.path) {
this.beginPath();
}
this.path.moveTo(x, y);
this.path.element = null;
},
/**
* Adds the given point to the current subpath, connected to the previous one by a straight line.
* @param x
* @param y
*/
lineTo: function (x, y) {
if (!this.path) {
this.beginPath();
}
this.path.lineTo(x, y);
this.path.element = null;
},
/**
* Adds a new closed subpath to the path, representing the given rectangle.
* @param x
* @param y
* @param width
* @param height
*/
rect: function (x, y, width, height) {
this.moveTo(x, y);
this.lineTo(x + width, y);
this.lineTo(x + width, y + height);
this.lineTo(x, y + height);
this.closePath();
},
/**
* Paints the box that outlines the given rectangle onto the canvas, using the current stroke style.
* @param x
* @param y
* @param width
* @param height
*/
strokeRect: function (x, y, width, height) {
this.beginPath();
this.rect(x, y, width, height);
this.stroke();
},
/**
* Paints the given rectangle onto the canvas, using the current fill style.
* @param x
* @param y
* @param width
* @param height
*/
fillRect: function (x, y, width, height) {
this.beginPath();
this.rect(x, y, width, height);
this.fill();
},
/**
* Marks the current subpath as closed, and starts a new subpath with a point the same as the start and end of the newly closed subpath.
*/
closePath: function () {
if (!this.path) {
this.beginPath();
}
this.path.closePath();
this.path.element = null;
},
/**
* Arc command using svg parameters.
* @param r1
* @param r2
* @param rotation
* @param large
* @param swipe
* @param x2
* @param y2
*/
arcSvg: function (r1, r2, rotation, large, swipe, x2, y2) {
if (!this.path) {
this.beginPath();
}
this.path.arcSvg(r1, r2, rotation, large, swipe, x2, y2);
this.path.element = null;
},
/**
* Adds points to the subpath such that the arc described by the circumference of the circle described by the arguments, starting at the given start angle and ending at the given end angle, going in the given direction (defaulting to clockwise), is added to the path, connected to the previous point by a straight line.
* @param x
* @param y
* @param radius
* @param startAngle
* @param endAngle
* @param anticlockwise
*/
arc: function (x, y, radius, startAngle, endAngle, anticlockwise) {
if (!this.path) {
this.beginPath();
}
this.path.arc(x, y, radius, startAngle, endAngle, anticlockwise);
this.path.element = null;
},
/**
* Adds points to the subpath such that the arc described by the circumference of the ellipse described by the arguments, starting at the given start angle and ending at the given end angle, going in the given direction (defaulting to clockwise), is added to the path, connected to the previous point by a straight line.
* @param x
* @param y
* @param radiusX
* @param radiusY
* @param rotation
* @param startAngle
* @param endAngle
* @param anticlockwise
*/
ellipse: function (x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
if (!this.path) {
this.beginPath();
}
this.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
this.path.element = null;
},
/**
* Adds an arc with the given control points and radius to the current subpath, connected to the previous point by a straight line.
* If two radii are provided, the first controls the width of the arc's ellipse, and the second controls the height. If only one is provided, or if they are the same, the arc is from a circle.
* In the case of an ellipse, the rotation argument controls the clockwise inclination of the ellipse relative to the x-axis.
* @param x1
* @param y1
* @param x2
* @param y2
* @param radiusX
* @param radiusY
* @param rotation
*/
arcTo: function (x1, y1, x2, y2, radiusX, radiusY, rotation) {
if (!this.path) {
this.beginPath();
}
this.path.arcTo(x1, y1, x2, y2, radiusX, radiusY, rotation);
this.path.element = null;
},
/**
* Adds the given point to the current subpath, connected to the previous one by a cubic Bézier curve with the given control points.
* @param x1
* @param y1
* @param x2
* @param y2
* @param x3
* @param y3
*/
bezierCurveTo: function (x1, y1, x2, y2, x3, y3) {
if (!this.path) {
this.beginPath();
}
this.path.bezierCurveTo(x1, y1, x2, y2, x3, y3);
this.path.element = null;
},
/**
* Strokes the given text at the given position. If a maximum width is provided, the text will be scaled to fit that width if necessary.
* @param text
* @param x
* @param y
*/
strokeText: function (text, x, y) {
text = String(text);
if (this.strokeStyle) {
var element = this.getElement('text'),
tspan = this.surface.getSvgElement(element, 'tspan', 0);
this.surface.setElementAttributes(element, {
"x": x,
"y": y,
"transform": this.matrix.toSvg(),
"stroke": this.strokeStyle,
"fill": "none",
"opacity": this.globalAlpha,
"stroke-opacity": this.strokeOpacity,
"style": "font: " + this.font
});
if (tspan.dom.firstChild) {
tspan.dom.removeChild(tspan.dom.firstChild);
}
tspan.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
}
},
/**
* Fills the given text at the given position. If a maximum width is provided, the text will be scaled to fit that width if necessary.
* @param text
* @param x
* @param y
*/
fillText: function (text, x, y) {
text = String(text);
if (this.fillStyle) {
var element = this.getElement('text'),
tspan = this.surface.getSvgElement(element, 'tspan', 0);
this.surface.setElementAttributes(element, {
"x": x,
"y": y,
"transform": this.matrix.toSvg(),
"fill": this.fillStyle,
"opacity": this.globalAlpha,
"fill-opacity": this.fillOpacity,
"style": "font: " + this.font
});
if (tspan.dom.firstChild) {
tspan.dom.removeChild(tspan.dom.firstChild);
}
this.surface.setElementAttributes(tspan, {
"alignment-baseline": "middle",
"baseline-shift": "-50%"
});
tspan.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
}
},
/**
* Draws the given image onto the canvas.
* If the first argument isn't an img, canvas, or video element, throws a TypeMismatchError exception. If the image has no image data, throws an InvalidStateError exception. If the one of the source rectangle dimensions is zero, throws an IndexSizeError exception. If the image isn't yet fully decoded, then nothing is drawn.
* @param image
* @param sx
* @param sy
* @param sw
* @param sh
* @param dx
* @param dy
* @param dw
* @param dh
*/
drawImage: function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
var me = this,
element = me.getElement('image'),
x = sx, y = sy,
width = typeof sw === 'undefined' ? image.width : sw,
height = typeof sh === 'undefined' ? image.height : sh,
viewBox = null;
if (typeof dh !== 'undefined') {
viewBox = sx + " " + sy + " " + sw + " " + sh;
x = dx;
y = dy;
width = dw;
height = dh;
}
element.dom.setAttributeNS("http:/" + "/www.w3.org/1999/xlink", "href", image.src);
me.surface.setElementAttributes(element, {
viewBox: viewBox,
x: x,
y: y,
width: width,
height: height,
opacity: me.globalAlpha,
transform: me.matrix.toSvg()
});
},
/**
* Fills the subpaths of the current default path or the given path with the current fill style.
*/
fill: function () {
if (!this.path) {
return;
}
if (this.fillStyle) {
var path,
fillGradient = this.fillGradient,
bbox = this.bbox,
element = this.path.element;
if (!element) {
path = this.path.toString();
element = this.path.element = this.getElement('path');
this.surface.setElementAttributes(element, {
"d": path,
"transform": this.matrix.toSvg()
});
}
this.surface.setElementAttributes(element, {
"fill": fillGradient && bbox ? fillGradient.getGradient(this, bbox) : this.fillStyle,
"fill-opacity": this.fillOpacity * this.globalAlpha
});
}
},
/**
* Strokes the subpaths of the current default path or the given path with the current stroke style.
*/
stroke: function () {
if (!this.path) {
return;
}
if (this.strokeStyle) {
var path,
strokeGradient = this.strokeGradient,
bbox = this.bbox,
element = this.path.element;
if (!element || !this.path.svgString) {
path = this.path.toString();
element = this.path.element = this.getElement('path');
this.surface.setElementAttributes(element, {
"fill": "none",
"d": path,
"transform": this.matrix.toSvg()
});
}
this.surface.setElementAttributes(element, {
"stroke": strokeGradient && bbox ? strokeGradient.getGradient(this, bbox) : this.strokeStyle,
"stroke-linecap": this.lineCap,
"stroke-linejoin": this.lineJoin,
"stroke-width": this.lineWidth,
"stroke-opacity": this.strokeOpacity * this.globalAlpha
});
}
},
/**
* @protected
*
* Note: After the method guarantees the transform matrix will be inverted.
* @param {Object} attr The attribute object
* @param {Boolean} [transformFillStroke] Indicate whether to transform fill and stroke. If this is not
* given, then uses `attr.transformFillStroke` instead.
*/
fillStroke: function (attr, transformFillStroke) {
var ctx = this,
fillStyle = ctx.fillStyle,
strokeStyle = ctx.strokeStyle,
fillOpacity = ctx.fillOpacity,
strokeOpacity = ctx.strokeOpacity;
if (transformFillStroke === undefined) {
transformFillStroke = attr.transformFillStroke;
}
if (!transformFillStroke) {
attr.inverseMatrix.toContext(ctx);
}
if (fillStyle && fillOpacity !== 0) {
ctx.fill();
}
if (strokeStyle && strokeOpacity !== 0) {
ctx.stroke();
}
},
appendPath: function (path) {
this.path = path.clone();
},
/**
* Returns an object that represents a linear gradient that paints along the line given by the coordinates represented by the arguments.
* @param x0
* @param y0
* @param x1
* @param y1
* @return {Ext.draw.engine.SvgContext.Gradient}
*/
createLinearGradient: function (x0, y0, x1, y1) {
var element = this.surface.getNextDef('linearGradient');
this.surface.setElementAttributes(element, {
"x1": x0,
"y1": y0,
"x2": x1,
"y2": y1,
"gradientUnits": "userSpaceOnUse"
});
return new Ext.draw.engine.SvgContext.Gradient(this, this.surface, element);
},
/**
* Returns a CanvasGradient object that represents a radial gradient that paints along the cone given by the circles represented by the arguments.
* If either of the radii are negative, throws an IndexSizeError exception.
* @param x0
* @param y0
* @param r0
* @param x1
* @param y1
* @param r1
* @return {Ext.draw.engine.SvgContext.Gradient}
*/
createRadialGradient: function (x0, y0, r0, x1, y1, r1) {
var element = this.surface.getNextDef('radialGradient');
this.surface.setElementAttributes(element, {
"fx": x0,
"fy": y0,
"cx": x1,
"cy": y1,
"r": r1,
"gradientUnits": "userSpaceOnUse"
});
return new Ext.draw.engine.SvgContext.Gradient(this, this.surface, element, r1 / r0);
}
});
/**
* @class Ext.draw.engine.SvgContext.Gradient
*/
Ext.define("Ext.draw.engine.SvgContext.Gradient", {
constructor: function (ctx, surface, element, compression) {
this.ctx = ctx;
this.surface = surface;
this.element = element;
this.position = 0;
this.compression = compression || 0;
},
/**
* Adds a color stop with the given color to the gradient at the given offset. 0.0 is the offset at one end of the gradient, 1.0 is the offset at the other end.
* @param offset
* @param color
*/
addColorStop: function (offset, color) {
var stop = this.surface.getSvgElement(this.element, 'stop', this.position++),
compression = this.compression;
this.surface.setElementAttributes(stop, {
"offset": (((1 - compression) * offset + compression) * 100).toFixed(2) + '%',
"stop-color": color
});
},
toString: function () {
return 'url(#' + this.element.getId() + ')';
}
});
/**
* @class Ext.draw.engine.Svg
* @extends Ext.draw.Surface
*
* SVG engine.
*/
Ext.define('Ext.draw.engine.Svg', {
extend: 'Ext.draw.Surface',
requires: ['Ext.draw.engine.SvgContext'],
statics: {
BBoxTextCache: {}
},
getElementConfig: function () {
return {
reference: 'element',
style: {
position: 'absolute'
},
children: [
{
reference: 'innerElement',
style: {
width: '100%',
height: '100%',
position: 'relative'
},
children: [
{
tag: 'svg',
reference: 'svgElement',
namespace: "http://www.w3.org/2000/svg",
version: 1.1,
cls: 'x-surface'
}
]
}
]
};
},
constructor: function (config) {
var me = this;
me.callSuper([config]);
me.mainGroup = me.createSvgNode("g");
me.defElement = me.createSvgNode("defs");
// me.svgElement is assigned in element creation of Ext.Component.
me.svgElement.appendChild(me.mainGroup);
me.svgElement.appendChild(me.defElement);
me.ctx = new Ext.draw.engine.SvgContext(me);
},
/**
* Creates a DOM element under the SVG namespace of the given type.
* @param type The type of the SVG DOM element.
* @return {*} The created element.
*/
createSvgNode: function (type) {
var node = document.createElementNS("http://www.w3.org/2000/svg", type);
return Ext.get(node);
},
/**
* @private
* Returns the SVG DOM element at the given position. If it does not already exist or is a different element tag
* it will be created and inserted into the DOM.
* @param group The parent DOM element.
* @param tag The SVG element tag.
* @param position The position of the element in the DOM.
* @return {Ext.dom.Element} The SVG element.
*/
getSvgElement: function (group, tag, position) {
var element;
if (group.dom.childNodes.length > position) {
element = group.dom.childNodes[position];
if (element.tagName === tag) {
return Ext.get(element);
} else {
Ext.destroy(element);
}
}
element = Ext.get(this.createSvgNode(tag));
if (position === 0) {
group.insertFirst(element);
} else {
element.insertAfter(Ext.fly(group.dom.childNodes[position - 1]));
}
element.cache = {};
return element;
},
/**
* @private
* Applies attributes to the given element.
* @param element The DOM element to be applied.
* @param attributes The attributes to apply to the element.
*/
setElementAttributes: function (element, attributes) {
var dom = element.dom,
cache = element.cache,
name, value;
for (name in attributes) {
value = attributes[name];
if (cache[name] !== value) {
cache[name] = value;
dom.setAttribute(name, value);
}
}
},
/**
* @private
* Gets the next reference element under the SVG 'defs' tag.
* @param tagName The type of reference element.
* @return {Ext.dom.Element} The reference element.
*/
getNextDef: function (tagName) {
return this.getSvgElement(this.defElement, tagName, this.defPosition++);
},
/**
* @inheritdoc
*/
clearTransform: function () {
var me = this;
me.mainGroup.set({transform: me.matrix.toSvg()});
},
/**
* @inheritdoc
*/
clear: function () {
this.ctx.clear();
this.defPosition = 0;
},
/**
* @inheritdoc
*/
renderSprite: function (sprite) {
var me = this,
region = me.getRegion(),
ctx = me.ctx;
if (sprite.attr.hidden || sprite.attr.opacity === 0) {
ctx.save();
ctx.restore();
return;
}
try {
ctx.save();
sprite.preRender(this);
sprite.applyTransformations();
sprite.useAttributes(ctx);
if (false === sprite.render(this, ctx, [0, 0, region[2], region[3]])) {
return false;
}
sprite.setDirty(false);
} finally {
ctx.restore();
}
},
/**
* Destroys the Canvas element and prepares it for Garbage Collection.
*/
destroy: function (path, matrix, band) {
var me = this;
me.ctx.destroy();
me.mainGroup.destroy();
delete me.mainGroup;
delete me.ctx;
me.callSuper(arguments);
}
});
/**
* @class Ext.draw.engine.Canvas
* @extends Ext.draw.Surface
*
* Provides specific methods to draw with 2D Canvas element.
*/
Ext.define('Ext.draw.engine.Canvas', {
extend: 'Ext.draw.Surface',
config: {
/**
* @cfg {Boolean} highPrecision
* True to have the canvas use JavaScript Number instead of single precision floating point for transforms.
*
* For example, when using huge data to plot line series, the transform matrix of the canvas will have
* a big element. Due to the implementation of SVGMatrix, the elements are restored by 32-bits floats, which
* will work incorrectly. To compensate that, we enable the canvas context to perform all the transform by
* JavaScript. Do not use it if you are not encountering 32-bits floating point errors problem since it will
* have a performance penalty.
*/
highPrecision: false
},
requires: ['Ext.draw.Animator'],
statics: {
contextOverrides: {
setGradientBBox: function (bbox) {
this.bbox = bbox;
},
/**
* Fills the subpaths of the current default path or the given path with the current fill style.
*/
fill: function () {
var fillStyle = this.fillStyle,
fillGradient = this.fillGradient,
fillOpacity = this.fillOpacity,
rgba = 'rgba(0, 0, 0, 0)',
rgba0 = 'rgba(0, 0, 0, 0.0)',
bbox = this.bbox,
alpha = this.globalAlpha;
if (fillStyle !== rgba && fillStyle !== rgba0 && fillOpacity !== 0) {
if (fillGradient && bbox) {
this.fillStyle = fillGradient.getGradient(this, bbox);
}
if (fillOpacity !== 1) {
this.globalAlpha = alpha * fillOpacity;
}
this.$fill();
if (fillOpacity !== 1) {
this.globalAlpha = alpha;
}
if (fillGradient && bbox) {
this.fillStyle = fillStyle;
}
}
},
/**
* Strokes the subpaths of the current default path or the given path with the current stroke style.
*/
stroke: function (transformFillStroke) {
var strokeStyle = this.strokeStyle,
strokeGradient = this.strokeGradient,
strokeOpacity = this.strokeOpacity,
rgba = 'rgba(0, 0, 0, 0)',
rgba0 = 'rgba(0, 0, 0, 0.0)',
bbox = this.bbox,
alpha = this.globalAlpha;
if (strokeStyle !== rgba && strokeStyle !== rgba0 && strokeOpacity !== 0) {
if (strokeGradient && bbox) {
this.strokeStyle = strokeGradient.getGradient(this, bbox);
}
if (strokeOpacity !== 1) {
this.globalAlpha = alpha * strokeOpacity;
}
this.$stroke();
if (strokeOpacity !== 1) {
this.globalAlpha = alpha;
}
if (strokeGradient && bbox) {
this.strokeStyle = strokeStyle;
}
}
},
fillStroke: function (attr, transformFillStroke) {
var ctx = this,
fillStyle = this.fillStyle,
fillOpacity = this.fillOpacity,
strokeStyle = this.strokeStyle,
strokeOpacity = this.strokeOpacity,
shadowColor = ctx.shadowColor,
shadowBlur = ctx.shadowBlur,
rgba = 'rgba(0, 0, 0, 0)',
rgba0 = 'rgba(0, 0, 0, 0.0)';
if (transformFillStroke === undefined) {
transformFillStroke = attr.transformFillStroke;
}
if (!transformFillStroke) {
attr.inverseMatrix.toContext(ctx);
}
if (fillStyle !== rgba && fillStyle !== rgba0 && fillOpacity !== 0) {
ctx.fill();
ctx.shadowColor = 'rgba(0,0,0,0)';
ctx.shadowBlur = 0;
}
if (strokeStyle !== rgba && strokeStyle !== rgba0 && strokeOpacity !== 0) {
ctx.stroke();
}
ctx.shadowColor = shadowColor;
ctx.shadowBlur = shadowBlur;
},
/**
* Adds points to the subpath such that the arc described by the circumference of the ellipse described by the arguments, starting at the given start angle and ending at the given end angle, going in the given direction (defaulting to clockwise), is added to the path, connected to the previous point by a straight line.
* @param cx
* @param cy
* @param rx
* @param ry
* @param rotation
* @param start
* @param end
* @param anticlockwise
*/
ellipse: function (cx, cy, rx, ry, rotation, start, end, anticlockwise) {
var cos = Math.cos(rotation),
sin = Math.sin(rotation);
this.transform(cos * rx, sin * rx, -sin * ry, cos * ry, cx, cy);
this.arc(0, 0, 1, start, end, anticlockwise);
this.transform(
cos / rx, -sin / ry,
sin / rx, cos / ry,
-(cos * cx + sin * cy) / rx, (sin * cx - cos * cy) / ry);
},
/**
* Uses the given path commands to begin a new path on the canvas.
* @param path
*/
appendPath: function (path) {
var me = this,
i = 0, j = 0,
types = path.types,
coords = path.coords,
ln = path.types.length;
me.beginPath();
for (; i < ln; i++) {
switch (types[i]) {
case "M":
me.moveTo(coords[j], coords[j + 1]);
j += 2;
break;
case "L":
me.lineTo(coords[j], coords[j + 1]);
j += 2;
break;
case "C":
me.bezierCurveTo(
coords[j], coords[j + 1],
coords[j + 2], coords[j + 3],
coords[j + 4], coords[j + 5]
);
j += 6;
break;
case "Z":
me.closePath();
break;
default:
}
}
}
}
},
splitThreshold: 1800,
getElementConfig: function () {
return {
reference: 'element',
style: {
position: 'absolute'
},
children: [
{
reference: 'innerElement',
style: {
width: '100%',
height: '100%',
position: 'relative'
}
}
]
};
},
/**
* @private
*
* Creates the canvas element.
*/
createCanvas: function () {
var canvas = Ext.Element.create({
tag: 'canvas',
cls: 'x-surface'
}), name, overrides = Ext.draw.engine.Canvas.contextOverrides,
ctx = canvas.dom.getContext('2d'),
backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
this.devicePixelRatio /= backingStoreRatio;
if (ctx.ellipse) {
delete overrides.ellipse;
}
for (name in overrides) {
ctx['$' + name] = ctx[name];
}
Ext.apply(ctx, overrides);
if (this.getHighPrecision()) {
this.enablePrecisionCompensation(ctx);
} else {
this.disablePrecisionCompensation(ctx);
}
this.innerElement.appendChild(canvas);
this.canvases.push(canvas);
this.contexts.push(ctx);
},
/**
* Initialize the canvas element.
*/
initElement: function () {
this.callSuper();
this.canvases = [];
this.contexts = [];
this.createCanvas();
this.activeCanvases = 0;
},
updateHighPrecision: function (pc) {
var contexts = this.contexts,
ln = contexts.length,
i, context;
for (i = 0; i < ln; i++) {
context = contexts[i];
if (pc) {
this.enablePrecisionCompensation(context);
} else {
this.disablePrecisionCompensation(context);
}
}
},
precisionMethods: {
rect: false,
fillRect: false,
strokeRect: false,
clearRect: false,
moveTo: false,
lineTo: false,
arc: false,
arcTo: false,
save: false,
restore: false,
updatePrecisionCompensate: false,
setTransform: false,
transform: false,
scale: false,
translate: false,
rotate: false,
quadraticCurveTo: false,
bezierCurveTo: false,
createLinearGradient: false,
createRadialGradient: false,
fillText: false,
strokeText: false,
drawImage: false
},
/**
* @private
* Clears canvas of compensation for canvas' use of single precision floating point.
* @param {CanvasRenderingContext2D} ctx The canvas context.
*/
disablePrecisionCompensation: function (ctx) {
var precisionMethods = this.precisionMethods,
name;
for (name in precisionMethods) {
delete ctx[name];
}
this.setDirty(true);
},
/**
* @private
* Compensate for canvas' use of single precision floating point.
* @param {CanvasRenderingContext2D} ctx The canvas context.
*/
enablePrecisionCompensation: function (ctx) {
var surface = this,
xx = 1, yy = 1,
dx = 0, dy = 0,
matrix = new Ext.draw.Matrix(),
transStack = [],
comp = {},
originalCtx = ctx.constructor.prototype;
var override = {
/**
* Adds a new closed subpath to the path, representing the given rectangle.
* @param x
* @param y
* @param w
* @param h
* @return {*}
*/
rect: function (x, y, w, h) {
return originalCtx.rect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
},
/**
* Paints the given rectangle onto the canvas, using the current fill style.
* @param x
* @param y
* @param w
* @param h
*/
fillRect: function (x, y, w, h) {
this.updatePrecisionCompensateRect();
originalCtx.fillRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
this.updatePrecisionCompensate();
},
/**
* Paints the box that outlines the given rectangle onto the canvas, using the current stroke style.
* @param x
* @param y
* @param w
* @param h
*/
strokeRect: function (x, y, w, h) {
this.updatePrecisionCompensateRect();
originalCtx.strokeRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
this.updatePrecisionCompensate();
},
/**
* Clears all pixels on the canvas in the given rectangle to transparent black.
* @param x
* @param y
* @param w
* @param h
*/
clearRect: function (x, y, w, h) {
return originalCtx.clearRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
},
/**
* Creates a new subpath with the given point.
* @param x
* @param y
*/
moveTo: function (x, y) {
return originalCtx.moveTo.call(this, x * xx + dx, y * yy + dy);
},
/**
* Adds the given point to the current subpath, connected to the previous one by a straight line.
* @param x
* @param y
*/
lineTo: function (x, y) {
return originalCtx.lineTo.call(this, x * xx + dx, y * yy + dy);
},
/**
* Adds points to the subpath such that the arc described by the circumference of the circle described by the arguments, starting at the given start angle and ending at the given end angle, going in the given direction (defaulting to clockwise), is added to the path, connected to the previous point by a straight line.
* @param x
* @param y
* @param radius
* @param startAngle
* @param endAngle
* @param anticlockwise
*/
arc: function (x, y, radius, startAngle, endAngle, anticlockwise) {
this.updatePrecisionCompensateRect();
originalCtx.arc.call(this, x * xx + dx, y * xx + dy, radius * xx, startAngle, endAngle, anticlockwise);
this.updatePrecisionCompensate();
},
/**
* Adds an arc with the given control points and radius to the current subpath, connected to the previous point by a straight line.
* If two radii are provided, the first controls the width of the arc's ellipse, and the second controls the height. If only one is provided, or if they are the same, the arc is from a circle.
* In the case of an ellipse, the rotation argument controls the clockwise inclination of the ellipse relative to the x-axis.
* @param x1
* @param y1
* @param x2
* @param y2
* @param radius
*/
arcTo: function (x1, y1, x2, y2, radius) {
this.updatePrecisionCompensateRect();
originalCtx.arcTo.call(this, x1 * xx + dx, y1 * yy + dy, x2 * xx + dx, y2 * yy + dy, radius * xx);
this.updatePrecisionCompensate();
},
/**
* Pushes the context state to the state stack.
*/
save: function () {
transStack.push(matrix);
matrix = matrix.clone();
return originalCtx.save.call(this);
},
/**
* Pops the state stack and restores the state.
*/
restore: function () {
matrix = transStack.pop();
originalCtx.restore.call(this);
this.updatePrecisionCompensate();
},
updatePrecisionCompensate: function () {
matrix.precisionCompensate(surface.devicePixelRatio, comp);
xx = comp.xx;
yy = comp.yy;
dx = comp.dx;
dy = comp.dy;
return originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c, comp.d, 0, 0);
},
updatePrecisionCompensateRect: function () {
matrix.precisionCompensateRect(surface.devicePixelRatio, comp);
xx = comp.xx;
yy = comp.yy;
dx = comp.dx;
dy = comp.dy;
return originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c, comp.d, 0, 0);
},
/**
* Changes the transformation matrix to the matrix given by the arguments as described below.
* @param x2x
* @param x2y
* @param y2x
* @param y2y
* @param newDx
* @param newDy
*/
setTransform: function (x2x, x2y, y2x, y2y, newDx, newDy) {
matrix.set(x2x, x2y, y2x, y2y, newDx, newDy);
this.updatePrecisionCompensate();
},
/**
* Changes the transformation matrix to apply the matrix given by the arguments as described below.
* @param x2x
* @param x2y
* @param y2x
* @param y2y
* @param newDx
* @param newDy
*/
transform: function (x2x, x2y, y2x, y2y, newDx, newDy) {
matrix.append(x2x, x2y, y2x, y2y, newDx, newDy);
this.updatePrecisionCompensate();
},
/**
* Scales the transformation matrix.
* @param sx
* @param sy
* @return {*}
*/
scale: function (sx, sy) {
return this.transform(sx, 0, 0, sy, 0, 0);
},
/**
* Translates the transformation matrix.
* @param dx
* @param dy
* @return {*}
*/
translate: function (dx, dy) {
return this.transform(1, 0, 0, 1, dx, dy);
},
/**
* Rotates the transformation matrix.
* @param radians
* @return {*}
*/
rotate: function (radians) {
var cos = Math.cos(radians),
sin = Math.sin(radians);
return this.transform(cos, sin, -sin, cos, 0, 0);
},
/**
* Adds the given point to the current subpath, connected to the previous one by a quadratic Bézier curve with the given control point.
* @param cx
* @param cy
* @param x
* @param y
* @return {*}
*/
quadraticCurveTo: function (cx, cy, x, y) {
return originalCtx.quadraticCurveTo.call(this,
cx * xx + dx,
cy * yy + dy,
x * xx + dx,
y * yy + dy
);
},
/**
* Adds the given point to the current subpath, connected to the previous one by a cubic Bézier curve with the given control points.
* @param c1x
* @param c1y
* @param c2x
* @param c2y
* @param x
* @param y
* @return {*}
*/
bezierCurveTo: function (c1x, c1y, c2x, c2y, x, y) {
return originalCtx.bezierCurveTo.call(this,
c1x * xx + dx,
c1y * yy + dy,
c2x * xx + dx,
c2y * yy + dy,
x * xx + dx,
y * yy + dy
);
},
/**
* Returns an object that represents a linear gradient that paints along the line given by the coordinates represented by the arguments.
* @param x0
* @param y0
* @param x1
* @param y1
* @return {*}
*/
createLinearGradient: function (x0, y0, x1, y1) {
this.updatePrecisionCompensateRect();
var grad = originalCtx.createLinearGradient.call(this,
x0 * xx + dx,
y0 * yy + dy,
x1 * xx + dx,
y1 * yy + dy
);
this.updatePrecisionCompensate();
return grad;
},
/**
* Returns a CanvasGradient object that represents a radial gradient that paints along the cone given by the circles represented by the arguments.
* If either of the radii are negative, throws an IndexSizeError exception.
* @param x0
* @param y0
* @param r0
* @param x1
* @param y1
* @param r1
* @return {*}
*/
createRadialGradient: function (x0, y0, r0, x1, y1, r1) {
this.updatePrecisionCompensateRect();
var grad = originalCtx.createLinearGradient.call(this,
x0 * xx + dx,
y0 * xx + dy,
r0 * xx,
x1 * xx + dx,
y1 * xx + dy,
r1 * xx
);
this.updatePrecisionCompensate();
return grad;
},
/**
* Fills the given text at the given position. If a maximum width is provided, the text will be scaled to fit that width if necessary.
* @param text
* @param x
* @param y
* @param maxWidth
*/
fillText: function (text, x, y, maxWidth) {
originalCtx.setTransform.apply(this, matrix.elements);
if (typeof maxWidth === 'undefined') {
originalCtx.fillText.call(this, text, x, y);
} else {
originalCtx.fillText.call(this, text, x, y, maxWidth);
}
this.updatePrecisionCompensate();
},
/**
* Strokes the given text at the given position. If a maximum width is provided, the text will be scaled to fit that width if necessary.
* @param text
* @param x
* @param y
* @param maxWidth
*/
strokeText: function (text, x, y, maxWidth) {
originalCtx.setTransform.apply(this, matrix.elements);
if (typeof maxWidth === 'undefined') {
originalCtx.strokeText.call(this, text, x, y);
} else {
originalCtx.strokeText.call(this, text, x, y, maxWidth);
}
this.updatePrecisionCompensate();
},
/**
* Fills the subpaths of the current default path or the given path with the current fill style.
*/
fill: function () {
this.updatePrecisionCompensateRect();
originalCtx.fill.call(this);
this.updatePrecisionCompensate();
},
/**
* Strokes the subpaths of the current default path or the given path with the current stroke style.
*/
stroke: function () {
this.updatePrecisionCompensateRect();
originalCtx.stroke.call(this);
this.updatePrecisionCompensate();
},
/**
* Draws the given image onto the canvas.
* If the first argument isn't an img, canvas, or video element, throws a TypeMismatchError exception. If the image has no image data, throws an InvalidStateError exception. If the one of the source rectangle dimensions is zero, throws an IndexSizeError exception. If the image isn't yet fully decoded, then nothing is drawn.
* @param img_elem
* @param arg1
* @param arg2
* @param arg3
* @param arg4
* @param dst_x
* @param dst_y
* @param dw
* @param dh
* @return {*}
*/
drawImage: function (img_elem, arg1, arg2, arg3, arg4, dst_x, dst_y, dw, dh) {
switch (arguments.length) {
case 3:
return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx, arg2 * yy + dy);
case 5:
return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx, arg2 * yy + dy, arg3 * xx, arg4 * yy);
case 9:
return originalCtx.drawImage.call(this, img_elem, arg1, arg2, arg3, arg4, dst_x * xx + dx, dst_y * yy * dy, dw * xx, dh * yy);
}
}
};
Ext.apply(ctx, override);
this.setDirty(true);
},
updateRegion: function (region) {
this.callSuper([region]);
var me = this,
l = Math.floor(region[0]),
t = Math.floor(region[1]),
r = Math.ceil(region[0] + region[2]),
b = Math.ceil(region[1] + region[3]),
devicePixelRatio = me.devicePixelRatio,
w = r - l,
h = b - t,
splitThreshold = Math.round(me.splitThreshold / devicePixelRatio),
splits = Math.ceil(w / splitThreshold),
activeCanvases = me.activeCanvases,
i, offsetX, dom, leftWidth;
for (i = 0, offsetX = 0; i < splits; i++, offsetX += splitThreshold) {
if (i >= me.canvases.length) {
me.createCanvas();
}
dom = me.canvases[i].dom;
dom.style.left = offsetX + 'px';
if (h * devicePixelRatio !== dom.height) {
dom.height = h * devicePixelRatio;
dom.style.height = h + 'px';
}
leftWidth = Math.min(splitThreshold, w - offsetX);
if (leftWidth * devicePixelRatio !== dom.width) {
dom.width = leftWidth * devicePixelRatio;
dom.style.width = leftWidth + 'px';
}
me.applyDefaults(me.contexts[i]);
}
for (; i < activeCanvases; i++) {
dom = me.canvases[i].dom;
dom.width = 0;
dom.height = 0;
}
me.activeCanvases = splits;
me.clear();
},
/**
* @inheritdoc
*/
clearTransform: function () {
var me = this,
activeCanvases = me.activeCanvases,
i, ctx;
for (i = 0; i < activeCanvases; i++) {
ctx = me.contexts[i];
ctx.translate(-me.splitThreshold * i, 0);
ctx.scale(me.devicePixelRatio, me.devicePixelRatio);
me.matrix.toContext(ctx);
}
},
/**
* @private
* @inheritdoc
*/
renderSprite: function (sprite) {
var me = this,
region = me._region,
surfaceMatrix = me.matrix,
parent = sprite._parent,
matrix = Ext.draw.Matrix.fly([1, 0, 0, 1, 0, 0]),
bbox, i, offsetX, ctx, width, left = 0, top, right = region[2], bottom;
while (parent && (parent !== me)) {
matrix.prependMatrix(parent.matrix || parent.attr && parent.attr.matrix);
parent = parent.getParent();
}
matrix.prependMatrix(surfaceMatrix);
bbox = sprite.getBBox();
if (bbox) {
bbox = matrix.transformBBox(bbox);
}
sprite.preRender(me);
if (sprite.attr.hidden || sprite.attr.globalAlpha === 0) {
sprite.setDirty(false);
return;
}
top = 0;
bottom = top + region[3];
for (i = 0, offsetX = 0; i < me.activeCanvases; i++, offsetX += me.splitThreshold / me.devicePixelRatio) {
ctx = me.contexts[i];
width = Math.min(region[2] - offsetX, me.splitThreshold / me.devicePixelRatio);
left = offsetX;
right = left + width;
if (bbox) {
if (bbox.x > right ||
bbox.x + bbox.width < left ||
bbox.y > bottom ||
bbox.y + bbox.height < top) {
continue;
}
}
try {
ctx.save();
// Set attributes to context.
sprite.useAttributes(ctx);
// Render shape
if (false === sprite.render(me, ctx, [left, top, width, bottom - top])) {
return false;
}
} finally {
ctx.restore();
}
}
sprite.setDirty(false);
},
applyDefaults: function (ctx) {
ctx.strokeStyle = 'rgba(0,0,0,0)';
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.textAlign = 'start';
ctx.textBaseline = 'top';
ctx.miterLimit = 1;
},
/**
* @inheritdoc
*/
clear: function () {
var me = this,
activeCanvases = this.activeCanvases,
i, canvas, ctx, width, height;
for (i = 0; i < activeCanvases; i++) {
canvas = me.canvases[i].dom;
ctx = me.contexts[i];
width = canvas.width;
height = canvas.height;
if (Ext.os.is.Android && !Ext.os.is.Android4) {
// TODO: Verify this is the proper check (Chrome)
// On chrome this is faster:
//noinspection SillyAssignmentJS
canvas.width = canvas.width;
// Fill the gap between surface defaults and canvas defaults
me.applyDefaults(ctx);
} else {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, width, height);
}
}
me.setDirty(true);
},
/**
* Destroys the Canvas element and prepares it for Garbage Collection.
*/
destroy: function () {
var me = this,
i, ln = me.canvases.length;
for (i = 0; i < ln; i++) {
me.contexts[i] = null;
me.canvases[i].destroy();
me.canvases[i] = null;
}
delete me.contexts;
delete me.canvases;
me.callSuper(arguments);
}
}, function () {
if (Ext.os.is.Android4 && Ext.browser.is.Chrome) {
this.prototype.splitThreshold = 3000;
} else if (Ext.os.is.Android) {
this.prototype.splitThreshold = 1e10;
}
});
/**
* The Draw Component is a surface in which sprites can be rendered. The Draw Component
* manages and holds a `Surface` instance: an interface that has
* an SVG or VML implementation depending on the browser capabilities and where
* Sprites can be appended.
* One way to create a draw component is:
*
* var drawComponent = new Ext.draw.Component({
* items: [{
* type: 'circle',
* fill: '#79BB3F',
* radius: 100,
* x: 100,
* y: 100
* }]
* });
*
* new Ext.Panel({
* fullscreen: true,
* items: [drawComponent]
* });
*
* In this case we created a draw component and added a sprite to it.
* The *type* of the sprite is *circle* so if you run this code you'll see a yellow-ish
* circle in a Window. When setting `viewBox` to `false` we are responsible for setting the object's position and
* dimensions accordingly.
*
* You can also add sprites by using the surface's add method:
*
* drawComponent.getSurface('main').add({
* type: 'circle',
* fill: '#79BB3F',
* radius: 100,
* x: 100,
* y: 100
* });
*
* For more information on Sprites, the core elements added to a draw component's surface,
* refer to the {@link Ext.draw.sprite.Sprite} documentation.
*/
Ext.define('Ext.draw.Component', {
extend: 'Ext.Container',
xtype: 'draw',
defaultType: 'surface',
requires: [
'Ext.draw.Surface',
'Ext.draw.engine.Svg',
'Ext.draw.engine.Canvas'
],
engine: 'Ext.draw.engine.Canvas',
statics: {
WATERMARK: 'Powered by <span style="color:#22E962; font-weight: 900">Sencha Touch</span> <span style="color:#75cdff; font-weight: 900">GPL</span>'
},
config: {
cls: 'x-draw-component',
/**
* @deprecated 2.2.0 Please implement custom resize event handler.
* Resize the draw component by the content size of the main surface.
*
* __Note:__ It is applied only when there is only one surface.
*/
autoSize: false,
/**
* @deprecated 2.2.0 Please implement custom resize event handler.
* Pan/Zoom the content in main surface to fit the component size.
*
* __Note:__ It is applied only when there is only one surface.
*/
viewBox: false,
/**
* @deprecated 2.2.0 Please implement custom resize event handler.
* Fit the main surface to the size of component.
*
* __Note:__ It is applied only when there is only one surface.
*/
fitSurface: true,
/**
* @cfg {Function} [resizeHandler] The resize function that can be configured to have a behavior.
*/
resizeHandler: null,
background: null,
sprites: null
},
constructor: function (config) {
config = config || {};
// If use used `items` config, they are actually using `sprites`
if (config.items) {
config.sprites = config.items;
delete config.items;
}
this.callSuper(arguments);
this.frameCallbackId = Ext.draw.Animator.addFrameCallback('renderFrame', this);
},
initialize: function () {
var me = this;
me.callSuper();
me.element.on('resize', 'onResize', this);
},
applySprites: function (sprites) {
// Never update
if (!sprites) {
return;
}
sprites = Ext.Array.from(sprites);
var ln = sprites.length,
i, surface;
for (i = 0; i < ln; i++) {
if (sprites[i].surface instanceof Ext.draw.Surface) {
surface = sprites[i].surface;
} else if (Ext.isString(sprites[i].surface)) {
surface = this.getSurface(sprites[i].surface);
} else {
surface = this.getSurface('main');
}
surface.add(sprites[i]);
}
},
getElementConfig: function () {
return {
reference: 'element',
className: 'x-container',
children: [
{
reference: 'innerElement',
className: 'x-inner',
children: [
{
reference: 'watermarkElement',
cls: 'x-chart-watermark',
html: Ext.draw.Component.WATERMARK,
style: Ext.draw.Component.WATERMARK ? '': 'display: none'
}
]
}
]
};
},
updateBackground: function (background) {
this.element.setStyle({
background: background
});
},
/**
* @protected
* Place water mark after resize.
*/
onPlaceWatermark: function () {
// Do nothing
},
onResize: function () {
var me = this,
size = me.element.getSize();
me.fireEvent('resize', me, size);
if (me.getResizeHandler()) {
me.getResizeHandler().call(me, size);
} else {
me.resizeHandler(size);
}
me.renderFrame();
me.onPlaceWatermark();
},
resizeHandler: function (size) {
var me = this;
me.getItems().each(function (surface) {
surface.setRegion([0, 0, size.width, size.height]);
});
},
/**
* Get a surface by the given id or create one if it doesn't exist.
* @param {String} [id="main"]
* @return {Ext.draw.Surface}
*/
getSurface: function (id) {
id = this.getId() + '-' + (id || 'main');
var me = this,
surfaces = me.getItems(),
surface = surfaces.get(id),
size;
if (!surface) {
surface = me.add({xclass: me.engine, id: id});
if (me.getFitSurface()) {
size = me.element.getSize();
surface.setRegion([0, 0, size.width, size.height]);
}
surface.renderFrame();
}
return surface;
},
/**
* Render all the surfaces in the component.
*/
renderFrame: function () {
var me = this,
i, ln, bbox,
surfaces = me.getItems();
for (i = 0, ln = surfaces.length; i < ln; i++) {
surfaces.items[i].renderFrame();
}
},
destroy: function () {
Ext.draw.Animator.removeFrameCallback(this.frameCallbackId);
this.callSuper();
}
}, function () {
if (location.search.match('svg')) {
Ext.draw.Component.prototype.engine = 'Ext.draw.engine.Svg';
} else if (Ext.os.is.Android4 && !Ext.browser.is.Chrome && Ext.os.version.getMinor() === 1) {
// http://code.google.com/p/android/issues/detail?id=37529
Ext.draw.Component.prototype.engine = 'Ext.draw.engine.Svg';
}
});
/**
* @class Ext.chart.Markers
* @extends Ext.draw.sprite.Instancing
*
* Marker sprite. A specialized version of instancing sprite that groups instances.
* Putting a marker is grouped by its category id. Clearing removes that category.
*/
Ext.define("Ext.chart.Markers", {
extend: 'Ext.draw.sprite.Instancing',
revisions: 0,
constructor: function () {
this.callSuper(arguments);
this.map = {};
this.revisions = {};
},
/**
* Clear the markers in the category
* @param {String} category
*/
clear: function (category) {
category = category || 'default';
if (!(category in this.revisions)) {
this.revisions[category] = 1;
} else {
this.revisions[category]++;
}
},
/**
* Put a marker in the category with additional
* attributes.
* @param {String} category
* @param {Object} markerAttr
* @param {String|Number} index
* @param {Boolean} [canonical]
* @param {Boolean} [keepRevision]
*/
putMarkerFor: function (category, markerAttr, index, canonical, keepRevision) {
category = category || 'default';
var me = this,
map = me.map[category] || (me.map[category] = {});
if (index in map) {
me.setAttributesFor(map[index], markerAttr, canonical);
} else {
map[index] = me.instances.length;
me.createInstance(markerAttr, null, canonical);
}
me.instances[map[index]].category = category;
if (!keepRevision) {
me.instances[map[index]].revision = me.revisions[category] || (me.revisions[category] = 1);
}
},
/**
*
* @param {String} id
* @param {Mixed} index
* @param {Boolean} [isWithoutTransform]
*/
getMarkerBBoxFor: function (category, index, isWithoutTransform) {
if (category in this.map) {
if (index in this.map[category]) {
return this.getBBoxFor(this.map[category][index], isWithoutTransform);
}
}
},
getBBox: function () { return null; },
render: function (surface, ctx, clipRegion) {
var me = this,
revisions = me.revisions,
mat = me.attr.matrix,
template = me.getTemplate(),
originalAttr = template.attr,
instances = me.instances,
i, ln = me.instances.length;
mat.toContext(ctx);
template.preRender(surface, ctx, clipRegion);
template.useAttributes(ctx);
for (i = 0; i < ln; i++) {
if (instances[i].hidden || instances[i].revision !== revisions[instances[i].category]) {
continue;
}
ctx.save();
template.attr = instances[i];
template.applyTransformations();
template.useAttributes(ctx);
template.render(surface, ctx, clipRegion);
ctx.restore();
}
template.attr = originalAttr;
}
});
/**
* @class Ext.chart.label.Callout
* @extends Ext.draw.modifier.Modifier
*
* This is a modifier to place labels and callouts by additional attributes.
*/
Ext.define("Ext.chart.label.Callout", {
extend: 'Ext.draw.modifier.Modifier',
prepareAttributes: function (attr) {
if (!attr.hasOwnProperty('calloutOriginal')) {
attr.calloutOriginal = Ext.Object.chain(attr);
}
if (this._previous) {
this._previous.prepareAttributes(attr.calloutOriginal);
}
},
setAttrs: function (attr, changes) {
var callout = attr.callout,
origin = attr.calloutOriginal,
bbox = attr.bbox.plain,
width = (bbox.width || 0) + attr.labelOverflowPadding,
height = (bbox.height || 0) + attr.labelOverflowPadding,
dx, dy, r;
if ('callout' in changes) {
callout = changes.callout;
}
if ('callout' in changes || 'calloutPlaceX' in changes || 'calloutPlaceY' in changes || 'x' in changes || 'y' in changes) {
var rotationRads = 'rotationRads' in changes ? origin.rotationRads = changes.rotationRads : origin.rotationRads,
x = 'x' in changes ? (origin.x = changes.x) : origin.x,
y = 'y' in changes ? (origin.y = changes.y) : origin.y,
calloutPlaceX = 'calloutPlaceX' in changes ? changes.calloutPlaceX : attr.calloutPlaceX,
calloutPlaceY = 'calloutPlaceY' in changes ? changes.calloutPlaceY : attr.calloutPlaceY,
calloutVertical = 'calloutVertical' in changes ? changes.calloutVertical : attr.calloutVertical,
temp;
// Normalize Rotations
rotationRads %= Math.PI * 2;
if (Math.cos(rotationRads) < 0) {
rotationRads = (rotationRads + Math.PI) % (Math.PI * 2);
}
if (rotationRads > Math.PI) {
rotationRads -= Math.PI * 2;
}
if (calloutVertical) {
rotationRads = rotationRads * (1 - callout) + Math.PI / 2 * callout;
temp = width;
width = height;
height = temp;
} else {
rotationRads = rotationRads * (1 - callout);
}
changes.rotationRads = rotationRads;
// Placing label.
changes.x = x * (1 - callout) + calloutPlaceX * callout;
changes.y = y * (1 - callout) + calloutPlaceY * callout;
// Placing the end of the callout line.
dx = calloutPlaceX - x;
dy = calloutPlaceY - y;
if (Math.abs(dy * width) > Math.abs(height * dx)) {
// on top/bottom
if (dy > 0) {
changes.calloutEndX = changes.x - (height / (dy * 2) * dx) * callout;
changes.calloutEndY = changes.y - height / 2 * callout;
} else {
changes.calloutEndX = changes.x + (height / (dy * 2) * dx) * callout;
changes.calloutEndY = changes.y + height / 2 * callout;
}
} else {
// on left/right
if (dx > 0) {
changes.calloutEndX = changes.x - width / 2;
changes.calloutEndY = changes.y - (width / (dx * 2) * dy) * callout;
} else {
changes.calloutEndX = changes.x + width / 2;
changes.calloutEndY = changes.y + (width / (dx * 2) * dy) * callout;
}
}
}
return changes;
},
pushDown: function (attr, changes) {
changes = Ext.draw.modifier.Modifier.prototype.pushDown.call(this, attr.calloutOriginal, changes);
return this.setAttrs(attr, changes);
},
popUp: function (attr, changes) {
attr = attr.__proto__;
changes = this.setAttrs(attr, changes);
if (this._next) {
return this._next.popUp(attr, changes);
} else {
return Ext.apply(attr, changes);
}
}
});
/**
* @class Ext.chart.label.Label
* @extends Ext.draw.sprite.Text
*
* Sprite used to represent labels in series.
*/
Ext.define("Ext.chart.label.Label", {
extend: "Ext.draw.sprite.Text",
requires: ['Ext.chart.label.Callout'],
inheritableStatics: {
def: {
processors: {
callout: 'limited01',
calloutPlaceX: 'number',
calloutPlaceY: 'number',
calloutStartX: 'number',
calloutStartY: 'number',
calloutEndX: 'number',
calloutEndY: 'number',
calloutColor: 'color',
calloutVertical: 'bool',
labelOverflowPadding: 'number'
},
defaults: {
callout: 0,
calloutPlaceX: 0,
calloutPlaceY: 0,
calloutStartX: 0,
calloutStartY: 0,
calloutEndX: 0,
calloutEndY: 0,
calloutVertical: false,
calloutColor: 'black',
labelOverflowPadding: 5
},
dirtyTriggers: {
callout: 'transform',
calloutPlaceX: 'transform',
calloutPlaceY: 'transform',
labelOverflowPadding: 'transform',
calloutRotation: 'transform'
}
}
},
config: {
/**
* @cfg {Object} fx Animation configuration.
*/
fx: {
customDuration: {
callout: 200
}
}
},
prepareModifiers: function () {
this.callSuper(arguments);
this.calloutModifier = new Ext.chart.label.Callout({sprite: this});
this.fx.setNext(this.calloutModifier);
this.calloutModifier.setNext(this.topModifier);
},
render: function (surface, ctx, clipRegion) {
var me = this,
attr = me.attr;
ctx.save();
ctx.globalAlpha *= Math.max(0, attr.callout - 0.5) * 2;
if (ctx.globalAlpha > 0) {
ctx.strokeStyle = attr.calloutColor;
ctx.fillStyle = attr.calloutColor;
ctx.beginPath();
ctx.moveTo(me.attr.calloutStartX, me.attr.calloutStartY);
ctx.lineTo(me.attr.calloutEndX, me.attr.calloutEndY);
ctx.stroke();
ctx.beginPath();
ctx.arc(me.attr.calloutStartX, me.attr.calloutStartY, 1, 0, 2 * Math.PI, true);
ctx.fill();
ctx.beginPath();
ctx.arc(me.attr.calloutEndX, me.attr.calloutEndY, 1, 0, 2 * Math.PI, true);
ctx.fill();
}
ctx.restore();
Ext.draw.sprite.Text.prototype.render.apply(this, arguments);
}
});
/**
* Series is the abstract class containing the common logic to all chart series. Series includes
* methods from Labels, Highlights, Tips and Callouts mixins. This class implements the logic of
* animating, hiding, showing all elements and returning the color of the series to be used as a legend item.
*
* ## Listeners
*
* The series class supports listeners via the Observable syntax. Some of these listeners are:
*
* - `itemmouseup` When the user interacts with a marker.
* - `itemmousedown` When the user interacts with a marker.
* - `itemmousemove` When the user interacts with a marker.
* - (similar `item*` events occur for many raw mouse and touch events)
* - `afterrender` Will be triggered when the animation ends or when the series has been rendered completely.
*
* For example:
*
* series: [{
* type: 'column',
* axis: 'left',
* listeners: {
* 'afterrender': function() {
* console('afterrender');
* }
* },
* xField: 'category',
* yField: 'data1'
* }]
*
*/
Ext.define('Ext.chart.series.Series', {
requires: ['Ext.chart.Markers', 'Ext.chart.label.Label'],
mixins: {
observable: 'Ext.mixin.Observable'
},
/**
* @property {String} type
* The type of series. Set in subclasses.
* @protected
*/
type: null,
/**
* @property {String} seriesType
* Default series sprite type.
*/
seriesType: 'sprite',
identifiablePrefix: 'ext-line-',
observableType: 'series',
config: {
/**
* @private
* @cfg {Object} chart The chart that the series is bound.
*/
chart: null,
/**
* @cfg {String} title
* The human-readable name of the series.
*/
title: null,
/**
* @cfg {Function} renderer
* A function that can be overridden to set custom styling properties to each rendered element.
* Passes in (sprite, record, attributes, index, store) to the function.
*
* @param sprite The sprite affected by the renderer.
* @param record The store record associated with the sprite.
* @param attributes The list of attributes to be applied to the sprite.
* @param index The index of the sprite.
* @param store The store used by the series.
* @return {*} The resultant attributes.
*/
renderer: function (sprite, record, attributes, index, store) {
return attributes;
},
/**
* @cfg {Boolean} showInLegend
* Whether to show this series in the legend.
*/
showInLegend: true,
//@private triggerdrawlistener flag
triggerAfterDraw: false,
/**
* @private
* Not supported.
*/
themeStyle: {},
/**
* @cfg {Object} style Custom style configuration for the sprite used in the series.
*/
style: {},
/**
* @cfg {Object} subStyle This is the cyclic used if the series has multiple sprites.
*/
subStyle: {},
/**
* @cfg {Array} colors
* An array of color values which will be used, in order, as the pie slice fill colors.
*/
colors: null,
/**
* @protected
* @cfg {Object} store The store of values used in the series.
*/
store: null,
/**
* @cfg {Object} label
* The style object for labels.
*/
label: {textBaseline: 'middle', textAlign: 'center', font: '14px Helvetica'},
/**
* @cfg {Number} labelOverflowPadding
* Extra distance value for which the labelOverflow listener is triggered.
*/
labelOverflowPadding: 5,
/**
* @cfg {String} labelField
* The store record field name to be used for the series labels.
*/
labelField: null,
/**
* @cfg {Object} marker
* The sprite template used by marker instances on the series.
*/
marker: null,
/**
* @cfg {Object} markerSubStyle
* This is cyclic used if series have multiple marker sprites.
*/
markerSubStyle: null,
/**
* @protected
* @cfg {Object} itemInstancing The sprite template used to create sprite instances in the series.
*/
itemInstancing: null,
/**
* @cfg {Object} background Sets the background of the surface the series is attached.
*/
background: null,
/**
* @cfg {Object} highlightItem The item currently highlighted in the series.
*/
highlightItem: null,
/**
* @protected
* @cfg {Object} surface The surface that the series is attached.
*/
surface: null,
/**
* @protected
* @cfg {Object} overlaySurface The surface that series markers are attached.
*/
overlaySurface: null,
/**
* @cfg {Boolean|Array} hidden
*/
hidden: false,
/**
* @cfg {Object} highlightCfg The sprite configuration used when highlighting items in the series.
*/
highlightCfg: null
},
directions: [],
sprites: null,
getFields: function (fieldCategory) {
var me = this,
fields = [], fieldsItem,
i, ln;
for (i = 0, ln = fieldCategory.length; i < ln; i++) {
fieldsItem = me['get' + fieldCategory[i] + 'Field']();
fields.push(fieldsItem);
}
return fields;
},
updateColors: function (colorSet) {
this.setSubStyle({fillStyle: colorSet});
this.doUpdateStyles();
},
applyHighlightCfg: function (highlight, oldHighlight) {
return Ext.apply(oldHighlight || {}, highlight);
},
applyItemInstancing: function (instancing, oldInstancing) {
return Ext.merge(oldInstancing || {}, instancing);
},
setAttributesForItem: function (item, change) {
if (item && item.sprite) {
if (item.sprite.itemsMarker && item.category === 'items') {
item.sprite.putMarker(item.category, change, item.index, false, true);
}
if (item.sprite.isMarkerHolder && item.category === 'markers') {
item.sprite.putMarker(item.category, change, item.index, false, true);
} else if (item.sprite instanceof Ext.draw.sprite.Instancing) {
item.sprite.setAttributesFor(item.index, change);
} else {
item.sprite.setAttributes(change);
}
}
},
applyHighlightItem: function (newHighlightItem, oldHighlightItem) {
if (newHighlightItem === oldHighlightItem) {
return;
}
if (Ext.isObject(newHighlightItem) && Ext.isObject(oldHighlightItem)) {
if (newHighlightItem.sprite === oldHighlightItem.sprite &&
newHighlightItem.index === oldHighlightItem.index
) {
return;
}
}
return newHighlightItem;
},
updateHighlightItem: function (newHighlightItem, oldHighlightItem) {
this.setAttributesForItem(oldHighlightItem, {highlighted: false});
this.setAttributesForItem(newHighlightItem, {highlighted: true});
},
constructor: function (config) {
var me = this;
me.getId();
me.sprites = [];
me.dataRange = [];
Ext.ComponentManager.register(me);
me.mixins.observable.constructor.apply(me, arguments);
},
applyStore: function (store) {
return Ext.StoreManager.lookup(store);
},
getStore: function () {
return this._store || this.getChart() && this.getChart().getStore();
},
updateStore: function (newStore, oldStore) {
var me = this,
chartStore = this.getChart() && this.getChart().getStore();
newStore = newStore || chartStore;
oldStore = oldStore || chartStore;
if (oldStore) {
oldStore.un('updaterecord', 'onUpdateRecord', me);
oldStore.un('refresh', 'refresh', me);
}
if (newStore) {
newStore.on('updaterecord', 'onUpdateRecord', me);
newStore.on('refresh', 'refresh', me);
me.refresh();
}
},
onStoreChanged: function () {
var store = this.getStore();
if (store) {
this.refresh();
}
},
coordinateStacked: function (direction, directionOffset, directionCount) {
var me = this,
store = me.getStore(),
items = store.getData().items,
axis = me['get' + direction + 'Axis'](),
hidden = me.getHidden(),
range = {min: 0, max: 0},
directions = me['fieldCategory' + direction],
fieldCategoriesItem,
i, j, k, fields, field, data, dataStart = [], style = {},
stacked = me.getStacked(),
sprites = me.getSprites();
if (sprites.length > 0) {
for (i = 0; i < directions.length; i++) {
fieldCategoriesItem = directions[i];
fields = me.getFields([fieldCategoriesItem]);
for (j = 0; j < items.length; j++) {
dataStart[j] = 0;
}
for (j = 0; j < fields.length; j++) {
style = {};
field = fields[j];
if (hidden[j]) {
style['dataStart' + fieldCategoriesItem] = dataStart;
style['data' + fieldCategoriesItem] = dataStart;
sprites[j].setAttributes(style);
continue;
}
data = me.coordinateData(items, field, axis);
if (stacked) {
style['dataStart' + fieldCategoriesItem] = dataStart;
dataStart = dataStart.slice(0);
for (k = 0; k < items.length; k++) {
dataStart[k] += data[k];
}
style['data' + fieldCategoriesItem] = dataStart;
} else {
style['dataStart' + fieldCategoriesItem] = dataStart;
style['data' + fieldCategoriesItem] = data;
}
sprites[j].setAttributes(style);
if (stacked) {
me.getRangeOfData(dataStart, range);
} else {
me.getRangeOfData(data, range);
}
}
}
me.dataRange[directionOffset] = range.min;
me.dataRange[directionOffset + directionCount] = range.max;
style = {};
style['dataMin' + direction] = range.min;
style['dataMax' + direction] = range.max;
for (i = 0; i < sprites.length; i++) {
sprites[i].setAttributes(style);
}
}
},
coordinate: function (direction, directionOffset, directionCount) {
var me = this,
store = me.getStore(),
items = store.getData().items,
axis = me['get' + direction + 'Axis'](),
range = {min: Infinity, max: -Infinity},
fieldCategory = me['fieldCategory' + direction] || [direction],
fields = me.getFields(fieldCategory),
i, field, data, style = {},
sprites = me.getSprites();
if (sprites.length > 0) {
for (i = 0; i < fieldCategory.length; i++) {
field = fields[i];
data = me.coordinateData(items, field, axis);
me.getRangeOfData(data, range);
style['data' + fieldCategory[i]] = data;
}
me.dataRange[directionOffset] = range.min;
me.dataRange[directionOffset + directionCount] = range.max;
style['dataMin' + direction] = range.min;
style['dataMax' + direction] = range.max;
for (i = 0; i < sprites.length; i++) {
sprites[i].setAttributes(style);
}
}
},
/**
* @private
* This method will return an array containing data coordinated by a specific axis.
* @param items
* @param field
* @param axis
* @return {Array}
*/
coordinateData: function (items, field, axis) {
var data = [],
length = items.length,
layout = axis && axis.getLayout(),
coord = axis ? function (x, field, idx, items) {
return layout.getCoordFor(x, field, idx, items);
} : function (x) { return +x; },
i;
for (i = 0; i < length; i++) {
data[i] = coord(items[i].data[field], field, i, items);
}
return data;
},
getRangeOfData: function (data, range) {
var i, length = data.length,
value, min = range.min, max = range.max;
for (i = 0; i < length; i++) {
value = data[i];
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
}
range.min = min;
range.max = max;
},
updateLabelData: function () {
var me = this,
store = me.getStore(),
items = store.getData().items,
sprites = me.getSprites(),
labelField = me.getLabelField(),
i, ln, labels;
if (sprites.length > 0 && labelField) {
labels = [];
for (i = 0, ln = items.length; i < ln; i++) {
labels.push(items[i].get(labelField));
}
for (i = 0, ln = sprites.length; i < ln; i++) {
sprites[i].setAttributes({labels: labels});
}
}
},
processData: function () {
if (!this.getStore()) {
return;
}
var me = this,
directions = this.directions,
i, ln = directions.length,
fieldCategory, axis;
for (i = 0; i < ln; i++) {
fieldCategory = directions[i];
if (me['get' + fieldCategory + 'Axis']) {
axis = me['get' + fieldCategory + 'Axis']();
if (axis) {
axis.processData(me);
continue;
}
}
if (me['coordinate' + fieldCategory]) {
me['coordinate' + fieldCategory]();
}
}
me.updateLabelData();
},
applyBackground: function (background) {
if (this.getChart()) {
this.getSurface().setBackground(background);
return this.getSurface().getBackground();
} else {
return background;
}
},
updateChart: function (newChart, oldChart) {
var me = this;
if (oldChart) {
oldChart.un("axeschanged", 'onAxesChanged', me);
// TODO: destroy them
me.sprites = [];
me.setSurface(null);
me.setOverlaySurface(null);
me.onChartDetached(oldChart);
}
if (newChart) {
me.setSurface(newChart.getSurface(this.getId() + '-surface', 'series'));
me.setOverlaySurface(newChart.getSurface(me.getId() + '-overlay-surface', 'overlay'));
me.getOverlaySurface().waitFor(me.getSurface());
newChart.on("axeschanged", 'onAxesChanged', me);
if (newChart.getAxes()) {
me.onAxesChanged(newChart);
}
me.onChartAttached(newChart);
}
me.updateStore(me._store, null);
},
onAxesChanged: function (chart) {
var me = this,
axes = chart.getAxes(), axis,
directionMap = {}, directionMapItem,
fieldMap = {}, fieldMapItem,
directions = this.directions, direction,
i, ln, j, ln2, k, ln3;
for (i = 0, ln = directions.length; i < ln; i++) {
direction = directions[i];
fieldMap[direction] = me.getFields(me['fieldCategory' + direction]);
}
for (i = 0, ln = axes.length; i < ln; i++) {
axis = axes[i];
if (!directionMap[axis.getDirection()]) {
directionMap[axis.getDirection()] = [axis];
} else {
directionMap[axis.getDirection()].push(axis);
}
}
for (i = 0, ln = directions.length; i < ln; i++) {
direction = directions[i];
if (directionMap[direction]) {
directionMapItem = directionMap[direction];
for (j = 0, ln2 = directionMapItem.length; j < ln2; j++) {
axis = directionMapItem[j];
if (axis.getFields().length === 0) {
me['set' + direction + 'Axis'](axis);
} else {
fieldMapItem = fieldMap[direction];
if (fieldMapItem) {
for (k = 0, ln3 = fieldMapItem.length; k < ln3; k++) {
if (axis.fieldsMap[fieldMapItem[k]]) {
me['set' + direction + 'Axis'](axis);
break;
}
}
}
}
}
}
}
},
onChartDetached: function (oldChart) {
this.fireEvent("chartdetached", oldChart);
},
onChartAttached: function (chart) {
var me = this;
me.setBackground(me.getBackground());
me.fireEvent("chartattached", chart);
me.processData();
},
updateOverlaySurface: function (overlaySurface) {
var me = this;
if (overlaySurface) {
if (me.getLabel()) {
me.getOverlaySurface().add(me.getLabel());
}
}
},
applyLabel: function (newLabel, oldLabel) {
if (!oldLabel) {
oldLabel = new Ext.chart.Markers({zIndex: 10});
oldLabel.setTemplate(new Ext.chart.label.Label(newLabel));
} else {
oldLabel.getTemplate().setAttributes(newLabel);
}
return oldLabel;
},
createItemInstancingSprite: function (sprite, itemInstancing) {
var me = this,
template,
markers = new Ext.chart.Markers();
markers.setAttributes({zIndex: 1e100});
var config = Ext.apply({}, itemInstancing);
if (me.getHighlightCfg()) {
config.highlightCfg = me.getHighlightCfg();
config.modifiers = ['highlight'];
}
markers.setTemplate(config);
template = markers.getTemplate();
template.setAttributes(me.getStyle());
template.fx.on('animationstart', 'onSpriteAnimationStart', this);
template.fx.on('animationend', 'onSpriteAnimationEnd', this);
sprite.bindMarker("items", markers);
me.getSurface().add(markers);
return markers;
},
getDefaultSpriteConfig: function () {
return {
type: this.seriesType
};
},
createSprite: function () {
var me = this,
surface = me.getSurface(),
itemInstancing = me.getItemInstancing(),
marker, config,
sprite = surface.add(me.getDefaultSpriteConfig());
sprite.setAttributes(this.getStyle());
if (itemInstancing) {
sprite.itemsMarker = me.createItemInstancingSprite(sprite, itemInstancing);
}
if (sprite.bindMarker) {
if (me.getMarker()) {
marker = new Ext.chart.Markers();
config = Ext.merge({}, me.getMarker());
if (me.getHighlightCfg()) {
config.highlightCfg = me.getHighlightCfg();
config.modifiers = ['highlight'];
}
marker.setTemplate(config);
marker.getTemplate().fx.setCustomDuration({
translationX: 0,
translationY: 0
});
sprite.dataMarker = marker;
sprite.bindMarker("markers", marker);
me.getOverlaySurface().add(marker);
}
if (me.getLabelField()) {
sprite.bindMarker("labels", me.getLabel());
}
}
if (sprite.setDataItems) {
sprite.setDataItems(me.getStore().getData());
}
sprite.fx.on('animationstart', 'onSpriteAnimationStart', me);
sprite.fx.on('animationend', 'onSpriteAnimationEnd', me);
me.sprites.push(sprite);
return sprite;
},
/**
* Performs drawing of this series.
*/
getSprites: Ext.emptyFn,
onUpdateRecord: function () {
// TODO: do something REALLY FAST.
this.processData();
},
refresh: function () {
this.processData();
},
isXType: function (xtype) {
return xtype === 'series';
},
getItemId: function () {
return this.getId();
},
applyStyle: function (style, oldStyle) {
// TODO: Incremental setter
var cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + this.seriesType));
if (cls && cls.def) {
style = cls.def.normalize(style);
}
return Ext.apply(oldStyle || Ext.Object.chain(this.getThemeStyle()), style);
},
applyMarker: function (marker, oldMarker) {
var type = (marker && marker.type) || (oldMarker && oldMarker.type) || this.seriesType,
cls;
if (type) {
cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + type));
if (cls && cls.def) {
marker = cls.def.normalize(marker, true);
marker.type = type;
return Ext.merge(oldMarker || {}, marker);
}
return Ext.merge(oldMarker || {}, marker);
}
},
applyMarkerSubStyle: function (marker, oldMarker) {
var type = (marker && marker.type) || (oldMarker && oldMarker.type) || this.seriesType,
cls;
if (type) {
cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + type));
if (cls && cls.def) {
marker = cls.def.batchedNormalize(marker, true);
return Ext.merge(oldMarker || {}, marker);
}
return Ext.merge(oldMarker || {}, marker);
}
},
applySubStyle: function (subStyle, oldSubStyle) {
var cls = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias('sprite.' + this.seriesType));
if (cls && cls.def) {
subStyle = cls.def.batchedNormalize(subStyle, true);
return Ext.merge(oldSubStyle || {}, subStyle);
}
return Ext.merge(oldSubStyle || {}, subStyle);
},
updateHidden: function (hidden) {
// TODO: remove this when jacky fix the problem.
this.getColors();
this.getSubStyle();
this.setSubStyle({hidden: hidden});
this.processData();
this.doUpdateStyles();
},
/**
*
* @param {Number} index
* @param {Boolean} value
*/
setHiddenByIndex: function (index, value) {
if (Ext.isArray(this.getHidden())) {
this.getHidden()[index] = value;
this.updateHidden(this.getHidden());
} else {
this.setHidden(value);
}
},
updateStyle: function () {
this.doUpdateStyles();
},
updateSubStyle: function () {
this.doUpdateStyles();
},
doUpdateStyles: function () {
var sprites = this.sprites,
itemInstancing = this.getItemInstancing(),
i = 0, ln = sprites && sprites.length,
markerCfg = this.getMarker(),
style;
for (; i < ln; i++) {
style = this.getStyleByIndex(i);
if (itemInstancing) {
sprites[i].itemsMarker.getTemplate().setAttributes(style);
} else {
sprites[i].setAttributes(style);
}
if (markerCfg && sprites[i].dataMarker) {
sprites[i].dataMarker.getTemplate().setAttributes(this.getMarkerStyleByIndex(i));
}
}
},
getMarkerStyleByIndex: function (i) {
return this.getOverriddenStyleByIndex(i, this.getOverriddenStyleByIndex(i, this.getMarkerSubStyle(), this.getMarker()), this.getStyleByIndex(i));
},
getStyleByIndex: function (i) {
return this.getOverriddenStyleByIndex(i, this.getSubStyle(), this.getStyle());
},
getOverriddenStyleByIndex: function (i, subStyle, baseStyle) {
var subStyleItem,
result = Ext.Object.chain(baseStyle || {});
for (var name in subStyle) {
subStyleItem = subStyle[name];
if (Ext.isArray(subStyleItem)) {
result[name] = subStyleItem[i % subStyle[name].length];
} else {
result[name] = subStyleItem;
}
}
return result;
},
/**
* For a given x/y point relative to the main region, find a corresponding item from this
* series, if any.
* @param {Number} x
* @param {Number} y
* @param {Object} [target] optional target to receive the result
* @return {Object} An object describing the item, or null if there is no matching item. The exact contents of
* this object will vary by series type, but should always contain at least the following:
*
* @return {Ext.data.Model} return.record the record of the item.
* @return {Array} return.point the x/y coordinates relative to the chart box of a single point
* for this data item, which can be used as e.g. a tooltip anchor point.
* @return {Ext.draw.sprite.Sprite} return.sprite the item's rendering Sprite.
* @return {Number} return.subSprite the index if sprite is an instancing sprite.
*/
getItemForPoint: Ext.emptyFn,
onSpriteAnimationStart: function (sprite) {
this.fireEvent('animationstart', sprite);
},
onSpriteAnimationEnd: function (sprite) {
this.fireEvent('animationend', sprite);
},
/**
* Provide legend information to target array.
*
* @param {Array} target
*
* The information consists:
* @param {String} target.name
* @param {String} target.markColor
* @param {Boolean} target.disabled
* @param {String} target.series
* @param {Number} target.index
*/
provideLegendInfo: function (target) {
target.push({
name: this.getTitle() || this.getId(),
mark: 'black',
disabled: false,
series: this.getId(),
index: 0
});
},
destroy: function () {
Ext.ComponentManager.unregister(this);
var store = this.getStore();
if (store && store.getAutoDestroy()) {
Ext.destroy(store);
}
this.setStore(null);
this.callSuper();
}
});
/**
* @class Ext.chart.interactions.Abstract
*
* Defines a common abstract parent class for all interactions.
*
*/
Ext.define('Ext.chart.interactions.Abstract', {
xtype: 'interaction',
mixins: {
observable: "Ext.mixin.Observable"
},
config: {
/**
* @cfg {String} gesture
* Specifies which gesture type should be used for starting the interaction.
*/
gesture: 'tap',
/**
* @cfg {Ext.chart.AbstractChart} chart The chart that the interaction is bound.
*/
chart: null,
/**
* @cfg {Boolean} enabled 'true' if the interaction is enabled.
*/
enabled: true
},
/**
* Android device is emerging too many events so if we re-render every frame it will take for-ever to finish a frame.
* This throttle technique will limit the timespan between two frames.
*/
throttleGap: 0,
stopAnimationBeforeSync: false,
constructor: function (config) {
var me = this;
me.initConfig(config);
Ext.ComponentManager.register(this);
},
/**
* @protected
* A method to be implemented by subclasses where all event attachment should occur.
*/
initialize: Ext.emptyFn,
updateChart: function (newChart, oldChart) {
var me = this, gestures = me.getGestures();
if (oldChart === newChart) {
return;
}
if (oldChart) {
me.removeChartListener(oldChart);
}
if (newChart) {
me.addChartListener();
}
},
getGestures: function () {
var gestures = {};
gestures[this.getGesture()] = this.onGesture;
return gestures;
},
/**
* @protected
* Placeholder method.
*/
onGesture: Ext.emptyFn,
/**
* @protected Find and return a single series item corresponding to the given event,
* or null if no matching item is found.
* @param {Event} e
* @return {Object} the item object or null if none found.
*/
getItemForEvent: function (e) {
var me = this,
chart = me.getChart(),
chartXY = chart.getEventXY(e);
return chart.getItemForPoint(chartXY[0], chartXY[1]);
},
/**
* @protected Find and return all series items corresponding to the given event.
* @param {Event} e
* @return {Array} array of matching item objects
*/
getItemsForEvent: function (e) {
var me = this,
chart = me.getChart(),
chartXY = chart.getEventXY(e);
return chart.getItemsForPoint(chartXY[0], chartXY[1]);
},
/**
* @private
*/
addChartListener: function () {
var me = this,
chart = me.getChart(),
gestures = me.getGestures(),
gesture, fn;
me.listeners = me.listeners || {};
function insertGesture(name, fn) {
chart.on(
name,
// wrap the handler so it does not fire if the event is locked by another interaction
me.listeners[name] = function (e) {
var locks = me.getLocks();
if (!(name in locks) || locks[name] === me) {
if (e && e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
return (Ext.isFunction(fn) ? fn : me[fn]).apply(this, arguments);
}
},
me
);
}
for (gesture in gestures) {
insertGesture(gesture, gestures[gesture]);
}
},
removeChartListener: function (chart) {
var me = this,
gestures = me.getGestures(),
gesture, fn;
function removeGesture(name) {
chart.un(name, me.listeners[name]);
delete me.listeners[name];
}
for (gesture in gestures) {
removeGesture(gesture);
}
},
lockEvents: function () {
var me = this,
locks = me.getLocks(),
args = Array.prototype.slice.call(arguments),
i = args.length;
while (i--) {
locks[args[i]] = me;
}
},
unlockEvents: function () {
var locks = this.getLocks(),
args = Array.prototype.slice.call(arguments),
i = args.length;
while (i--) {
delete locks[args[i]];
}
},
getLocks: function () {
var chart = this.getChart();
return chart.lockedEvents || (chart.lockedEvents = {});
},
isMultiTouch: function () {
return !(Ext.os.is.MultiTouch === false || (Ext.os.is.Android3 || Ext.os.is.Android2) || Ext.os.is.Desktop);
},
initializeDefaults: Ext.emptyFn,
doSync: function () {
var chart = this.getChart();
if (this.syncTimer) {
clearTimeout(this.syncTimer);
this.syncTimer = null;
}
if (this.stopAnimationBeforeSync) {
chart.resizing = true;
}
chart.redraw();
if (this.stopAnimationBeforeSync) {
chart.resizing = false;
}
this.syncThrottle = +new Date() + this.throttleGap;
},
sync: function () {
var me = this;
if (me.throttleGap && Ext.frameStartTime < me.syncThrottle) {
if (me.syncTimer) {
return;
}
me.syncTimer = setTimeout(function () {
me.doSync();
}, me.throttleGap);
} else {
me.doSync();
}
},
getItemId: function () {
return this.getId();
},
isXType: function (xtype) {
return xtype === 'interaction';
},
destroy: function () {
Ext.ComponentManager.unregister(this);
this.listeners = [];
this.callSuper();
}
}, function () {
if (Ext.os.is.Android2) {
this.prototype.throttleGap = 20;
} else if (Ext.os.is.Android4) {
this.prototype.throttleGap = 40;
}
});
/**
* @class Ext.chart.MarkerHolder
* @extends Ext.mixin.Mixin
*
* Mixin that provides the functionality to place markers.
*/
Ext.define("Ext.chart.MarkerHolder", {
extend: 'Ext.mixin.Mixin',
mixinConfig: {
id: 'markerHolder',
hooks: {
constructor: 'constructor',
preRender: 'preRender'
}
},
isMarkerHolder: true,
constructor: function () {
this.boundMarkers = {};
this.cleanRedraw = false;
},
/**
*
* @param name {String}
* @param marker {Ext.chart.Markers}
*/
bindMarker: function (name, marker) {
if (marker) {
if (!this.boundMarkers[name]) {
this.boundMarkers[name] = [];
}
Ext.Array.include(this.boundMarkers[name], marker);
}
},
getBoundMarker: function (name) {
return this.boundMarkers[name];
},
preRender: function () {
var boundMarkers = this.boundMarkers, boundMarkersItem,
name, i, ln, id = this.getId(),
parent = this.getParent(),
matrix = this.surfaceMatrix ? this.surfaceMatrix.set(1, 0, 0, 1, 0, 0) : (this.surfaceMatrix = new Ext.draw.Matrix());
this.cleanRedraw = !this.attr.dirty;
if (!this.cleanRedraw) {
for (name in this.boundMarkers) {
if (boundMarkers[name]) {
for (boundMarkersItem = boundMarkers[name], i = 0, ln = boundMarkersItem.length; i < ln; i++) {
boundMarkersItem[i].clear(id);
}
}
}
}
while (parent && parent.attr && parent.attr.matrix) {
matrix.prependMatrix(parent.attr.matrix);
parent = parent.getParent();
}
matrix.prependMatrix(parent.matrix);
this.surfaceMatrix = matrix;
this.inverseSurfaceMatrix = matrix.inverse(this.inverseSurfaceMatrix);
},
putMarker: function (name, markerAttr, index, canonical, keepRevision) {
var boundMarkersItem, i, ln, id = this.getId();
if (this.boundMarkers[name]) {
for (boundMarkersItem = this.boundMarkers[name], i = 0, ln = boundMarkersItem.length; i < ln; i++) {
boundMarkersItem[i].putMarkerFor(id, markerAttr, index, canonical);
}
}
},
getMarkerBBox: function (name, index, isWithoutTransform) {
var boundMarkersItem, i, ln, id = this.getId();
if (this.boundMarkers[name]) {
for (boundMarkersItem = this.boundMarkers[name], i = 0, ln = boundMarkersItem.length; i < ln; i++) {
return boundMarkersItem[i].getMarkerBBoxFor(id, index, isWithoutTransform);
}
}
}
});
/**
* @private
* @class Ext.chart.axis.sprite.Axis
* @extends Ext.draw.sprite.Sprite
*
* The axis sprite. Currently all types of the axis will be rendered with this sprite.
* TODO(touch-2.2): Split different types of axis into different sprite classes.
*/
Ext.define("Ext.chart.axis.sprite.Axis", {
extend: 'Ext.draw.sprite.Sprite',
mixins: {
markerHolder: "Ext.chart.MarkerHolder"
},
requires: ['Ext.draw.sprite.Text'],
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Boolean} grid 'true' if the axis has a grid.
*/
grid: 'bool',
/**
* @cfg {Boolean} axisLine 'true' if the main line of the axis is drawn.
*/
axisLine: 'bool',
/**
* @cfg {Boolean} minorTricks 'true' if the axis has sub ticks.
*/
minorTicks: 'bool',
/**
* @cfg {Number} minorTickSize The length of the minor ticks.
*/
minorTickSize: 'number',
/**
* @cfg {Boolean} majorTicks 'true' if the axis has major ticks.
*/
majorTicks: 'bool',
/**
* @cfg {Number} majorTickSize The length of the major ticks.
*/
majorTickSize: 'number',
/**
* @cfg {Number} length The total length of the axis.
*/
length: 'number',
/**
* @private
* @cfg {Number} startGap Axis start determined by the chart inset padding.
*/
startGap: 'number',
/**
* @private
* @cfg {Number} endGap Axis end determined by the chart inset padding.
*/
endGap: 'number',
/**
* @cfg {Number} dataMin The minimum value of the axis data.
*/
dataMin: 'number',
/**
* @cfg {Number} dataMax The maximum value of the axis data.
*/
dataMax: 'number',
/**
* @cfg {Number} visibleMin The minimum value that is displayed.
*/
visibleMin: 'number',
/**
* @cfg {Number} visibleMax The maximum value that is displayed.
*/
visibleMax: 'number',
/**
* @cfg {String} position The position of the axis on the chart.
*/
position: 'enums(left,right,top,bottom,angular,radial)',
/**
* @cfg {Number} minStepSize The minimum step size between ticks.
*/
minStepSize: 'number',
/**
* @private
* @cfg {Number} estStepSize The estimated step size between ticks.
*/
estStepSize: 'number',
/**
* @private
* Unused.
*/
titleOffset: 'number',
/**
* @cfg {Number} textPadding The padding around axis labels to determine collision.
*/
textPadding: 'number',
/**
* @cfg {Number} min The minimum value of the axis.
*/
min: 'number',
/**
* @cfg {Number} max The maximum value of the axis.
*/
max: 'number',
/**
* @cfg {Number} centerX The central point of the angular axis on the x-axis.
*/
centerX: 'number',
/**
* @cfg {Number} centerX The central point of the angular axis on the y-axis.
*/
centerY: 'number',
/**
* @private
* @cfg {Number} radius
* Unused.
*/
radius: 'number',
/**
* @cfg {Number} The starting rotation of the angular axis.
*/
baseRotation: 'number',
/**
* @private
* Unused.
*/
data: 'default',
/**
* @cfg {Boolean} 'true' if the estimated step size is adjusted by text size.
*/
enlargeEstStepSizeByText: 'bool'
},
defaults: {
grid: false,
axisLine: true,
minorTicks: false,
minorTickSize: 3,
majorTicks: true,
majorTickSize: 5,
length: 0,
startGap: 0,
endGap: 0,
visibleMin: 0,
visibleMax: 1,
dataMin: 0,
dataMax: 1,
position: '',
minStepSize: 0,
estStepSize: 42,
min: 0,
max: 1,
centerX: 0,
centerY: 0,
radius: 1,
baseRotation: 0,
data: null,
titleOffset: 0,
textPadding: 5,
scalingCenterY: 0,
scalingCenterX: 0,
// Override default
strokeStyle: 'black',
enlargeEstStepSizeByText: false
},
dirtyTriggers: {
minorTickSize: 'bbox',
majorTickSize: 'bbox',
position: 'bbox,layout',
axisLine: 'bbox,layout',
min: 'layout',
max: 'layout',
length: 'layout',
minStepSize: 'layout',
estStepSize: 'layout',
data: 'layout',
dataMin: 'layout',
dataMax: 'layout',
visibleMin: 'layout',
visibleMax: 'layout',
enlargeEstStepSizeByText: 'layout'
},
updaters: {
'layout': function () {
this.doLayout();
}
}
}
},
config: {
/**
* @cfg {Object} label
*
* The label configuration object for the Axis. This object may include style attributes
* like `spacing`, `padding`, `font` that receives a string or number and
* returns a new string with the modified values.
*/
label: null,
/**
* @cfg {Object|Ext.chart.axis.layout.Layout} layout The layout configuration used by the axis.
*/
layout: null,
/**
* @cfg {Object|Ext.chart.axis.segmenter.Segmenter} segmenter The method of segmenter used by the axis.
*/
segmenter: null,
/**
* @cfg {Function} renderer Allows direct customisation of rendered axis sprites.
*/
renderer: null,
/**
* @private
* @cfg {Object} layoutContext Stores the context after calculating layout.
*/
layoutContext: null,
/**
* @cfg {Ext.chart.axis.Axis} axis The axis represented by the this sprite.
*/
axis: null
},
thickness: 0,
stepSize: 0,
getBBox: function () { return null; },
doLayout: function () {
var me = this,
attr = me.attr,
layout = me.getLayout(),
min = attr.dataMin + (attr.dataMax - attr.dataMin) * attr.visibleMin,
max = attr.dataMin + (attr.dataMax - attr.dataMin) * attr.visibleMax,
context = {
attr: attr,
segmenter: me.getSegmenter()
};
if (attr.position === 'left' || attr.position === 'right') {
attr.translationX = 0;
attr.translationY = max * attr.length / (max - min);
attr.scalingX = 1;
attr.scalingY = -attr.length / (max - min);
attr.scalingCenterY = 0;
attr.scalingCenterX = 0;
me.applyTransformations(true);
} else if (attr.position === 'top' || attr.position === 'bottom') {
attr.translationX = -min * attr.length / (max - min);
attr.translationY = 0;
attr.scalingX = attr.length / (max - min);
attr.scalingY = 1;
attr.scalingCenterY = 0;
attr.scalingCenterX = 0;
me.applyTransformations(true);
}
if (layout) {
layout.calculateLayout(context);
me.setLayoutContext(context);
}
},
iterate: function (snaps, fn) {
var i, position;
if (snaps.getLabel) {
if (snaps.min < snaps.from) {
fn.call(this, snaps.min, snaps.getLabel(snaps.min), -1, snaps);
}
for (i = 0; i <= snaps.steps; i++) {
fn.call(this, snaps.get(i), snaps.getLabel(i), i, snaps);
}
if (snaps.max > snaps.to) {
fn.call(this, snaps.max, snaps.getLabel(snaps.max), snaps.steps + 1, snaps);
}
} else {
if (snaps.min < snaps.from) {
fn.call(this, snaps.min, snaps.min, -1, snaps);
}
for (i = 0; i <= snaps.steps; i++) {
position = snaps.get(i);
fn.call(this, position, position, i, snaps);
}
if (snaps.max > snaps.to) {
fn.call(this, snaps.max, snaps.max, snaps.steps + 1, snaps);
}
}
},
renderTicks: function (surface, ctx, layout, clipRegion) {
var me = this,
attr = me.attr,
docked = attr.position,
matrix = attr.matrix,
halfLineWidth = 0.5 * attr.lineWidth,
xx = matrix.getXX(),
dx = matrix.getDX(),
yy = matrix.getYY(),
dy = matrix.getDY(),
majorTicks = layout.majorTicks,
majorTickSize = attr.majorTickSize,
minorTicks = layout.minorTicks,
minorTickSize = attr.minorTickSize;
if (majorTicks) {
switch (docked) {
case 'right':
me.iterate(majorTicks, function (position, labelText, i) {
position = surface.roundPixel(position * yy + dy) + halfLineWidth;
ctx.moveTo(0, position);
ctx.lineTo(majorTickSize, position);
});
break;
case 'left':
me.iterate(majorTicks, function (position, labelText, i) {
position = surface.roundPixel(position * yy + dy) + halfLineWidth;
ctx.moveTo(clipRegion[2] - majorTickSize, position);
ctx.lineTo(clipRegion[2], position);
});
break;
case 'bottom':
me.iterate(majorTicks, function (position, labelText, i) {
position = surface.roundPixel(position * xx + dx) - halfLineWidth;
ctx.moveTo(position, 0);
ctx.lineTo(position, majorTickSize);
});
break;
case 'top':
me.iterate(majorTicks, function (position, labelText, i) {
position = surface.roundPixel(position * xx + dx) - halfLineWidth;
ctx.moveTo(position, clipRegion[3]);
ctx.lineTo(position, clipRegion[3] - majorTickSize);
});
break;
case 'angular':
me.iterate(majorTicks, function (position, labelText, i) {
position = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
ctx.moveTo(
attr.centerX + (attr.length) * Math.cos(position),
attr.centerY + (attr.length) * Math.sin(position)
);
ctx.lineTo(
attr.centerX + (attr.length + majorTickSize) * Math.cos(position),
attr.centerY + (attr.length + majorTickSize) * Math.sin(position)
);
});
break;
}
}
},
renderLabels: function (surface, ctx, layout, clipRegion) {
var me = this,
attr = me.attr,
halfLineWidth = 0.5 * attr.lineWidth,
docked = attr.position,
matrix = attr.matrix,
textPadding = attr.textPadding,
xx = matrix.getXX(),
dx = matrix.getDX(),
yy = matrix.getYY(),
dy = matrix.getDY(),
thickness = 0,
majorTicks = layout.majorTicks,
padding = Math.max(attr.majorTickSize, attr.minorTickSize) + attr.lineWidth,
label = this.getLabel(), font,
lastLabelText = null,
textSize = 0, textCount = 0,
segmenter = layout.segmenter,
renderer = this.getRenderer(),
labelInverseMatrix, lastBBox = null, bbox, fly, text;
if (majorTicks && label && !label.attr.hidden) {
font = label.attr.font;
if (ctx.font !== font) {
ctx.font = font;
} // This can profoundly improve performance.
label.setAttributes({translationX: 0, translationY: 0}, true, true);
label.applyTransformations();
labelInverseMatrix = label.attr.inverseMatrix.elements.slice(0);
switch (docked) {
case 'left':
label.setAttributes({
textAlign: 'center',
textBaseline: 'middle',
translationX: surface.roundPixel(clipRegion[2] - padding + dx) - halfLineWidth - me.thickness / 2
}, true, true);
break;
case 'right':
label.setAttributes({
textAlign: 'center',
textBaseline: 'middle',
translationX: surface.roundPixel(padding + dx) - halfLineWidth + me.thickness / 2
}, true, true);
break;
case 'top':
label.setAttributes({
textAlign: 'center',
textBaseline: 'middle',
translationY: surface.roundPixel(clipRegion[3] - padding) - halfLineWidth - me.thickness / 2
}, true, true);
break;
case 'bottom':
label.setAttributes({
textAlign: 'center',
textBaseline: 'middle',
translationY: surface.roundPixel(padding) - halfLineWidth + me.thickness / 2
}, true, true);
break;
case 'radial' :
label.setAttributes({
textAlign: 'center',
textBaseline: 'middle',
translationX: attr.centerX
}, true, true);
break;
case 'angular':
label.setAttributes({
textAlign: 'center',
textBaseline: 'middle',
translationY: attr.centerY
}, true, true);
break;
}
// TODO: there are better ways to detect collision.
if (docked === 'left' || docked === 'right') {
me.iterate(majorTicks, function (position, labelText, i) {
if (labelText === undefined) {
return;
}
text = renderer ? renderer.call(this, labelText, layout, lastLabelText) : segmenter.renderer(labelText, layout, lastLabelText);
lastLabelText = labelText;
label.setAttributes({
text: String(text),
translationY: surface.roundPixel(position * yy + dy)
}, true, true);
label.applyTransformations();
thickness = Math.max(thickness, label.getBBox().width + padding);
if (thickness <= me.thickness) {
fly = Ext.draw.Matrix.fly(label.attr.matrix.elements.slice(0));
bbox = fly.prepend.apply(fly, labelInverseMatrix).transformBBox(label.getBBox(true));
if (lastBBox && !Ext.draw.Draw.isBBoxIntersect(bbox, lastBBox, textPadding)) {
return;
}
surface.renderSprite(label);
lastBBox = bbox;
textSize += bbox.height;
textCount++;
}
});
} else if (docked === 'top' || docked === 'bottom') {
me.iterate(majorTicks, function (position, labelText, i) {
if (labelText === undefined) {
return;
}
text = renderer ? renderer.call(this, labelText, layout, lastLabelText) : segmenter.renderer(labelText, layout, lastLabelText);
lastLabelText = labelText;
label.setAttributes({
text: String(text),
translationX: surface.roundPixel(position * xx + dx)
}, true, true);
label.applyTransformations();
thickness = Math.max(thickness, label.getBBox().height + padding);
if (thickness <= me.thickness) {
fly = Ext.draw.Matrix.fly(label.attr.matrix.elements.slice(0));
bbox = fly.prepend.apply(fly, labelInverseMatrix).transformBBox(label.getBBox(true));
if (lastBBox && !Ext.draw.Draw.isBBoxIntersect(bbox, lastBBox, textPadding)) {
return;
}
surface.renderSprite(label);
lastBBox = bbox;
textSize += bbox.width;
textCount++;
}
});
} else if (docked === 'radial') {
me.iterate(majorTicks, function (position, labelText, i) {
if (labelText === undefined) {
return;
}
text = renderer ? renderer.call(this, labelText, layout, lastLabelText) : segmenter.renderer(labelText, layout, lastLabelText);
lastLabelText = labelText;
if (typeof text !== 'undefined') {
label.setAttributes({
text: String(text),
translationY: attr.centerY - surface.roundPixel(position) / attr.max * attr.length
}, true, true);
label.applyTransformations();
bbox = label.attr.matrix.transformBBox(label.getBBox(true));
if (lastBBox && !Ext.draw.Draw.isBBoxIntersect(bbox, lastBBox)) {
return;
}
surface.renderSprite(label);
lastBBox = bbox;
textSize += bbox.width;
textCount++;
}
});
} else if (docked === 'angular') {
me.iterate(majorTicks, function (position, labelText, i) {
if (labelText === undefined) {
return;
}
text = renderer ? renderer.call(this, labelText, layout, lastLabelText) : segmenter.renderer(labelText, layout, lastLabelText);
lastLabelText = labelText;
if (typeof text !== 'undefined') {
var angle = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
label.setAttributes({
text: String(text),
translationX: attr.centerX + (attr.length + 10) * Math.cos(angle),
translationY: attr.centerY + (attr.length + 10) * Math.sin(angle)
}, true, true);
label.applyTransformations();
bbox = label.attr.matrix.transformBBox(label.getBBox(true));
if (lastBBox && !Ext.draw.Draw.isBBoxIntersect(bbox, lastBBox)) {
return;
}
surface.renderSprite(label);
lastBBox = bbox;
textSize += bbox.width;
textCount++;
}
});
}
if (attr.enlargeEstStepSizeByText && textCount) {
textSize /= textCount;
textSize += padding;
textSize *= 2;
if (attr.estStepSize < textSize) {
attr.estStepSize = textSize;
}
}
if (Math.abs(me.thickness - (thickness)) > 1) {
me.thickness = thickness;
attr.bbox.plain.dirty = true;
attr.bbox.transform.dirty = true;
me.doThicknessChanged();
return false;
}
}
},
renderAxisLine: function (surface, ctx, layout, clipRegion) {
var me = this,
attr = me.attr,
halfWidth = attr.lineWidth * 0.5,
docked = attr.position;
if (attr.axisLine) {
switch (docked) {
case 'left':
ctx.moveTo(clipRegion[2] - halfWidth, -attr.endGap);
ctx.lineTo(clipRegion[2] - halfWidth, attr.length + attr.startGap);
break;
case 'right':
ctx.moveTo(halfWidth, -attr.endGap);
ctx.lineTo(halfWidth, attr.length + attr.startGap);
break;
case 'bottom':
ctx.moveTo(-attr.startGap, halfWidth);
ctx.lineTo(attr.length + attr.endGap, halfWidth);
break;
case 'top':
ctx.moveTo(-attr.startGap, clipRegion[3] - halfWidth);
ctx.lineTo(attr.length + attr.endGap, clipRegion[3] - halfWidth);
break;
case 'angular':
ctx.moveTo(attr.centerX + attr.length, attr.centerY);
ctx.arc(attr.centerX, attr.centerY, attr.length, 0, Math.PI * 2, true);
break;
}
}
},
renderGridLines: function (surface, ctx, layout, clipRegion) {
var me = this,
attr = me.attr,
matrix = attr.matrix,
xx = matrix.getXX(),
yy = matrix.getYY(),
dx = matrix.getDX(),
dy = matrix.getDY(),
position = attr.position,
majorTicks = layout.majorTicks,
anchor, j, lastAnchor;
if (attr.grid) {
if (majorTicks) {
if (position === 'left' || position === 'right') {
lastAnchor = attr.min * yy + dy;
me.iterate(majorTicks, function (position, labelText, i) {
anchor = position * yy + dy;
me.putMarker('horizontal-' + (i % 2 ? 'odd' : 'even'), {
y: anchor,
height: lastAnchor - anchor
}, j = i, true);
lastAnchor = anchor;
});
j++;
anchor = 0;
me.putMarker('horizontal-' + (j % 2 ? 'odd' : 'even'), {
y: anchor,
height: lastAnchor - anchor
}, j, true);
} else if (position === 'top' || position === 'bottom') {
lastAnchor = attr.min * xx + dx;
me.iterate(majorTicks, function (position, labelText, i) {
anchor = position * xx + dx;
me.putMarker('vertical-' + (i % 2 ? 'odd' : 'even'), {
x: anchor,
width: lastAnchor - anchor
}, j = i, true);
lastAnchor = anchor;
});
j++;
anchor = attr.length;
me.putMarker('vertical-' + (j % 2 ? 'odd' : 'even'), {
x: anchor,
width: lastAnchor - anchor
}, j, true);
} else if (position === 'radial') {
me.iterate(majorTicks, function (position, labelText, i) {
anchor = position / attr.max * attr.length;
me.putMarker('circular-' + (i % 2 ? 'odd' : 'even'), {
scalingX: anchor,
scalingY: anchor
}, i, true);
lastAnchor = anchor;
});
} else if (position === 'angular') {
me.iterate(majorTicks, function (position, labelText, i) {
anchor = position / (attr.max + 1) * Math.PI * 2 + attr.baseRotation;
me.putMarker('radial-' + (i % 2 ? 'odd' : 'even'), {
rotationRads: anchor,
rotationCenterX: 0,
rotationCenterY: 0,
scalingX: attr.length,
scalingY: attr.length
}, i, true);
lastAnchor = anchor;
});
}
}
}
},
doThicknessChanged: function () {
var axis = this.getAxis();
if (axis) {
axis.onThicknessChanged();
}
},
render: function (surface, ctx, clipRegion) {
var me = this,
layout = me.getLayoutContext();
if (layout) {
if (false === me.renderLabels(surface, ctx, layout, clipRegion)) {
return false;
}
ctx.beginPath();
me.renderTicks(surface, ctx, layout, clipRegion);
me.renderAxisLine(surface, ctx, layout, clipRegion);
me.renderGridLines(surface, ctx, layout, clipRegion);
ctx.stroke();
}
}
});
/**
* @abstract
* @class Ext.chart.axis.segmenter.Segmenter
*
* Interface for a segmenter in an Axis. A segmenter defines the operations you can do to a specific
* data type.
*
* See {@link Ext.chart.axis.Axis}.
*
*/
Ext.define("Ext.chart.axis.segmenter.Segmenter", {
config: {
/**
* @cfg {Ext.chart.axis.Axis} axis The axis that the Segmenter is bound.
*/
axis: null
},
constructor: function (config) {
this.initConfig(config);
},
/**
* This method formats the value.
*
* @param {*} value The value to format.
* @param {Object} context Axis layout context.
* @return {String}
*/
renderer: function (value, context) {
return String(value);
},
/**
* Convert from any data into the target type.
* @param {*} value The value to convert from
* @return {*} The converted value.
*/
from: function (value) {
return value;
},
/**
* Returns the difference between the min and max value based on the given unit scale.
*
* @param {*} min The smaller value.
* @param {*} max The larger value.
* @param {*} unit The unit scale. Unit can be any type.
* @return {Number} The number of `unit`s between min and max. It is the minimum n that min + n * unit >= max.
*/
diff: Ext.emptyFn,
/**
* Align value with step of units.
* For example, for the date segmenter, if The unit is "Month" and step is 3, the value will be aligned by
* seasons.
*
* @param {*} value The value to be aligned.
* @param {Number} step The step of units.
* @param {*} unit The unit.
* @return {*} Aligned value.
*/
align: Ext.emptyFn,
/**
* Add `step` `unit`s to the value.
* @param {*} value The value to be added.
* @param {Number} step The step of units. Negative value are allowed.
* @param {*} unit The unit.
*/
add: Ext.emptyFn,
/**
* Given a start point and estimated step size of a range, determine the preferred step size.
*
* @param {*} start The start point of range.
* @param {*} estStepSize The estimated step size.
* @return {Object} Return the step size by an object of step x unit.
* @return {Number} return.step The step count of units.
* @return {*} return.unit The unit.
*/
preferredStep: Ext.emptyFn
});
/**
* @class Ext.chart.axis.segmenter.Names
* @extends Ext.chart.axis.segmenter.Segmenter
*
* Names data type. Names will be calculated as their indices in the methods in this class.
* The `preferredStep` always return `{ unit: 1, step: 1 }` to indicate "show every item".
*
*/
Ext.define("Ext.chart.axis.segmenter.Names", {
extend: 'Ext.chart.axis.segmenter.Segmenter',
alias: 'segmenter.names',
renderer: function (value, context) {
return value;
},
diff: function (min, max, unit) {
return Math.floor(max - min);
},
align: function (value, step, unit) {
return Math.floor(value);
},
add: function (value, step, unit) {
return value + step;
},
preferredStep: function (min, estStepSize, minIdx, data) {
return {
unit: 1,
step: 1
};
}
});
/**
* @class Ext.chart.axis.segmenter.Time
* @extends Ext.chart.axis.segmenter.Segmenter
*
* Time data type.
*/
Ext.define("Ext.chart.axis.segmenter.Time", {
extend: 'Ext.chart.axis.segmenter.Segmenter',
alias: 'segmenter.time',
config: {
/**
* @cfg {Object} step
* If specified, the will override the result of {@link #preferredStep}.
*/
step: null
},
renderer: function (value, context) {
var ExtDate = Ext.Date;
switch (context.majorTicks.unit) {
case 'y':
return ExtDate.format(value, 'Y');
case 'mo':
return ExtDate.format(value, 'Y-m');
case 'd':
return ExtDate.format(value, 'Y-m-d');
}
return ExtDate.format(value, 'Y-m-d\nH:i:s');
},
from: function (value) {
return new Date(value);
},
diff: function (min, max, unit) {
var ExtDate = Ext.Date;
if (isFinite(min)) {
min = new Date(min);
}
if (isFinite(max)) {
max = new Date(max);
}
return ExtDate.diff(min, max, unit);
},
align: function (date, step, unit) {
if (unit === 'd' && step >= 7) {
date = Ext.Date.align(date, 'd', step);
date.setDate(date.getDate() - date.getDay() + 1);
return date;
} else {
return Ext.Date.align(date, unit, step);
}
},
add: function (value, step, unit) {
return Ext.Date.add(new Date(value), unit, step);
},
preferredStep: function (min, estStepSize) {
if (this.getStep()) {
return this.getStep();
}
var from = new Date(+min),
to = new Date(+min + Math.ceil(estStepSize)),
ExtDate = Ext.Date,
units = [
[ExtDate.YEAR, 1, 2, 5, 10, 20, 50, 100, 200, 500],
[ExtDate.MONTH, 1, 3, 6],
[ExtDate.DAY, 1, 7, 14],
[ExtDate.HOUR, 1, 6, 12],
[ExtDate.MINUTE, 1, 5, 15, 30],
[ExtDate.SECOND, 1, 5, 15, 30],
[ExtDate.MILLI, 1, 2, 5, 10, 20, 50, 100, 200, 500]
],
result;
for (var i = 0; i < units.length; i++) {
var unit = units[i][0],
diff = this.diff(from, to, unit);
if (diff > 0) {
for (var j = 1; j < units[i].length; j++) {
if (diff <= units[i][j]) {
result = {
unit: unit,
step: units[i][j]
};
break;
}
}
if (!result) {
i--;
result = {
unit: units[i][0],
step: 1
};
}
break;
}
}
if (!result) {
result = {unit: ExtDate.DAY, step: 1}; // Default step is one Day.
}
return result;
}
});
/**
* @class Ext.chart.axis.segmenter.Numeric
* @extends Ext.chart.axis.segmenter.Segmenter
*
* Numeric data type.
*/
Ext.define("Ext.chart.axis.segmenter.Numeric", {
extend: 'Ext.chart.axis.segmenter.Segmenter',
alias: 'segmenter.numeric',
renderer: function (value, context) {
return value.toFixed(Math.max(0, context.majorTicks.unit.fixes));
},
diff: function (min, max, unit) {
return Math.floor((max - min) / unit.scale);
},
align: function (value, step, unit) {
return Math.floor(value / (unit.scale * step)) * unit.scale * step;
},
add: function (value, step, unit) {
return value + step * (unit.scale);
},
preferredStep: function (min, estStepSize) {
var logs = Math.floor(Math.log(estStepSize) * Math.LOG10E),
scale = Math.pow(10, logs);
estStepSize /= scale;
if (estStepSize < 2) {
estStepSize = 2;
} else if (estStepSize < 5) {
estStepSize = 5;
} else if (estStepSize < 10) {
estStepSize = 10;
logs++;
}
return {
unit: { fixes: -logs, scale: scale },
step: estStepSize
};
}
});
/**
* @abstract
* @class Ext.chart.axis.layout.Layout
*
* Interface used by Axis to process its data into a meaningful layout.
*/
Ext.define("Ext.chart.axis.layout.Layout", {
config: {
/**
* @cfg {Ext.chart.axis.Axis} axis The axis that the Layout is bound.
*/
axis: null
},
constructor: function (config) {
this.initConfig();
},
/**
* Processes the data of the series bound to the axis.
* @param series The bound series.
*/
processData: function (series) {
var me = this,
axis = me.getAxis(),
direction = axis.getDirection(),
boundSeries = axis.boundSeries,
i, ln, item;
if (series) {
series['coordinate' + direction]();
} else {
for (i = 0, ln = boundSeries.length; i < ln; i++) {
item = boundSeries[i];
if (item['get' + direction + 'Axis']() === axis) {
item['coordinate' + direction]();
}
}
}
},
/**
* Calculates the position of major ticks for the axis.
* @param context
*/
calculateMajorTicks: function (context) {
var me = this,
attr = context.attr,
range = attr.max - attr.min,
zoom = range / attr.length * (attr.visibleMax - attr.visibleMin),
viewMin = attr.min + range * attr.visibleMin,
viewMax = attr.min + range * attr.visibleMax,
estStepSize = attr.estStepSize * zoom,
out = me.snapEnds(context, attr.min, attr.max, estStepSize);
if (out) {
me.trimByRange(context, out, viewMin - zoom * (1 + attr.startGap), viewMax + zoom * (1 + attr.endGap));
context.majorTicks = out;
}
},
/**
* Calculates the position of sub ticks for the axis.
* @param context
*/
calculateMinorTicks: function (context) {
// TODO: Finish Minor ticks.
},
/**
* Calculates the position of tick marks for the axis.
* @param context
* @return {*}
*/
calculateLayout: function (context) {
var me = this,
attr = context.attr,
majorTicks = attr.majorTicks,
minorTicks = attr.minorTicks;
if (attr.length === 0) {
return null;
}
if (majorTicks) {
this.calculateMajorTicks(context);
if (minorTicks) {
this.calculateMinorTicks(context);
}
}
},
/**
* Snaps the data bound to the axis to meaningful tick marks.
* @param context
* @param min
* @param max
* @param estStepSize
*/
snapEnds: Ext.emptyFn,
/**
* Trims the layout of the axis by the defined minimum and maximum.
* @param context
* @param out
* @param trimMin
* @param trimMax
*/
trimByRange: function (context, out, trimMin, trimMax) {
var segmenter = context.segmenter,
unit = out.unit,
beginIdx = segmenter.diff(out.from, trimMin, unit),
endIdx = segmenter.diff(out.from, trimMax, unit),
begin = Math.max(0, Math.ceil(beginIdx / out.step)),
end = Math.min(out.steps, Math.floor(endIdx / out.step));
if (end < out.steps) {
out.to = segmenter.add(out.from, end * out.step, unit);
}
if (out.max > trimMax) {
out.max = out.to;
}
if (out.from < trimMin) {
out.from = segmenter.add(out.from, begin * out.step, unit);
while (out.from < trimMin) {
begin++;
out.from = segmenter.add(out.from, out.step, unit);
}
}
if (out.min < trimMin) {
out.min = out.from;
}
out.steps = end - begin;
}
});
/**
* @class Ext.chart.axis.layout.Discrete
* @extends Ext.chart.axis.layout.Layout
*
* Simple processor for data that cannot be interpolated.
*/
Ext.define("Ext.chart.axis.layout.Discrete", {
extend: 'Ext.chart.axis.layout.Layout',
alias: 'axisLayout.discrete',
processData: function () {
var me = this,
axis = me.getAxis(),
boundSeries = axis.boundSeries,
direction = axis.getDirection(),
i, ln, item;
this.labels = [];
this.labelMap = {};
for (i = 0, ln = boundSeries.length; i < ln; i++) {
item = boundSeries[i];
if (item['get' + direction + 'Axis']() === axis) {
item['coordinate' + direction]();
}
}
},
// @inheritdoc
calculateLayout: function (context) {
context.data = this.labels;
this.callSuper([context]);
},
//@inheritdoc
calculateMajorTicks: function (context) {
var me = this,
attr = context.attr,
data = context.data,
range = attr.max - attr.min,
zoom = range / attr.length * (attr.visibleMax - attr.visibleMin),
viewMin = attr.min + range * attr.visibleMin,
viewMax = attr.min + range * attr.visibleMax,
estStepSize = attr.estStepSize * zoom;
var out = me.snapEnds(context, Math.max(0, attr.min), Math.min(attr.max, data.length - 1), estStepSize);
if (out) {
me.trimByRange(context, out, viewMin - zoom * (1 + attr.startGap), viewMax + zoom * (1 + attr.endGap));
context.majorTicks = out;
}
},
// @inheritdoc
snapEnds: function (context, min, max, estStepSize) {
estStepSize = Math.ceil(estStepSize);
var steps = Math.floor((max - min) / estStepSize),
data = context.data;
return {
min: min,
max: max,
from: min,
to: steps * estStepSize + min,
step: estStepSize,
steps: steps,
unit: 1,
getLabel: function (current) {
return data[this.from + this.step * current];
},
get: function (current) {
return this.from + this.step * current;
}
};
},
// @inheritdoc
trimByRange: function (context, out, trimMin, trimMax) {
var unit = out.unit,
beginIdx = Math.ceil((trimMin - out.from) / unit) * unit,
endIdx = Math.floor((trimMax - out.from) / unit) * unit,
begin = Math.max(0, Math.ceil(beginIdx / out.step)),
end = Math.min(out.steps, Math.floor(endIdx / out.step));
if (end < out.steps) {
out.to = end;
}
if (out.max > trimMax) {
out.max = out.to;
}
if (out.from < trimMin) {
out.from = out.from + begin * out.step * unit;
while (out.from < trimMin) {
begin++;
out.from += out.step * unit;
}
}
if (out.min < trimMin) {
out.min = out.from;
}
out.steps = end - begin;
},
getCoordFor: function (value, field, idx, items) {
this.labels.push(value);
return this.labels.length - 1;
}
});
/**
* @class Ext.chart.axis.layout.Continuous
* @extends Ext.chart.axis.layout.Layout
*
* Processor for axis data that can be interpolated.
*/
Ext.define("Ext.chart.axis.layout.Continuous", {
extend: 'Ext.chart.axis.layout.Layout',
alias: 'axisLayout.continuous',
config: {
adjustMinimumByMajorUnit: false,
adjustMaximumByMajorUnit: false
},
getCoordFor: function (value, field, idx, items) {
return +value;
},
//@inheritdoc
snapEnds: function (context, min, max, estStepSize) {
var segmenter = context.segmenter,
out = context.segmenter.preferredStep(min, estStepSize),
unit = out.unit,
step = out.step,
from = segmenter.align(min, step, unit),
steps = segmenter.diff(min, max, unit) + 1;
return {
min: segmenter.from(min),
max: segmenter.from(max),
from: from,
to: segmenter.add(from, steps * step, unit),
step: step,
steps: steps,
unit: unit,
get: function (current) {
return segmenter.add(this.from, this.step * current, unit);
}
};
}
});
/**
* @class Ext.chart.axis.layout.CombineDuplicate
* @extends Ext.chart.axis.layout.Discrete
*
* Discrete processor that combines duplicate data points.
*/
Ext.define("Ext.chart.axis.layout.CombineDuplicate", {
extend: 'Ext.chart.axis.layout.Discrete',
alias: 'axisLayout.combineDuplicate',
getCoordFor: function (value, field, idx, items) {
if (!(value in this.labelMap)) {
var result = this.labelMap[value] = this.labels.length;
this.labels.push(value);
return result;
}
return this.labelMap[value];
}
});
/**
* @class Ext.chart.axis.Axis
*
* Defines axis for charts.
*
* Using the current model, the type of axis can be easily extended. By default, Sencha Touch provides three different
* type of axis:
*
* * **Numeric**: the data attached with this axes are considered to be numeric and continuous.
* * **Time**: the data attached with this axes are considered (or get converted into) date/time and they are continuous.
* * **Category**: the data attached with this axes conforms a finite set. They be evenly placed on the axis and displayed in the same form they were provided.
*
* The behavior of axis can be easily changed by setting different types of axis layout and axis segmenter to the axis.
*
* Axis layout defines how the data points are places. Using continuous layout, the data points will be distributed by
* there numeric value. Using discrete layout the data points will be spaced evenly, Furthermore, if you want to combine
* the data points with the duplicate values in a discrete layout, you should use combinedDuplicate layout.
*
* Segmenter defines the way to segment data range. For example, if you have a Date-type data range from Jan 1, 1997 to
* Jan 1, 2017, the segmenter will segement the data range into years, months or days based on the current zooming
* level.
*
* It is possible to write custom axis layouts and segmenters to extends this behavior by simply implement interfaces
* {@link Ext.chart.axis.layout.Layout} and {@link Ext.chart.axis.segmenter.Segmenter}.
*
* Here's an example for the axes part of a chart definition:
* An example of axis for a series (in this case for an area chart that has multiple layers of yFields) could be:
*
* axes: [{
* type: 'Numeric',
* position: 'left',
* title: 'Number of Hits',
* grid: {
* odd: {
* opacity: 1,
* fill: '#ddd',
* stroke: '#bbb',
* lineWidth: 1
* }
* },
* minimum: 0
* }, {
* type: 'Category',
* position: 'bottom',
* title: 'Month of the Year',
* grid: true,
* label: {
* rotate: {
* degrees: 315
* }
* }
* }]
*
* In this case we use a `Numeric` axis for displaying the values of the Area series and a `Category` axis for displaying the names of
* the store elements. The numeric axis is placed on the left of the screen, while the category axis is placed at the bottom of the chart.
* Both the category and numeric axes have `grid` set, which means that horizontal and vertical lines will cover the chart background. In the
* category axis the labels will be rotated so they can fit the space better.
*/
Ext.define('Ext.chart.axis.Axis', {
xtype: 'axis',
mixins: {
observable: 'Ext.mixin.Observable'
},
requires: [
'Ext.chart.axis.sprite.Axis',
'Ext.chart.axis.segmenter.*',
'Ext.chart.axis.layout.*'
],
config: {
/**
* @cfg {String} position
* Where to set the axis. Available options are `left`, `bottom`, `right`, `top`, `radial` and `angular`.
*/
position: 'bottom',
/**
* @cfg {Array} fields
* An array containing the names of the record fields which should be mapped along the axis.
* This is optional if the binding between series and fields is clear.
*/
fields: [],
/**
* @cfg {Object} label
*
* The label configuration object for the Axis. This object may include style attributes
* like `spacing`, `padding`, `font` that receives a string or number and
* returns a new string with the modified values.
*/
label: { x: 0, y: 0, textBaseline: 'middle', textAlign: 'center', fontSize: 12, fontFamily: 'Helvetica' },
/**
* @cfg {Object} grid
* The grid configuration object for the Axis style. Can contain `stroke` or `fill` attributes.
* Also may contain an `odd` or `even` property in which you only style things on odd or even rows.
* For example:
*
*
* grid {
* odd: {
* stroke: '#555'
* },
* even: {
* stroke: '#ccc'
* }
* }
*/
grid: false,
/**
* @cfg {Function} renderer Allows direct customisation of rendered axis sprites.
*/
renderer: null,
/**
* @protected
* @cfg {Ext.chart.AbstractChart} chart The Chart that the Axis is bound.
*/
chart: null,
/**
* @cfg {Object} style
* The style for the axis line and ticks.
* Refer to the {@link Ext.chart.axis.sprite.Axis}
*/
style: null,
/**
* @cfg {Number} titleMargin
* The margin between axis title and axis.
*/
titleMargin: 4,
/**
* @cfg {Object} background
* The background config for the axis surface.
*/
background: null,
/**
* @cfg {Number} minimum
* The minimum value drawn by the axis. If not set explicitly, the axis
* minimum will be calculated automatically.
*/
minimum: NaN,
/**
* @cfg {Number} maximum
* The maximum value drawn by the axis. If not set explicitly, the axis
* maximum will be calculated automatically.
*/
maximum: NaN,
/**
* @cfg {Number} minZoom
* The minimum zooming level for axis.
*/
minZoom: 1,
/**
* @cfg {Number} maxZoom
* The maximum zooming level for axis
*/
maxZoom: 10000,
/**
* @cfg {Object|Ext.chart.axis.layout.Layout} layout
* The axis layout config. See {@link Ext.chart.axis.layout.Layout}
*/
layout: 'continuous',
/**
* @cfg {Object|Ext.chart.axis.segmenter.Segmenter} segmenter
* The segmenter config. See {@link Ext.chart.axis.segmenter.Segmenter}
*/
segmenter: 'numeric',
/**
* @cfg {Boolean} hidden
* Indicate whether to hide the axis.
* If the axis is hidden, one of the axis line, ticks, labels or the title will be shown and
* no margin will be taken.
* The coordination mechanism works fine no matter if the axis is hidden.
*/
hidden: false,
/**
* @private
* @cfg {Number} majorTickSteps
* Will be supported soon.
* If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value.
*/
majorTickSteps: false,
/**
* @private
* @cfg {Number} [minorTickSteps=0]
* Will be supported soon.
* The number of small ticks between two major ticks.
*/
minorTickSteps: false,
/**
* @private
* @cfg {Boolean} adjustMaximumByMajorUnit
* Will be supported soon.
*/
adjustMaximumByMajorUnit: false,
/**
* @private
* @cfg {Boolean} adjustMinimumByMajorUnit
* Will be supported soon.
*
*/
adjustMinimumByMajorUnit: false,
/**
* @cfg {String|Object} title
* The title for the Axis.
* If given a String, the text style of the title sprite will be set,
* otherwise the style will be set.
*/
title: { fontSize: 18, fontFamily: 'Helvetica'},
/**
* @cfg {Number} increment
* Given a minimum and maximum bound for the series to be rendered (that can be obtained
* automatically or by manually setting `minimum` and `maximum`) tick marks will be added
* on each `increment` from the minimum value to the maximum one.
*/
increment: 0.5,
/**
* @private
* @cfg {Number} length
* Length of the axis position. Equals to the size of inner region on the docking side of this axis.
* WARNING: Meant to be set automatically by chart. Do not set it manually.
*/
length: 0,
/**
* @private
* @cfg {Array} center
* Center of the polar axis.
* WARNING: Meant to be set automatically by chart. Do not set it manually.
*/
center: null,
/**
* @private
* @cfg {Number} radius
* Radius of the polar axis.
* WARNING: Meant to be set automatically by chart. Do not set it manually.
*/
radius: null,
/**
* @private
* @cfg {Number} rotation
* Rotation of the polar axis.
* WARNING: Meant to be set automatically by chart. Do not set it manually.
*/
rotation: null,
/**
* @cfg {Boolean} [labelInSpan]
* Draws the labels in the middle of the spans.
*/
labelInSpan: null,
/**
* @cfg {Array} visibleRange
* Specify the proportion of the axis to be rendered. The series bound to
* this axis will be synchronized and transformed.
*/
visibleRange: [0, 1],
/**
* @private
* @cfg {Boolean} needHighPrecision
*/
needHighPrecision: false
},
observableType: 'component',
titleOffset: 0,
animating: 0,
prevMin: 0,
prevMax: 1,
boundSeries: [],
sprites: null,
/**
* @private
* @property {Array} The full data range of the axis. Should not be set directly, clear it to `null` and use
* `getRange` to update.
*/
range: null,
xValues: [],
yValues: [],
applyRotation: function (rotation) {
var twoPie = Math.PI * 2;
return (rotation % twoPie + Math.PI) % twoPie - Math.PI;
},
updateRotation: function (rotation) {
var sprites = this.getSprites(),
position = this.getPosition();
if (!this.getHidden() && position === 'angular' && sprites[0]) {
sprites[0].setAttributes({
baseRotation: rotation
});
}
},
applyTitle: function (title, oldTitle) {
var surface;
if (Ext.isString(title)) {
title = { text: title };
}
if (!oldTitle) {
oldTitle = Ext.create('sprite.text', title);
if ((surface = this.getSurface())) {
surface.add(oldTitle);
}
} else {
oldTitle.setAttributes(title);
}
return oldTitle;
},
constructor: function (config) {
var me = this;
me.sprites = [];
this.labels = [];
this.initConfig(config);
me.getId();
me.mixins.observable.constructor.apply(me, arguments);
Ext.ComponentManager.register(me);
},
/**
* @private
* @return {String}
*/
getAlignment: function () {
switch (this.getPosition()) {
case 'left':
case 'right':
return "vertical";
case 'top':
case 'bottom':
return "horizontal";
case 'radial':
return "radial";
case 'angular':
return "angular";
}
},
/**
* @private
* @return {String}
*/
getGridAlignment: function () {
switch (this.getPosition()) {
case 'left':
case 'right':
return "horizontal";
case 'top':
case 'bottom':
return "vertical";
case 'radial':
return "circular";
case 'angular':
return "radial";
}
},
/**
* @private
* Get the surface for drawing the series sprites
*/
getSurface: function () {
if (!this.surface) {
var chart = this.getChart();
if (!chart) {
return null;
}
var surface = this.surface = chart.getSurface(this.getId(), 'axis'),
gridSurface = this.gridSurface = chart.getSurface("grid-" + this.getId(), 'grid'),
sprites = this.getSprites(),
sprite = sprites[0],
grid = this.getGrid(),
gridAlignment = this.getGridAlignment(),
gridSprite;
if (grid) {
gridSprite = this.gridSpriteEven = new Ext.chart.Markers();
gridSprite.setTemplate({xclass: 'grid.' + gridAlignment});
if (Ext.isObject(grid)) {
gridSprite.getTemplate().setAttributes(grid);
if (Ext.isObject(grid.even)) {
gridSprite.getTemplate().setAttributes(grid.even);
}
}
gridSurface.add(gridSprite);
sprite.bindMarker(gridAlignment + '-even', gridSprite);
gridSprite = this.gridSpriteOdd = new Ext.chart.Markers();
gridSprite.setTemplate({xclass: 'grid.' + gridAlignment});
if (Ext.isObject(grid)) {
gridSprite.getTemplate().setAttributes(grid);
if (Ext.isObject(grid.odd)) {
gridSprite.getTemplate().setAttributes(grid.odd);
}
}
gridSurface.add(gridSprite);
sprite.bindMarker(gridAlignment + '-odd', gridSprite);
gridSurface.waitFor(surface);
}
}
return this.surface;
},
/**
*
* Mapping data value into coordinate.
*
* @param {*} value
* @param {String} field
* @param {Number} [idx]
* @param {Ext.util.MixedCollection} [items]
* @return {Number}
*/
getCoordFor: function (value, field, idx, items) {
return this.getLayout().getCoordFor(value, field, idx, items);
},
applyPosition: function (pos) {
return pos.toLowerCase();
},
applyLabel: function (newText, oldText) {
if (!oldText) {
oldText = new Ext.draw.sprite.Text({});
}
oldText.setAttributes(newText);
return oldText;
},
applyLayout: function (layout, oldLayout) {
// TODO: finish this
layout = Ext.factory(layout, null, oldLayout, 'axisLayout');
layout.setAxis(this);
return layout;
},
applySegmenter: function (segmenter, oldSegmenter) {
// TODO: finish this
segmenter = Ext.factory(segmenter, null, oldSegmenter, 'segmenter');
segmenter.setAxis(this);
return segmenter;
},
updateMinimum: function () {
this.range = null;
},
updateMaximum: function () {
this.range = null;
},
hideLabels: function () {
this.getSprites()[0].setDirty(true);
this.setLabel({hidden: true});
},
showLabels: function () {
this.getSprites()[0].setDirty(true);
this.setLabel({hidden: false});
},
/**
* @private
* Reset the axis to its original state, before any user interaction.
*
*/
reset: function () {
// TODO: finish this
},
/**
* Invokes renderFrame on this axis's surface(s)
*/
renderFrame: function () {
this.getSurface().renderFrame();
},
updateChart: function (newChart, oldChart) {
var me = this, surface;
if (oldChart) {
oldChart.un("serieschanged", me.onSeriesChanged, me);
}
if (newChart) {
newChart.on("serieschanged", me.onSeriesChanged, me);
if (newChart.getSeries()) {
me.onSeriesChanged(newChart);
}
me.surface = null;
surface = me.getSurface();
surface.add(me.getSprites());
surface.add(me.getTitle());
}
},
applyBackground: function (background) {
var rect = Ext.ClassManager.getByAlias('sprite.rect');
return rect.def.normalize(background);
},
/**
* @protected
* Invoked when data has changed.
*/
processData: function () {
this.getLayout().processData();
this.range = null;
},
getDirection: function () {
return this.getChart().getDirectionForAxis(this.getPosition());
},
isSide: function () {
var position = this.getPosition();
return position === 'left' || position === 'right';
},
applyFields: function (fields) {
return [].concat(fields);
},
updateFields: function (fields) {
this.fieldsMap = {};
for (var i = 0; i < fields.length; i++) {
this.fieldsMap[fields[i]] = true;
}
},
applyVisibleRange: function (visibleRange, oldVisibleRange) {
// If it is in reversed order swap them
if (visibleRange[0] > visibleRange[1]) {
var temp = visibleRange[0];
visibleRange[0] = visibleRange[1];
visibleRange[0] = temp;
}
if (visibleRange[1] === visibleRange[0]) {
visibleRange[1] += 1 / this.getMaxZoom();
}
if (visibleRange[1] > visibleRange[0] + 1) {
visibleRange[0] = 0;
visibleRange[1] = 1;
} else if (visibleRange[0] < 0) {
visibleRange[1] -= visibleRange[0];
visibleRange[0] = 0;
} else if (visibleRange[1] > 1) {
visibleRange[0] -= visibleRange[1] - 1;
visibleRange[1] = 1;
}
if (oldVisibleRange && visibleRange[0] === oldVisibleRange[0] && visibleRange[1] === oldVisibleRange[1]) {
return undefined;
}
return visibleRange;
},
updateVisibleRange: function (visibleRange) {
this.fireEvent('transformed', this, visibleRange);
},
onSeriesChanged: function (chart) {
var me = this,
series = chart.getSeries(),
getAxisMethod = 'get' + me.getDirection() + 'Axis',
boundSeries = [], i, ln = series.length;
for (i = 0; i < ln; i++) {
if (this === series[i][getAxisMethod]()) {
boundSeries.push(series[i]);
}
}
me.boundSeries = boundSeries;
me.getLayout().processData();
},
applyRange: function (newRange) {
if (!newRange) {
return this.dataRange.slice(0);
} else {
return [
newRange[0] === null ? this.dataRange[0] : newRange[0],
newRange[1] === null ? this.dataRange[1] : newRange[1]
];
}
},
/**
* Get the range derived from all the bound series.
* @return {Array}
*/
getRange: function () {
var me = this,
getRangeMethod = 'get' + me.getDirection() + 'Range';
if (me.range) {
return me.range;
}
if (!isNaN(me.getMinimum()) && !isNaN(me.getMaximum())) {
return this.range = [me.getMinimum(), me.getMaximum()];
}
var min = Infinity,
max = -Infinity,
boundSeries = me.boundSeries,
series, i, ln;
// For each series bound to this axis, ask the series for its min/max values
// and use them to find the overall min/max.
for (i = 0, ln = boundSeries.length; i < ln; i++) {
series = boundSeries[i];
var minMax = series[getRangeMethod]();
if (minMax) {
if (minMax[0] < min) {
min = minMax[0];
}
if (minMax[1] > max) {
max = minMax[1];
}
}
}
if (!isFinite(max)) {
max = me.prevMax;
}
if (!isFinite(min)) {
min = me.prevMin;
}
if (this.getLabelInSpan()) {
max += this.getIncrement();
min -= this.getIncrement();
}
if (!isNaN(me.getMinimum())) {
min = me.getMinimum();
} else {
me.prevMin = min;
}
if (!isNaN(me.getMaximum())) {
max = me.getMaximum();
} else {
me.prevMax = max;
}
return this.range = [min, max];
},
applyStyle: function (style, oldStyle) {
var cls = Ext.ClassManager.getByAlias('sprite.' + this.seriesType);
if (cls && cls.def) {
style = cls.def.normalize(style);
}
oldStyle = Ext.apply(oldStyle || {}, style);
return oldStyle;
},
updateCenter: function (center) {
var sprites = this.getSprites(),
axisSprite = sprites[0],
centerX = center[0],
centerY = center[1];
if (axisSprite) {
axisSprite.setAttributes({
centerX: centerX,
centerY: centerY
});
}
if (this.gridSpriteEven) {
this.gridSpriteEven.getTemplate().setAttributes({
translationX: centerX,
translationY: centerY,
rotationCenterX: centerX,
rotationCenterY: centerY
});
}
if (this.gridSpriteOdd) {
this.gridSpriteOdd.getTemplate().setAttributes({
translationX: centerX,
translationY: centerY,
rotationCenterX: centerX,
rotationCenterY: centerY
});
}
},
getSprites: function () {
if (!this.getChart()) {
return;
}
var me = this,
range = me.getRange(),
position = me.getPosition(),
chart = me.getChart(),
animation = chart.getAnimate(),
baseSprite, style,
gridAlignment = me.getGridAlignment(),
length = me.getLength();
// If animation is false, then stop animation.
if (animation === false) {
animation = {
duration: 0
};
}
if (range) {
style = Ext.applyIf({
position: position,
axis: me,
min: range[0],
max: range[1],
length: length,
grid: me.getGrid(),
hidden: me.getHidden(),
titleOffset: me.titleOffset,
layout: me.getLayout(),
segmenter: me.getSegmenter(),
label: me.getLabel()
}, me.getStyle());
// If the sprites are not created.
if (!me.sprites.length) {
baseSprite = new Ext.chart.axis.sprite.Axis(style);
baseSprite.fx.setCustomDuration({
baseRotation: 0
});
baseSprite.fx.on("animationstart", "onAnimationStart", me);
baseSprite.fx.on("animationend", "onAnimationEnd", me);
me.sprites.push(baseSprite);
me.updateTitleSprite();
} else {
baseSprite = me.sprites[0];
baseSprite.fx.setConfig(animation);
baseSprite.setAttributes(style);
baseSprite.setLayout(me.getLayout());
baseSprite.setSegmenter(me.getSegmenter());
baseSprite.setLabel(me.getLabel());
}
if (me.getRenderer()) {
baseSprite.setRenderer(me.getRenderer());
}
}
return me.sprites;
},
updateTitleSprite: function () {
if (!this.sprites[0]) {
return;
}
var me = this,
thickness = this.sprites[0].thickness,
surface = me.getSurface(),
title = this.getTitle(),
position = me.getPosition(),
titleMargin = me.getTitleMargin(),
length = me.getLength(),
anchor = surface.roundPixel(length / 2);
if (title) {
switch (position) {
case 'top':
title.setAttributes({
x: anchor,
y: titleMargin / 2,
textBaseline: 'top',
textAlign: 'center'
}, true, true);
title.applyTransformations();
me.titleOffset = title.getBBox().height + titleMargin;
break;
case 'bottom':
title.setAttributes({
x: anchor,
y: thickness + titleMargin,
textBaseline: 'top',
textAlign: 'center'
}, true, true);
title.applyTransformations();
me.titleOffset = title.getBBox().height + titleMargin;
break;
case 'left':
title.setAttributes({
x: titleMargin / 2,
y: anchor,
textBaseline: 'top',
textAlign: 'center',
rotationCenterX: titleMargin / 2,
rotationCenterY: anchor,
rotationRads: -Math.PI / 2
}, true, true);
title.applyTransformations();
me.titleOffset = title.getBBox().width + titleMargin;
break;
case 'right':
title.setAttributes({
x: thickness - titleMargin / 2,
y: anchor,
textBaseline: 'bottom',
textAlign: 'center',
rotationCenterX: thickness,
rotationCenterY: anchor,
rotationRads: Math.PI / 2
}, true, true);
title.applyTransformations();
me.titleOffset = title.getBBox().width + titleMargin;
break;
}
}
},
onThicknessChanged: function () {
var me = this;
me.getChart().onThicknessChanged();
},
getThickness: function () {
if (this.getHidden()) {
return 0;
}
return (this.sprites[0] && this.sprites[0].thickness || 1) + this.titleOffset;
},
onAnimationStart: function () {
this.animating++;
if (this.animating === 1) {
this.fireEvent("animationstart");
}
},
onAnimationEnd: function () {
this.animating--;
if (this.animating === 0) {
this.fireEvent("animationend");
}
},
// Methods used in ComponentQuery and controller
getItemId: function () {
return this.getId();
},
getAncestorIds: function () {
return [this.getChart().getId()];
},
isXType: function (xtype) {
return xtype === 'axis';
},
destroy: function () {
Ext.ComponentManager.unregister(this);
this.callSuper();
}
});
/**
* @private
*/
Ext.define('Ext.mixin.Sortable', {
extend: 'Ext.mixin.Mixin',
requires: [
'Ext.util.Sorter'
],
mixinConfig: {
id: 'sortable'
},
config: {
/**
* @cfg {Array} sorters
* An array with sorters. A sorter can be an instance of {@link Ext.util.Sorter}, a string
* indicating a property name, an object representing an Ext.util.Sorter configuration,
* or a sort function.
*/
sorters: null,
/**
* @cfg {String} defaultSortDirection
* The default sort direction to use if one is not specified.
*/
defaultSortDirection: "ASC",
/**
* @cfg {String} sortRoot
* The root inside each item in which the properties exist that we want to sort on.
* This is useful for sorting records in which the data exists inside a `data` property.
*/
sortRoot: null
},
/**
* @property {Boolean} dirtySortFn
* A flag indicating whether the currently cashed sort function is still valid.
* @readonly
*/
dirtySortFn: false,
/**
* @property currentSortFn
* This is the cached sorting function which is a generated function that calls all the
* configured sorters in the correct order.
* @readonly
*/
sortFn: null,
/**
* @property {Boolean} sorted
* A read-only flag indicating if this object is sorted.
* @readonly
*/
sorted: false,
applySorters: function(sorters, collection) {
if (!collection) {
collection = this.createSortersCollection();
}
collection.clear();
this.sorted = false;
if (sorters) {
this.addSorters(sorters);
}
return collection;
},
createSortersCollection: function() {
this._sorters = Ext.create('Ext.util.Collection', function(sorter) {
return sorter.getId();
});
return this._sorters;
},
/**
* This method adds a sorter.
* @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of
* Ext.util.Sorter, a string indicating a property name, an object representing an Ext.util.Sorter
* configuration, or a sort function.
* @param {String} defaultDirection The default direction for each sorter in the array. Defaults
* to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
*/
addSorter: function(sorter, defaultDirection) {
this.addSorters([sorter], defaultDirection);
},
/**
* This method adds all the sorters in a passed array.
* @param {Array} sorters An array with sorters. A sorter can be an instance of Ext.util.Sorter, a string
* indicating a property name, an object representing an Ext.util.Sorter configuration,
* or a sort function.
* @param {String} defaultDirection The default direction for each sorter in the array. Defaults
* to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
*/
addSorters: function(sorters, defaultDirection) {
var currentSorters = this.getSorters();
return this.insertSorters(currentSorters ? currentSorters.length : 0, sorters, defaultDirection);
},
/**
* This method adds a sorter at a given index.
* @param {Number} index The index at which to insert the sorter.
* @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of Ext.util.Sorter,
* a string indicating a property name, an object representing an Ext.util.Sorter configuration,
* or a sort function.
* @param {String} defaultDirection The default direction for each sorter in the array. Defaults
* to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
*/
insertSorter: function(index, sorter, defaultDirection) {
return this.insertSorters(index, [sorter], defaultDirection);
},
/**
* This method inserts all the sorters in the passed array at the given index.
* @param {Number} index The index at which to insert the sorters.
* @param {Array} sorters Can be an instance of Ext.util.Sorter, a string indicating a property name,
* an object representing an Ext.util.Sorter configuration, or a sort function.
* @param {String} defaultDirection The default direction for each sorter in the array. Defaults
* to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
*/
insertSorters: function(index, sorters, defaultDirection) {
// We begin by making sure we are dealing with an array of sorters
if (!Ext.isArray(sorters)) {
sorters = [sorters];
}
var ln = sorters.length,
direction = defaultDirection || this.getDefaultSortDirection(),
sortRoot = this.getSortRoot(),
currentSorters = this.getSorters(),
newSorters = [],
sorterConfig, i, sorter, currentSorter;
if (!currentSorters) {
// This will guarantee that we get the collection
currentSorters = this.createSortersCollection();
}
// We first have to convert every sorter into a proper Sorter instance
for (i = 0; i < ln; i++) {
sorter = sorters[i];
sorterConfig = {
direction: direction,
root: sortRoot
};
// If we are dealing with a string we assume it is a property they want to sort on.
if (typeof sorter === 'string') {
currentSorter = currentSorters.get(sorter);
if (!currentSorter) {
sorterConfig.property = sorter;
} else {
if (defaultDirection) {
currentSorter.setDirection(defaultDirection);
} else {
// If we already have a sorter for this property we just toggle its direction.
currentSorter.toggle();
}
continue;
}
}
// If it is a function, we assume its a sorting function.
else if (Ext.isFunction(sorter)) {
sorterConfig.sorterFn = sorter;
}
// 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(sorter)) {
if (!sorter.isSorter) {
if (sorter.fn) {
sorter.sorterFn = sorter.fn;
delete sorter.fn;
}
sorterConfig = Ext.apply(sorterConfig, sorter);
}
else {
newSorters.push(sorter);
if (!sorter.getRoot()) {
sorter.setRoot(sortRoot);
}
continue;
}
}
// Finally we get to the point where it has to be invalid
// <debug>
else {
Ext.Logger.warn('Invalid sorter specified:', sorter);
}
// </debug>
// If a sorter config was created, make it an instance
sorter = Ext.create('Ext.util.Sorter', sorterConfig);
newSorters.push(sorter);
}
// Now lets add the newly created sorters.
for (i = 0, ln = newSorters.length; i < ln; i++) {
currentSorters.insert(index + i, newSorters[i]);
}
this.dirtySortFn = true;
if (currentSorters.length) {
this.sorted = true;
}
return currentSorters;
},
/**
* This method removes a sorter.
* @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of Ext.util.Sorter,
* a string indicating a property name, an object representing an Ext.util.Sorter configuration,
* or a sort function.
*/
removeSorter: function(sorter) {
return this.removeSorters([sorter]);
},
/**
* This method removes all the sorters in a passed array.
* @param {Array} sorters Each value in the array can be a string (property name),
* function (sorterFn) or {@link Ext.util.Sorter Sorter} instance.
*/
removeSorters: function(sorters) {
// We begin by making sure we are dealing with an array of sorters
if (!Ext.isArray(sorters)) {
sorters = [sorters];
}
var ln = sorters.length,
currentSorters = this.getSorters(),
i, sorter;
for (i = 0; i < ln; i++) {
sorter = sorters[i];
if (typeof sorter === 'string') {
currentSorters.removeAtKey(sorter);
}
else if (typeof sorter === 'function') {
currentSorters.each(function(item) {
if (item.getSorterFn() === sorter) {
currentSorters.remove(item);
}
});
}
else if (sorter.isSorter) {
currentSorters.remove(sorter);
}
}
if (!currentSorters.length) {
this.sorted = false;
}
},
/**
* This updates the cached sortFn based on the current sorters.
* @return {Function} The generated sort function.
* @private
*/
updateSortFn: function() {
var sorters = this.getSorters().items;
this.sortFn = function(r1, r2) {
var ln = sorters.length,
result, i;
// We loop over each sorter and check if r1 should be before or after r2
for (i = 0; i < ln; i++) {
result = sorters[i].sort.call(this, r1, r2);
// If the result is -1 or 1 at this point it means that the sort is done.
// Only if they are equal (0) we continue to see if a next sort function
// actually might find a winner.
if (result !== 0) {
break;
}
}
return result;
};
this.dirtySortFn = false;
return this.sortFn;
},
/**
* Returns an up to date sort function.
* @return {Function} The sort function.
*/
getSortFn: function() {
if (this.dirtySortFn) {
return this.updateSortFn();
}
return this.sortFn;
},
/**
* This method will sort an array based on the currently configured {@link #sorters}.
* @param {Array} data The array you want to have sorted.
* @return {Array} The array you passed after it is sorted.
*/
sort: function(data) {
Ext.Array.sort(data, this.getSortFn());
return data;
},
/**
* This method returns the index that a given item would be inserted into a given array based
* on the current sorters.
* @param {Array} items The array that you want to insert the item into.
* @param {Mixed} item The item that you want to insert into the items array.
* @return {Number} The index for the given item in the given array based on the current sorters.
*/
findInsertionIndex: function(items, item, sortFn) {
var start = 0,
end = items.length - 1,
sorterFn = sortFn || this.getSortFn(),
middle,
comparison;
while (start <= end) {
middle = (start + end) >> 1;
comparison = sorterFn(item, items[middle]);
if (comparison >= 0) {
start = middle + 1;
} else if (comparison < 0) {
end = middle - 1;
}
}
return start;
}
});
/**
* @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 whether the currently cashed filter function is still valid.
* @readonly
*/
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.
* @readonly
*/
filterFn: null,
/**
* @property {Boolean} filtered
* A read-only flag indicating if this object is filtered.
* @readonly
*/
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 {@link Ext.util.Filter},
* an object representing an Ext.util.Filter configuration, or a filter function.
* @return {Object}
*/
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 {@link Ext.util.Filter},
* an object representing an Ext.util.Filter configuration, or a filter function.
* @return {Object}
*/
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 {@link Ext.util.Filter},
* an object representing an Ext.util.Filter configuration, or a filter function.
* @return {Array}
*/
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
// <debug>
else {
Ext.Logger.warn('Invalid filter specified:', filter);
}
// </debug>
// 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;
}
});
/**
* @private
*/
Ext.define('Ext.util.Collection', {
/**
* @cfg {Object[]} filters
* Array of {@link Ext.util.Filter Filters} for this collection.
*/
/**
* @cfg {Object[]} sorters
* Array of {@link Ext.util.Sorter Sorters} for this collection.
*/
config: {
autoFilter: true,
autoSort: true
},
mixins: {
sortable: 'Ext.mixin.Sortable',
filterable: 'Ext.mixin.Filterable'
},
constructor: function(keyFn, config) {
var me = this;
/**
* @property {Array} [all=[]]
* An array containing all the items (unsorted, unfiltered)
*/
me.all = [];
/**
* @property {Array} [items=[]]
* An array containing the filtered items (sorted)
*/
me.items = [];
/**
* @property {Array} [keys=[]]
* An array containing all the filtered keys (sorted)
*/
me.keys = [];
/**
* @property {Object} [indices={}]
* An object used as map to get a sorted and filtered index of an item
*/
me.indices = {};
/**
* @property {Object} [map={}]
* An object used as map to get an object based on its key
*/
me.map = {};
/**
* @property {Number} [length=0]
* The count of items in the collection filtered and sorted
*/
me.length = 0;
if (keyFn) {
me.getKey = keyFn;
}
this.initConfig(config);
},
updateAutoSort: function(autoSort, oldAutoSort) {
if (oldAutoSort === false && autoSort && this.items.length) {
this.sort();
}
},
updateAutoFilter: function(autoFilter, oldAutoFilter) {
if (oldAutoFilter === false && autoFilter && this.all.length) {
this.filter();
}
},
insertSorters: function() {
// We override the insertSorters method that exists on the Sortable mixin. This method always
// gets called whenever you add or insert a new sorter. We do this because we actually want
// to sort right after this happens.
this.mixins.sortable.insertSorters.apply(this, arguments);
if (this.getAutoSort() && this.items.length) {
this.sort();
}
return this;
},
removeSorters: function(sorters) {
// We override the removeSorters method that exists on the Sortable mixin. This method always
// gets called whenever you remove a sorter. If we are still sorted after we removed this sorter,
// then we have to resort the whole collection.
this.mixins.sortable.removeSorters.call(this, sorters);
if (this.sorted && this.getAutoSort() && this.items.length) {
this.sort();
}
return this;
},
applyFilters: function(filters) {
var collection = this.mixins.filterable.applyFilters.call(this, filters);
if (!filters && this.all.length && this.getAutoFilter()) {
this.filter();
}
return collection;
},
addFilters: function(filters) {
// We override the insertFilters method that exists on the Filterable mixin. This method always
// gets called whenever you add or insert a new filter. We do this because we actually want
// to filter right after this happens.
this.mixins.filterable.addFilters.call(this, filters);
if (this.items.length && this.getAutoFilter()) {
this.filter();
}
return this;
},
removeFilters: function(filters) {
// We override the removeFilters method that exists on the Filterable mixin. This method always
// gets called whenever you remove a filter. If we are still filtered after we removed this filter,
// then we have to re-filter the whole collection.
this.mixins.filterable.removeFilters.call(this, filters);
if (this.filtered && this.all.length && this.getAutoFilter()) {
this.filter();
}
return this;
},
/**
* This method will sort a collection based on the currently configured sorters.
* @param {Object} property
* @param {Object} value
* @param {Object} anyMatch
* @param {Object} caseSensitive
* @return {Array}
*/
filter: function(property, value, anyMatch, caseSensitive) {
// Support for the simple case of filtering by property/value
if (property) {
if (Ext.isString(property)) {
this.addFilters({
property : property,
value : value,
anyMatch : anyMatch,
caseSensitive: caseSensitive
});
return this.items;
}
else {
this.addFilters(property);
return this.items;
}
}
this.items = this.mixins.filterable.filter.call(this, this.all.slice());
this.updateAfterFilter();
if (this.sorted && this.getAutoSort()) {
this.sort();
}
},
updateAfterFilter: function() {
var items = this.items,
keys = this.keys,
indices = this.indices = {},
ln = items.length,
i, item, key;
keys.length = 0;
for (i = 0; i < ln; i++) {
item = items[i];
key = this.getKey(item);
indices[key] = i;
keys[i] = key;
}
this.length = items.length;
this.dirtyIndices = false;
},
sort: function(sorters, defaultDirection) {
var items = this.items,
keys = this.keys,
indices = this.indices,
ln = items.length,
i, item, key;
// If we pass sorters to this method we have to add them first.
// Because adding a sorter automatically sorts the items collection
// we can just return items after we have added the sorters
if (sorters) {
this.addSorters(sorters, defaultDirection);
return this.items;
}
// We save the keys temporarily on each item
for (i = 0; i < ln; i++) {
items[i]._current_key = keys[i];
}
// Now we sort our items array
this.handleSort(items);
// And finally we update our keys and indices
for (i = 0; i < ln; i++) {
item = items[i];
key = item._current_key;
keys[i] = key;
indices[key] = i;
delete item._current_key;
}
this.dirtyIndices = true;
},
handleSort: function(items) {
this.mixins.sortable.sort.call(this, items);
},
/**
* Adds an item to the collection. Fires the {@link #add} event when complete.
* @param {String} key
*
* The key to associate with the item, or the new item.
*
* If a {@link #getKey} implementation was specified for this MixedCollection, or if the key of the stored items is
* in a property called **id**, the MixedCollection will be able to _derive_ the key for the new item. In this case
* just pass the new item in this parameter.
* @param {Object} item The item to add.
* @return {Object} The item added.
*/
add: function(key, item) {
var me = this,
filtered = this.filtered,
sorted = this.sorted,
all = this.all,
items = this.items,
keys = this.keys,
indices = this.indices,
filterable = this.mixins.filterable,
currentLength = items.length,
index = currentLength;
if (arguments.length == 1) {
item = key;
key = me.getKey(item);
}
if (typeof key != 'undefined' && key !== null) {
if (typeof me.map[key] != 'undefined') {
return me.replace(key, item);
}
me.map[key] = item;
}
all.push(item);
if (filtered && this.getAutoFilter() && filterable.isFiltered.call(me, item)) {
return null;
}
me.length++;
if (sorted && this.getAutoSort()) {
index = this.findInsertionIndex(items, item);
}
if (index !== currentLength) {
this.dirtyIndices = true;
Ext.Array.splice(keys, index, 0, key);
Ext.Array.splice(items, index, 0, item);
} else {
indices[key] = currentLength;
keys.push(key);
items.push(item);
}
return item;
},
/**
* MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation simply
* returns **`item.id`** but you can provide your own implementation to return a different value as in the following
* examples:
*
* // normal way
* var mc = new Ext.util.MixedCollection();
* mc.add(someEl.dom.id, someEl);
* mc.add(otherEl.dom.id, otherEl);
* //and so on
*
* // using getKey
* var mc = new Ext.util.MixedCollection();
* mc.getKey = function(el){
* return el.dom.id;
* };
* mc.add(someEl);
* mc.add(otherEl);
*
* // or via the constructor
* var mc = new Ext.util.MixedCollection(false, function(el){
* return el.dom.id;
* });
* mc.add(someEl);
* mc.add(otherEl);
* @param {Object} item The item for which to find the key.
* @return {Object} The key for the passed item.
*/
getKey: function(item) {
return item.id;
},
/**
* Replaces an item in the collection. Fires the {@link #replace} event when complete.
* @param {String} oldKey
*
* The key associated with the item to replace, or the replacement item.
*
* If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key of your stored items is
* in a property called **id**, then the MixedCollection will be able to _derive_ the key of the replacement item.
* If you want to replace an item with one having the same key value, then just pass the replacement item in this
* parameter.
* @param {Object} item {Object} item (optional) If the first parameter passed was a key, the item to associate with
* that key.
* @return {Object} The new item.
*/
replace: function(oldKey, item) {
var me = this,
sorted = me.sorted,
filtered = me.filtered,
filterable = me.mixins.filterable,
items = me.items,
keys = me.keys,
all = me.all,
map = me.map,
returnItem = null,
oldItemsLn = items.length,
oldItem, index, newKey;
if (arguments.length == 1) {
item = oldKey;
oldKey = newKey = me.getKey(item);
} else {
newKey = me.getKey(item);
}
oldItem = map[oldKey];
if (typeof oldKey == 'undefined' || oldKey === null || typeof oldItem == 'undefined') {
return me.add(newKey, item);
}
me.map[newKey] = item;
if (newKey !== oldKey) {
delete me.map[oldKey];
}
if (sorted && me.getAutoSort()) {
Ext.Array.remove(items, oldItem);
Ext.Array.remove(keys, oldKey);
Ext.Array.remove(all, oldItem);
all.push(item);
me.dirtyIndices = true;
if (filtered && me.getAutoFilter()) {
// If the item is now filtered we check if it was not filtered
// before. If that is the case then we subtract from the length
if (filterable.isFiltered.call(me, item)) {
if (oldItemsLn !== items.length) {
me.length--;
}
return null;
}
// If the item was filtered, but now it is not anymore then we
// add to the length
else if (oldItemsLn === items.length) {
me.length++;
returnItem = item;
}
}
index = this.findInsertionIndex(items, item);
Ext.Array.splice(keys, index, 0, newKey);
Ext.Array.splice(items, index, 0, item);
} else {
if (filtered) {
if (me.getAutoFilter() && filterable.isFiltered.call(me, item)) {
if (items.indexOf(oldItem) !== -1) {
Ext.Array.remove(items, oldItem);
Ext.Array.remove(keys, oldKey);
me.length--;
me.dirtyIndices = true;
}
return null;
}
else if (items.indexOf(oldItem) === -1) {
items.push(item);
keys.push(newKey);
me.indices[newKey] = me.length;
me.length++;
return item;
}
}
index = me.items.indexOf(oldItem);
keys[index] = newKey;
items[index] = item;
this.dirtyIndices = true;
}
return returnItem;
},
/**
* Adds all elements of an Array or an Object to the collection.
* @param {Object/Array} objs An Object containing properties which will be added to the collection, or an Array of
* values, each of which are added to the collection. Functions references will be added to the collection if {@link
* Ext.util.MixedCollection#allowFunctions allowFunctions} has been set to `true`.
*/
addAll: function(addItems) {
var me = this,
filtered = me.filtered,
sorted = me.sorted,
all = me.all,
items = me.items,
keys = me.keys,
map = me.map,
autoFilter = me.getAutoFilter(),
autoSort = me.getAutoSort(),
newKeys = [],
newItems = [],
filterable = me.mixins.filterable,
addedItems = [],
ln, key, i, item;
if (Ext.isObject(addItems)) {
for (key in addItems) {
if (addItems.hasOwnProperty(key)) {
newItems.push(items[key]);
newKeys.push(key);
}
}
} else {
newItems = addItems;
ln = addItems.length;
for (i = 0; i < ln; i++) {
newKeys.push(me.getKey(addItems[i]));
}
}
for (i = 0; i < ln; i++) {
key = newKeys[i];
item = newItems[i];
if (typeof key != 'undefined' && key !== null) {
if (typeof map[key] != 'undefined') {
me.replace(key, item);
continue;
}
map[key] = item;
}
all.push(item);
if (filtered && autoFilter && filterable.isFiltered.call(me, item)) {
continue;
}
me.length++;
keys.push(key);
items.push(item);
addedItems.push(item);
}
if (addedItems.length) {
me.dirtyIndices = true;
if (sorted && autoSort) {
me.sort();
}
return addedItems;
}
return null;
},
/**
* Executes the specified function once for every item in the collection.
* The function should return a Boolean value. Returning `false` from the function will stop the iteration.
* @param {Function} fn The function to execute for each item.
* @param {Mixed} fn.item The collection item.
* @param {Number} fn.index The item's index.
* @param {Number} fn.length The total number of items in the collection.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the current
* item in the iteration.
*/
each: function(fn, scope) {
var items = this.items.slice(), // each safe for removal
i = 0,
len = items.length,
item;
for (; i < len; i++) {
item = items[i];
if (fn.call(scope || item, item, i, len) === false) {
break;
}
}
},
/**
* Executes the specified function once for every key in the collection, passing each key, and its associated item
* as the first two parameters.
* @param {Function} fn The function to execute for each item.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the browser
* window.
*/
eachKey: function(fn, scope) {
var keys = this.keys,
items = this.items,
ln = keys.length, i;
for (i = 0; i < ln; i++) {
fn.call(scope || window, keys[i], items[i], i, ln);
}
},
/**
* Returns the first item in the collection which elicits a `true` return value from the passed selection function.
* @param {Function} fn The selection function to execute for each item.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to the browser
* window.
* @return {Object} The first item in the collection which returned `true` from the selection function.
*/
findBy: function(fn, scope) {
var keys = this.keys,
items = this.items,
i = 0,
len = items.length;
for (; i < len; i++) {
if (fn.call(scope || window, items[i], keys[i])) {
return items[i];
}
}
return null;
},
/**
* Filter by a function. Returns a _new_ collection that has been filtered. The passed function will be called with
* each object in the collection. If the function returns `true`, the value is included otherwise it is filtered.
* @param {Function} fn The function to be called.
* @param fn.o The object.
* @param fn.k The key.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to this
* MixedCollection.
* @return {Ext.util.MixedCollection} The new filtered collection
*/
filterBy: function(fn, scope) {
var me = this,
newCollection = new this.self(),
keys = me.keys,
items = me.all,
length = items.length,
i;
newCollection.getKey = me.getKey;
for (i = 0; i < length; i++) {
if (fn.call(scope || me, items[i], me.getKey(items[i]))) {
newCollection.add(keys[i], items[i]);
}
}
return newCollection;
},
/**
* Inserts an item at the specified index in the collection. Fires the {@link #add} event when complete.
* @param {Number} index The index to insert the item at.
* @param {String} key The key to associate with the new item, or the item itself.
* @param {Object} item If the second parameter was a key, the new item.
* @return {Object} The item inserted.
*/
insert: function(index, key, item) {
var me = this,
sorted = this.sorted,
map = this.map,
filtered = this.filtered;
if (arguments.length == 2) {
item = key;
key = me.getKey(item);
}
if (index >= me.length || (sorted && me.getAutoSort())) {
return me.add(key, item);
}
if (typeof key != 'undefined' && key !== null) {
if (typeof map[key] != 'undefined') {
me.replace(key, item);
return false;
}
map[key] = item;
}
this.all.push(item);
if (filtered && this.getAutoFilter() && this.mixins.filterable.isFiltered.call(me, item)) {
return null;
}
me.length++;
Ext.Array.splice(me.items, index, 0, item);
Ext.Array.splice(me.keys, index, 0, key);
me.dirtyIndices = true;
return item;
},
insertAll: function(index, insertItems) {
if (index >= this.items.length || (this.sorted && this.getAutoSort())) {
return this.addAll(insertItems);
}
var me = this,
filtered = this.filtered,
sorted = this.sorted,
all = this.all,
items = this.items,
keys = this.keys,
map = this.map,
autoFilter = this.getAutoFilter(),
autoSort = this.getAutoSort(),
newKeys = [],
newItems = [],
addedItems = [],
filterable = this.mixins.filterable,
insertedUnfilteredItem = false,
ln, key, i, item;
if (sorted && this.getAutoSort()) {
// <debug>
Ext.Logger.error('Inserting a collection of items into a sorted Collection is invalid. Please just add these items or remove the sorters.');
// </debug>
}
if (Ext.isObject(insertItems)) {
for (key in insertItems) {
if (insertItems.hasOwnProperty(key)) {
newItems.push(items[key]);
newKeys.push(key);
}
}
} else {
newItems = insertItems;
ln = insertItems.length;
for (i = 0; i < ln; i++) {
newKeys.push(me.getKey(insertItems[i]));
}
}
for (i = 0; i < ln; i++) {
key = newKeys[i];
item = newItems[i];
if (typeof key != 'undefined' && key !== null) {
if (typeof map[key] != 'undefined') {
me.replace(key, item);
continue;
}
map[key] = item;
}
all.push(item);
if (filtered && autoFilter && filterable.isFiltered.call(me, item)) {
continue;
}
me.length++;
Ext.Array.splice(items, index + i, 0, item);
Ext.Array.splice(keys, index + i, 0, key);
insertedUnfilteredItem = true;
addedItems.push(item);
}
if (insertedUnfilteredItem) {
this.dirtyIndices = true;
if (sorted && autoSort) {
this.sort();
}
return addedItems;
}
return null;
},
/**
* Remove an item from the collection.
* @param {Object} item The item to remove.
* @return {Object} The item removed or `false` if no item was removed.
*/
remove: function(item) {
var index = this.items.indexOf(item);
if (index === -1) {
Ext.Array.remove(this.all, item);
if (typeof this.getKey == 'function') {
var key = this.getKey(item);
if (key !== undefined) {
delete this.map[key];
}
}
return item;
}
return this.removeAt(this.items.indexOf(item));
},
/**
* Remove all items in the passed array from the collection.
* @param {Array} items An array of items to be removed.
* @return {Ext.util.MixedCollection} this object
*/
removeAll: function(items) {
if (items) {
var ln = items.length, i;
for (i = 0; i < ln; i++) {
this.remove(items[i]);
}
}
return this;
},
/**
* Remove an item from a specified index in the collection. Fires the {@link #remove} event when complete.
* @param {Number} index The index within the collection of the item to remove.
* @return {Object} The item removed or `false` if no item was removed.
*/
removeAt: function(index) {
var me = this,
items = me.items,
keys = me.keys,
all = me.all,
item, key;
if (index < me.length && index >= 0) {
item = items[index];
key = keys[index];
if (typeof key != 'undefined') {
delete me.map[key];
}
Ext.Array.erase(items, index, 1);
Ext.Array.erase(keys, index, 1);
Ext.Array.remove(all, item);
delete me.indices[key];
me.length--;
this.dirtyIndices = true;
return item;
}
return false;
},
/**
* Removed an item associated with the passed key from the collection.
* @param {String} key The key of the item to remove.
* @return {Object/Boolean} The item removed or `false` if no item was removed.
*/
removeAtKey: function(key) {
return this.removeAt(this.indexOfKey(key));
},
/**
* Returns the number of items in the collection.
* @return {Number} the number of items in the collection.
*/
getCount: function() {
return this.length;
},
/**
* Returns index within the collection of the passed Object.
* @param {Object} item The item to find the index of.
* @return {Number} Index of the item. Returns -1 if not found.
*/
indexOf: function(item) {
if (this.dirtyIndices) {
this.updateIndices();
}
var index = this.indices[this.getKey(item)];
return (index === undefined) ? -1 : index;
},
/**
* Returns index within the collection of the passed key.
* @param {String} key The key to find the index of.
* @return {Number} Index of the key.
*/
indexOfKey: function(key) {
if (this.dirtyIndices) {
this.updateIndices();
}
var index = this.indices[key];
return (index === undefined) ? -1 : index;
},
updateIndices: function() {
var items = this.items,
ln = items.length,
indices = this.indices = {},
i, item, key;
for (i = 0; i < ln; i++) {
item = items[i];
key = this.getKey(item);
indices[key] = i;
}
this.dirtyIndices = false;
},
/**
* Returns the item associated with the passed key OR index. Key has priority over index. This is the equivalent of
* calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
* @param {String/Number} key The key or index of the item.
* @return {Object} If the item is found, returns the item. If the item was not found, returns `undefined`. If an item
* was found, but is a Class, returns `null`.
*/
get: function(key) {
var me = this,
fromMap = me.map[key],
item;
if (fromMap !== undefined) {
item = fromMap;
}
else if (typeof key == 'number') {
item = me.items[key];
}
return typeof item != 'function' || me.getAllowFunctions() ? item : null; // for prototype!
},
/**
* Returns the item at the specified index.
* @param {Number} index The index of the item.
* @return {Object} The item at the specified index.
*/
getAt: function(index) {
return this.items[index];
},
/**
* Returns the item associated with the passed key.
* @param {String/Number} key The key of the item.
* @return {Object} The item associated with the passed key.
*/
getByKey: function(key) {
return this.map[key];
},
/**
* Returns `true` if the collection contains the passed Object as an item.
* @param {Object} item The Object to look for in the collection.
* @return {Boolean} `true` if the collection contains the Object as an item.
*/
contains: function(item) {
var key = this.getKey(item);
if (key) {
return this.containsKey(key);
} else {
return Ext.Array.contains(this.items, item);
}
},
/**
* Returns `true` if the collection contains the passed Object as a key.
* @param {String} key The key to look for in the collection.
* @return {Boolean} `true` if the collection contains the Object as a key.
*/
containsKey: function(key) {
return typeof this.map[key] != 'undefined';
},
/**
* Removes all items from the collection. Fires the {@link #clear} event when complete.
*/
clear: function(){
var me = this;
me.length = 0;
me.items.length = 0;
me.keys.length = 0;
me.all.length = 0;
me.dirtyIndices = true;
me.indices = {};
me.map = {};
},
/**
* Returns the first item in the collection.
* @return {Object} the first item in the collection.
*/
first: function() {
return this.items[0];
},
/**
* Returns the last item in the collection.
* @return {Object} the last item in the collection.
*/
last: function() {
return this.items[this.length - 1];
},
/**
* Returns a range of items in this collection
* @param {Number} [startIndex=0] The starting index.
* @param {Number} [endIndex=-1] The ending index. Defaults to the last item.
* @return {Array} An array of items.
*/
getRange: function(start, end) {
var me = this,
items = me.items,
range = [],
i;
if (items.length < 1) {
return range;
}
start = start || 0;
end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
if (start <= end) {
for (i = start; i <= end; i++) {
range[range.length] = items[i];
}
} else {
for (i = start; i >= end; i--) {
range[range.length] = items[i];
}
}
return range;
},
/**
* Find the index of the first matching object in this collection by a function. If the function returns `true` it
* is considered a match.
* @param {Function} fn The function to be called.
* @param fn.o The object.
* @param fn.k The key.
* @param {Object} scope The scope (`this` reference) in which the function is executed. Defaults to this
* MixedCollection.
* @param {Number} [start=0] The index to start searching at.
* @return {Number} The matched index, or -1 if the item was not found.
*/
findIndexBy: function(fn, scope, start) {
var me = this,
keys = me.keys,
items = me.items,
i = start || 0,
ln = items.length;
for (; i < ln; i++) {
if (fn.call(scope || me, items[i], keys[i])) {
return i;
}
}
return -1;
},
/**
* Creates a shallow copy of this collection
* @return {Ext.util.MixedCollection}
*/
clone: function() {
var me = this,
copy = new this.self(),
keys = me.keys,
items = me.items,
i = 0,
ln = items.length;
for(; i < ln; i++) {
copy.add(keys[i], items[i]);
}
copy.getKey = me.getKey;
return copy;
},
destroy: function() {
this.callSuper();
this.clear();
}
});
/**
* @docauthor Evan Trimboli <evan@sencha.com>
* @aside guide stores
*
* Contains a collection of all stores that are created that have an identifier. An identifier can be assigned by
* setting the {@link Ext.data.Store#storeId storeId} property. When a store is in the StoreManager, it can be
* referred to via it's identifier:
*
* Ext.create('Ext.data.Store', {
* model: 'SomeModel',
* storeId: 'myStore'
* });
*
* var store = Ext.data.StoreManager.lookup('myStore');
*
* Also note that the {@link #lookup} method is aliased to {@link Ext#getStore} for convenience.
*
* If a store is registered with the StoreManager, you can also refer to the store by it's identifier when registering
* it with any Component that consumes data from a store:
*
* Ext.create('Ext.data.Store', {
* model: 'SomeModel',
* storeId: 'myStore'
* });
*
* Ext.create('Ext.view.View', {
* store: 'myStore'
* // other configuration here
* });
*/
Ext.define('Ext.data.StoreManager', {
extend: 'Ext.util.Collection',
alternateClassName: ['Ext.StoreMgr', 'Ext.data.StoreMgr', 'Ext.StoreManager'],
singleton: true,
uses: ['Ext.data.ArrayStore', 'Ext.data.Store'],
/**
* Registers one or more Stores with the StoreManager. You do not normally need to register stores manually. Any
* store initialized with a {@link Ext.data.Store#storeId} will be auto-registered.
* @param {Ext.data.Store...} stores Any number of Store instances.
*/
register : function() {
for (var i = 0, s; (s = arguments[i]); i++) {
this.add(s);
}
},
/**
* Unregisters one or more Stores with the StoreManager.
* @param {String/Object...} stores Any number of Store instances or ID-s.
*/
unregister : function() {
for (var i = 0, s; (s = arguments[i]); i++) {
this.remove(this.lookup(s));
}
},
/**
* Gets a registered Store by id.
* @param {String/Object} store The `id` of the Store, or a Store instance, or a store configuration.
* @return {Ext.data.Store}
*/
lookup : function(store) {
// handle the case when we are given an array or an array of arrays.
if (Ext.isArray(store)) {
var fields = ['field1'],
expand = !Ext.isArray(store[0]),
data = store,
i,
len;
if (expand) {
data = [];
for (i = 0, len = store.length; i < len; ++i) {
data.push([store[i]]);
}
} else {
for(i = 2, len = store[0].length; i <= len; ++i){
fields.push('field' + i);
}
}
return Ext.create('Ext.data.ArrayStore', {
data : data,
fields: fields,
// See https://sencha.jira.com/browse/TOUCH-1541
autoDestroy: true,
autoCreated: true,
expanded: expand
});
}
if (Ext.isString(store)) {
// store id
return this.get(store);
} else {
// store instance or store config
if (store instanceof Ext.data.Store) {
return store;
} else {
return Ext.factory(store, Ext.data.Store, null, 'store');
}
}
},
// getKey implementation for MixedCollection
getKey : function(o) {
return o.getStoreId();
}
}, function() {
/**
* Creates a new store for the given id and config, then registers it with the {@link Ext.data.StoreManager Store Manager}.
* Sample usage:
*
* Ext.regStore('AllUsers', {
* model: 'User'
* });
*
* // the store can now easily be used throughout the application
* new Ext.List({
* store: 'AllUsers'
* // ...
* });
*
* @param {String} id The id to set on the new store.
* @param {Object} config The store config.
* @member Ext
* @method regStore
*/
Ext.regStore = function(name, config) {
var store;
if (Ext.isObject(name)) {
config = name;
} else {
if (config instanceof Ext.data.Store) {
config.setStoreId(name);
} else {
config.storeId = name;
}
}
if (config instanceof Ext.data.Store) {
store = config;
} else {
store = Ext.create('Ext.data.Store', config);
}
return Ext.data.StoreManager.register(store);
};
/**
* Shortcut to {@link Ext.data.StoreManager#lookup}.
* @member Ext
* @method getStore
* @alias Ext.data.StoreManager#lookup
*/
Ext.getStore = function(name) {
return Ext.data.StoreManager.lookup(name);
};
});
/**
* A DataItem is a container for {@link Ext.dataview.DataView} with useComponents: true. It ties together
* {@link Ext.data.Model records} to its contained Components via a {@link #dataMap dataMap} configuration.
*
* For example, lets say you have a `text` configuration which, when applied, gets turned into an instance of an
* Ext.Component. We want to update the {@link #html} of a sub-component when the 'text' field of the record gets
* changed.
*
* As you can see below, it is simply a matter of setting the key of the object to be the getter of the config
* (getText), and then give that property a value of an object, which then has 'setHtml' (the html setter) as the key,
* and 'text' (the field name) as the value. You can continue this for a as many sub-components as you wish.
*
* dataMap: {
* // When the record is updated, get the text configuration, and
* // call {@link #setHtml} with the 'text' field of the record.
* getText: {
* setHtml: 'text'
* },
*
* // When the record is updated, get the userName configuration, and
* // call {@link #setHtml} with the 'from_user' field of the record.
* getUserName: {
* setHtml: 'from_user'
* },
*
* // When the record is updated, get the avatar configuration, and
* // call `setSrc` with the 'profile_image_url' field of the record.
* getAvatar: {
* setSrc: 'profile_image_url'
* }
* }
*/
Ext.define('Ext.dataview.component.DataItem', {
extend: 'Ext.Container',
xtype : 'dataitem',
config: {
baseCls: Ext.baseCSSPrefix + 'data-item',
defaultType: 'component',
/**
* @cfg {Ext.data.Model} record The model instance of this DataItem. It is controlled by the Component DataView.
* @accessor
*/
record: null,
/**
* @cfg {String} itemCls
* An additional CSS class to apply to items within the DataView.
* @accessor
*/
itemCls: null,
/**
* @cfg dataMap
* The dataMap allows you to map {@link #record} fields to specific configurations in this component.
*
* For example, lets say you have a `text` configuration which, when applied, gets turned into an instance of an Ext.Component.
* We want to update the {@link #html} of this component when the 'text' field of the record gets changed.
* For example:
*
* dataMap: {
* getText: {
* setHtml: 'text'
* }
* }
*
* In this example, it is simply a matter of setting the key of the object to be the getter of the config (`getText`), and then give that
* property a value of an object, which then has `setHtml` (the html setter) as the key, and `text` (the field name) as the value.
*/
dataMap: {},
/*
* @private dataview
*/
dataview: null,
items: [{
xtype: 'component'
}]
},
updateBaseCls: function(newBaseCls, oldBaseCls) {
var me = this;
me.callParent(arguments);
},
updateItemCls: function(newCls, oldCls) {
if (oldCls) {
this.removeCls(oldCls);
}
if (newCls) {
this.addCls(newCls);
}
},
doMapData: function(dataMap, data, item) {
var componentName, component, setterMap, setterName;
for (componentName in dataMap) {
setterMap = dataMap[componentName];
component = this[componentName]();
if (component) {
for (setterName in setterMap) {
if (data && component[setterName] && data[setterMap[setterName]]) {
component[setterName](data[setterMap[setterName]]);
}
}
}
}
if (item) {
// Bypassing setter because sometimes we pass the same object (different properties)
item.updateData(data);
}
},
/**
* Updates this container's child items, passing through the `dataMap`.
* @param newRecord
* @private
*/
updateRecord: function(newRecord) {
if (!newRecord) {
return;
}
this._record = newRecord;
var me = this,
dataview = me.dataview || this.getDataview(),
data = dataview.prepareData(newRecord.getData(true), dataview.getStore().indexOf(newRecord), newRecord),
items = me.getItems(),
item = items.first(),
dataMap = me.getDataMap();
if (!item) {
return;
}
if (dataMap) {
this.doMapData(dataMap, data, item);
}
/**
* @event updatedata
* Fires whenever the data of the DataItem is updated.
* @param {Ext.dataview.component.DataItem} this The DataItem instance.
* @param {Object} newData The new data.
*/
me.fireEvent('updatedata', me, data);
}
});
/**
* @private
*/
Ext.define('Ext.dataview.component.Container', {
extend: 'Ext.Container',
requires: [
'Ext.dataview.component.DataItem'
],
/**
* @event itemtouchstart
* Fires whenever an item is touched
* @param {Ext.dataview.component.Container} this
* @param {Ext.dataview.component.DataItem} item The item touched
* @param {Number} index The index of the item touched
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtouchmove
* Fires whenever an item is moved
* @param {Ext.dataview.component.Container} this
* @param {Ext.dataview.component.DataItem} item The item moved
* @param {Number} index The index of the item moved
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtouchend
* Fires whenever an item is touched
* @param {Ext.dataview.component.Container} this
* @param {Ext.dataview.component.DataItem} item The item touched
* @param {Number} index The index of the item touched
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtap
* Fires whenever an item is tapped
* @param {Ext.dataview.component.Container} this
* @param {Ext.dataview.component.DataItem} item The item tapped
* @param {Number} index The index of the item tapped
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtaphold
* Fires whenever an item is tapped
* @param {Ext.dataview.component.Container} this
* @param {Ext.dataview.component.DataItem} item The item tapped
* @param {Number} index The index of the item tapped
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemsingletap
* Fires whenever an item is doubletapped
* @param {Ext.dataview.component.Container} this
* @param {Ext.dataview.component.DataItem} item The item singletapped
* @param {Number} index The index of the item singletapped
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemdoubletap
* Fires whenever an item is doubletapped
* @param {Ext.dataview.component.Container} this
* @param {Ext.dataview.component.DataItem} item The item doubletapped
* @param {Number} index The index of the item doubletapped
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemswipe
* Fires whenever an item is swiped
* @param {Ext.dataview.component.Container} this
* @param {Ext.dataview.component.DataItem} item The item swiped
* @param {Number} index The index of the item swiped
* @param {Ext.EventObject} e The event object
*/
constructor: function() {
this.itemCache = [];
this.callParent(arguments);
},
//@private
doInitialize: function() {
this.innerElement.on({
touchstart: 'onItemTouchStart',
touchend: 'onItemTouchEnd',
tap: 'onItemTap',
taphold: 'onItemTapHold',
touchmove: 'onItemTouchMove',
singletap: 'onItemSingleTap',
doubletap: 'onItemDoubleTap',
swipe: 'onItemSwipe',
delegate: '> .' + Ext.baseCSSPrefix + 'data-item',
scope: this
});
},
//@private
initialize: function() {
this.callParent();
this.doInitialize();
},
onItemTouchStart: function(e) {
var me = this,
target = e.getTarget(),
item = Ext.getCmp(target.id);
item.on({
touchmove: 'onItemTouchMove',
scope : me,
single: true
});
me.fireEvent('itemtouchstart', me, item, me.indexOf(item), e);
},
onItemTouchMove: function(e) {
var me = this,
target = e.getTarget(),
item = Ext.getCmp(target.id);
me.fireEvent('itemtouchmove', me, item, me.indexOf(item), e);
},
onItemTouchEnd: function(e) {
var me = this,
target = e.getTarget(),
item = Ext.getCmp(target.id);
item.un({
touchmove: 'onItemTouchMove',
scope : me
});
me.fireEvent('itemtouchend', me, item, me.indexOf(item), e);
},
onItemTap: function(e) {
var me = this,
target = e.getTarget(),
item = Ext.getCmp(target.id);
me.fireEvent('itemtap', me, item, me.indexOf(item), e);
},
onItemTapHold: function(e) {
var me = this,
target = e.getTarget(),
item = Ext.getCmp(target.id);
me.fireEvent('itemtaphold', me, item, me.indexOf(item), e);
},
onItemSingleTap: function(e) {
var me = this,
target = e.getTarget(),
item = Ext.getCmp(target.id);
me.fireEvent('itemsingletap', me, item, me.indexOf(item), e);
},
onItemDoubleTap: function(e) {
var me = this,
target = e.getTarget(),
item = Ext.getCmp(target.id);
me.fireEvent('itemdoubletap', me, item, me.indexOf(item), e);
},
onItemSwipe: function(e) {
var me = this,
target = e.getTarget(),
item = Ext.getCmp(target.id);
me.fireEvent('itemswipe', me, item, me.indexOf(item), e);
},
moveItemsToCache: function(from, to) {
var me = this,
dataview = me.dataview,
maxItemCache = dataview.getMaxItemCache(),
items = me.getViewItems(),
itemCache = me.itemCache,
cacheLn = itemCache.length,
pressedCls = dataview.getPressedCls(),
selectedCls = dataview.getSelectedCls(),
i = to - from,
item;
for (; i >= 0; i--) {
item = items[from + i];
if (cacheLn !== maxItemCache) {
me.remove(item, false);
item.removeCls([pressedCls, selectedCls]);
itemCache.push(item);
cacheLn++;
}
else {
item.destroy();
}
}
if (me.getViewItems().length == 0) {
this.dataview.showEmptyText();
}
},
moveItemsFromCache: function(records) {
var me = this,
dataview = me.dataview,
store = dataview.getStore(),
ln = records.length,
xtype = dataview.getDefaultType(),
itemConfig = dataview.getItemConfig(),
itemCache = me.itemCache,
cacheLn = itemCache.length,
items = [],
i, item, record;
if (ln) {
dataview.hideEmptyText();
}
for (i = 0; i < ln; i++) {
records[i]._tmpIndex = store.indexOf(records[i]);
}
Ext.Array.sort(records, function(record1, record2) {
return record1._tmpIndex > record2._tmpIndex ? 1 : -1;
});
for (i = 0; i < ln; i++) {
record = records[i];
if (cacheLn) {
cacheLn--;
item = itemCache.pop();
this.updateListItem(record, item);
}
else {
item = me.getDataItemConfig(xtype, record, itemConfig);
}
item = this.insert(record._tmpIndex, item);
delete record._tmpIndex;
}
return items;
},
getViewItems: function() {
return this.getInnerItems();
},
updateListItem: function(record, item) {
if (item.updateRecord) {
if (item.getRecord() === record) {
item.updateRecord(record);
} else {
item.setRecord(record);
}
}
},
getDataItemConfig: function(xtype, record, itemConfig) {
var dataview = this.dataview,
dataItemConfig = {
xtype: xtype,
record: record,
itemCls: dataview.getItemCls(),
defaults: itemConfig,
dataview: dataview
};
return Ext.merge(dataItemConfig, itemConfig);
},
doRemoveItemCls: function(cls) {
var items = this.getViewItems(),
ln = items.length,
i = 0;
for (; i < ln; i++) {
items[i].removeCls(cls);
}
},
doAddItemCls: function(cls) {
var items = this.getViewItems(),
ln = items.length,
i = 0;
for (; i < ln; i++) {
items[i].addCls(cls);
}
},
updateAtNewIndex: function(oldIndex, newIndex, record) {
this.moveItemsToCache(oldIndex, oldIndex);
this.moveItemsFromCache([record]);
},
destroy: function() {
var me = this,
itemCache = me.itemCache,
ln = itemCache.length,
i = 0;
for (; i < ln; i++) {
itemCache[i].destroy();
}
this.callParent();
}
});
/**
* @private
*/
Ext.define('Ext.dataview.element.Container', {
extend: 'Ext.Component',
/**
* @event itemtouchstart
* Fires whenever an item is touched
* @param {Ext.dataview.element.Container} this
* @param {Ext.dom.Element} item The item touched
* @param {Number} index The index of the item touched
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtouchmove
* Fires whenever an item is moved
* @param {Ext.dataview.element.Container} this
* @param {Ext.dom.Element} item The item moved
* @param {Number} index The index of the item moved
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtouchend
* Fires whenever an item is touched
* @param {Ext.dataview.element.Container} this
* @param {Ext.dom.Element} item The item touched
* @param {Number} index The index of the item touched
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtap
* Fires whenever an item is tapped
* @param {Ext.dataview.element.Container} this
* @param {Ext.dom.Element} item The item tapped
* @param {Number} index The index of the item tapped
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtaphold
* Fires whenever an item is tapped
* @param {Ext.dataview.element.Container} this
* @param {Ext.dom.Element} item The item tapped
* @param {Number} index The index of the item tapped
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemsingletap
* Fires whenever an item is singletapped
* @param {Ext.dataview.element.Container} this
* @param {Ext.dom.Element} item The item singletapped
* @param {Number} index The index of the item singletapped
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemdoubletap
* Fires whenever an item is doubletapped
* @param {Ext.dataview.element.Container} this
* @param {Ext.dom.Element} item The item doubletapped
* @param {Number} index The index of the item doubletapped
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemswipe
* Fires whenever an item is swiped
* @param {Ext.dataview.element.Container} this
* @param {Ext.dom.Element} item The item swiped
* @param {Number} index The index of the item swiped
* @param {Ext.EventObject} e The event object
*/
doInitialize: function() {
this.element.on({
touchstart: 'onItemTouchStart',
touchend: 'onItemTouchEnd',
tap: 'onItemTap',
taphold: 'onItemTapHold',
touchmove: 'onItemTouchMove',
singletap: 'onItemSingleTap',
doubletap: 'onItemDoubleTap',
swipe: 'onItemSwipe',
delegate: '> div',
scope: this
});
},
//@private
initialize: function() {
this.callParent();
this.doInitialize();
},
updateBaseCls: function(newBaseCls, oldBaseCls) {
var me = this;
me.callParent([newBaseCls + '-container', oldBaseCls]);
},
onItemTouchStart: function(e) {
var me = this,
target = e.getTarget(),
index = me.getViewItems().indexOf(target);
Ext.get(target).on({
touchmove: 'onItemTouchMove',
scope : me,
single: true
});
me.fireEvent('itemtouchstart', me, Ext.get(target), index, e);
},
onItemTouchEnd: function(e) {
var me = this,
target = e.getTarget(),
index = me.getViewItems().indexOf(target);
Ext.get(target).un({
touchmove: 'onItemTouchMove',
scope : me
});
me.fireEvent('itemtouchend', me, Ext.get(target), index, e);
},
onItemTouchMove: function(e) {
var me = this,
target = e.getTarget(),
index = me.getViewItems().indexOf(target);
me.fireEvent('itemtouchmove', me, Ext.get(target), index, e);
},
onItemTap: function(e) {
var me = this,
target = e.getTarget(),
index = me.getViewItems().indexOf(target);
me.fireEvent('itemtap', me, Ext.get(target), index, e);
},
onItemTapHold: function(e) {
var me = this,
target = e.getTarget(),
index = me.getViewItems().indexOf(target);
me.fireEvent('itemtaphold', me, Ext.get(target), index, e);
},
onItemDoubleTap: function(e) {
var me = this,
target = e.getTarget(),
index = me.getViewItems().indexOf(target);
me.fireEvent('itemdoubletap', me, Ext.get(target), index, e);
},
onItemSingleTap: function(e) {
var me = this,
target = e.getTarget(),
index = me.getViewItems().indexOf(target);
me.fireEvent('itemsingletap', me, Ext.get(target), index, e);
},
onItemSwipe: function(e) {
var me = this,
target = e.getTarget(),
index = me.getViewItems().indexOf(target);
me.fireEvent('itemswipe', me, Ext.get(target), index, e);
},
updateListItem: function(record, item) {
var me = this,
dataview = me.dataview,
store = dataview.getStore(),
index = store.indexOf(record),
data = dataview.prepareData(record.getData(true), index, record);
data.xcount = store.getCount();
data.xindex = typeof data.xindex === 'number' ? data.xindex : index;
item.innerHTML = dataview.getItemTpl().apply(data);
},
addListItem: function(index, record) {
var me = this,
dataview = me.dataview,
store = dataview.getStore(),
data = dataview.prepareData(record.getData(true), index, record),
element = me.element,
childNodes = element.dom.childNodes,
ln = childNodes.length,
wrapElement;
data.xcount = typeof data.xcount === 'number' ? data.xcount : store.getCount();
data.xindex = typeof data.xindex === 'number' ? data.xindex : index;
wrapElement = Ext.Element.create(this.getItemElementConfig(index, data));
if (!ln || index == ln) {
wrapElement.appendTo(element);
} else {
wrapElement.insertBefore(childNodes[index]);
}
},
getItemElementConfig: function(index, data) {
var dataview = this.dataview,
itemCls = dataview.getItemCls(),
cls = dataview.getBaseCls() + '-item';
if (itemCls) {
cls += ' ' + itemCls;
}
return {
cls: cls,
html: dataview.getItemTpl().apply(data)
};
},
doRemoveItemCls: function(cls) {
var elements = this.getViewItems(),
ln = elements.length,
i = 0;
for (; i < ln; i++) {
Ext.fly(elements[i]).removeCls(cls);
}
},
doAddItemCls: function(cls) {
var elements = this.getViewItems(),
ln = elements.length,
i = 0;
for (; i < ln; i++) {
Ext.fly(elements[i]).addCls(cls);
}
},
// Remove
moveItemsToCache: function(from, to) {
var me = this,
items = me.getViewItems(),
i = to - from,
item;
for (; i >= 0; i--) {
item = items[from + i];
Ext.get(item).destroy();
}
if (me.getViewItems().length == 0) {
this.dataview.showEmptyText();
}
},
// Add
moveItemsFromCache: function(records) {
var me = this,
dataview = me.dataview,
store = dataview.getStore(),
ln = records.length,
i, record;
if (ln) {
dataview.hideEmptyText();
}
for (i = 0; i < ln; i++) {
records[i]._tmpIndex = store.indexOf(records[i]);
}
Ext.Array.sort(records, function(record1, record2) {
return record1._tmpIndex > record2._tmpIndex ? 1 : -1;
});
for (i = 0; i < ln; i++) {
record = records[i];
me.addListItem(record._tmpIndex, record);
delete record._tmpIndex;
}
},
// Transform ChildNodes into a proper Array so we can do indexOf...
getViewItems: function() {
return Array.prototype.slice.call(this.element.dom.childNodes);
},
updateAtNewIndex: function(oldIndex, newIndex, record) {
this.moveItemsToCache(oldIndex, oldIndex);
this.moveItemsFromCache([record]);
},
destroy: function() {
var elements = this.getViewItems(),
ln = elements.length,
i = 0;
for (; i < ln; i++) {
Ext.get(elements[i]).destroy();
}
this.callParent();
}
});
/**
* 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 `true` to disable selection.
* This configuration will lock the selection model that the DataView uses.
* @accessor
*/
disableSelection: null,
/**
* @cfg {String} mode
* Modes of selection.
* Valid values are `'SINGLE'`, `'SIMPLE'`, and `'MULTI'`.
* @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'`.
* @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.
* @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);
newStore.un('clear', 'onSelectionStoreClear', this);
}
}
if (newStore) {
newStore.on(bindEvents);
newStore.onBefore('clear', 'onSelectionStoreClear', this);
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) {
var me = this,
store = me.getStore(),
records = [],
tmp, i;
if (me.getDisableSelection()) {
return;
}
// swap values
if (startRecord > endRecord) {
tmp = endRecord;
endRecord = startRecord;
startRecord = tmp;
}
for (i = startRecord; i <= endRecord; i++) {
records.push(store.getAt(i));
}
this.doMultiSelect(records, keepExisting);
},
/**
* 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);
if (!suppressEvent) {
me.fireSelectionChange([record]);
}
},
/**
* 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);
}
if (change && !suppressEvent) {
this.fireSelectionChange(records);
}
},
/**
* 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);
}
}
if (change && !suppressEvent) {
me.fireSelectionChange(records);
}
},
/**
* 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(records) {
var me = this;
me.fireAction('selectionchange', [me, records], 'getSelection');
},
/**
* Returns an array of the currently selected records.
* @return {Array} An array of selected records.
*/
getSelection: function() {
return this.selected.getRange();
},
/**
* Returns `true` if the specified row is selected.
* @param {Ext.data.Model/Number} record The record or index of the record to check.
* @return {Boolean}
*/
isSelected: function(record) {
record = Ext.isNumber(record) ? this.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);
}
},
// prune records from the SelectionModel if
// they were selected at the time they were
// removed.
onSelectionStoreRemove: function(store, records) {
var me = this,
selected = me.selected,
ln = records.length,
record, i;
if (me.getDisableSelection()) {
return;
}
for (i = 0; i < ln; i++) {
record = records[i];
if (selected.remove(record)) {
if (me.getLastSelected() == record) {
me.setLastSelected(null);
}
if (me.getLastFocused() == record) {
me.setLastFocused(null);
}
me.fireSelectionChange([record]);
}
}
},
onSelectionStoreClear: function(store) {
var records = store.getData().items;
this.onSelectionStoreRemove(store, records);
},
/**
* 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.
*/
});
/**
* @aside guide dataview
*
* DataView makes it easy to create lots of components dynamically, usually based off a {@link Ext.data.Store Store}.
* It's great for rendering lots of data from your server backend or any other data source and is what powers
* components like {@link Ext.List}.
*
* Use DataView whenever you want to show sets of the same component many times, for examples in apps like these:
*
* - List of messages in an email app
* - Showing latest news/tweets
* - Tiled set of albums in an HTML5 music player
*
* # Creating a Simple DataView
*
* At its simplest, a DataView is just a Store full of data and a simple template that we use to render each item:
*
* @example miniphone preview
* var touchTeam = Ext.create('Ext.DataView', {
* fullscreen: true,
* store: {
* fields: ['name', 'age'],
* data: [
* {name: 'Jamie', age: 100},
* {name: 'Rob', age: 21},
* {name: 'Tommy', age: 24},
* {name: 'Jacky', age: 24},
* {name: 'Ed', age: 26}
* ]
* },
*
* itemTpl: '<div>{name} is {age} years old</div>'
* });
*
* Here we just defined everything inline so it's all local with nothing being loaded from a server. For each of the 5
* data items defined in our Store, DataView will render a {@link Ext.Component Component} and pass in the name and age
* data. The component will use the tpl we provided above, rendering the data in the curly bracket placeholders we
* provided.
*
* Because DataView is integrated with Store, any changes to the Store are immediately reflected on the screen. For
* example, if we add a new record to the Store it will be rendered into our DataView:
*
* touchTeam.getStore().add({
* name: 'Abe Elias',
* age: 33
* });
*
* We didn't have to manually update the DataView, it's just automatically updated. The same happens if we modify one
* of the existing records in the Store:
*
* touchTeam.getStore().getAt(0).set('age', 42);
*
* This will get the first record in the Store (Jamie), change the age to 42 and automatically update what's on the
* screen.
*
* @example miniphone
* var touchTeam = Ext.create('Ext.DataView', {
* fullscreen: true,
* store: {
* fields: ['name', 'age'],
* data: [
* {name: 'Jamie', age: 100},
* {name: 'Rob', age: 21},
* {name: 'Tommy', age: 24},
* {name: 'Jacky', age: 24},
* {name: 'Ed', age: 26}
* ]
* },
*
* itemTpl: '<div>{name} is {age} years old</div>'
* });
*
* touchTeam.getStore().add({
* name: 'Abe Elias',
* age: 33
* });
*
* touchTeam.getStore().getAt(0).set('age', 42);
*
* # Loading data from a server
*
* We often want to load data from our server or some other web service so that we don't have to hard code it all
* locally. Let's say we want to load all of the latest tweets about Sencha Touch into a DataView, and for each one
* render the user's profile picture, user name and tweet message. To do this all we have to do is modify the
* {@link #store} and {@link #itemTpl} a little:
*
* @example portrait
* Ext.create('Ext.DataView', {
* fullscreen: true,
* cls: 'twitterView',
* store: {
* autoLoad: true,
* fields: ['from_user', 'text', 'profile_image_url'],
*
* proxy: {
* type: 'jsonp',
* url: 'http://search.twitter.com/search.json?q=Sencha Touch',
*
* reader: {
* type: 'json',
* rootProperty: 'results'
* }
* }
* },
*
* itemTpl: '<img src="{profile_image_url}" /><h2>{from_user}</h2><p>{text}</p><div style="clear: both"></div>'
* });
*
* The Store no longer has hard coded data, instead we've provided a {@link Ext.data.proxy.Proxy Proxy}, which fetches
* the data for us. In this case we used a JSON-P proxy so that we can load from Twitter's JSON-P search API. We also
* specified the fields present for each tweet, and used Store's {@link Ext.data.Store#autoLoad autoLoad} configuration
* to load automatically. Finally, we configured a Reader to decode the response from Twitter, telling it to expect
* JSON and that the tweets can be found in the 'results' part of the JSON response.
*
* The last thing we did is update our template to render the image, Twitter username and message. All we need to do
* now is add a little CSS to style the list the way we want it and we end up with a very basic Twitter viewer. Click
* the preview button on the example above to see it in action.
*/
Ext.define('Ext.dataview.DataView', {
extend: 'Ext.Container',
alternateClassName: 'Ext.DataView',
mixins: ['Ext.mixin.Selectable'],
xtype: 'dataview',
requires: [
'Ext.LoadMask',
'Ext.data.StoreManager',
'Ext.dataview.component.Container',
'Ext.dataview.element.Container'
],
/**
* @event containertap
* Fires when a tap occurs and it is not on a template node.
* @removed 2.0.0
*/
/**
* @event itemtouchstart
* Fires whenever an item is touched
* @param {Ext.dataview.DataView} this
* @param {Number} index The index of the item touched
* @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem touched
* @param {Ext.data.Model} record The record associated to the item
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtouchmove
* Fires whenever an item is moved
* @param {Ext.dataview.DataView} this
* @param {Number} index The index of the item moved
* @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem moved
* @param {Ext.data.Model} record The record associated to the item
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtouchend
* Fires whenever an item is touched
* @param {Ext.dataview.DataView} this
* @param {Number} index The index of the item touched
* @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem touched
* @param {Ext.data.Model} record The record associated to the item
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtap
* Fires whenever an item is tapped
* @param {Ext.dataview.DataView} this
* @param {Number} index The index of the item tapped
* @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem tapped
* @param {Ext.data.Model} record The record associated to the item
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemtaphold
* Fires whenever an item's taphold event fires
* @param {Ext.dataview.DataView} this
* @param {Number} index The index of the item touched
* @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem touched
* @param {Ext.data.Model} record The record associated to the item
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemsingletap
* Fires whenever an item is singletapped
* @param {Ext.dataview.DataView} this
* @param {Number} index The index of the item singletapped
* @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem singletapped
* @param {Ext.data.Model} record The record associated to the item
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemdoubletap
* Fires whenever an item is doubletapped
* @param {Ext.dataview.DataView} this
* @param {Number} index The index of the item doubletapped
* @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem doubletapped
* @param {Ext.data.Model} record The record associated to the item
* @param {Ext.EventObject} e The event object
*/
/**
* @event itemswipe
* Fires whenever an item is swiped
* @param {Ext.dataview.DataView} this
* @param {Number} index The index of the item swiped
* @param {Ext.Element/Ext.dataview.component.DataItem} target The element or DataItem swiped
* @param {Ext.data.Model} record The record associated to the item
* @param {Ext.EventObject} e The event object
*/
/**
* @event select
* @preventable doItemSelect
* Fires whenever an item is selected
* @param {Ext.dataview.DataView} this
* @param {Ext.data.Model} record The record associated to the item
*/
/**
* @event deselect
* @preventable doItemDeselect
* Fires whenever an item is deselected
* @param {Ext.dataview.DataView} this
* @param {Ext.data.Model} record The record associated to the item
* @param {Boolean} supressed Flag to suppress the event
*/
/**
* @event refresh
* @preventable doRefresh
* Fires whenever the DataView is refreshed
* @param {Ext.dataview.DataView} this
*/
/**
* @hide
* @event add
*/
/**
* @hide
* @event remove
*/
/**
* @hide
* @event move
*/
config: {
/**
* @cfg layout
* Hide layout config in DataView. It only causes confusion.
* @accessor
* @private
*/
/**
* @cfg {Ext.data.Store/Object} store
* Can be either a Store instance or a configuration object that will be turned into a Store. The Store is used
* to populate the set of items that will be rendered in the DataView. See the DataView intro documentation for
* more information about the relationship between Store and DataView.
* @accessor
*/
store: null,
/**
* @cfg baseCls
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'dataview',
/**
* @cfg {String} emptyText
* The text to display in the view when there is no data to display
*/
emptyText: null,
/**
* @cfg {Boolean} deferEmptyText `true` to defer `emptyText` being applied until the store's first load.
*/
deferEmptyText: true,
/**
* @cfg {String/String[]/Ext.XTemplate} itemTpl
* The `tpl` to use for each of the items displayed in this DataView.
*/
itemTpl: '<div>{text}</div>',
/**
* @cfg {String} pressedCls
* The CSS class to apply to an item on the view while it is being pressed.
* @accessor
*/
pressedCls: 'x-item-pressed',
/**
* @cfg {String} itemCls
* An additional CSS class to apply to items within the DataView.
* @accessor
*/
itemCls: null,
/**
* @cfg {String} selectedCls
* The CSS class to apply to an item on the view while it is selected.
* @accessor
*/
selectedCls: 'x-item-selected',
/**
* @cfg {String} triggerEvent
* Determines what type of touch event causes an item to be selected.
* Valid options are: 'itemtap', 'itemsingletap', 'itemdoubletap', 'itemswipe', 'itemtaphold'.
* @accessor
*/
triggerEvent: 'itemtap',
/**
* @cfg {String} triggerCtEvent
* Determines what type of touch event is recognized as a touch on the container.
* Valid options are 'tap' and 'singletap'.
* @accessor
*/
triggerCtEvent: 'tap',
/**
* @cfg {Boolean} deselectOnContainerClick
* When set to true, tapping on the DataView's background (i.e. not on
* an item in the DataView) will deselect any currently selected items.
* @accessor
*/
deselectOnContainerClick: true,
/**
* @cfg scrollable
* @inheritdoc
*/
scrollable: true,
/**
* @cfg {Boolean/Object} inline
* When set to `true` the items within the DataView will have their display set to inline-block
* and be arranged horizontally. By default the items will wrap to the width of the DataView.
* Passing an object with `{ wrap: false }` will turn off this wrapping behavior and overflowed
* items will need to be scrolled to horizontally.
* @accessor
*/
inline: null,
/**
* @cfg {Number} pressedDelay
* The amount of delay between the `tapstart` and the moment we add the `pressedCls`.
*
* Settings this to `true` defaults to 100ms.
* @accessor
*/
pressedDelay: 100,
/**
* @cfg {String} loadingText
* A string to display during data load operations. If specified, this text will be
* displayed in a loading div and the view's contents will be cleared while loading, otherwise the view's
* contents will continue to display normally until the new data is loaded and the contents are replaced.
*/
loadingText: 'Loading...',
/**
* @cfg {Boolean} useComponents
* Flag the use a component based DataView implementation. This allows the full use of components in the
* DataView at the cost of some performance.
*
* Checkout the [DataView Guide](#!/guide/dataview) for more information on using this configuration.
* @accessor
*/
useComponents: null,
/**
* @cfg {Object} itemConfig
* A configuration object that is passed to every item created by a component based DataView. Because each
* item that a DataView renders is a Component, we can pass configuration options to each component to
* easily customize how each child component behaves.
*
* __Note:__ this is only used when `{@link #useComponents}` is `true`.
* @accessor
*/
itemConfig: {},
/**
* @cfg {Number} maxItemCache
* Maintains a cache of reusable components when using a component based DataView. Improving performance at
* the cost of memory.
*
* __Note:__ this is currently only used when `{@link #useComponents}` is `true`.
* @accessor
*/
maxItemCache: 20,
/**
* @cfg {String} defaultType
* The xtype used for the component based DataView.
*
* __Note:__ this is only used when `{@link #useComponents}` is `true`.
* @accessor
*/
defaultType: 'dataitem',
/**
* @cfg {Boolean} scrollToTopOnRefresh
* Scroll the DataView to the top when the DataView is refreshed.
* @accessor
*/
scrollToTopOnRefresh: true
},
constructor: function(config) {
var me = this,
layout;
me.hasLoadedStore = false;
me.mixins.selectable.constructor.apply(me, arguments);
me.indexOffset = 0;
me.callParent(arguments);
//<debug>
layout = this.getLayout();
if (layout && !layout.isAuto) {
Ext.Logger.error('The base layout for a DataView must always be an Auto Layout');
}
//</debug>
},
updateItemCls: function(newCls, oldCls) {
var container = this.container;
if (container) {
if (oldCls) {
container.doRemoveItemCls(oldCls);
}
if (newCls) {
container.doAddItemCls(newCls);
}
}
},
storeEventHooks: {
beforeload: 'onBeforeLoad',
load: 'onLoad',
refresh: 'refresh',
addrecords: 'onStoreAdd',
removerecords: 'onStoreRemove',
updaterecord: 'onStoreUpdate'
},
initialize: function() {
this.callParent();
var me = this,
container;
me.on(me.getTriggerCtEvent(), me.onContainerTrigger, me);
container = me.container = this.add(new Ext.dataview[me.getUseComponents() ? 'component' : 'element'].Container({
baseCls: this.getBaseCls()
}));
container.dataview = me;
me.on(me.getTriggerEvent(), me.onItemTrigger, me);
container.on({
itemtouchstart: 'onItemTouchStart',
itemtouchend: 'onItemTouchEnd',
itemtap: 'onItemTap',
itemtaphold: 'onItemTapHold',
itemtouchmove: 'onItemTouchMove',
itemsingletap: 'onItemSingleTap',
itemdoubletap: 'onItemDoubleTap',
itemswipe: 'onItemSwipe',
scope: me
});
if (me.getStore()) {
if (me.isPainted()) {
me.refresh();
}
else {
me.on({
painted: 'refresh',
single: true
});
}
}
},
applyInline: function(config) {
if (Ext.isObject(config)) {
config = Ext.apply({}, config);
}
return config;
},
updateInline: function(newInline, oldInline) {
var baseCls = this.getBaseCls();
if (oldInline) {
this.removeCls([baseCls + '-inlineblock', baseCls + '-nowrap']);
}
if (newInline) {
this.addCls(baseCls + '-inlineblock');
if (Ext.isObject(newInline) && newInline.wrap === false) {
this.addCls(baseCls + '-nowrap');
}
else {
this.removeCls(baseCls + '-nowrap');
}
}
},
/**
* Function which can be overridden to provide custom formatting for each Record that is used by this
* DataView's {@link #tpl template} to render each node.
* @param {Object/Object[]} data The raw data object that was used to create the Record.
* @param {Number} recordIndex the index number of the Record being prepared for rendering.
* @param {Ext.data.Model} record The Record being prepared for rendering.
* @return {Array/Object} The formatted data in a format expected by the internal {@link #tpl template}'s `overwrite()` method.
* (either an array if your params are numeric (i.e. `{0}`) or an object (i.e. `{foo: 'bar'}`))
*/
prepareData: function(data, index, record) {
return data;
},
// apply to the selection model to maintain visual UI cues
onContainerTrigger: function(e) {
var me = this;
if (e.target != me.element.dom) {
return;
}
if (me.getDeselectOnContainerClick() && me.getStore()) {
me.deselectAll();
}
},
// apply to the selection model to maintain visual UI cues
onItemTrigger: function(me, index) {
this.selectWithEvent(this.getStore().getAt(index));
},
doAddPressedCls: function(record) {
var me = this,
item = me.getItemAt(me.getStore().indexOf(record));
if (Ext.isElement(item)) {
item = Ext.get(item);
}
if (item) {
item.addCls(me.getPressedCls());
}
},
onItemTouchStart: function(container, target, index, e) {
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
me.fireAction('itemtouchstart', [me, index, target, record, e], 'doItemTouchStart');
},
doItemTouchStart: function(me, index, target, record) {
var pressedDelay = me.getPressedDelay();
if (record) {
if (pressedDelay > 0) {
me.pressedTimeout = Ext.defer(me.doAddPressedCls, pressedDelay, me, [record]);
}
else {
me.doAddPressedCls(record);
}
}
},
onItemTouchEnd: function(container, target, index, e) {
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
if (this.hasOwnProperty('pressedTimeout')) {
clearTimeout(this.pressedTimeout);
delete this.pressedTimeout;
}
if (record && target) {
target.removeCls(me.getPressedCls());
}
me.fireEvent('itemtouchend', me, index, target, record, e);
},
onItemTouchMove: function(container, target, index, e) {
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
if (me.hasOwnProperty('pressedTimeout')) {
clearTimeout(me.pressedTimeout);
delete me.pressedTimeout;
}
if (record && target) {
target.removeCls(me.getPressedCls());
}
me.fireEvent('itemtouchmove', me, index, target, record, e);
},
onItemTap: function(container, target, index, e) {
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
me.fireEvent('itemtap', me, index, target, record, e);
},
onItemTapHold: function(container, target, index, e) {
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
me.fireEvent('itemtaphold', me, index, target, record, e);
},
onItemSingleTap: function(container, target, index, e) {
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
me.fireEvent('itemsingletap', me, index, target, record, e);
},
onItemDoubleTap: function(container, target, index, e) {
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
me.fireEvent('itemdoubletap', me, index, target, record, e);
},
onItemSwipe: function(container, target, index, e) {
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
me.fireEvent('itemswipe', me, index, target, record, e);
},
// invoked by the selection model to maintain visual UI cues
onItemSelect: function(record, suppressEvent) {
var me = this;
if (suppressEvent) {
me.doItemSelect(me, record);
} else {
me.fireAction('select', [me, record], 'doItemSelect');
}
},
// invoked by the selection model to maintain visual UI cues
doItemSelect: function(me, record) {
if (me.container && !me.isDestroyed) {
var item = me.getItemAt(me.getStore().indexOf(record));
if (Ext.isElement(item)) {
item = Ext.get(item);
}
if (item) {
item.removeCls(me.getPressedCls());
item.addCls(me.getSelectedCls());
}
}
},
// invoked by the selection model to maintain visual UI cues
onItemDeselect: function(record, suppressEvent) {
var me = this;
if (me.container && !me.isDestroyed) {
if (suppressEvent) {
me.doItemDeselect(me, record);
}
else {
me.fireAction('deselect', [me, record, suppressEvent], 'doItemDeselect');
}
}
},
doItemDeselect: function(me, record) {
var item = me.getItemAt(me.getStore().indexOf(record));
if (Ext.isElement(item)) {
item = Ext.get(item);
}
if (item) {
item.removeCls([me.getPressedCls(), me.getSelectedCls()]);
}
},
updateData: function(data) {
var store = this.getStore();
if (!store) {
this.setStore(Ext.create('Ext.data.Store', {
data: data
}));
} else {
store.add(data);
}
},
applyStore: function(store) {
var me = this,
bindEvents = Ext.apply({}, me.storeEventHooks, { scope: me }),
proxy, reader;
if (store) {
store = Ext.data.StoreManager.lookup(store);
if (store && Ext.isObject(store) && store.isStore) {
store.on(bindEvents);
proxy = store.getProxy();
if (proxy) {
reader = proxy.getReader();
if (reader) {
reader.on('exception', 'handleException', this);
}
}
}
//<debug warn>
else {
Ext.Logger.warn("The specified Store cannot be found", this);
}
//</debug>
}
return store;
},
/**
* Method called when the Store's Reader throws an exception
* @method handleException
*/
handleException: function() {
this.setMasked(false);
},
updateStore: function(newStore, oldStore) {
var me = this,
bindEvents = Ext.apply({}, me.storeEventHooks, { scope: me }),
proxy, reader;
if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
me.onStoreClear();
if (oldStore.getAutoDestroy()) {
oldStore.destroy();
}
else {
oldStore.un(bindEvents);
proxy = oldStore.getProxy();
if (proxy) {
reader = proxy.getReader();
if (reader) {
reader.un('exception', 'handleException', this);
}
}
}
}
if (newStore) {
if (newStore.isLoaded()) {
this.hasLoadedStore = true;
}
if (newStore.isLoading()) {
me.onBeforeLoad();
}
if (me.container) {
me.refresh();
}
}
},
onBeforeLoad: function() {
var loadingText = this.getLoadingText();
if (loadingText && this.isPainted()) {
this.setMasked({
xtype: 'loadmask',
message: loadingText
});
}
this.hideEmptyText();
},
updateEmptyText: function(newEmptyText, oldEmptyText) {
var me = this,
store;
if (oldEmptyText && me.emptyTextCmp) {
me.remove(me.emptyTextCmp, true);
delete me.emptyTextCmp;
}
if (newEmptyText) {
me.emptyTextCmp = me.add({
xtype: 'component',
cls: me.getBaseCls() + '-emptytext',
html: newEmptyText,
hidden: true
});
store = me.getStore();
if (store && me.hasLoadedStore && !store.getCount()) {
this.showEmptyText();
}
}
},
onLoad: function(store) {
//remove any masks on the store
this.hasLoadedStore = true;
this.setMasked(false);
if (!store.getCount()) {
this.showEmptyText();
}
},
/**
* Refreshes the view by reloading the data from the store and re-rendering the template.
*/
refresh: function() {
var me = this,
container = me.container;
if (!me.getStore()) {
if (!me.hasLoadedStore && !me.getDeferEmptyText()) {
me.showEmptyText();
}
return;
}
if (container) {
me.fireAction('refresh', [me], 'doRefresh');
}
},
applyItemTpl: function(config) {
return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
},
onAfterRender: function() {
var me = this;
me.callParent(arguments);
me.updateStore(me.getStore());
},
/**
* Returns an item at the specified index.
* @param {Number} index Index of the item.
* @return {Ext.dom.Element/Ext.dataview.component.DataItem} item Item at the specified index.
*/
getItemAt: function(index) {
return this.getViewItems()[index - this.indexOffset];
},
/**
* Returns an index for the specified item.
* @param {Number} item The item to locate.
* @return {Number} Index for the specified item.
*/
getItemIndex: function(item) {
var index = this.getViewItems().indexOf(item);
return (index === -1) ? index : this.indexOffset + index;
},
/**
* Returns an array of the current items in the DataView.
* @return {Ext.dom.Element[]/Ext.dataview.component.DataItem[]} Array of Items.
*/
getViewItems: function() {
return this.container.getViewItems();
},
doRefresh: function(me) {
var container = me.container,
store = me.getStore(),
records = store.getRange(),
items = me.getViewItems(),
recordsLn = records.length,
itemsLn = items.length,
deltaLn = recordsLn - itemsLn,
scrollable = me.getScrollable(),
i, item;
if (this.getScrollToTopOnRefresh() && scrollable) {
scrollable.getScroller().scrollToTop();
}
// No items, hide all the items from the collection.
if (recordsLn < 1) {
me.onStoreClear();
return;
} else {
me.hideEmptyText();
}
// Too many items, hide the unused ones
if (deltaLn < 0) {
container.moveItemsToCache(itemsLn + deltaLn, itemsLn - 1);
// Items can changed, we need to refresh our references
items = me.getViewItems();
itemsLn = items.length;
}
// Not enough items, create new ones
else if (deltaLn > 0) {
container.moveItemsFromCache(store.getRange(itemsLn));
}
// Update Data and insert the new html for existing items
for (i = 0; i < itemsLn; i++) {
item = items[i];
container.updateListItem(records[i], item);
}
},
showEmptyText: function() {
if (this.getEmptyText() && (this.hasLoadedStore || !this.getDeferEmptyText()) ) {
this.emptyTextCmp.show();
}
},
hideEmptyText: function() {
if (this.getEmptyText()) {
this.emptyTextCmp.hide();
}
},
destroy: function() {
var store = this.getStore();
if (store && store.getAutoDestroy()) {
store.destroy();
}
this.callParent(arguments);
},
onStoreClear: function() {
var me = this,
container = me.container,
items = me.getViewItems();
container.moveItemsToCache(0, items.length - 1);
this.showEmptyText();
},
/**
* @private
* @param store
* @param records
*/
onStoreAdd: function(store, records) {
if (records) {
this.hideEmptyText();
this.container.moveItemsFromCache(records);
}
},
/**
* @private
* @param store
* @param records
* @param indices
*/
onStoreRemove: function(store, records, indices) {
var container = this.container,
ln = records.length,
i;
for (i = 0; i < ln; i++) {
container.moveItemsToCache(indices[i], indices[i]);
}
},
/**
* @private
* @param store
* @param record
* @param {Number} newIndex
* @param {Number} oldIndex
*/
onStoreUpdate: function(store, record, newIndex, oldIndex) {
var me = this,
container = me.container;
oldIndex = (typeof oldIndex === 'undefined') ? newIndex : oldIndex;
if (oldIndex !== newIndex) {
container.updateAtNewIndex(oldIndex, newIndex, record);
if (me.isSelected(record)) {
me.doItemSelect(me, record);
}
}
else {
// Bypassing setter because sometimes we pass the same record (different data)
container.updateListItem(record, me.getViewItems()[newIndex]);
}
}
});
/**
* @class Ext.chart.Legend
* @extends Ext.dataview.DataView
*
* A default legend for charts.
*
* @example preview
* var chart = new Ext.chart.Chart({
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* legend: {
* position: 'bottom'
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data1'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 0
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'area',
* title: ['Data1', 'Data2', 'Data3'],
* subStyle: {
* fill: ['blue', 'green', 'red']
* },
* xField: 'name',
* yField: ['data1', 'data2', 'data3']
*
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define("Ext.chart.Legend", {
xtype: 'legend',
extend: "Ext.dataview.DataView",
config: {
itemTpl: [
"<span class=\"x-legend-item-marker {[values.disabled?\'x-legend-inactive\':\'\']}\" style=\"background:{mark};\"></span>{name}"
],
baseCls: 'x-legend',
padding: 5,
disableSelection: true,
inline: true,
/**
* @cfg {String} position
* @deprecated Use `docked` instead.
* Delegates to `docked`
*/
position: 'top',
horizontalHeight: 48,
verticalWidth: 150
},
constructor: function () {
this.callSuper(arguments);
var scroller = this.getScrollable().getScroller(),
onDrag = scroller.onDrag;
scroller.onDrag = function (e) {
e.stopPropagation();
onDrag.call(this, e);
};
},
doSetDocked: function (docked) {
this.callSuper(arguments);
if (docked === 'top' || docked === 'bottom') {
this.setLayout({type: 'hbox', pack: 'center'});
this.setInline(true);
// TODO: Remove this when possible
this.setWidth(null);
this.setHeight(this.getHorizontalHeight());
this.setScrollable({direction: 'horizontal' });
} else {
this.setLayout({pack: 'center'});
this.setInline(false);
// TODO: Remove this when possible
this.setWidth(this.getVerticalWidth());
this.setHeight(null);
this.setScrollable({direction: 'vertical' });
}
},
updatePosition: function (position) {
this.setDocked(position);
},
onItemTap: function (container, target, index, e) {
this.callSuper(arguments);
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
record.beginEdit();
record.set('disabled', !record.get('disabled'));
record.commit();
}
});
/**
* @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
*/
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
*/
filters: null,
/**
* @cfg {Ext.util.Sorter[]} sorters
* Optional array of sorter objects. Only applies to 'read' actions.
* @accessor
*/
sorters: null,
/**
* @cfg {Ext.util.Grouper} grouper
* Optional grouping configuration. Only applies to 'read' actions where grouping is desired.
* @accessor
*/
grouper: null,
/**
* @cfg {Number} start
* The start index (offset), used in paging when running a 'read' action.
* @accessor
*/
start: null,
/**
* @cfg {Number} limit
* The number of records to load. Used on 'read' actions when paging is being used.
* @accessor
*/
limit: null,
/**
* @cfg {Ext.data.Batch} batch
* The batch that this Operation is a part of.
* @accessor
*/
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.
* @accessor
*/
callback: null,
/**
* @cfg {Object} scope
* Scope for the {@link #callback} function.
* @accessor
*/
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.
* @accessor
*/
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.
* @accessor
*/
model: undefined,
addRecords: false
},
/**
* @property {Boolean} started
* Property tracking the start status of this Operation. Use {@link #isStarted}.
* @private
* @readonly
*/
started: false,
/**
* @property {Boolean} running
* Property tracking the run status of this Operation. Use {@link #isRunning}.
* @private
* @readonly
*/
running: false,
/**
* @property {Boolean} complete
* Property tracking the completion status of this Operation. Use {@link #isComplete}.
* @private
* @readonly
*/
complete: false,
/**
* @property {Boolean} success
* 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
* @readonly
*/
success: undefined,
/**
* @property {Boolean} exception
* Property tracking the exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
* @private
* @readonly
*/
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);
}
// <debug>
if (!model) {
Ext.Logger.warn('Unless you define your model using metadata, an Operation needs to have a model defined.');
}
// </debug>
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);
resultSet.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);
}
// <debug>
else {
Ext.Logger.warn('Unable to match the record that came back from the server.');
}
// </debug>
}
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);
}
// <debug>
else {
Ext.Logger.warn('Unable to match the updated record that came back from the server.');
}
// </debug>
}
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);
}
// <debug>
else {
Ext.Logger.warn('Unable to match the destroyed record that came back from the server.');
}
// </debug>
}
},
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 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,
/**
* @cfg {String} message
* The message that was read in from the data
*/
message: 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);
}
});
/**
* @author Ed Spencer
*
* Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance or a {@link
* Ext.data.Store Store} - often in response to an AJAX request. In general there is usually no need to create
* a Reader instance directly, since a Reader is almost always used together with a {@link Ext.data.proxy.Proxy Proxy},
* and is configured using the Proxy's {@link Ext.data.proxy.Proxy#cfg-reader reader} configuration property:
*
* Ext.define("User", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id",
* "name"
* ]
* }
* });
*
* Ext.create("Ext.data.Store", {
* model: "User",
* autoLoad: true,
* storeId: "usersStore",
* proxy: {
* type: "ajax",
* url : "users.json",
* reader: {
* type: "json",
* rootProperty: "users"
* }
* }
* });
*
* Ext.create("Ext.List", {
* fullscreen: true,
* itemTpl: "{name} (id: '{id}')",
* store: "usersStore"
* });
*
* The above reader is configured to consume a JSON string that looks something like this:
*
* {
* "success": true,
* "users": [
* { "name": "User 1" },
* { "name": "User 2" }
* ]
* }
*
*
* # Loading Nested Data
*
* Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.association.Association
* associations} configured on each Model. Below is an example demonstrating the flexibility of these associations in a
* fictional CRM system which manages a User, their Orders, OrderItems and Products. First we'll define the models:
*
* Ext.define("User", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id",
* "name"
* ],
* hasMany: {
* model: "Order",
* name: "orders"
* },
* proxy: {
* type: "rest",
* url : "users.json",
* reader: {
* type: "json",
* rootProperty: "users"
* }
* }
* }
* });
*
* Ext.define("Order", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id", "total"
* ],
* hasMany: {
* model: "OrderItem",
* name: "orderItems",
* associationKey: "order_items"
* },
* belongsTo: "User"
* }
* });
*
* Ext.define("OrderItem", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id",
* "price",
* "quantity",
* "order_id",
* "product_id"
* ],
* belongsTo: [
* "Order", {
* model: "Product",
* associationKey: "product"
* }
* ]
* }
* });
*
* Ext.define("Product", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id",
* "name"
* ]
* },
* hasMany: "OrderItem"
* });
*
* var store = Ext.create('Ext.data.Store', {
* model: "User"
* });
*
* store.load({
* callback: function() {
* var output = [];
*
* // the user that was loaded
* var user = store.first();
*
* output.push("Orders for " + user.get('name') + ":");
*
* // iterate over the Orders for each User
* user.orders().each(function(order) {
* output.push("Order ID: " + order.get('id') + ", which contains items:");
*
* // iterate over the OrderItems for each Order
* order.orderItems().each(function(orderItem) {
* // We know that the Product data is already loaded, so we can use the
* // synchronous getProduct() method. Usually, we would use the
* // asynchronous version (see Ext.data.association.BelongsTo).
* var product = orderItem.getProduct();
* output.push(orderItem.get("quantity") + " orders of " + product.get("name"));
* });
* });
* Ext.Msg.alert('Output:', output.join("<br/>"));
* }
* });
*
* This may be a lot to take in - basically a User has many Orders, each of which is composed of several OrderItems.
* Finally, each OrderItem has a single Product. This allows us to consume data like this (_users.json_):
*
* {
* "users": [
* {
* "id": 123,
* "name": "Ed",
* "orders": [
* {
* "id": 50,
* "total": 100,
* "order_items": [
* {
* "id" : 20,
* "price" : 40,
* "quantity": 2,
* "product" : {
* "id": 1000,
* "name": "MacBook Pro"
* }
* },
* {
* "id" : 21,
* "price" : 20,
* "quantity": 3,
* "product" : {
* "id": 1001,
* "name": "iPhone"
* }
* }
* ]
* }
* ]
* }
* ]
* }
*
* The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's sake), all of the
* Orders for each User (again just 1 in this case), all of the OrderItems for each Order (2 order items in this case),
* and finally the Product associated with each OrderItem.
*
* Running the code above results in the following:
*
* Orders for Ed:
* Order ID: 50, which contains items:
* 2 orders of MacBook Pro
* 3 orders of iPhone
*/
Ext.define('Ext.data.reader.Reader', {
requires: [
'Ext.data.ResultSet'
],
alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],
mixins: ['Ext.mixin.Observable'],
// @private
isReader: true,
config: {
/**
* @cfg {String} idProperty
* Name of the property within a raw object that contains a record identifier value. Defaults to The id of the
* model. If an `idProperty` is explicitly specified it will override that of the one specified on the model
*/
idProperty: undefined,
/**
* @cfg {String} clientIdProperty
* The name of the property with a response that contains the existing client side id for a record that we are reading.
*/
clientIdProperty: 'clientId',
/**
* @cfg {String} totalProperty
* Name of the property from which to retrieve the total number of records in the dataset. This is only needed if
* the whole dataset is not passed in one go, but is being paged from the remote server.
*/
totalProperty: 'total',
/**
* @cfg {String} successProperty
* Name of the property from which to retrieve the success attribute. See
* {@link Ext.data.proxy.Server}.{@link Ext.data.proxy.Server#exception exception} for additional information.
*/
successProperty: 'success',
/**
* @cfg {String} messageProperty (optional)
* The name of the property which contains a response message. This property is optional.
*/
messageProperty: null,
/**
* @cfg {String} rootProperty
* The name of the property which contains the Array of row objects. For JSON reader it's dot-separated list
* of property names. For XML reader it's a CSS selector. For array reader it's not applicable.
*
* By default the natural root of the data will be used. The root JSON array, the root XML element, or the array.
*
* The data packet value for this property should be an empty array to clear the data or show no data.
*/
rootProperty: '',
/**
* @cfg {Boolean} implicitIncludes
* `true` to automatically parse models nested within other models in a response object. See the
* {@link Ext.data.reader.Reader} intro docs for full explanation.
*/
implicitIncludes: true,
model: undefined
},
constructor: function(config) {
this.initConfig(config);
},
/**
* @property {Object} metaData
* The raw meta data that was most recently read, if any. Meta data can include existing
* Reader config options like {@link #idProperty}, {@link #totalProperty}, etc. that get
* automatically applied to the Reader, and those can still be accessed directly from the Reader
* if needed. However, meta data is also often used to pass other custom data to be processed
* by application code. For example, it is common when reconfiguring the data model of a grid to
* also pass a corresponding column model config to be applied to the grid. Any such data will
* not get applied to the Reader directly (it just gets passed through and is ignored by Ext).
* This `metaData` property gives you access to all meta data that was passed, including any such
* custom data ignored by the reader.
*
* This is a read-only property, and it will get replaced each time a new meta data object is
* passed to the reader.
* @readonly
*/
fieldCount: 0,
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);
}
return model;
},
applyIdProperty: function(idProperty) {
if (!idProperty && this.getModel()) {
idProperty = this.getModel().getIdProperty();
}
return idProperty;
},
updateModel: function(model) {
if (model) {
if (!this.getIdProperty()) {
this.setIdProperty(model.getIdProperty());
}
this.buildExtractors();
}
},
createAccessor: Ext.emptyFn,
createFieldAccessExpression: function() {
return 'undefined';
},
/**
* @private
* This builds optimized functions for retrieving record data and meta data from an object.
* Subclasses may need to implement their own getRoot function.
*/
buildExtractors: function() {
if (!this.getModel()) {
return;
}
var me = this,
totalProp = me.getTotalProperty(),
successProp = me.getSuccessProperty(),
messageProp = me.getMessageProperty();
//build the extractors for all the meta data
if (totalProp) {
me.getTotal = me.createAccessor(totalProp);
}
if (successProp) {
me.getSuccess = me.createAccessor(successProp);
}
if (messageProp) {
me.getMessage = me.createAccessor(messageProp);
}
me.extractRecordData = me.buildRecordDataExtractor();
},
/**
* @private
* Return a function which will read a raw row object in the format this Reader accepts, and populates
* a record's data object with converted data values.
*
* The returned function must be passed the following parameters:
*
* - `dest` - A record's empty data object into which the new field value properties are injected.
* - `source` - A raw row data object of whatever type this Reader consumes
* - `record - The record which is being populated.
*/
buildRecordDataExtractor: function() {
var me = this,
model = me.getModel(),
fields = model.getFields(),
ln = fields.length,
fieldVarName = [],
clientIdProp = me.getModel().getClientIdProperty(),
prefix = '__field',
code = [
'var me = this,\n',
' fields = me.getModel().getFields(),\n',
' idProperty = me.getIdProperty(),\n',
' idPropertyIsFn = (typeof idProperty == "function"),',
' value,\n',
' internalId'
], i, field, varName, fieldName;
fields = fields.items;
for (i = 0; i < ln; i++) {
field = fields[i];
fieldName = field.getName();
if (fieldName === model.getIdProperty()) {
fieldVarName[i] = 'idField';
} else {
fieldVarName[i] = prefix + i;
}
code.push(',\n ', fieldVarName[i], ' = fields.get("', field.getName(), '")');
}
code.push(';\n\n return function(source) {\n var dest = {};\n');
code.push(' if (idPropertyIsFn) {\n');
code.push(' idField.setMapping(idProperty);\n');
code.push(' }\n');
for (i = 0; i < ln; i++) {
field = fields[i];
varName = fieldVarName[i];
fieldName = field.getName();
if (fieldName === model.getIdProperty() && field.getMapping() === null && model.getIdProperty() !== this.getIdProperty()) {
field.setMapping(this.getIdProperty());
}
// createFieldAccessExpression must be implemented in subclasses to extract data from the source object in the correct way.
code.push(' try {\n');
code.push(' value = ', me.createFieldAccessExpression(field, varName, 'source'), ';\n');
code.push(' if (value !== undefined) {\n');
code.push(' dest["' + field.getName() + '"] = value;\n');
code.push(' }\n');
code.push(' } catch(e){}\n');
}
// set the client id as the internalId of the record.
// clientId handles the case where a client side record did not previously exist on the server,
// so the server is passing back a client id that can be used to pair the server side record up with the client record
if (clientIdProp) {
code.push(' internalId = ' + me.createFieldAccessExpression(Ext.create('Ext.data.Field', {name: clientIdProp}), null, 'source') + ';\n');
code.push(' if (internalId !== undefined) {\n');
code.push(' dest["_clientId"] = internalId;\n }\n');
}
code.push(' return dest;\n');
code.push(' };');
// Here we are creating a new Function and invoking it immediately in the scope of this Reader
// It declares several vars capturing the configured context of this Reader, and returns a function
// which, when passed a record data object, a raw data row in the format this Reader is configured to read,
// and the record which is being created, will populate the record's data object from the raw row data.
return Ext.functionFactory(code.join('')).call(me);
},
getFields: function() {
return this.getModel().getFields().items;
},
/**
* @private
* By default this function just returns what is passed to it. It can be overridden in a subclass
* to return something else. See XmlReader for an example.
* @param {Object} data The data object
* @return {Object} The normalized data object
*/
getData: function(data) {
return data;
},
/**
* Takes a raw response object (as passed to this.read) and returns the useful data segment of it.
* This must be implemented by each subclass
* @param {Object} response The response object
* @return {Object} The useful data from the response
*/
getResponseData: function(response) {
return response;
},
/**
* @private
* This will usually need to be implemented in a subclass. Given a generic data object (the type depends on the type
* of data we are reading), this function should return the object as configured by the Reader's 'rootProperty' meta data config.
* See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned.
* @param {Object} data The data object
* @return {Object} The same data object
*/
getRoot: function(data) {
return data;
},
/**
* Reads the given response object. This method normalizes the different types of response object that may be passed
* to it, before handing off the reading of records to the {@link #readRecords} function.
* @param {Object} response The response object. This may be either an XMLHttpRequest object or a plain JS object
* @return {Ext.data.ResultSet} The parsed ResultSet object
*/
read: function(response) {
var data = response,
Model = this.getModel(),
resultSet, records, i, ln, record;
if (response) {
data = this.getResponseData(response);
}
if (data) {
resultSet = this.readRecords(data);
records = resultSet.getRecords();
for (i = 0, ln = records.length; i < ln; i++) {
record = records[i];
records[i] = new Model(record.data, record.id, record.node);
}
return resultSet;
} else {
return this.nullResultSet;
}
},
process: function(response) {
var data = response;
if (response) {
data = this.getResponseData(response);
}
if (data) {
return this.readRecords(data);
} else {
return this.nullResultSet;
}
},
/**
* Abstracts common functionality used by all Reader subclasses. Each subclass is expected to call this function
* before running its own logic and returning the Ext.data.ResultSet instance. For most Readers additional
* processing should not be needed.
* @param {Object} data The raw data object
* @return {Ext.data.ResultSet} A ResultSet object
*/
readRecords: function(data) {
var me = this;
/**
* @property {Object} rawData
* The raw data object that was last passed to readRecords. Stored for further processing if needed
*/
me.rawData = data;
data = me.getData(data);
if (data.metaData) {
me.onMetaChange(data.metaData);
}
// <debug>
if (!me.getModel()) {
Ext.Logger.warn('In order to read record data, a Reader needs to have a Model defined on it.');
}
// </debug>
// If we pass an array as the data, we don't use getRoot on the data.
// Instead the root equals to the data.
var isArray = Ext.isArray(data),
root = isArray ? data : me.getRoot(data),
success = true,
recordCount = 0,
total, value, records, message;
if (isArray && !data.length) {
return me.nullResultSet;
}
// buildExtractors should have put getTotal, getSuccess, or getMessage methods on the instance.
// So we can check them directly
if (me.getTotal) {
value = parseInt(me.getTotal(data), 10);
if (!isNaN(value)) {
total = value;
}
}
if (me.getSuccess) {
value = me.getSuccess(data);
if (value === false || value === 'false') {
success = false;
}
}
if (me.getMessage) {
message = me.getMessage(data);
}
if (root) {
records = me.extractData(root);
recordCount = records.length;
} else {
recordCount = 0;
records = [];
}
return new Ext.data.ResultSet({
total : total,
count : recordCount,
records: records,
success: success,
message: message
});
},
/**
* Returns extracted, type-cast rows of data.
* @param {Object[]/Object} root from server response
* @private
*/
extractData : function(root) {
var me = this,
records = [],
length = root.length,
model = me.getModel(),
idProperty = model.getIdProperty(),
fieldsCollection = model.getFields(),
node, i, data, id, clientId;
/*
* We check here whether the fields are dirty since the last read.
* This works around an issue when a Model is used for both a Tree and another
* source, because the tree decorates the model with extra fields and it causes
* issues because the readers aren't notified.
*/
if (fieldsCollection.isDirty) {
me.buildExtractors(true);
delete fieldsCollection.isDirty;
}
if (!root.length && Ext.isObject(root)) {
root = [root];
length = 1;
}
for (i = 0; i < length; i++) {
clientId = null;
id = null;
node = root[i];
// When you use a Memory proxy, and you set data: [] to contain record instances
// this node will already be a record. In this case we should not try to extract
// the record data from the object, but just use the record data attribute.
if (node.isModel) {
data = node.data;
} else {
data = me.extractRecordData(node);
}
if (data._clientId !== undefined) {
clientId = data._clientId;
delete data._clientId;
}
if (data[idProperty] !== undefined) {
id = data[idProperty];
}
if (me.getImplicitIncludes()) {
me.readAssociated(data, node);
}
records.push({
clientId: clientId,
id: id,
data: data,
node: node
});
}
return records;
},
/**
* @private
* Loads a record's associations from the data object. This pre-populates `hasMany` and `belongsTo` associations
* on the record provided.
* @param {Ext.data.Model} record The record to load associations for
* @param {Object} data The data object
*/
readAssociated: function(record, data) {
var associations = this.getModel().associations.items,
length = associations.length,
i = 0,
association, associationData, associationKey;
for (; i < length; i++) {
association = associations[i];
associationKey = association.getAssociationKey();
associationData = this.getAssociatedDataRoot(data, associationKey);
if (associationData) {
record[associationKey] = associationData;
}
}
},
/**
* @private
* Used internally by `readAssociated`. Given a data object (which could be json, xml etc) for a specific
* record, this should return the relevant part of that data for the given association name. If a complex
* mapping, this will traverse arrays and objects to resolve the data.
* @param {Object} data The raw data object
* @param {String} associationName The name of the association to get data for (uses associationKey if present)
* @return {Object} The root
*/
getAssociatedDataRoot: function(data, associationName) {
var re = /[\[\.]/,
i = String(associationName).search(re);
if (i >= 0) {
return Ext.functionFactory('obj', 'return obj' + (i > 0 ? '.' : '') + associationName)(data);
}
return data[associationName];
},
/**
* @private
* Reconfigures the meta data tied to this Reader
*/
onMetaChange : function(meta) {
var fields = meta.fields,
me = this,
newModel, config, idProperty;
// save off the raw meta data
me.metaData = meta;
// set any reader-specific configs from meta if available
if (meta.rootProperty !== undefined) {
me.setRootProperty(meta.rootProperty);
}
else if (meta.root !== undefined) {
me.setRootProperty(meta.root);
}
if (meta.idProperty !== undefined) {
me.setIdProperty(meta.idProperty);
}
if (meta.totalProperty !== undefined) {
me.setTotalProperty(meta.totalProperty);
}
if (meta.successProperty !== undefined) {
me.setSuccessProperty(meta.successProperty);
}
if (meta.messageProperty !== undefined) {
me.setMessageProperty(meta.messageProperty);
}
if (fields) {
if (me.getModel()) {
me.getModel().setFields(fields);
me.buildExtractors();
}
else {
idProperty = me.getIdProperty();
config = {fields: fields};
if (idProperty) {
config.idProperty = idProperty;
}
newModel = Ext.define("Ext.data.reader.MetaModel" + Ext.id(), {
extend: 'Ext.data.Model',
config: config
});
me.setModel(newModel);
}
}
else {
me.buildExtractors();
}
}
// Convert old properties in data into a config object
}, function() {
Ext.apply(this.prototype, {
// @private
// Empty ResultSet to return when response is falsy (null|undefined|empty string)
nullResultSet: new Ext.data.ResultSet({
total : 0,
count : 0,
records: [],
success: false
})
});
});
/**
* The JSON Reader is used by a Proxy to read a server response that is sent back in JSON format. This usually happens
* as a result of loading a Store - for example we might create something like this:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['id', 'name', 'email']
* }
* });
*
* var store = Ext.create('Ext.data.Store', {
* model: 'User',
* proxy: {
* type: 'ajax',
* url : 'users.json',
* reader: {
* type: 'json'
* }
* }
* });
*
* The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're not
* already familiar with them.
*
* We created the simplest type of JSON Reader possible by simply telling our {@link Ext.data.Store Store}'s {@link
* Ext.data.proxy.Proxy Proxy} that we want a JSON Reader. The Store automatically passes the configured model to the
* Store, so it is as if we passed this instead:
*
* reader: {
* type : 'json',
* model: 'User'
* }
*
* The reader we set up is ready to read data from our server - at the moment it will accept a response like this:
*
* [
* {
* "id": 1,
* "name": "Ed Spencer",
* "email": "ed@sencha.com"
* },
* {
* "id": 2,
* "name": "Abe Elias",
* "email": "abe@sencha.com"
* }
* ]
*
* ## Reading other JSON formats
*
* If you already have your JSON format defined and it doesn't look quite like what we have above, you can usually pass
* JsonReader a couple of configuration options to make it parse your format. For example, we can use the
* {@link #rootProperty} configuration to parse data that comes back like this:
*
* {
* "users": [
* {
* "id": 1,
* "name": "Ed Spencer",
* "email": "ed@sencha.com"
* },
* {
* "id": 2,
* "name": "Abe Elias",
* "email": "abe@sencha.com"
* }
* ]
* }
*
* To parse this we just pass in a {@link #rootProperty} configuration that matches the 'users' above:
*
* reader: {
* type: 'json',
* rootProperty: 'users'
* }
*
* Sometimes the JSON structure is even more complicated. Document databases like CouchDB often provide metadata around
* each record inside a nested structure like this:
*
* {
* "total": 122,
* "offset": 0,
* "users": [
* {
* "id": "ed-spencer-1",
* "value": 1,
* "user": {
* "id": 1,
* "name": "Ed Spencer",
* "email": "ed@sencha.com"
* }
* }
* ]
* }
*
* In the case above the record data is nested an additional level inside the "users" array as each "user" item has
* additional metadata surrounding it ('id' and 'value' in this case). To parse data out of each "user" item in the JSON
* above we need to specify the {@link #record} configuration like this:
*
* reader: {
* type: 'json',
* record: 'user',
* rootProperty: 'users'
* }
*
* ## Response MetaData
*
* The server can return metadata in its response, in addition to the record data, that describe attributes
* of the data set itself or are used to reconfigure the Reader. To pass metadata in the response you simply
* add a `metaData` attribute to the root of the response data. The metaData attribute can contain anything,
* but supports a specific set of properties that are handled by the Reader if they are present:
*
* - {@link #idProperty}: property name for the primary key field of the data
* - {@link #rootProperty}: the property name of the root response node containing the record data
* - {@link #totalProperty}: property name for the total number of records in the data
* - {@link #successProperty}: property name for the success status of the response
* - {@link #messageProperty}: property name for an optional response message
* - {@link Ext.data.Model#cfg-fields fields}: Config used to reconfigure the Model's fields before converting the
* response data into records
*
* An initial Reader configuration containing all of these properties might look like this ("fields" would be
* included in the Model definition, not shown):
*
* reader: {
* type: 'json',
* idProperty: 'id',
* rootProperty: 'root',
* totalProperty: 'total',
* successProperty: 'success',
* messageProperty: 'message'
* }
*
* If you were to pass a response object containing attributes different from those initially defined above, you could
* use the `metaData` attribute to reconfigure the Reader on the fly. For example:
*
* {
* "count": 1,
* "ok": true,
* "msg": "Users found",
* "users": [{
* "userId": 123,
* "name": "Ed Spencer",
* "email": "ed@sencha.com"
* }],
* "metaData": {
* "idProperty": 'userId',
* "rootProperty": "users",
* "totalProperty": 'count',
* "successProperty": 'ok',
* "messageProperty": 'msg'
* }
* }
*
* You can also place any other arbitrary data you need into the `metaData` attribute which will be ignored by the Reader,
* but will be accessible via the Reader's {@link #metaData} property. Application code can then process the passed
* metadata in any way it chooses.
*
* A simple example for how this can be used would be customizing the fields for a Model that is bound to a grid. By passing
* the `fields` property the Model will be automatically updated by the Reader internally, but that change will not be
* reflected automatically in the grid unless you also update the column configuration. You could do this manually, or you
* could simply pass a standard grid column config object as part of the `metaData` attribute
* and then pass that along to the grid. Here's a very simple example for how that could be accomplished:
*
* // response format:
* {
* ...
* "metaData": {
* "fields": [
* { "name": "userId", "type": "int" },
* { "name": "name", "type": "string" },
* { "name": "birthday", "type": "date", "dateFormat": "Y-j-m" },
* ],
* "columns": [
* { "text": "User ID", "dataIndex": "userId", "width": 40 },
* { "text": "User Name", "dataIndex": "name", "flex": 1 },
* { "text": "Birthday", "dataIndex": "birthday", "flex": 1, "format": 'Y-j-m', "xtype": "datecolumn" }
* ]
* }
* }
*/
Ext.define('Ext.data.reader.Json', {
extend: 'Ext.data.reader.Reader',
alternateClassName: 'Ext.data.JsonReader',
alias : 'reader.json',
config: {
/**
* @cfg {String} [record=null]
* The optional location within the JSON response that the record data itself can be found at. See the
* JsonReader intro docs for more details. This is not often needed.
*/
record: null,
/**
* @cfg {Boolean} [useSimpleAccessors=false]
* `true` to ensure that field names/mappings are treated as literals when reading values. For
* example, by default, using the mapping "foo.bar.baz" will try and read a property foo from the root, then a
* property bar from foo, then a property baz from bar. Setting the simple accessors to `true` will read the
* property with the name "foo.bar.baz" direct from the root object.
*/
useSimpleAccessors: false
},
objectRe: /[\[\.]/,
// @inheritdoc
getResponseData: function(response) {
var responseText = response;
// Handle an XMLHttpRequest object
if (response && response.responseText) {
responseText = response.responseText;
}
// Handle the case where data has already been decoded
if (typeof responseText !== 'string') {
return responseText;
}
var data;
try {
data = Ext.decode(responseText);
}
catch (ex) {
/**
* @event exception Fires whenever the reader is unable to parse a response.
* @param {Ext.data.reader.Xml} reader A reference to this reader.
* @param {XMLHttpRequest} response The XMLHttpRequest response object.
* @param {String} error The error message.
*/
this.fireEvent('exception', this, response, 'Unable to parse the JSON returned by the server: ' + ex.toString());
Ext.Logger.warn('Unable to parse the JSON returned by the server: ' + ex.toString());
}
//<debug>
if (!data) {
this.fireEvent('exception', this, response, 'JSON object not found');
Ext.Logger.error('JSON object not found');
}
//</debug>
return data;
},
// @inheritdoc
buildExtractors: function() {
var me = this,
root = me.getRootProperty();
me.callParent(arguments);
if (root) {
me.rootAccessor = me.createAccessor(root);
} else {
delete me.rootAccessor;
}
},
/**
* We create this method because `root` is now a config so `getRoot` is already defined, but in the old
* data package `getRoot` was passed a data argument and it would return the data inside of the `root`
* property. This method handles both cases.
* @param data (Optional)
* @return {String/Object} Returns the config root value if this method was called without passing
* data. Else it returns the object in the data bound to the root.
* @private
*/
getRoot: function(data) {
var fieldsCollection = this.getModel().getFields();
/*
* We check here whether the fields are dirty since the last read.
* This works around an issue when a Model is used for both a Tree and another
* source, because the tree decorates the model with extra fields and it causes
* issues because the readers aren't notified.
*/
if (fieldsCollection.isDirty) {
this.buildExtractors(true);
delete fieldsCollection.isDirty;
}
if (this.rootAccessor) {
return this.rootAccessor.call(this, data);
} else {
return data;
}
},
/**
* @private
* We're just preparing the data for the superclass by pulling out the record objects we want. If a {@link #record}
* was specified we have to pull those out of the larger JSON object, which is most of what this function is doing
* @param {Object} root The JSON root node
* @return {Ext.data.Model[]} The records
*/
extractData: function(root) {
var recordName = this.getRecord(),
data = [],
length, i;
if (recordName) {
length = root.length;
if (!length && Ext.isObject(root)) {
length = 1;
root = [root];
}
for (i = 0; i < length; i++) {
data[i] = root[i][recordName];
}
} else {
data = root;
}
return this.callParent([data]);
},
/**
* @private
* Returns an accessor function for the given property string. Gives support for properties such as the following:
* 'someProperty'
* 'some.property'
* 'some["property"]'
* This is used by buildExtractors to create optimized extractor functions when casting raw data into model instances.
*/
createAccessor: function() {
var re = /[\[\.]/;
return function(expr) {
if (Ext.isEmpty(expr)) {
return Ext.emptyFn;
}
if (Ext.isFunction(expr)) {
return expr;
}
if (this.getUseSimpleAccessors() !== true) {
var i = String(expr).search(re);
if (i >= 0) {
return Ext.functionFactory('obj', 'var value; try {value = obj' + (i > 0 ? '.' : '') + expr + '} catch(e) {}; return value;');
}
}
return function(obj) {
return obj[expr];
};
};
}(),
/**
* @private
* Returns an accessor expression for the passed Field. Gives support for properties such as the following:
* 'someProperty'
* 'some.property'
* 'some["property"]'
* This is used by buildExtractors to create optimized on extractor function which converts raw data into model instances.
*/
createFieldAccessExpression: function(field, fieldVarName, dataName) {
var me = this,
re = me.objectRe,
hasMap = (field.getMapping() !== null),
map = hasMap ? field.getMapping() : field.getName(),
result, operatorSearch;
if (typeof map === 'function') {
result = fieldVarName + '.getMapping()(' + dataName + ', this)';
}
else if (me.getUseSimpleAccessors() === true || ((operatorSearch = String(map).search(re)) < 0)) {
if (!hasMap || isNaN(map)) {
// If we don't provide a mapping, we may have a field name that is numeric
map = '"' + map + '"';
}
result = dataName + "[" + map + "]";
}
else {
result = dataName + (operatorSearch > 0 ? '.' : '') + map;
}
return result;
}
});
/**
* @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. Note that any fields that have
* {@link Ext.data.Field#persist} set to false will still be ignored.
*/
writeAllFields: true,
/**
* @cfg {String} nameProperty This property is used to read the key for each value that will be sent to the server.
* For example:
*
* Ext.define('Person', {
* extend: 'Ext.data.Model',
* fields: [{
* name: 'first',
* mapping: 'firstName'
* }, {
* name: 'last',
* mapping: 'lastName'
* }, {
* name: 'age'
* }]
* });
*
* new Ext.data.writer.Writer({
* writeAllFields: true,
* nameProperty: 'mapping'
* });
*
* The following data will be sent to the server:
*
* {
* firstName: 'first name value',
* lastName: 'last name value',
* age: 1
* }
*
* If the value is not present, the field name will always be used.
*/
nameProperty: 'name'
},
/**
* Creates new Writer.
* @param {Object} config (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.getDateFormat() || '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;
if (writeAll) {
fields.each(function(field) {
if (field.getPersist()) {
name = field.config[nameProperty] || field.getName();
value = record.get(field.getName());
if (field.getType().type == 'date') {
value = this.writeDate(field, 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);
if (field.getPersist()) {
name = field.config[nameProperty] || field.getName();
value = changes[key];
if (field.getType().type == 'date') {
value = this.writeDate(field, 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
});
/**
* 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} rootProperty
* 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'}]}
*
*/
rootProperty: 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
},
applyRootProperty: function(root) {
if (!root && (this.getEncode() || this.getEncodeRequest())) {
root = 'data';
}
return root;
},
//inherit docs
writeRecords: function(request, data) {
var root = this.getRootProperty(),
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 {
//<debug>
Ext.Logger.error('Must specify a root when using encode');
//</debug>
}
} 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'}]
*/
/**
* @author Ed Spencer
* @class Ext.data.Batch
*
* Provides a mechanism to run one or more {@link Ext.data.Operation operations} in a given order. Fires the `operationcomplete` event
* after the completion of each Operation, and the `complete` event when all Operations have been successfully executed. Fires an `exception`
* event if any of the Operations encounter an exception.
*
* Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes.
*/
Ext.define('Ext.data.Batch', {
mixins: {
observable: 'Ext.mixin.Observable'
},
config: {
/**
* @cfg {Boolean} autoStart `true` to immediately start processing the batch as soon as it is constructed.
*/
autoStart: false,
/**
* @cfg {Boolean} pauseOnException `true` to automatically pause the execution of the batch if any operation encounters an exception.
*/
pauseOnException: true,
/**
* @cfg {Ext.data.Proxy} proxy The proxy this Batch belongs to. Used to make the requests for each operation in the Batch.
*/
proxy: null
},
/**
* The index of the current operation being executed.
* @property current
* @type Number
*/
current: -1,
/**
* The total number of operations in this batch.
* @property total
* @type Number
* @readonly
*/
total: 0,
/**
* `true` if the batch is currently running.
* @property isRunning
* @type Boolean
*/
isRunning: false,
/**
* `true` if this batch has been executed completely.
* @property isComplete
* @type Boolean
*/
isComplete: false,
/**
* `true` if this batch has encountered an exception. This is cleared at the start of each operation.
* @property hasException
* @type Boolean
*/
hasException: false,
/**
* @event complete
* Fired when all operations of this batch have been completed.
* @param {Ext.data.Batch} batch The batch object.
* @param {Object} operation The last operation that was executed.
*/
/**
* @event exception
* Fired when a operation encountered an exception.
* @param {Ext.data.Batch} batch The batch object.
* @param {Object} operation The operation that encountered the exception.
*/
/**
* @event operationcomplete
* Fired when each operation of the batch completes.
* @param {Ext.data.Batch} batch The batch object.
* @param {Object} operation The operation that just completed.
*/
/**
* Creates new Batch object.
* @param {Object} config (optional) Config object.
*/
constructor: function(config) {
var me = this;
me.initConfig(config);
/**
* Ordered array of operations that will be executed by this batch
* @property {Ext.data.Operation[]} operations
*/
me.operations = [];
},
/**
* Adds a new operation to this batch.
* @param {Object} operation The {@link Ext.data.Operation Operation} object.
*/
add: function(operation) {
this.total++;
operation.setBatch(this);
this.operations.push(operation);
},
/**
* Kicks off the execution of the batch, continuing from the next operation if the previous
* operation encountered an exception, or if execution was paused.
*/
start: function() {
this.hasException = false;
this.isRunning = true;
this.runNextOperation();
},
/**
* @private
* Runs the next operation, relative to `this.current`.
*/
runNextOperation: function() {
this.runOperation(this.current + 1);
},
/**
* Pauses execution of the batch, but does not cancel the current operation.
*/
pause: function() {
this.isRunning = false;
},
/**
* Executes a operation by its numeric index.
* @param {Number} index The operation index to run.
*/
runOperation: function(index) {
var me = this,
operations = me.operations,
operation = operations[index],
onProxyReturn;
if (operation === undefined) {
me.isRunning = false;
me.isComplete = true;
me.fireEvent('complete', me, operations[operations.length - 1]);
} else {
me.current = index;
onProxyReturn = function(operation) {
var hasException = operation.hasException();
if (hasException) {
me.hasException = true;
me.fireEvent('exception', me, operation);
} else {
me.fireEvent('operationcomplete', me, operation);
}
if (hasException && me.getPauseOnException()) {
me.pause();
} else {
operation.setCompleted();
me.runNextOperation();
}
};
operation.setStarted();
me.getProxy()[operation.getAction()](operation, onProxyReturn, me);
}
}
});
/**
* @author Ed Spencer
* @aside guide proxies
*
* Proxies are used by {@link Ext.data.Store Stores} to handle the loading and saving of {@link Ext.data.Model Model}
* data. Usually developers will not need to create or interact with proxies directly.
*
* # Types of Proxy
*
* There are two main types of Proxy - {@link Ext.data.proxy.Client Client} and {@link Ext.data.proxy.Server Server}.
* The Client proxies save their data locally and include the following subclasses:
*
* - {@link Ext.data.proxy.LocalStorage LocalStorageProxy} - saves its data to localStorage if the browser supports it
* - {@link Ext.data.proxy.Memory MemoryProxy} - holds data in memory only, any data is lost when the page is refreshed
*
* The Server proxies save their data by sending requests to some remote server. These proxies include:
*
* - {@link Ext.data.proxy.Ajax Ajax} - sends requests to a server on the same domain
* - {@link Ext.data.proxy.JsonP JsonP} - uses JSON-P to send requests to a server on a different domain
*
* Proxies operate on the principle that all operations performed are either Create, Read, Update or Delete. These four
* operations are mapped to the methods {@link #create}, {@link #read}, {@link #update} and {@link #destroy}
* respectively. Each Proxy subclass implements these functions.
*
* The CRUD methods each expect an {@link Ext.data.Operation Operation} object as the sole argument. The Operation
* encapsulates information about the action the Store wishes to perform, the {@link Ext.data.Model model} instances
* that are to be modified, etc. See the {@link Ext.data.Operation Operation} documentation for more details. Each CRUD
* method also accepts a callback function to be called asynchronously on completion.
*
* Proxies also support batching of Operations via a {@link Ext.data.Batch batch} object, invoked by the {@link #batch}
* method.
*/
Ext.define('Ext.data.proxy.Proxy', {
extend: 'Ext.Evented',
alias: 'proxy.proxy',
alternateClassName: ['Ext.data.DataProxy', 'Ext.data.Proxy'],
requires: [
'Ext.data.reader.Json',
'Ext.data.writer.Json',
'Ext.data.Batch',
'Ext.data.Operation'
],
config: {
/**
* @cfg {String} batchOrder
* Comma-separated ordering 'create', 'update' and 'destroy' actions when batching. Override this to set a different
* order for the batched CRUD actions to be executed in.
* @accessor
*/
batchOrder: 'create,update,destroy',
/**
* @cfg {Boolean} batchActions
* True to batch actions of a particular type when synchronizing the store.
* @accessor
*/
batchActions: true,
/**
* @cfg {String/Ext.data.Model} model (required)
* The name of the Model to tie to this Proxy. Can be either the string name of the Model, or a reference to the
* Model constructor.
* @accessor
*/
model: null,
/**
* @cfg {Object/String/Ext.data.reader.Reader} reader
* The Ext.data.reader.Reader to use to decode the server's response or data read from client. This can either be a
* Reader instance, a config object or just a valid Reader type name (e.g. 'json', 'xml').
* @accessor
*/
reader: {
type: 'json'
},
/**
* @cfg {Object/String/Ext.data.writer.Writer} writer
* The Ext.data.writer.Writer to use to encode any request sent to the server or saved to client. This can either be
* a Writer instance, a config object or just a valid Writer type name (e.g. 'json', 'xml').
* @accessor
*/
writer: {
type: 'json'
}
},
isProxy: true,
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);
}
return model;
},
updateModel: function(model) {
if (model) {
var reader = this.getReader();
if (reader && !reader.getModel()) {
reader.setModel(model);
}
}
},
applyReader: function(reader, currentReader) {
return Ext.factory(reader, Ext.data.Reader, currentReader, 'reader');
},
updateReader: function(reader) {
if (reader) {
var model = this.getModel();
if (!model) {
model = reader.getModel();
if (model) {
this.setModel(model);
}
} else {
reader.setModel(model);
}
if (reader.onMetaChange) {
reader.onMetaChange = Ext.Function.createSequence(reader.onMetaChange, this.onMetaChange, this);
}
}
},
onMetaChange: function(data) {
var model = this.getReader().getModel();
if (!this.getModel() && model) {
this.setModel(model);
}
/**
* @event metachange
* Fires whenever the server has sent back new metadata to reconfigure the Reader.
* @param {Ext.data.Proxy} this
* @param {Object} data The metadata sent back from the server
*/
this.fireEvent('metachange', this, data);
},
applyWriter: function(writer, currentWriter) {
return Ext.factory(writer, Ext.data.Writer, currentWriter, 'writer');
},
/**
* Performs the given create operation. If you override this method in a custom Proxy, remember to always call the provided
* callback method when you are done with your operation.
* @param {Ext.data.Operation} operation The Operation to perform
* @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
* @param {Object} scope Scope to execute the callback function in
* @method
*/
create: Ext.emptyFn,
/**
* Performs the given read operation. If you override this method in a custom Proxy, remember to always call the provided
* callback method when you are done with your operation.
* @param {Ext.data.Operation} operation The Operation to perform
* @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
* @param {Object} scope Scope to execute the callback function in
* @method
*/
read: Ext.emptyFn,
/**
* Performs the given update operation. If you override this method in a custom Proxy, remember to always call the provided
* callback method when you are done with your operation.
* @param {Ext.data.Operation} operation The Operation to perform
* @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
* @param {Object} scope Scope to execute the callback function in
* @method
*/
update: Ext.emptyFn,
/**
* Performs the given destroy operation. If you override this method in a custom Proxy, remember to always call the provided
* callback method when you are done with your operation.
* @param {Ext.data.Operation} operation The Operation to perform
* @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
* @param {Object} scope Scope to execute the callback function in
* @method
*/
destroy: Ext.emptyFn,
onDestroy: function() {
Ext.destroy(this.getReader(), this.getWriter());
},
/**
* Performs a batch of {@link Ext.data.Operation Operations}, in the order specified by {@link #batchOrder}. Used
* internally by {@link Ext.data.Store}'s {@link Ext.data.Store#sync sync} method. Example usage:
*
* myProxy.batch({
* create : [myModel1, myModel2],
* update : [myModel3],
* destroy: [myModel4, myModel5]
* });
*
* Where the myModel* above are {@link Ext.data.Model Model} instances - in this case 1 and 2 are new instances and
* have not been saved before, 3 has been saved previously but needs to be updated, and 4 and 5 have already been
* saved but should now be destroyed.
*
* @param {Object} options Object containing one or more properties supported by the batch method:
*
* @param {Object} options.operations Object containing the Model instances to act upon, keyed by action name
*
* @param {Object} [options.listeners] Event listeners object passed straight through to the Batch -
* see {@link Ext.data.Batch} for details
*
* @param {Ext.data.Batch/Object} [options.batch] A {@link Ext.data.Batch} object (or batch config to apply
* to the created batch). If unspecified a default batch will be auto-created.
*
* @param {Function} [options.callback] The function to be called upon completion of processing the batch.
* The callback is called regardless of success or failure and is passed the following parameters:
* @param {Ext.data.Batch} options.callback.batch The {@link Ext.data.Batch batch} that was processed,
* containing all operations in their current state after processing
* @param {Object} options.callback.options The options argument that was originally passed into batch
*
* @param {Function} [options.success] The function to be called upon successful completion of the batch. The
* success function is called only if no exceptions were reported in any operations. If one or more exceptions
* occurred then the `failure` function will be called instead. The success function is called
* with the following parameters:
* @param {Ext.data.Batch} options.success.batch The {@link Ext.data.Batch batch} that was processed,
* containing all operations in their current state after processing
* @param {Object} options.success.options The options argument that was originally passed into batch
*
* @param {Function} [options.failure] The function to be called upon unsuccessful completion of the batch. The
* failure function is called when one or more operations returns an exception during processing (even if some
* operations were also successful). The failure function is called with the following parameters:
* @param {Ext.data.Batch} options.failure.batch The {@link Ext.data.Batch batch} that was processed,
* containing all operations in their current state after processing
* @param {Object} options.failure.options The options argument that was originally passed into batch
*
* @param {Object} [options.scope] The scope in which to execute any callbacks (i.e. the `this` object inside
* the callback, success and/or failure functions). Defaults to the proxy.
*
* @return {Ext.data.Batch} The newly created Batch
*/
batch: function(options, /* deprecated */listeners) {
var me = this,
useBatch = me.getBatchActions(),
model = me.getModel(),
batch,
records;
if (options.operations === undefined) {
// the old-style (operations, listeners) signature was called
// so convert to the single options argument syntax
options = {
operations: options,
listeners: listeners
};
// <debug warn>
Ext.Logger.deprecate('Passes old-style signature to Proxy.batch (operations, listeners). Please convert to single options argument syntax.');
// </debug>
}
if (options.batch && options.batch.isBatch) {
batch = options.batch;
} else {
batch = new Ext.data.Batch(options.batch || {});
}
batch.setProxy(me);
batch.on('complete', Ext.bind(me.onBatchComplete, me, [options], 0));
if (options.listeners) {
batch.on(options.listeners);
}
Ext.each(me.getBatchOrder().split(','), function(action) {
records = options.operations[action];
if (records) {
if (useBatch) {
batch.add(new Ext.data.Operation({
action: action,
records: records,
model: model
}));
} else {
Ext.each(records, function(record) {
batch.add(new Ext.data.Operation({
action : action,
records: [record],
model: model
}));
});
}
}
}, me);
batch.start();
return batch;
},
/**
* @private
* The internal callback that the proxy uses to call any specified user callbacks after completion of a batch
*/
onBatchComplete: function(batchOptions, batch) {
var scope = batchOptions.scope || this;
if (batch.hasException) {
if (Ext.isFunction(batchOptions.failure)) {
Ext.callback(batchOptions.failure, scope, [batch, batchOptions]);
}
} else if (Ext.isFunction(batchOptions.success)) {
Ext.callback(batchOptions.success, scope, [batch, batchOptions]);
}
if (Ext.isFunction(batchOptions.callback)) {
Ext.callback(batchOptions.callback, scope, [batch, batchOptions]);
}
}
}, function() {
// Ext.data2.proxy.ProxyMgr.registerType('proxy', this);
//backwards compatibility
// Ext.data.DataProxy = this;
// Ext.deprecate('platform', '2.0', function() {
// Ext.data2.DataProxy = this;
// }, this);
});
/**
* @author Ed Spencer
*
* Base class for any client-side storage. Used as a superclass for {@link Ext.data.proxy.Memory Memory} and
* {@link Ext.data.proxy.WebStorage Web Storage} proxies. Do not use directly, use one of the subclasses instead.
* @private
*/
Ext.define('Ext.data.proxy.Client', {
extend: 'Ext.data.proxy.Proxy',
alternateClassName: 'Ext.proxy.ClientProxy',
/**
* Abstract function that must be implemented by each ClientProxy subclass. This should purge all record data
* from the client side storage, as well as removing any supporting data (such as lists of record IDs)
*/
clear: function() {
//<debug>
Ext.Logger.error("The Ext.data.proxy.Client subclass that you are using has not defined a 'clear' function. See src/data/ClientProxy.js for details.");
//</debug>
}
});
/**
* @author Ed Spencer
* @aside guide proxies
*
* In-memory proxy. This proxy simply uses a local variable for data storage/retrieval, so its contents are lost on
* every page refresh.
*
* Usually this Proxy isn't used directly, serving instead as a helper to a {@link Ext.data.Store Store} where a reader
* is required to load data. For example, say we have a Store for a User model and have some inline data we want to
* load, but this data isn't in quite the right format: we can use a MemoryProxy with a JsonReader to read it into our
* Store:
*
* //this is the model we will be using in the store
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* {name: 'id', type: 'int'},
* {name: 'name', type: 'string'},
* {name: 'phone', type: 'string', mapping: 'phoneNumber'}
* ]
* }
* });
*
* //this data does not line up to our model fields - the phone field is called phoneNumber
* var data = {
* users: [
* {
* id: 1,
* name: 'Ed Spencer',
* phoneNumber: '555 1234'
* },
* {
* id: 2,
* name: 'Abe Elias',
* phoneNumber: '666 1234'
* }
* ]
* };
*
* //note how we set the 'root' in the reader to match the data structure above
* var store = Ext.create('Ext.data.Store', {
* autoLoad: true,
* model: 'User',
* data : data,
* proxy: {
* type: 'memory',
* reader: {
* type: 'json',
* root: 'users'
* }
* }
* });
*/
Ext.define('Ext.data.proxy.Memory', {
extend: 'Ext.data.proxy.Client',
alias: 'proxy.memory',
alternateClassName: 'Ext.data.MemoryProxy',
isMemoryProxy: true,
config: {
/**
* @cfg {Object} data
* Optional data to pass to configured Reader.
*/
data: []
},
/**
* @private
* Fake processing function to commit the records, set the current operation
* to successful and call the callback if provided. This function is shared
* by the create, update and destroy methods to perform the bare minimum
* processing required for the proxy to register a result from the action.
*/
finishOperation: function(operation, callback, scope) {
if (operation) {
var i = 0,
recs = operation.getRecords(),
len = recs.length;
for (i; i < len; i++) {
recs[i].commit();
}
operation.setCompleted();
operation.setSuccessful();
Ext.callback(callback, scope || this, [operation]);
}
},
/**
* Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
* then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
* there is no real back end in this case there's not much else to do. This method can be easily overridden to
* implement more complex logic if needed.
* @param {Ext.data.Operation} operation The Operation to perform
* @param {Function} callback Callback function to be called when the Operation has completed (whether
* successful or not)
* @param {Object} scope Scope to execute the callback function in
* @method
*/
create: function() {
this.finishOperation.apply(this, arguments);
},
/**
* Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
* then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
* there is no real back end in this case there's not much else to do. This method can be easily overridden to
* implement more complex logic if needed.
* @param {Ext.data.Operation} operation The Operation to perform
* @param {Function} callback Callback function to be called when the Operation has completed (whether
* successful or not)
* @param {Object} scope Scope to execute the callback function in
* @method
*/
update: function() {
this.finishOperation.apply(this, arguments);
},
/**
* Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
* then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
* there is no real back end in this case there's not much else to do. This method can be easily overridden to
* implement more complex logic if needed.
* @param {Ext.data.Operation} operation The Operation to perform
* @param {Function} callback Callback function to be called when the Operation has completed (whether
* successful or not)
* @param {Object} scope Scope to execute the callback function in
* @method
*/
destroy: function() {
this.finishOperation.apply(this, arguments);
},
/**
* Reads data from the configured {@link #data} object. Uses the Proxy's {@link #reader}, if present.
* @param {Ext.data.Operation} operation The read Operation
* @param {Function} callback The callback to call when reading has completed
* @param {Object} scope The scope to call the callback function in
*/
read: function(operation, callback, scope) {
var me = this,
reader = me.getReader();
if (operation.process('read', reader.process(me.getData())) === false) {
this.fireEvent('exception', this, null, operation);
}
Ext.callback(callback, scope || me, [operation]);
},
clear: Ext.emptyFn
});
/**
* @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.
*
* - `asText` - Removes any tags and converts the value to a string.
* - `asUCText` - Removes any tags and converts the value to an uppercase string.
* - `asUCString` - Converts the value to an uppercase string.
* - `asDate` - Converts the value into Unix epoch time.
* - `asFloat` - Converts the value to a floating point number.
* - `asInt` - Converts the value to an integer number.
*
* It is also possible to create a custom `sortType` that can be used throughout
* an application.
*
* 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
* }]
* }
* });
*
* @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
*
* This is a static class containing the system-supplied data types which may be given to a {@link Ext.data.Field Field}.
*
* The properties in this class are used as type indicators in the {@link Ext.data.Field Field} class, so to
* test whether a Field is of a certain type, compare the {@link Ext.data.Field#type type} property against properties
* of this class.
*
* Developers may add their own application-specific data types to this class. Definition names must be UPPERCASE.
* each type definition must contain three properties:
*
* - `convert`: {Function} - 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 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`: {Mixed} - The data object containing the row as read by the Reader.
* Depending on the Reader type, this could be an Array ({@link Ext.data.reader.Array ArrayReader}), an object
* ({@link Ext.data.reader.Json JsonReader}), or an XML element.
* - `sortType`: {Function} - A function to convert the stored data into comparable form, as defined by {@link Ext.data.SortTypes}.
* - `type`: {String} - A textual data type name.
*
* For example, to create a VELatLong field (See the Microsoft Bing Mapping API) containing the latitude/longitude value of a datapoint on a map from a JsonReader data block
* which contained the properties `lat` and `long`, you would define a new data type like this:
*
* // Add a new Field data type which stores a VELatLong object in the Record.
* Ext.data.Types.VELATLONG = {
* convert: function(v, data) {
* return new VELatLong(data.lat, data.long);
* },
* sortType: function(v) {
* return v.Latitude; // When sorting, order by latitude
* },
* type: 'VELatLong'
* };
*
* Then, when declaring a Model, use:
*
* 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 }
* ]
* }
* });
*
* @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.
* 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.
*
* The synonym `INTEGER` is equivalent.
*/
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.
*
* The synonym `NUMBER` is equivalent.
*/
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
* This data type means that the raw data is converted into a Boolean before it is placed into
* a Record. The string "true" and the number 1 are converted to Boolean `true`.
*
* The synonym `BOOLEAN` is equivalent.
*/
BOOL: {
convert: function(value) {
if ((value === undefined || value === null || value === '') && this.getAllowNull()) {
return null;
}
return value !== 'false' && !!value;
},
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(Types.dashesRe, "/")));
//<debug>
if (isNaN(parsed)) {
Ext.Logger.warn("Cannot parse the passed value (" + value + ") into a valid date");
}
//</debug>
}
}
return isNaN(parsed) ? null : parsed;
},
sortType: sortTypes.asDate,
type: 'date'
}
});
Ext.apply(Types, {
/**
* @property {Object} BOOLEAN
* This data type means that the raw data is converted into a Boolean before it is placed into
* a Record. The string "true" and the number 1 are converted to Boolean `true`.
*
* The synonym `BOOL` is equivalent.
*/
BOOLEAN: this.BOOL,
/**
* @property {Object} INTEGER
* This data type means that the raw data is converted into an integer before it is placed into a Record.
*
*The synonym `INT` is equivalent.
*/
INTEGER: this.INT,
/**
* @property {Object} NUMBER
* This data type means that the raw data is converted into a number before it is placed into a Record.
*
* The synonym `FLOAT` is equivalent.
*/
NUMBER: this.FLOAT
});
});
/**
* @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.
*/
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`).
*/
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"`).
*/
sortDir : "ASC",
/**
* @cfg {Boolean} allowBlank
* @private
*
* Used for validating a {@link Ext.data.Model model}. 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.
*/
persist: true,
// Used in LocalStorage stuff
encode: null,
decode: null,
bubbleEvents: 'action'
},
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 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 Ed Spencer
*
* The ModelManager keeps track of all {@link Ext.data.Model} types defined in your application.
*
* ## Creating Model Instances
*
* Model instances can be created by using the {@link Ext.ClassManager#create Ext.create} method. Ext.create replaces
* the deprecated {@link #create Ext.ModelManager.create} method. It is also possible to create a model instance
* this by using the Model type directly. The following 3 snippets are equivalent:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['first', 'last']
* }
* });
*
* // method 1, create using Ext.create (recommended)
* Ext.create('User', {
* first: 'Ed',
* last: 'Spencer'
* });
*
* // method 2, create on the type directly
* new User({
* first: 'Ed',
* last: 'Spencer'
* });
*
* // method 3, create through the manager (deprecated)
* Ext.ModelManager.create({
* first: 'Ed',
* last: 'Spencer'
* }, 'User');
*
* ## Accessing Model Types
*
* A reference to a Model type can be obtained by using the {@link #getModel} function. Since models types
* are normal classes, you can access the type directly. The following snippets are equivalent:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['first', 'last']
* }
* });
*
* // method 1, access model type through the manager
* var UserType = Ext.ModelManager.getModel('User');
*
* // method 2, reference the type directly
* var UserType = User;
*/
Ext.define('Ext.data.ModelManager', {
extend: 'Ext.AbstractManager',
alternateClassName: ['Ext.ModelMgr', 'Ext.ModelManager'],
singleton: true,
/**
* @property defaultProxyType
* The string type of the default Model Proxy.
* @removed 2.0.0
*/
/**
* @property associationStack
* Private stack of associations that must be created once their associated model has been defined.
* @removed 2.0.0
*/
modelNamespace: null,
/**
* Registers a model definition. All model plugins marked with `isDefault: true` are bootstrapped
* immediately, as are any addition plugins defined in the model config.
* @param name
* @param config
* @return {Object}
*/
registerType: function(name, config) {
var proto = config.prototype,
model;
if (proto && proto.isModel) {
// registering an already defined model
model = config;
} else {
config = {
extend: config.extend || 'Ext.data.Model',
config: config
};
model = Ext.define(name, config);
}
this.types[name] = model;
return model;
},
onModelDefined: Ext.emptyFn,
// /**
// * @private
// * Private callback called whenever a model has just been defined. This sets up any associations
// * that were waiting for the given model to be defined.
// * @param {Function} model The model that was just created.
// */
// onModelDefined: function(model) {
// var stack = this.associationStack,
// length = stack.length,
// create = [],
// association, i, created;
//
// for (i = 0; i < length; i++) {
// association = stack[i];
//
// if (association.associatedModel == model.modelName) {
// create.push(association);
// }
// }
//
// for (i = 0, length = create.length; i < length; i++) {
// created = create[i];
// this.types[created.ownerModel].prototype.associations.add(Ext.data.association.Association.create(created));
// Ext.Array.remove(stack, created);
// }
// },
//
// /**
// * Registers an association where one of the models defined doesn't exist yet.
// * The ModelManager will check when new models are registered if it can link them
// * together.
// * @private
// * @param {Ext.data.association.Association} association The association.
// */
// registerDeferredAssociation: function(association){
// this.associationStack.push(association);
// },
/**
* Returns the {@link Ext.data.Model} for a given model name.
* @param {String/Object} id The `id` of the model or the model instance.
* @return {Ext.data.Model} A model class.
*/
getModel: function(id) {
var model = id;
if (typeof model == 'string') {
model = this.types[model];
if (!model && this.modelNamespace) {
model = this.types[this.modelNamespace + '.' + model];
}
}
return model;
},
/**
* Creates a new instance of a Model using the given data.
*
* __Note:__ This method is deprecated. Use {@link Ext.ClassManager#create Ext.create} instead. For example:
*
* Ext.create('User', {
* first: 'Ed',
* last: 'Spencer'
* });
*
* @param {Object} data Data to initialize the Model's fields with.
* @param {String} name The name of the model to create.
* @param {Number} id (optional) Unique id of the Model instance (see {@link Ext.data.Model}).
* @return {Object}
*/
create: function(config, name, id) {
var con = typeof name == 'function' ? name : this.types[name || config.name];
return new con(config, id);
}
}, function() {
/**
* Old way for creating Model classes. Instead use:
*
* Ext.define("MyModel", {
* extend: "Ext.data.Model",
* fields: []
* });
*
* @param {String} name Name of the Model class.
* @param {Object} config A configuration object for the Model you wish to create.
* @return {Ext.data.Model} The newly registered Model.
* @member Ext
* @deprecated 2.0.0 Please use {@link Ext#define} instead.
*/
Ext.regModel = function() {
//<debug>
Ext.Logger.deprecate('Ext.regModel has been deprecated. Models can now be created by ' +
'extending Ext.data.Model: Ext.define("MyModel", {extend: "Ext.data.Model", fields: []});.');
//</debug>
return this.ModelManager.registerType.apply(this.ModelManager, arguments);
};
});
/**
* @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
* Whether or not to disable caching for this request.
*/
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,
/**
* @cfg {String} username
* Most oData feeds require basic HTTP authentication. This configuration allows
* you to specify the username.
* @accessor
*/
username: null,
/**
* @cfg {String} password
* Most oData feeds require basic HTTP authentication. This configuration allows
* you to specify the password.
* @accessor
*/
password: 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
*
* ServerProxy is a superclass of {@link Ext.data.proxy.JsonP JsonPProxy} and {@link Ext.data.proxy.Ajax AjaxProxy}, and
* would not usually be used directly.
* @private
*/
Ext.define('Ext.data.proxy.Server', {
extend: 'Ext.data.proxy.Proxy',
alias : 'proxy.server',
alternateClassName: 'Ext.data.ServerProxy',
requires : ['Ext.data.Request'],
config: {
/**
* @cfg {String} url
* The URL from which to request the data object.
*/
url: null,
/**
* @cfg {String} pageParam
* The name of the `page` parameter to send in a request. Set this to `false` if you don't
* want to send a page parameter.
*/
pageParam: 'page',
/**
* @cfg {String} startParam
* The name of the `start` parameter to send in a request. Set this to `false` if you don't
* want to send a start parameter.
*/
startParam: 'start',
/**
* @cfg {String} limitParam
* The name of the `limit` parameter to send in a request. Set this to `false` if you don't
* want to send a limit parameter.
*/
limitParam: 'limit',
/**
* @cfg {String} groupParam
* The name of the `group` parameter to send in a request. Set this to `false` if you don't
* want to send a group parameter.
*/
groupParam: 'group',
/**
* @cfg {String} sortParam
* The name of the `sort` parameter to send in a request. Set this to `undefined` if you don't
* want to send a sort parameter.
*/
sortParam: 'sort',
/**
* @cfg {String} filterParam
* The name of the 'filter' parameter to send in a request. Set this to `undefined` if you don't
* want to send a filter parameter.
*/
filterParam: 'filter',
/**
* @cfg {String} directionParam
* The name of the direction parameter to send in a request.
*
* __Note:__ This is only used when `simpleSortMode` is set to `true`.
*/
directionParam: 'dir',
/**
* @cfg {Boolean} enablePagingParams This can be set to `false` if you want to prevent the paging params to be
* sent along with the requests made by this proxy.
*/
enablePagingParams: true,
/**
* @cfg {Boolean} simpleSortMode
* Enabling `simpleSortMode` in conjunction with `remoteSort` will only send one sort property and a direction when a
* remote sort is requested. The `directionParam` and `sortParam` will be sent with the property name and either 'ASC'
* or 'DESC'.
*/
simpleSortMode: false,
/**
* @cfg {Boolean} noCache
* Disable caching by adding a unique parameter name to the request. Set to `false` to allow caching.
*/
noCache : true,
/**
* @cfg {String} cacheString
* The name of the cache param added to the url when using `noCache`.
*/
cacheString: "_dc",
/**
* @cfg {Number} timeout
* The number of milliseconds to wait for a response.
*/
timeout : 30000,
/**
* @cfg {Object} api
* Specific urls to call on CRUD action methods "create", "read", "update" and "destroy". Defaults to:
*
* api: {
* create : undefined,
* read : undefined,
* update : undefined,
* destroy : undefined
* }
*
* The url is built based upon the action being executed [create|read|update|destroy] using the commensurate
* {@link #api} property, or if undefined default to the configured
* {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.
*
* For example:
*
* api: {
* create : '/controller/new',
* read : '/controller/load',
* update : '/controller/update',
* destroy : '/controller/destroy_action'
* }
*
* If the specific URL for a given CRUD action is undefined, the CRUD action request will be directed to the
* configured {@link Ext.data.proxy.Server#url url}.
*/
api: {
create : undefined,
read : undefined,
update : undefined,
destroy : undefined
},
/**
* @cfg {Object} extraParams
* Extra parameters that will be included on every request. Individual requests with params of the same name
* will override these params when they are in conflict.
*/
extraParams: {}
},
constructor: function(config) {
config = config || {};
if (config.nocache !== undefined) {
config.noCache = config.nocache;
// <debug>
Ext.Logger.warn('nocache configuration on Ext.data.proxy.Server has been deprecated. Please use noCache.');
// </debug>
}
this.callParent([config]);
},
//in a ServerProxy all four CRUD operations are executed in the same manner, so we delegate to doRequest in each case
create: function() {
return this.doRequest.apply(this, arguments);
},
read: function() {
return this.doRequest.apply(this, arguments);
},
update: function() {
return this.doRequest.apply(this, arguments);
},
destroy: function() {
return this.doRequest.apply(this, arguments);
},
/**
* Sets a value in the underlying {@link #extraParams}.
* @param {String} name The key for the new value
* @param {Object} value The value
*/
setExtraParam: function(name, value) {
this.getExtraParams()[name] = value;
},
/**
* Creates and returns an Ext.data.Request object based on the options passed by the {@link Ext.data.Store Store}
* that this Proxy is attached to.
* @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute
* @return {Ext.data.Request} The request object
*/
buildRequest: function(operation) {
var me = this,
params = Ext.applyIf(operation.getParams() || {}, me.getExtraParams() || {}),
request;
//copy any sorters, filters etc into the params so they can be sent over the wire
params = Ext.applyIf(params, me.getParams(operation));
request = Ext.create('Ext.data.Request', {
params : params,
action : operation.getAction(),
records : operation.getRecords(),
url : operation.getUrl(),
operation: operation,
proxy : me
});
request.setUrl(me.buildUrl(request));
operation.setRequest(request);
return request;
},
/**
* This method handles the processing of the response and is usually overridden by subclasses to
* do additional processing.
* @param {Boolean} success Whether or not this request was successful
* @param {Ext.data.Operation} operation The operation we made this request for
* @param {Ext.data.Request} request The request that was made
* @param {Object} response The response that we got
* @param {Function} callback The callback to be fired onces the response is processed
* @param {Object} scope The scope in which we call the callback
* @protected
*/
processResponse: function(success, operation, request, response, callback, scope) {
var me = this,
action = operation.getAction(),
reader, resultSet;
if (success === true) {
reader = me.getReader();
try {
resultSet = reader.process(response);
} catch(e) {
operation.setException(e.message);
me.fireEvent('exception', this, response, operation);
return;
}
// This could happen if the model was configured using metaData
if (!operation.getModel()) {
operation.setModel(this.getModel());
}
if (operation.process(action, resultSet, request, response) === false) {
this.fireEvent('exception', this, response, operation);
}
} else {
me.setException(operation, response);
/**
* @event exception
* Fires when the server returns an exception
* @param {Ext.data.proxy.Proxy} this
* @param {Object} response The response from the AJAX request
* @param {Ext.data.Operation} operation The operation that triggered request
*/
me.fireEvent('exception', this, response, operation);
}
//this callback is the one that was passed to the 'read' or 'write' function above
if (typeof callback == 'function') {
callback.call(scope || me, operation);
}
me.afterRequest(request, success);
},
/**
* Sets up an exception on the operation
* @private
* @param {Ext.data.Operation} operation The operation
* @param {Object} response The response
*/
setException: function(operation, response) {
if (Ext.isObject(response)) {
operation.setException({
status: response.status,
statusText: response.statusText
});
}
},
/**
* Encode any values being sent to the server. Can be overridden in subclasses.
* @private
* @param {Array} value An array of sorters/filters.
* @return {Object} The encoded value
*/
applyEncoding: function(value) {
return Ext.encode(value);
},
/**
* Encodes the array of {@link Ext.util.Sorter} objects into a string to be sent in the request url. By default,
* this simply JSON-encodes the sorter data
* @param {Ext.util.Sorter[]} sorters The array of {@link Ext.util.Sorter Sorter} objects
* @return {String} The encoded sorters
*/
encodeSorters: function(sorters) {
var min = [],
length = sorters.length,
i = 0;
for (; i < length; i++) {
min[i] = {
property : sorters[i].getProperty(),
direction: sorters[i].getDirection()
};
}
return this.applyEncoding(min);
},
/**
* Encodes the array of {@link Ext.util.Filter} objects into a string to be sent in the request url. By default,
* this simply JSON-encodes the filter data
* @param {Ext.util.Filter[]} filters The array of {@link Ext.util.Filter Filter} objects
* @return {String} The encoded filters
*/
encodeFilters: function(filters) {
var min = [],
length = filters.length,
i = 0;
for (; i < length; i++) {
min[i] = {
property: filters[i].getProperty(),
value : filters[i].getValue()
};
}
return this.applyEncoding(min);
},
/**
* @private
* Copy any sorters, filters etc into the params so they can be sent over the wire
*/
getParams: function(operation) {
var me = this,
params = {},
grouper = operation.getGrouper(),
sorters = operation.getSorters(),
filters = operation.getFilters(),
page = operation.getPage(),
start = operation.getStart(),
limit = operation.getLimit(),
simpleSortMode = me.getSimpleSortMode(),
pageParam = me.getPageParam(),
startParam = me.getStartParam(),
limitParam = me.getLimitParam(),
groupParam = me.getGroupParam(),
sortParam = me.getSortParam(),
filterParam = me.getFilterParam(),
directionParam = me.getDirectionParam();
if (me.getEnablePagingParams()) {
if (pageParam && page !== null) {
params[pageParam] = page;
}
if (startParam && start !== null) {
params[startParam] = start;
}
if (limitParam && limit !== null) {
params[limitParam] = limit;
}
}
if (groupParam && grouper) {
// Grouper is a subclass of sorter, so we can just use the sorter method
params[groupParam] = me.encodeSorters([grouper]);
}
if (sortParam && sorters && sorters.length > 0) {
if (simpleSortMode) {
params[sortParam] = sorters[0].getProperty();
params[directionParam] = sorters[0].getDirection();
} else {
params[sortParam] = me.encodeSorters(sorters);
}
}
if (filterParam && filters && filters.length > 0) {
params[filterParam] = me.encodeFilters(filters);
}
return params;
},
/**
* Generates a url based on a given Ext.data.Request object. By default, ServerProxy's buildUrl will add the
* cache-buster param to the end of the url. Subclasses may need to perform additional modifications to the url.
* @param {Ext.data.Request} request The request object
* @return {String} The url
*/
buildUrl: function(request) {
var me = this,
url = me.getUrl(request);
//<debug>
if (!url) {
Ext.Logger.error("You are using a ServerProxy but have not supplied it with a url.");
}
//</debug>
if (me.getNoCache()) {
url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.getCacheString(), Ext.Date.now()));
}
return url;
},
/**
* Get the url for the request taking into account the order of priority,
* - The request
* - The api
* - The url
* @private
* @param {Ext.data.Request} request The request
* @return {String} The url
*/
getUrl: function(request) {
return request ? request.getUrl() || this.getApi()[request.getAction()] || this._url : this._url;
},
/**
* In ServerProxy subclasses, the {@link #create}, {@link #read}, {@link #update} and {@link #destroy} methods all
* pass through to doRequest. Each ServerProxy subclass must implement the doRequest method - see {@link
* Ext.data.proxy.JsonP} and {@link Ext.data.proxy.Ajax} for examples. This method carries the same signature as
* each of the methods that delegate to it.
*
* @param {Ext.data.Operation} operation The Ext.data.Operation object
* @param {Function} callback The callback function to call when the Operation has completed
* @param {Object} scope The scope in which to execute the callback
* @protected
* @template
*/
doRequest: function(operation, callback, scope) {
//<debug>
Ext.Logger.error("The doRequest function has not been implemented on your Ext.data.proxy.Server subclass. See src/data/ServerProxy.js for details");
//</debug>
},
/**
* Optional callback function which can be used to clean up after a request has been completed.
* @param {Ext.data.Request} request The Request object
* @param {Boolean} success True if the request was successful
* @method
*/
afterRequest: Ext.emptyFn
});
/**
* @author Ed Spencer
* @aside guide proxies
*
* AjaxProxy is one of the most widely-used ways of getting data into your application. It uses AJAX
* requests to load data from the server, usually to be placed into a {@link Ext.data.Store Store}.
* Let's take a look at a typical setup. Here we're going to set up a Store that has an AjaxProxy.
* To prepare, we'll also set up a {@link Ext.data.Model Model}:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['id', 'name', 'email']
* }
* });
*
* // The Store contains the AjaxProxy as an inline configuration
* var store = Ext.create('Ext.data.Store', {
* model: 'User',
* proxy: {
* type: 'ajax',
* url : 'users.json'
* }
* });
*
* store.load();
*
* Our example is going to load user data into a Store, so we start off by defining a
* {@link Ext.data.Model Model} with the fields that we expect the server to return. Next we set up
* the Store itself, along with a {@link Ext.data.Store#proxy proxy} configuration. This
* configuration was automatically turned into an Ext.data.proxy.Ajax instance, with the url we
* specified being passed into AjaxProxy's constructor. It's as if we'd done this:
*
* Ext.create('Ext.data.proxy.Ajax', {
* config: {
* url: 'users.json',
* model: 'User',
* reader: 'json'
* }
* });
*
* A couple of extra configurations appeared here - {@link #model} and {@link #reader}. These are
* set by default when we create the proxy via the Store - the Store already knows about the Model,
* and Proxy's default {@link Ext.data.reader.Reader Reader} is {@link Ext.data.reader.Json JsonReader}.
*
* Now when we call store.load(), the AjaxProxy springs into action, making a request to the url we
* configured ('users.json' in this case). As we're performing a read, it sends a GET request to
* that url (see {@link #actionMethods} to customize this - by default any kind of read will be sent
* as a GET request and any kind of write will be sent as a POST request).
*
* ## Limitations
*
* AjaxProxy cannot be used to retrieve data from other domains. If your application is running on
* http://domainA.com it cannot load data from http://domainB.com because browsers have a built-in
* security policy that prohibits domains talking to each other via AJAX.
*
* If you need to read data from another domain and can't set up a proxy server (some software that
* runs on your own domain's web server and transparently forwards requests to http://domainB.com,
* making it look like they actually came from http://domainA.com), you can use
* {@link Ext.data.proxy.JsonP} and a technique known as JSON-P (JSON with Padding), which can help
* you get around the problem so long as the server on http://domainB.com is set up to support
* JSON-P responses. See {@link Ext.data.proxy.JsonP JsonPProxy}'s introduction docs for more details.
*
* ## Readers and Writers
*
* AjaxProxy can be configured to use any type of {@link Ext.data.reader.Reader Reader} to decode
* the server's response. If no Reader is supplied, AjaxProxy will default to using a
* {@link Ext.data.reader.Json JsonReader}. Reader configuration can be passed in as a simple
* object, which the Proxy automatically turns into a {@link Ext.data.reader.Reader Reader} instance:
*
* var proxy = Ext.create('Ext.data.proxy.Ajax', {
* config: {
* model: 'User',
* reader: {
* type: 'xml',
* root: 'users'
* }
* }
* });
*
* proxy.getReader(); //returns an {@link Ext.data.reader.Xml XmlReader} instance based on the config we supplied
*
* ## Url generation
*
* AjaxProxy automatically inserts any sorting, filtering, paging and grouping options into the url
* it generates for each request. These are controlled with the following configuration options:
*
* - {@link #pageParam} - controls how the page number is sent to the server (see also
* {@link #startParam} and {@link #limitParam})
* - {@link #sortParam} - controls how sort information is sent to the server
* - {@link #groupParam} - controls how grouping information is sent to the server
* - {@link #filterParam} - controls how filter information is sent to the server
*
* Each request sent by AjaxProxy is described by an {@link Ext.data.Operation Operation}. To see
* how we can customize the generated urls, let's say we're loading the Proxy with the following
* Operation:
*
* var operation = Ext.create('Ext.data.Operation', {
* action: 'read',
* page : 2
* });
*
* Now we'll issue the request for this Operation by calling {@link #read}:
*
* var proxy = Ext.create('Ext.data.proxy.Ajax', {
* url: '/users'
* });
*
* proxy.read(operation); // GET /users?page=2
*
* Easy enough - the Proxy just copied the page property from the Operation. We can customize how
* this page data is sent to the server:
*
* var proxy = Ext.create('Ext.data.proxy.Ajax', {
* url: '/users',
* pageParam: 'pageNumber'
* });
*
* proxy.read(operation); // GET /users?pageNumber=2
*
* Alternatively, our Operation could have been configured to send start and limit parameters
* instead of page:
*
* var operation = Ext.create('Ext.data.Operation', {
* action: 'read',
* start : 50,
* limit : 25
* });
*
* var proxy = Ext.create('Ext.data.proxy.Ajax', {
* url: '/users'
* });
*
* proxy.read(operation); // GET /users?start=50&limit;=25
*
* Again we can customize this url:
*
* var proxy = Ext.create('Ext.data.proxy.Ajax', {
* url: '/users',
* startParam: 'startIndex',
* limitParam: 'limitIndex'
* });
*
* proxy.read(operation); // GET /users?startIndex=50&limitIndex;=25
*
* AjaxProxy will also send sort and filter information to the server. Let's take a look at how this
* looks with a more expressive Operation object:
*
* var operation = Ext.create('Ext.data.Operation', {
* action: 'read',
* sorters: [
* Ext.create('Ext.util.Sorter', {
* property : 'name',
* direction: 'ASC'
* }),
* Ext.create('Ext.util.Sorter', {
* property : 'age',
* direction: 'DESC'
* })
* ],
* filters: [
* Ext.create('Ext.util.Filter', {
* property: 'eyeColor',
* value : 'brown'
* })
* ]
* });
*
* This is the type of object that is generated internally when loading a {@link Ext.data.Store Store}
* with sorters and filters defined. By default the AjaxProxy will JSON encode the sorters and
* filters, resulting in something like this (note that the url is escaped before sending the
* request, but is left unescaped here for clarity):
*
* var proxy = Ext.create('Ext.data.proxy.Ajax', {
* url: '/users'
* });
*
* proxy.read(operation); // GET /users?sort=[{"property":"name","direction":"ASC"},{"property":"age","direction":"DESC"}]&filter;=[{"property":"eyeColor","value":"brown"}]
*
* We can again customize how this is created by supplying a few configuration options. Let's say
* our server is set up to receive sorting information is a format like "sortBy=name#ASC,age#DESC".
* We can configure AjaxProxy to provide that format like this:
*
* var proxy = Ext.create('Ext.data.proxy.Ajax', {
* url: '/users',
* sortParam: 'sortBy',
* filterParam: 'filterBy',
*
* // our custom implementation of sorter encoding - turns our sorters into "name#ASC,age#DESC"
* encodeSorters: function(sorters) {
* var length = sorters.length,
* sortStrs = [],
* sorter, i;
*
* for (i = 0; i < length; i++) {
* sorter = sorters[i];
*
* sortStrs[i] = sorter.property + '#' + sorter.direction;
* }
*
* return sortStrs.join(",");
* }
* });
*
* proxy.read(operation); // GET /users?sortBy=name#ASC,age#DESC&filterBy;=[{"property":"eyeColor","value":"brown"}]
*
* We can also provide a custom {@link #encodeFilters} function to encode our filters.
*
* @constructor
* Note that if this HttpProxy is being used by a {@link Ext.data.Store Store}, then the Store's
* call to {@link Ext.data.Store#method-load load} will override any specified callback and params
* options. In this case, use the {@link Ext.data.Store Store}'s events to modify parameters, or
* react to loading events.
*
* @param {Object} config (optional) Config object.
* If an options parameter is passed, the singleton {@link Ext.Ajax} object will be used to
* make the request.
*/
Ext.define('Ext.data.proxy.Ajax', {
extend: 'Ext.data.proxy.Server',
requires: ['Ext.util.MixedCollection', 'Ext.Ajax'],
alias: 'proxy.ajax',
alternateClassName: ['Ext.data.HttpProxy', 'Ext.data.AjaxProxy'],
config: {
/**
* @cfg {Boolean} withCredentials
* This configuration is sometimes necessary when using cross-origin resource sharing.
* @accessor
*/
withCredentials: false,
/**
* @cfg {String} username
* Most oData feeds require basic HTTP authentication. This configuration allows
* you to specify the username.
* @accessor
*/
username: null,
/**
* @cfg {String} password
* Most oData feeds require basic HTTP authentication. This configuration allows
* you to specify the password.
* @accessor
*/
password: null,
/**
* @property {Object} actionMethods
* Mapping of action name to HTTP request method. In the basic AjaxProxy these are set to
* 'GET' for 'read' actions and 'POST' for 'create', 'update' and 'destroy' actions.
* The {@link Ext.data.proxy.Rest} maps these to the correct RESTful methods.
*/
actionMethods: {
create : 'POST',
read : 'GET',
update : 'POST',
destroy: 'POST'
},
/**
* @cfg {Object} [headers=undefined]
* Any headers to add to the Ajax request.
*/
headers: {}
},
/**
* Performs Ajax request.
* @protected
* @param operation
* @param callback
* @param scope
* @return {Object}
*/
doRequest: function(operation, callback, scope) {
var me = this,
writer = me.getWriter(),
request = me.buildRequest(operation);
request.setConfig({
headers : me.getHeaders(),
timeout : me.getTimeout(),
method : me.getMethod(request),
callback : me.createRequestCallback(request, operation, callback, scope),
scope : me,
proxy : me
});
if (operation.getWithCredentials() || me.getWithCredentials()) {
request.setWithCredentials(true);
request.setUsername(me.getUsername());
request.setPassword(me.getPassword());
}
// We now always have the writer prepare the request
request = writer.write(request);
Ext.Ajax.request(request.getCurrentConfig());
return request;
},
/**
* Returns the HTTP method name for a given request. By default this returns based on a lookup on
* {@link #actionMethods}.
* @param {Ext.data.Request} request The request object.
* @return {String} The HTTP method to use (should be one of 'GET', 'POST', 'PUT' or 'DELETE').
*/
getMethod: function(request) {
return this.getActionMethods()[request.getAction()];
},
/**
* @private
* @param {Ext.data.Request} request The Request object.
* @param {Ext.data.Operation} operation The Operation being executed.
* @param {Function} callback The callback function to be called when the request completes.
* This is usually the callback passed to `doRequest`.
* @param {Object} scope The scope in which to execute the callback function.
* @return {Function} The callback function.
*/
createRequestCallback: function(request, operation, callback, scope) {
var me = this;
return function(options, success, response) {
me.processResponse(success, operation, request, response, callback, scope);
};
}
});
/**
* @author Ed Spencer
* @aside guide models
*
* Associations enable you to express relationships between different {@link Ext.data.Model Models}. Let's say we're
* writing an ecommerce system where Users can make Orders - there's a relationship between these Models that we can
* express like this:
*
* Ext.define('MyApp.model.User', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: ['id', 'name', 'email'],
* hasMany: {
* model: 'MyApp.model.Order',
* name: 'orders'
* }
* }
* });
*
* Ext.define('MyApp.model.Order', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: ['id', 'user_id', 'status', 'price'],
* belongsTo: 'MyApp.model.User'
* }
* });
*
* We've set up two models - User and Order - and told them about each other. You can set up as many associations on
* each Model as you need using the two default types - {@link Ext.data.association.HasMany hasMany} and
* {@link Ext.data.association.BelongsTo belongsTo}. There's much more detail on the usage of each of those inside their
* documentation pages. If you're not familiar with Models already, {@link Ext.data.Model there is plenty on those too}.
*
* ## Further Reading
*
* - {@link Ext.data.association.HasMany hasMany associations}
* - {@link Ext.data.association.BelongsTo belongsTo associations}
* - {@link Ext.data.association.HasOne hasOne associations}
* - {@link Ext.data.Model using Models}
*
* ### Self-associating Models
*
* We can also have models that create parent/child associations between the same type. Below is an example, where
* groups can be nested inside other groups:
*
* // Server Data
* {
* "groups": {
* "id": 10,
* "parent_id": 100,
* "name": "Main Group",
* "parent_group": {
* "id": 100,
* "parent_id": null,
* "name": "Parent Group"
* },
* "nested" : {
* "child_groups": [{
* "id": 2,
* "parent_id": 10,
* "name": "Child Group 1"
* },{
* "id": 3,
* "parent_id": 10,
* "name": "Child Group 2"
* },{
* "id": 4,
* "parent_id": 10,
* "name": "Child Group 3"
* }]
* }
* }
* }
*
* // Client code
* Ext.define('MyApp.model.Group', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['id', 'parent_id', 'name'],
* proxy: {
* type: 'ajax',
* url: 'data.json',
* reader: {
* type: 'json',
* root: 'groups'
* }
* },
* associations: [{
* type: 'hasMany',
* model: 'MyApp.model.Group',
* primaryKey: 'id',
* foreignKey: 'parent_id',
* autoLoad: true,
* associationKey: 'nested.child_groups' // read child data from nested.child_groups
* }, {
* type: 'belongsTo',
* model: 'MyApp.model.Group',
* primaryKey: 'id',
* foreignKey: 'parent_id',
* associationKey: 'parent_group' // read parent data from parent_group
* }]
* }
* });
*
*
* Ext.onReady(function(){
* MyApp.model.Group.load(10, {
* success: function(group){
* console.log(group.getGroup().get('name'));
*
* group.groups().each(function(rec){
* console.log(rec.get('name'));
* });
* }
* });
*
* });
*/
Ext.define('Ext.data.association.Association', {
alternateClassName: 'Ext.data.Association',
requires: ['Ext.data.ModelManager'],
config: {
/**
* @cfg {Ext.data.Model/String} ownerModel (required) The full class name or reference to the class that owns this
* associations. This is a required configuration on every association.
* @accessor
*/
ownerModel: null,
/*
* @cfg {String} ownerName The name for the owner model. This defaults to the last part
* of the class name of the {@link #ownerModel}.
*/
ownerName: undefined,
/**
* @cfg {String} associatedModel (required) The full class name or reference to the class that the {@link #ownerModel}
* is being associated with. This is a required configuration on every association.
* @accessor
*/
associatedModel: null,
/**
* @cfg {String} associatedName The name for the associated model. This defaults to the last part
* of the class name of the {@link #associatedModel}.
* @accessor
*/
associatedName: undefined,
/**
* @cfg {String} associationKey The name of the property in the data to read the association from.
* Defaults to the {@link #associatedName} plus '_id'.
*/
associationKey: undefined,
/**
* @cfg {String} primaryKey The name of the primary key on the associated model.
* In general this will be the {@link Ext.data.Model#idProperty} of the Model.
*/
primaryKey: 'id',
/**
* @cfg {Ext.data.reader.Reader} reader A special reader to read associated data.
*/
reader: null,
/**
* @cfg {String} type The type configuration can be used when creating associations using a configuration object.
* Use `hasMany` to create a HasMany association.
*
* associations: [{
* type: 'hasMany',
* model: 'User'
* }]
*/
type: null,
name: undefined
},
statics: {
create: function(association) {
if (!association.isAssociation) {
if (Ext.isString(association)) {
association = {
type: association
};
}
association.type = association.type.toLowerCase();
return Ext.factory(association, Ext.data.association.Association, null, 'association');
}
return association;
}
},
/**
* Creates the Association object.
* @param {Object} config (optional) Config object.
*/
constructor: function(config) {
this.initConfig(config);
},
applyName: function(name) {
if (!name) {
name = this.getAssociatedName();
}
return name;
},
applyOwnerModel: function(ownerName) {
var ownerModel = Ext.data.ModelManager.getModel(ownerName);
if (ownerModel === undefined) {
Ext.Logger.error('The configured ownerModel was not valid (you tried ' + ownerName + ')');
}
return ownerModel;
},
applyOwnerName: function(ownerName) {
if (!ownerName) {
ownerName = this.getOwnerModel().modelName;
}
ownerName = ownerName.slice(ownerName.lastIndexOf('.')+1);
return ownerName;
},
updateOwnerModel: function(ownerModel, oldOwnerModel) {
if (oldOwnerModel) {
this.setOwnerName(ownerModel.modelName);
}
},
applyAssociatedModel: function(associatedName) {
var associatedModel = Ext.data.ModelManager.types[associatedName];
if (associatedModel === undefined) {
Ext.Logger.error('The configured associatedModel was not valid (you tried ' + associatedName + ')');
}
return associatedModel;
},
applyAssociatedName: function(associatedName) {
if (!associatedName) {
associatedName = this.getAssociatedModel().modelName;
}
associatedName = associatedName.slice(associatedName.lastIndexOf('.')+1);
return associatedName;
},
updateAssociatedModel: function(associatedModel, oldAssociatedModel) {
if (oldAssociatedModel) {
this.setAssociatedName(associatedModel.modelName);
}
},
applyReader: function(reader) {
if (reader) {
if (Ext.isString(reader)) {
reader = {
type: reader
};
}
if (!reader.isReader) {
Ext.applyIf(reader, {
type: 'json'
});
}
}
return Ext.factory(reader, Ext.data.Reader, this.getReader(), 'reader');
},
updateReader: function(reader) {
reader.setModel(this.getAssociatedModel());
}
// Convert old properties in data into a config object
});
/**
* General purpose inflector class that {@link #pluralize pluralizes}, {@link #singularize singularizes} and
* {@link #ordinalize ordinalizes} words. Sample usage:
*
* // turning singular words into plurals
* Ext.util.Inflector.pluralize('word'); // 'words'
* Ext.util.Inflector.pluralize('person'); // 'people'
* Ext.util.Inflector.pluralize('sheep'); // 'sheep'
*
* // turning plurals into singulars
* Ext.util.Inflector.singularize('words'); // 'word'
* Ext.util.Inflector.singularize('people'); // 'person'
* Ext.util.Inflector.singularize('sheep'); // 'sheep'
*
* // ordinalizing numbers
* Ext.util.Inflector.ordinalize(11); // "11th"
* Ext.util.Inflector.ordinalize(21); // "21st"
* Ext.util.Inflector.ordinalize(1043); // "1043rd"
*
* ## Customization
*
* The Inflector comes with a default set of US English pluralization rules. These can be augmented with additional
* rules if the default rules do not meet your application's requirements, or swapped out entirely for other languages.
* Here is how we might add a rule that pluralizes "ox" to "oxen":
*
* Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
*
* Each rule consists of two items - a regular expression that matches one or more rules, and a replacement string.
* In this case, the regular expression will only match the string "ox", and will replace that match with "oxen".
* Here's how we could add the inverse rule:
*
* Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
*
* __Note:__ The ox/oxen rules are present by default.
*/
Ext.define('Ext.util.Inflector', {
/* Begin Definitions */
singleton: true,
/* End Definitions */
/**
* @private
* The registered plural tuples. Each item in the array should contain two items - the first must be a regular
* expression that matchers the singular form of a word, the second must be a String that replaces the matched
* part of the regular expression. This is managed by the {@link #plural} method.
* @property 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);
}
});
/**
* @aside guide models
*
* Represents a one-to-many relationship between two models. Usually created indirectly via a model definition:
*
* Ext.define('Product', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* {name: 'id', type: 'int'},
* {name: 'user_id', type: 'int'},
* {name: 'name', type: 'string'}
* ]
* }
* });
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* {name: 'id', type: 'int'},
* {name: 'name', type: 'string'}
* ],
* // we can use the hasMany shortcut on the model to create a hasMany association
* hasMany: {model: 'Product', name: 'products'}
* }
* });
*
* `
*
* Above we created Product and User models, and linked them by saying that a User hasMany Products. This gives us a new
* function on every User instance, in this case the function is called 'products' because that is the name we specified
* in the association configuration above.
*
* This new function returns a specialized {@link Ext.data.Store Store} which is automatically filtered to load only
* Products for the given model instance:
*
* //first, we load up a User with id of 1
* var user = Ext.create('User', {id: 1, name: 'Ed'});
*
* //the user.products function was created automatically by the association and returns a {@link Ext.data.Store Store}
* //the created store is automatically scoped to the set of Products for the User with id of 1
* var products = user.products();
*
* //we still have all of the usual Store functions, for example it's easy to add a Product for this User
* products.add({
* name: 'Another Product'
* });
*
* //saves the changes to the store - this automatically sets the new Product's user_id to 1 before saving
* products.sync();
*
* The new Store is only instantiated the first time you call products() to conserve memory and processing time, though
* calling products() a second time returns the same store instance.
*
* _Custom filtering_
*
* The Store is automatically furnished with a filter - by default this filter tells the store to only return records
* where the associated model's foreign key matches the owner model's primary key. For example, if a User with ID = 100
* hasMany Products, the filter loads only Products with user_id == 100.
*
* Sometimes we want to filter by another field - for example in the case of a Twitter search application we may have
* models for Search and Tweet:
*
* Ext.define('Search', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* 'id', 'query'
* ],
*
* hasMany: {
* model: 'Tweet',
* name : 'tweets',
* filterProperty: 'query'
* }
* }
* });
*
* Ext.define('Tweet', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* 'id', 'text', 'from_user'
* ]
* }
* });
*
* //returns a Store filtered by the filterProperty
* var store = new Search({query: 'Sencha Touch'}).tweets();
*
* The tweets association above is filtered by the query property by setting the {@link #filterProperty}, and is
* equivalent to this:
*
* var store = Ext.create('Ext.data.Store', {
* model: 'Tweet',
* filters: [
* {
* property: 'query',
* value : 'Sencha Touch'
* }
* ]
* });
*/
Ext.define('Ext.data.association.HasMany', {
extend: 'Ext.data.association.Association',
alternateClassName: 'Ext.data.HasManyAssociation',
requires: ['Ext.util.Inflector'],
alias: 'association.hasmany',
config: {
/**
* @cfg {String} foreignKey
* The name of the foreign key on the associated model that links it to the owner model. Defaults to the
* lowercased name of the owner model plus "_id", e.g. an association with a model called Group hasMany Users
* would create 'group_id' as the foreign key. When the remote store is loaded, the store is automatically
* filtered so that only records with a matching foreign key are included in the resulting child store. This can
* be overridden by specifying the {@link #filterProperty}.
*
* Ext.define('Group', {
* extend: 'Ext.data.Model',
* fields: ['id', 'name'],
* hasMany: 'User'
* });
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* fields: ['id', 'name', 'group_id'], // refers to the id of the group that this user belongs to
* belongsTo: 'Group'
* });
*/
foreignKey: undefined,
/**
* @cfg {String} name
* The name of the function to create on the owner model to retrieve the child store. If not specified, the
* pluralized name of the child model is used.
*
* // This will create a users() method on any Group model instance
* Ext.define('Group', {
* extend: 'Ext.data.Model',
* fields: ['id', 'name'],
* hasMany: 'User'
* });
* var group = new Group();
* console.log(group.users());
*
* // The method to retrieve the users will now be getUserList
* Ext.define('Group', {
* extend: 'Ext.data.Model',
* fields: ['id', 'name'],
* hasMany: {model: 'User', name: 'getUserList'}
* });
* var group = new Group();
* console.log(group.getUserList());
*/
/**
* @cfg {Object} store
* Optional configuration object that will be passed to the generated Store. Defaults to an empty Object.
*/
store: undefined,
/**
* @cfg {String} storeName
* Optional The name of the store by which you can reference it on this class as a property.
*/
storeName: undefined,
/**
* @cfg {String} filterProperty
* Optionally overrides the default filter that is set up on the associated Store. If this is not set, a filter
* is automatically created which filters the association based on the configured {@link #foreignKey}. See intro
* docs for more details.
*/
filterProperty: null,
/**
* @cfg {Boolean} autoLoad
* `true` to automatically load the related store from a remote source when instantiated.
*/
autoLoad: false,
/**
* @cfg {Boolean} autoSync
* true to automatically synchronize the related store with the remote source
*/
autoSync: false
},
constructor: function(config) {
config = config || {};
if (config.storeConfig) {
// <debug>
Ext.Logger.warn('storeConfig is deprecated on an association. Instead use the store configuration.');
// </debug>
config.store = config.storeConfig;
delete config.storeConfig;
}
this.callParent([config]);
},
applyName: function(name) {
if (!name) {
name = Ext.util.Inflector.pluralize(this.getAssociatedName().toLowerCase());
}
return name;
},
applyStoreName: function(name) {
if (!name) {
name = this.getName() + 'Store';
}
return name;
},
applyForeignKey: function(foreignKey) {
if (!foreignKey) {
var inverse = this.getInverseAssociation();
if (inverse) {
foreignKey = inverse.getForeignKey();
} else {
foreignKey = this.getOwnerName().toLowerCase() + '_id';
}
}
return foreignKey;
},
applyAssociationKey: function(associationKey) {
if (!associationKey) {
var associatedName = this.getAssociatedName();
associationKey = Ext.util.Inflector.pluralize(associatedName[0].toLowerCase() + associatedName.slice(1));
}
return associationKey;
},
updateForeignKey: function(foreignKey, oldForeignKey) {
var fields = this.getAssociatedModel().getFields(),
field = fields.get(foreignKey);
if (!field) {
field = new Ext.data.Field({
name: foreignKey
});
fields.add(field);
fields.isDirty = true;
}
if (oldForeignKey) {
field = fields.get(oldForeignKey);
if (field) {
fields.remove(field);
fields.isDirty = true;
}
}
},
/**
* @private
* Creates a function that returns an Ext.data.Store which is configured to load a set of data filtered
* by the owner model's primary key - e.g. in a `hasMany` association where Group `hasMany` Users, this function
* returns a Store configured to return the filtered set of a single Group's Users.
* @return {Function} The store-generating function.
*/
applyStore: function(storeConfig) {
var me = this,
association = me,
associatedModel = me.getAssociatedModel(),
storeName = me.getStoreName(),
foreignKey = me.getForeignKey(),
primaryKey = me.getPrimaryKey(),
filterProperty = me.getFilterProperty(),
autoLoad = me.getAutoLoad(),
autoSync = me.getAutoSync();
return function() {
var record = this,
config, filter, store,
modelDefaults = {},
listeners = {
addrecords: me.onAddRecords,
removerecords: me.onRemoveRecords,
scope: me
};
if (record[storeName] === undefined) {
if (filterProperty) {
filter = {
property : filterProperty,
value : record.get(filterProperty),
exactMatch: true
};
} else {
filter = {
property : foreignKey,
value : record.get(primaryKey),
exactMatch: true
};
}
modelDefaults[foreignKey] = record.get(primaryKey);
config = Ext.apply({}, storeConfig, {
model : associatedModel,
filters : [filter],
remoteFilter : true,
autoSync : autoSync,
modelDefaults: modelDefaults,
listeners : listeners
});
store = record[storeName] = Ext.create('Ext.data.Store', config);
store.boundTo = record;
if (autoLoad) {
record[storeName].load();
}
}
return record[storeName];
};
},
onAddRecords: function(store, records) {
var ln = records.length,
id = store.boundTo.getId(),
i, record;
for (i = 0; i < ln; i++) {
record = records[i];
record.set(this.getForeignKey(), id);
}
this.updateInverseInstances(store.boundTo);
},
onRemoveRecords: function(store, records) {
var ln = records.length,
i, record;
for (i = 0; i < ln; i++) {
record = records[i];
record.set(this.getForeignKey(), null);
}
},
updateStore: function(store) {
this.getOwnerModel().prototype[this.getName()] = store;
},
/**
* Read associated data
* @private
* @param {Ext.data.Model} record The record we're writing to.
* @param {Ext.data.reader.Reader} reader The reader for the associated model.
* @param {Object} associationData The raw associated data.
*/
read: function(record, reader, associationData) {
var store = record[this.getName()](),
records = reader.read(associationData).getRecords();
store.add(records);
},
updateInverseInstances: function(record) {
var store = record[this.getName()](),
inverse = this.getInverseAssociation();
//if the inverse association was found, set it now on each record we've just created
if (inverse) {
store.each(function(associatedRecord) {
associatedRecord[inverse.getInstanceName()] = record;
});
}
},
getInverseAssociation: function() {
var ownerName = this.getOwnerModel().modelName;
//now that we've added the related records to the hasMany association, set the inverse belongsTo
//association on each of them if it exists
return this.getAssociatedModel().associations.findBy(function(assoc) {
return assoc.getType().toLowerCase() === 'belongsto' && assoc.getAssociatedModel().modelName === ownerName;
});
}
});
/**
* @author Ed Spencer
* @aside guide models
*
* Represents a many to one association with another model. The owner model is expected to have
* a foreign key which references the primary key of the associated model:
*
* Ext.define('Category', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* { name: 'id', type: 'int' },
* { name: 'name', type: 'string' }
* ]
* }
* });
*
* Ext.define('Product', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* { name: 'id', type: 'int' },
* { name: 'category_id', type: 'int' },
* { name: 'name', type: 'string' }
* ],
* // we can use the belongsTo shortcut on the model to create a belongsTo association
* associations: { type: 'belongsTo', model: 'Category' }
* }
* });
*
* In the example above we have created models for Products and Categories, and linked them together
* by saying that each Product belongs to a Category. This automatically links each Product to a Category
* based on the Product's category_id, and provides new functions on the Product model:
*
* ## Generated getter function
*
* The first function that is added to the owner model is a getter function:
*
* var product = new Product({
* id: 100,
* category_id: 20,
* name: 'Sneakers'
* });
*
* product.getCategory(function(category, operation) {
* // do something with the category object
* alert(category.get('id')); // alerts 20
* }, this);
*
* The getCategory function was created on the Product model when we defined the association. This uses the
* Category's configured {@link Ext.data.proxy.Proxy proxy} to load the Category asynchronously, calling the provided
* callback when it has loaded.
*
* The new getCategory function will also accept an object containing success, failure and callback properties
* - callback will always be called, success will only be called if the associated model was loaded successfully
* and failure will only be called if the associated model could not be loaded:
*
* product.getCategory({
* reload: true, // force a reload if the owner model is already cached
* callback: function(category, operation) {}, // a function that will always be called
* success : function(category, operation) {}, // a function that will only be called if the load succeeded
* failure : function(category, operation) {}, // a function that will only be called if the load did not succeed
* scope : this // optionally pass in a scope object to execute the callbacks in
* });
*
* In each case above the callbacks are called with two arguments - the associated model instance and the
* {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is
* useful when the instance could not be loaded.
*
* Once the getter has been called on the model, it will be cached if the getter is called a second time. To
* force the model to reload, specify reload: true in the options object.
*
* ## Generated setter function
*
* The second generated function sets the associated model instance - if only a single argument is passed to
* the setter then the following two calls are identical:
*
* // this call...
* product.setCategory(10);
*
* // is equivalent to this call:
* product.set('category_id', 10);
*
* An instance of the owner model can also be passed as a parameter.
*
* If we pass in a second argument, the model will be automatically saved and the second argument passed to
* the owner model's {@link Ext.data.Model#save save} method:
*
* product.setCategory(10, function(product, operation) {
* // the product has been saved
* alert(product.get('category_id')); //now alerts 10
* });
*
* //alternative syntax:
* product.setCategory(10, {
* callback: function(product, operation) {}, // a function that will always be called
* success : function(product, operation) {}, // a function that will only be called if the load succeeded
* failure : function(product, operation) {}, // a function that will only be called if the load did not succeed
* scope : this //optionally pass in a scope object to execute the callbacks in
* });
*
* ## Customization
*
* Associations reflect on the models they are linking to automatically set up properties such as the
* {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:
*
* Ext.define('Product', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* // ...
* ],
*
* associations: [
* { type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id' }
* ]
* }
* });
*
* Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id')
* with our own settings. Usually this will not be needed.
*/
Ext.define('Ext.data.association.BelongsTo', {
extend: 'Ext.data.association.Association',
alternateClassName: 'Ext.data.BelongsToAssociation',
alias: 'association.belongsto',
config: {
/**
* @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated
* model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a
* model called Product would set up a product_id foreign key.
*
* Ext.define('Order', {
* extend: 'Ext.data.Model',
* fields: ['id', 'date'],
* hasMany: 'Product'
* });
*
* Ext.define('Product', {
* extend: 'Ext.data.Model',
* fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to
* belongsTo: 'Group'
* });
* var product = new Product({
* id: 1,
* name: 'Product 1',
* order_id: 22
* }, 1);
* product.getOrder(); // Will make a call to the server asking for order_id 22
*
*/
foreignKey: undefined,
/**
* @cfg {String} getterName The name of the getter function that will be added to the local model's prototype.
* Defaults to 'get' + the name of the foreign model, e.g. getCategory
*/
getterName: undefined,
/**
* @cfg {String} setterName The name of the setter function that will be added to the local model's prototype.
* Defaults to 'set' + the name of the foreign model, e.g. setCategory
*/
setterName: undefined,
instanceName: undefined
},
applyForeignKey: function(foreignKey) {
if (!foreignKey) {
foreignKey = this.getAssociatedName().toLowerCase() + '_id';
}
return foreignKey;
},
updateForeignKey: function(foreignKey, oldForeignKey) {
var fields = this.getOwnerModel().getFields(),
field = fields.get(foreignKey);
if (!field) {
field = new Ext.data.Field({
name: foreignKey
});
fields.add(field);
fields.isDirty = true;
}
if (oldForeignKey) {
field = fields.get(oldForeignKey);
if (field) {
fields.isDirty = true;
fields.remove(field);
}
}
},
applyInstanceName: function(instanceName) {
if (!instanceName) {
instanceName = this.getAssociatedName() + 'BelongsToInstance';
}
return instanceName;
},
applyAssociationKey: function(associationKey) {
if (!associationKey) {
var associatedName = this.getAssociatedName();
associationKey = associatedName[0].toLowerCase() + associatedName.slice(1);
}
return associationKey;
},
applyGetterName: function(getterName) {
if (!getterName) {
var associatedName = this.getAssociatedName();
getterName = 'get' + associatedName[0].toUpperCase() + associatedName.slice(1);
}
return getterName;
},
applySetterName: function(setterName) {
if (!setterName) {
var associatedName = this.getAssociatedName();
setterName = 'set' + associatedName[0].toUpperCase() + associatedName.slice(1);
}
return setterName;
},
updateGetterName: function(getterName, oldGetterName) {
var ownerProto = this.getOwnerModel().prototype;
if (oldGetterName) {
delete ownerProto[oldGetterName];
}
if (getterName) {
ownerProto[getterName] = this.createGetter();
}
},
updateSetterName: function(setterName, oldSetterName) {
var ownerProto = this.getOwnerModel().prototype;
if (oldSetterName) {
delete ownerProto[oldSetterName];
}
if (setterName) {
ownerProto[setterName] = this.createSetter();
}
},
/**
* @private
* Returns a setter function to be placed on the owner model's prototype
* @return {Function} The setter function
*/
createSetter: function() {
var me = this,
foreignKey = me.getForeignKey(),
associatedModel = me.getAssociatedModel(),
currentOwner, newOwner, store;
//'this' refers to the Model instance inside this function
return function(value, options, scope) {
var inverse = me.getInverseAssociation(),
record = this;
// If we pass in an instance, pull the id out
if (value && value.isModel) {
value = value.getId();
}
if (Ext.isFunction(options)) {
options = {
callback: options,
scope: scope || record
};
}
// Remove the current belongsToInstance
delete record[me.getInstanceName()];
currentOwner = Ext.data.Model.cache[Ext.data.Model.generateCacheId(associatedModel.modelName, this.get(foreignKey))];
newOwner = Ext.data.Model.cache[Ext.data.Model.generateCacheId(associatedModel.modelName, value)];
record.set(foreignKey, value);
if (inverse) {
// We first add it to the new owner so that the record wouldnt be destroyed if it was the last store it was in
if (newOwner) {
if (inverse.getType().toLowerCase() === 'hasmany') {
store = newOwner[inverse.getName()]();
store.add(record);
} else {
newOwner[inverse.getInstanceName()] = record;
}
}
if (currentOwner) {
if (inverse.getType().toLowerCase() === 'hasmany') {
store = currentOwner[inverse.getName()]();
store.remove(record);
} else {
delete value[inverse.getInstanceName()];
}
}
}
if (newOwner) {
record[me.getInstanceName()] = newOwner;
}
if (Ext.isObject(options)) {
return record.save(options);
}
return record;
};
},
/**
* @private
* Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance
* the first time it is loaded so that subsequent calls to the getter always receive the same reference.
* @return {Function} The getter function
*/
createGetter: function() {
var me = this,
associatedModel = me.getAssociatedModel(),
foreignKey = me.getForeignKey(),
instanceName = me.getInstanceName();
//'this' refers to the Model instance inside this function
return function(options, scope) {
options = options || {};
var model = this,
foreignKeyId = model.get(foreignKey),
success,
instance,
args;
instance = model[instanceName];
if (!instance) {
instance = Ext.data.Model.cache[Ext.data.Model.generateCacheId(associatedModel.modelName, foreignKeyId)];
if (instance) {
model[instanceName] = instance;
}
}
if (options.reload === true || instance === undefined) {
if (typeof options == 'function') {
options = {
callback: options,
scope: scope || model
};
}
// Overwrite the success handler so we can assign the current instance
success = options.success;
options.success = function(rec) {
model[instanceName] = rec;
if (success) {
success.apply(this, arguments);
}
};
associatedModel.load(foreignKeyId, options);
} else {
args = [instance];
scope = scope || model;
Ext.callback(options, scope, args);
Ext.callback(options.success, scope, args);
Ext.callback(options.failure, scope, args);
Ext.callback(options.callback, scope, args);
return instance;
}
};
},
/**
* Read associated data
* @private
* @param {Ext.data.Model} record The record we're writing to
* @param {Ext.data.reader.Reader} reader The reader for the associated model
* @param {Object} associationData The raw associated data
*/
read: function(record, reader, associationData){
record[this.getInstanceName()] = reader.read([associationData]).getRecords()[0];
},
getInverseAssociation: function() {
var ownerName = this.getOwnerModel().modelName,
foreignKey = this.getForeignKey();
return this.getAssociatedModel().associations.findBy(function(assoc) {
var type = assoc.getType().toLowerCase();
return (type === 'hasmany' || type === 'hasone') && assoc.getAssociatedModel().modelName === ownerName && assoc.getForeignKey() === foreignKey;
});
}
});
/**
* @aside guide models
*
* Represents a one to one association with another model. The owner model is expected to have
* a foreign key which references the primary key of the associated model:
*
* Ext.define('Person', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* { name: 'id', type: 'int' },
* { name: 'name', type: 'string' },
* { name: 'address_id', type: 'int'}
* ],
*
* // we can use the hasOne shortcut on the model to create a hasOne association
* associations: { type: 'hasOne', model: 'Address' }
* }
* });
*
* Ext.define('Address', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* { name: 'id', type: 'int' },
* { name: 'number', type: 'string' },
* { name: 'street', type: 'string' },
* { name: 'city', type: 'string' },
* { name: 'zip', type: 'string' }
* ]
* }
* });
*
* In the example above we have created models for People and Addresses, and linked them together
* by saying that each Person has a single Address. This automatically links each Person to an Address
* based on the Persons address_id, and provides new functions on the Person model:
*
* ## Generated getter function
*
* The first function that is added to the owner model is a getter function:
*
* var person = Ext.create('Person', {
* id: 100,
* address_id: 20,
* name: 'John Smith'
* });
*
* person.getAddress(function(address, operation) {
* // do something with the address object
* alert(address.get('id')); // alerts 20
* }, this);
*
* The getAddress function was created on the Person model when we defined the association. This uses the
* Persons configured {@link Ext.data.proxy.Proxy proxy} to load the Address asynchronously, calling the provided
* callback when it has loaded.
*
* The new getAddress function will also accept an object containing success, failure and callback properties
* - callback will always be called, success will only be called if the associated model was loaded successfully
* and failure will only be called if the associated model could not be loaded:
*
* person.getAddress({
* reload: true, // force a reload if the owner model is already cached
* callback: function(address, operation) {}, // a function that will always be called
* success : function(address, operation) {}, // a function that will only be called if the load succeeded
* failure : function(address, operation) {}, // a function that will only be called if the load did not succeed
* scope : this // optionally pass in a scope object to execute the callbacks in
* });
*
* In each case above the callbacks are called with two arguments - the associated model instance and the
* {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is
* useful when the instance could not be loaded.
*
* Once the getter has been called on the model, it will be cached if the getter is called a second time. To
* force the model to reload, specify reload: true in the options object.
*
* ## Generated setter function
*
* The second generated function sets the associated model instance - if only a single argument is passed to
* the setter then the following two calls are identical:
*
* // this call...
* person.setAddress(10);
*
* // is equivalent to this call:
* person.set('address_id', 10);
*
* An instance of the owner model can also be passed as a parameter.
*
* If we pass in a second argument, the model will be automatically saved and the second argument passed to
* the owner model's {@link Ext.data.Model#save save} method:
*
* person.setAddress(10, function(address, operation) {
* // the address has been saved
* alert(address.get('address_id')); //now alerts 10
* });
*
* //alternative syntax:
* person.setAddress(10, {
* callback: function(address, operation) {}, // a function that will always be called
* success : function(address, operation) {}, // a function that will only be called if the load succeeded
* failure : function(address, operation) {}, // a function that will only be called if the load did not succeed
* scope : this //optionally pass in a scope object to execute the callbacks in
* });
*
* ## Customization
*
* Associations reflect on the models they are linking to automatically set up properties such as the
* {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:
*
* Ext.define('Person', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* // ...
* ],
*
* associations: [
* { type: 'hasOne', model: 'Address', primaryKey: 'unique_id', foreignKey: 'addr_id' }
* ]
* }
* });
*
* Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'address_id')
* with our own settings. Usually this will not be needed.
*/
Ext.define('Ext.data.association.HasOne', {
extend: 'Ext.data.association.Association',
alternateClassName: 'Ext.data.HasOneAssociation',
alias: 'association.hasone',
config: {
/**
* @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated
* model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a
* model called Person would set up a address_id foreign key.
*
* Ext.define('Person', {
* extend: 'Ext.data.Model',
* fields: ['id', 'name', 'address_id'], // refers to the id of the address object
* hasOne: 'Address'
* });
*
* Ext.define('Address', {
* extend: 'Ext.data.Model',
* fields: ['id', 'number', 'street', 'city', 'zip'],
* belongsTo: 'Person'
* });
* var Person = new Person({
* id: 1,
* name: 'John Smith',
* address_id: 13
* }, 1);
* person.getAddress(); // Will make a call to the server asking for address_id 13
*
*/
foreignKey: undefined,
/**
* @cfg {String} getterName The name of the getter function that will be added to the local model's prototype.
* Defaults to 'get' + the name of the foreign model, e.g. getAddress
*/
getterName: undefined,
/**
* @cfg {String} setterName The name of the setter function that will be added to the local model's prototype.
* Defaults to 'set' + the name of the foreign model, e.g. setAddress
*/
setterName: undefined,
instanceName: undefined
},
applyForeignKey: function(foreignKey) {
if (!foreignKey) {
var inverse = this.getInverseAssociation();
if (inverse) {
foreignKey = inverse.getForeignKey();
} else {
foreignKey = this.getAssociatedName().toLowerCase() + '_id';
}
}
return foreignKey;
},
updateForeignKey: function(foreignKey, oldForeignKey) {
var fields = this.getOwnerModel().getFields(),
field = fields.get(foreignKey);
if (!field) {
field = new Ext.data.Field({
name: foreignKey
});
fields.add(field);
fields.isDirty = true;
}
if (oldForeignKey) {
field = fields.get(oldForeignKey);
if (field) {
fields.remove(field);
fields.isDirty = true;
}
}
},
applyInstanceName: function(instanceName) {
if (!instanceName) {
instanceName = this.getAssociatedName() + 'HasOneInstance';
}
return instanceName;
},
applyAssociationKey: function(associationKey) {
if (!associationKey) {
var associatedName = this.getAssociatedName();
associationKey = associatedName[0].toLowerCase() + associatedName.slice(1);
}
return associationKey;
},
applyGetterName: function(getterName) {
if (!getterName) {
var associatedName = this.getAssociatedName();
getterName = 'get' + associatedName[0].toUpperCase() + associatedName.slice(1);
}
return getterName;
},
applySetterName: function(setterName) {
if (!setterName) {
var associatedName = this.getAssociatedName();
setterName = 'set' + associatedName[0].toUpperCase() + associatedName.slice(1);
}
return setterName;
},
updateGetterName: function(getterName, oldGetterName) {
var ownerProto = this.getOwnerModel().prototype;
if (oldGetterName) {
delete ownerProto[oldGetterName];
}
if (getterName) {
ownerProto[getterName] = this.createGetter();
}
},
updateSetterName: function(setterName, oldSetterName) {
var ownerProto = this.getOwnerModel().prototype;
if (oldSetterName) {
delete ownerProto[oldSetterName];
}
if (setterName) {
ownerProto[setterName] = this.createSetter();
}
},
/**
* @private
* Returns a setter function to be placed on the owner model's prototype
* @return {Function} The setter function
*/
createSetter: function() {
var me = this,
foreignKey = me.getForeignKey(),
instanceName = me.getInstanceName(),
associatedModel = me.getAssociatedModel();
//'this' refers to the Model instance inside this function
return function(value, options, scope) {
var Model = Ext.data.Model,
record;
if (value && value.isModel) {
value = value.getId();
}
this.set(foreignKey, value);
record = Model.cache[Model.generateCacheId(associatedModel.modelName, value)];
if (record) {
this[instanceName] = record;
}
if (Ext.isFunction(options)) {
options = {
callback: options,
scope: scope || this
};
}
if (Ext.isObject(options)) {
return this.save(options);
}
return this;
};
},
/**
* @private
* Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance
* the first time it is loaded so that subsequent calls to the getter always receive the same reference.
* @return {Function} The getter function
*/
createGetter: function() {
var me = this,
associatedModel = me.getAssociatedModel(),
foreignKey = me.getForeignKey(),
instanceName = me.getInstanceName();
//'this' refers to the Model instance inside this function
return function(options, scope) {
options = options || {};
var model = this,
foreignKeyId = model.get(foreignKey),
success, instance, args;
if (options.reload === true || model[instanceName] === undefined) {
if (typeof options == 'function') {
options = {
callback: options,
scope: scope || model
};
}
// Overwrite the success handler so we can assign the current instance
success = options.success;
options.success = function(rec){
model[instanceName] = rec;
if (success) {
success.call(this, arguments);
}
};
associatedModel.load(foreignKeyId, options);
} else {
instance = model[instanceName];
args = [instance];
scope = scope || model;
Ext.callback(options, scope, args);
Ext.callback(options.success, scope, args);
Ext.callback(options.failure, scope, args);
Ext.callback(options.callback, scope, args);
return instance;
}
};
},
/**
* Read associated data
* @private
* @param {Ext.data.Model} record The record we're writing to
* @param {Ext.data.reader.Reader} reader The reader for the associated model
* @param {Object} associationData The raw associated data
*/
read: function(record, reader, associationData) {
var inverse = this.getInverseAssociation(),
newRecord = reader.read([associationData]).getRecords()[0];
record[this.getInstanceName()] = newRecord;
//if the inverse association was found, set it now on each record we've just created
if (inverse) {
newRecord[inverse.getInstanceName()] = record;
}
},
getInverseAssociation: function() {
var ownerName = this.getOwnerModel().modelName;
return this.getAssociatedModel().associations.findBy(function(assoc) {
return assoc.getType().toLowerCase() === 'belongsto' && assoc.getAssociatedModel().modelName === ownerName;
});
}
});
/**
* @author Ed Spencer
* @class Ext.data.Error
*
* This is used when validating a record. The validate method will return an {@link Ext.data.Errors} collection
* containing Ext.data.Error instances. Each error has a field and a message.
*
* 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.
*/
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);
}
});
/**
* @author Ed Spencer
* @class Ext.data.Errors
* @extends Ext.util.Collection
*
* Wraps a collection of validation error responses and provides convenient functions for
* accessing and errors for specific fields.
*
* 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:
*
* //validate some existing model instance - in this case it returned two failures messages
* var errors = myModel.validate();
*
* errors.isValid(); // false
*
* errors.length; // 2
* errors.getByField('name'); // [{field: 'name', message: 'must be present'}]
* errors.getByField('title'); // [{field: 'title', message: 'is too short'}]
*/
Ext.define('Ext.data.Errors', {
extend: 'Ext.util.Collection',
requires: 'Ext.data.Error',
/**
* Returns `true` if there are no errors in the collection.
* @return {Boolean}
*/
isValid: function() {
return this.length === 0;
},
/**
* Returns all of the errors for the given field.
* @param {String} fieldName The field to get errors for.
* @return {Object[]} All errors for the given field.
*/
getByField: function(fieldName) {
var errors = [],
error, i;
for (i = 0; i < this.length; i++) {
error = this.items[i];
if (error.getField() == fieldName) {
errors.push(error);
}
}
return errors;
},
add: function() {
var obj = arguments.length == 1 ? arguments[0] : arguments[1];
if (!(obj instanceof Ext.data.Error)) {
obj = Ext.create('Ext.data.Error', {
field: obj.field || obj.name,
message: obj.error || obj.message
});
}
return this.callParent([obj]);
}
});
/**
* @author Ed Spencer
* @aside guide models
*
* A Model represents some object that your application manages. For example, one might define a Model for Users,
* Products, Cars, or any other real-world object that we want to model in the system. Models are registered via the
* {@link Ext.data.ModelManager model manager}, and are used by {@link Ext.data.Store stores}, which are in turn used by many
* of the data-bound components in Ext.
*
* Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: [
* {name: 'name', type: 'string'},
* {name: 'age', type: 'int'},
* {name: 'phone', type: 'string'},
* {name: 'alive', type: 'boolean', defaultValue: true}
* ]
* },
*
* changeName: function() {
* var oldName = this.get('name'),
* newName = oldName + " The Barbarian";
*
* this.set('name', newName);
* }
* });
*
* The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link
* Ext.data.ModelManager ModelManager}, and all other functions and properties are copied to the new Model's prototype.
*
* Now we can create instances of our User model and call any model logic we defined:
*
* var user = Ext.create('User', {
* name : 'Conan',
* age : 24,
* phone: '555-555-5555'
* });
*
* user.changeName();
* user.get('name'); // returns "Conan The Barbarian"
*
* # Validations
*
* Models have built-in support for validations, which are executed against the validator functions in {@link
* Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to
* models:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: [
* {name: 'name', type: 'string'},
* {name: 'age', type: 'int'},
* {name: 'phone', type: 'string'},
* {name: 'gender', type: 'string'},
* {name: 'username', type: 'string'},
* {name: 'alive', type: 'boolean', defaultValue: true}
* ],
*
* validations: [
* {type: 'presence', field: 'age'},
* {type: 'length', field: 'name', min: 2},
* {type: 'inclusion', field: 'gender', list: ['Male', 'Female']},
* {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']},
* {type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
* ]
* }
* });
*
* The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors}
* object:
*
* var instance = Ext.create('User', {
* name: 'Ed',
* gender: 'Male',
* username: 'edspencer'
* });
*
* var errors = instance.validate();
*
* # Associations
*
* Models can have associations with other Models via {@link Ext.data.association.HasOne},
* {@link Ext.data.association.BelongsTo belongsTo} and {@link Ext.data.association.HasMany hasMany} associations.
* For example, let's say we're writing a blog administration application which deals with Users, Posts and Comments.
* We can express the relationships between these models like this:
*
* Ext.define('Post', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: ['id', 'user_id'],
* belongsTo: 'User',
* hasMany : {model: 'Comment', name: 'comments'}
* }
* });
*
* Ext.define('Comment', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: ['id', 'user_id', 'post_id'],
* belongsTo: 'Post'
* }
* });
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: ['id'],
* hasMany: [
* 'Post',
* {model: 'Comment', name: 'comments'}
* ]
* }
* });
*
* See the docs for {@link Ext.data.association.HasOne}, {@link Ext.data.association.BelongsTo} and
* {@link Ext.data.association.HasMany} for details on the usage and configuration of associations.
* Note that associations can also be specified like this:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: ['id'],
* associations: [
* {type: 'hasMany', model: 'Post', name: 'posts'},
* {type: 'hasMany', model: 'Comment', name: 'comments'}
* ]
* }
* });
*
* # Using a Proxy
*
* Models are great for representing types of data and relationships, but sooner or later we're going to want to load or
* save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, which
* can be set directly on the Model:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: ['id', 'name', 'email'],
* proxy: {
* type: 'rest',
* url : '/users'
* }
* }
* });
*
* Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save data to and from a
* RESTful backend. Let's see how this works:
*
* var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
*
* user.save(); //POST /users
*
* Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this Model's
* data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't have an id,
* and performs the appropriate action - in this case issuing a POST request to the url we configured (/users). We
* configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
*
* Loading data via the Proxy is equally easy:
*
* //get a reference to the User model class
* var User = Ext.ModelManager.getModel('User');
*
* //Uses the configured RestProxy to make a GET request to /users/123
* User.load(123, {
* success: function(user) {
* console.log(user.getId()); //logs 123
* }
* });
*
* Models can also be updated and destroyed easily:
*
* //the user Model we loaded in the last snippet:
* user.set('name', 'Edward Spencer');
*
* //tells the Proxy to save the Model. In this case it will perform a PUT request to /users/123 as this Model already has an id
* user.save({
* success: function() {
* console.log('The User was updated');
* }
* });
*
* //tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
* user.erase({
* success: function() {
* console.log('The User was destroyed!');
* }
* });
*
* # Usage in Stores
*
* It is very common to want to load a set of Model instances to be displayed and manipulated in the UI. We do this by
* creating a {@link Ext.data.Store Store}:
*
* var store = Ext.create('Ext.data.Store', {
* model: 'User'
* });
*
* //uses the Proxy we set up on Model to load the Store data
* store.load();
*
* A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain a
* set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the {@link
* Ext.data.Store Store docs} for more information on Stores.
*/
Ext.define('Ext.data.Model', {
alternateClassName: 'Ext.data.Record',
mixins: {
observable: 'Ext.mixin.Observable'
},
/**
* Provides an easy way to quickly determine if a given class is a Model
* @property isModel
* @type Boolean
* @private
*/
isModel: true,
requires: [
'Ext.util.Collection',
'Ext.data.Field',
'Ext.data.identifier.Simple',
'Ext.data.ModelManager',
'Ext.data.proxy.Ajax',
'Ext.data.association.HasMany',
'Ext.data.association.BelongsTo',
'Ext.data.association.HasOne',
'Ext.data.Errors'
],
config: {
/**
* @cfg {String} idProperty
* The name of the field treated as this Model's unique `id`. Note that this field
* needs to have a type of 'auto'. Setting the field type to anything else will be undone by the
* framework. This is because new records that are created without an `id`, will have one generated.
*/
idProperty: 'id',
data: null,
/**
* @cfg {Object[]/String[]} fields
* The {@link Ext.data.Model field} definitions for all instances of this Model.
*
* __Note:__ this does not set the *values* of each
* field on an instance, it sets the collection of fields itself.
*
* Sample usage:
*
* Ext.define('MyApp.model.User', {
* extend: 'Ext.data.Model',
*
* config: {
* fields: [
* 'id',
* {name: 'age', type: 'int'},
* {name: 'taxRate', type: 'float'}
* ]
* }
* });
* @accessor
*/
fields: undefined,
/**
* @cfg {Object[]} validations
* An array of {@link Ext.data.Validations validations} for this model.
*/
validations: null,
/**
* @cfg {Object[]} associations
* An array of {@link Ext.data.association.Association associations} for this model.
*/
associations: null,
/**
* @cfg {String/Object/String[]/Object[]} hasMany
* One or more {@link Ext.data.association.HasMany HasMany associations} for this model.
*/
hasMany: null,
/**
* @cfg {String/Object/String[]/Object[]} hasOne
* One or more {@link Ext.data.association.HasOne HasOne associations} for this model.
*/
hasOne: null,
/**
* @cfg {String/Object/String[]/Object[]} belongsTo
* One or more {@link Ext.data.association.BelongsTo BelongsTo associations} for this model.
*/
belongsTo: null,
/**
* @cfg {Object/Ext.data.Proxy} proxy
* The string type of the default Model Proxy.
* @accessor
*/
proxy: null,
/**
* @cfg {Object/String} identifier
* The identifier strategy used when creating new instances of this Model that don't have an id defined.
* By default this uses the simple identifier strategy that generates id's like 'ext-record-12'. If you are
* saving these records in localstorage using a LocalStorage proxy you need to ensure that this identifier
* strategy is set to something that always generates unique id's. We provide one strategy by default that
* generates these unique id's which is the uuid strategy.
*/
identifier: {
type: 'simple'
},
/**
* @cfg {String} clientIdProperty
* The name of a property that is used for submitting this Model's unique client-side identifier
* to the server when multiple phantom records are saved as part of the same {@link Ext.data.Operation Operation}.
* In such a case, the server response should include the client id for each record
* so that the server response data can be used to update the client-side records if necessary.
* This property cannot have the same name as any of this Model's fields.
* @accessor
*/
clientIdProperty: 'clientId',
/**
* @method getIsErased Returns `true` if the record has been erased on the server.
*/
isErased: false,
/**
* @cfg {Boolean} useCache
* Change this to `false` if you want to ensure that new instances are created for each id. For example,
* this is needed when adding the same tree nodes to multiple trees.
*/
useCache: true
},
staticConfigs: [
'idProperty',
'fields',
'validations',
'associations',
'hasMany',
'hasOne',
'belongsTo',
'clientIdProperty',
'identifier',
'useCache',
'proxy'
],
statics: {
EDIT : 'edit',
REJECT : 'reject',
COMMIT : 'commit',
cache: {},
generateProxyMethod: function(name) {
return function() {
var prototype = this.prototype;
return prototype[name].apply(prototype, arguments);
};
},
generateCacheId: function(record, id) {
var modelName;
if (record && record.isModel) {
modelName = record.modelName;
if (id === undefined) {
id = record.getId();
}
} else {
modelName = record;
}
return modelName.replace(/\./g, '-').toLowerCase() + '-' + id;
}
},
inheritableStatics: {
/**
* Asynchronously loads a model instance by id. Sample usage:
*
* MyApp.User = Ext.define('User', {
* extend: 'Ext.data.Model',
* fields: [
* {name: 'id', type: 'int'},
* {name: 'name', type: 'string'}
* ]
* });
*
* MyApp.User.load(10, {
* scope: this,
* failure: function(record, operation) {
* //do something if the load failed
* },
* success: function(record, operation) {
* //do something if the load succeeded
* },
* callback: function(record, operation) {
* //do something whether the load succeeded or failed
* }
* });
*
* @param {Number} id The id of the model to load
* @param {Object} config (optional) config object containing success, failure and callback functions, plus
* optional scope
* @static
* @inheritable
*/
load: function(id, config, scope) {
var proxy = this.getProxy(),
idProperty = this.getIdProperty(),
record = null,
params = {},
callback, operation;
scope = scope || (config && config.scope) || this;
if (Ext.isFunction(config)) {
config = {
callback: config,
scope: scope
};
}
params[idProperty] = id;
config = Ext.apply({}, config);
config = Ext.applyIf(config, {
action: 'read',
params: params,
model: this
});
operation = Ext.create('Ext.data.Operation', config);
if (!proxy) {
Ext.Logger.error('You are trying to load a model that doesn\'t have a Proxy specified');
}
callback = function(operation) {
if (operation.wasSuccessful()) {
record = operation.getRecords()[0] || null;
Ext.callback(config.success, scope, [record, operation]);
} else {
Ext.callback(config.failure, scope, [record, operation]);
}
Ext.callback(config.callback, scope, [record, operation]);
};
proxy.read(operation, callback, this);
}
},
/**
* @property {Boolean} editing
* @readonly
* Internal flag used to track whether or not the model instance is currently being edited.
*/
editing : false,
/**
* @property {Boolean} dirty
* @readonly
* `true` if this Record has been modified.
*/
dirty : false,
/**
* @property {Boolean} phantom
* `true` when the record does not yet exist in a server-side database (see {@link #setDirty}).
* Any record which has a real database pk set as its id property is NOT a phantom -- it's real.
*/
phantom : false,
/**
* Creates new Model instance.
* @param {Object} data An object containing keys corresponding to this model's fields, and their associated values.
* @param {Number} id (optional) Unique ID to assign to this model instance.
* @param [raw]
* @param [convertedData]
*/
constructor: function(data, id, raw, convertedData) {
var me = this,
cached = null,
useCache = me.getUseCache(),
idProperty = me.getIdProperty();
/**
* @property {Object} modified key/value pairs of all fields whose values have changed.
* The value is the original value for the field.
*/
me.modified = {};
/**
* @property {Object} raw The raw data used to create this model if created via a reader.
*/
me.raw = raw || data || {};
/**
* @property {Array} stores
* An array of {@link Ext.data.Store} objects that this record is bound to.
*/
me.stores = [];
data = data || convertedData || {};
// We begin by checking if an id is passed to the constructor. If this is the case we override
// any possible id value that was passed in the data.
if (id || id === 0) {
// Lets skip using set here since it's so much faster
data[idProperty] = me.internalId = id;
}
id = data[idProperty];
if (useCache && (id || id === 0)) {
cached = Ext.data.Model.cache[Ext.data.Model.generateCacheId(this, id)];
if (cached) {
return cached.mergeData(convertedData || data || {});
}
}
if (convertedData) {
me.setConvertedData(data);
} else {
me.setData(data);
}
// If it does not have an id at this point, we generate it using the id strategy. This means
// that we will treat this record as a phantom record from now on
id = me.data[idProperty];
if (!id && id !== 0) {
me.data[idProperty] = me.internalId = me.id = me.getIdentifier().generate(me);
me.phantom = true;
if (this.associations.length) {
this.handleInlineAssociationData(data);
}
} else {
me.id = me.getIdentifier().generate(me);
}
if (useCache) {
Ext.data.Model.cache[Ext.data.Model.generateCacheId(me)] = me;
}
if (this.init && typeof this.init == 'function') {
this.init();
}
},
/**
* Private function that is used when you create a record that already exists in the model cache.
* In this case we loop over each field, and apply any data to the current instance that is not already
* marked as being dirty on that instance.
* @param data
* @return {Ext.data.Model} This record.
* @private
*/
mergeData: function(rawData) {
var me = this,
fields = me.getFields().items,
ln = fields.length,
modified = me.modified,
data = me.data,
i, field, fieldName, value, id;
for (i = 0; i < ln; i++) {
field = fields[i];
fieldName = field._name;
value = rawData[fieldName];
if (value !== undefined && !modified.hasOwnProperty(fieldName)) {
if (field._convert) {
value = field._convert(value, me);
}
data[fieldName] = value;
}
}
if (me.associations.length) {
me.handleInlineAssociationData(rawData);
}
return this;
},
/**
* This method is used to set the data for this Record instance.
* Note that the existing data is removed. If a field is not specified
* in the passed data it will use the field's default value. If a convert
* method is specified for the field it will be called on the value.
* @param rawData
* @return {Ext.data.Model} This record.
*/
setData: function(rawData) {
var me = this,
fields = me.fields.items,
ln = fields.length,
isArray = Ext.isArray(rawData),
data = me._data = me.data = {},
i, field, name, value, convert, id;
if (!rawData) {
return me;
}
for (i = 0; i < ln; i++) {
field = fields[i];
name = field._name;
convert = field._convert;
if (isArray) {
value = rawData[i];
}
else {
value = rawData[name];
if (typeof value == 'undefined') {
value = field._defaultValue;
}
}
if (convert) {
value = field._convert(value, me);
}
data[name] = value;
}
id = me.getId();
if (me.associations.length && (id || id === 0)) {
me.handleInlineAssociationData(rawData);
}
return me;
},
handleInlineAssociationData: function(data) {
var associations = this.associations.items,
ln = associations.length,
i, association, associationData, reader, proxy, associationKey;
for (i = 0; i < ln; i++) {
association = associations[i];
associationKey = association.getAssociationKey();
associationData = data[associationKey];
if (associationData) {
reader = association.getReader();
if (!reader) {
proxy = association.getAssociatedModel().getProxy();
// if the associated model has a Reader already, use that, otherwise attempt to create a sensible one
if (proxy) {
reader = proxy.getReader();
} else {
reader = new Ext.data.JsonReader({
model: association.getAssociatedModel()
});
}
}
association.read(this, reader, associationData);
}
}
},
/**
* Sets the model instance's id field to the given id.
* @param {Number/String} id The new id
*/
setId: function(id) {
var currentId = this.getId();
// Lets use the direct property instead of getter here
this.set(this.getIdProperty(), id);
// We don't update the this.id since we don't want to break listeners that already
// exist on the record instance.
this.internalId = id;
if (this.getUseCache()) {
delete Ext.data.Model.cache[Ext.data.Model.generateCacheId(this, currentId)];
Ext.data.Model.cache[Ext.data.Model.generateCacheId(this)] = this;
}
},
/**
* Returns the unique ID allocated to this model instance as defined by {@link #idProperty}.
* @return {Number/String} The `id`.
*/
getId: function() {
// Lets use the direct property instead of getter here
return this.get(this.getIdProperty());
},
/**
* This sets the data directly without converting and applying default values.
* This method is used when a Record gets instantiated by a Reader. Only use
* this when you are sure you are passing correctly converted data.
* @param data
* @return {Ext.data.Model} This Record.
*/
setConvertedData: function(data) {
this._data = this.data = data;
return this;
},
/**
* Returns the value of the given field.
* @param {String} fieldName The field to fetch the value for.
* @return {Object} The value.
*/
get: function(fieldName) {
return this.data[fieldName];
},
/**
* Sets the given field to the given value, marks the instance as dirty.
* @param {String/Object} fieldName The field to set, or an object containing key/value pairs.
* @param {Object} value The value to set.
*/
set: function(fieldName, value) {
var me = this,
// We are using the fields map since it saves lots of function calls
fieldMap = me.fields.map,
modified = me.modified,
notEditing = !me.editing,
modifiedCount = 0,
modifiedFieldNames = [],
field, key, i, currentValue, ln, convert;
/*
* If we're passed an object, iterate over that object. NOTE: we pull out fields with a convert function and
* set those last so that all other possible data is set before the convert function is called
*/
if (arguments.length == 1) {
for (key in fieldName) {
if (fieldName.hasOwnProperty(key)) {
//here we check for the custom convert function. Note that if a field doesn't have a convert function,
//we default it to its type's convert function, so we have to check that here. This feels rather dirty.
field = fieldMap[key];
if (field && field.hasCustomConvert()) {
modifiedFieldNames.push(key);
continue;
}
if (!modifiedCount && notEditing) {
me.beginEdit();
}
++modifiedCount;
me.set(key, fieldName[key]);
}
}
ln = modifiedFieldNames.length;
if (ln) {
if (!modifiedCount && notEditing) {
me.beginEdit();
}
modifiedCount += ln;
for (i = 0; i < ln; i++) {
field = modifiedFieldNames[i];
me.set(field, fieldName[field]);
}
}
if (notEditing && modifiedCount) {
me.endEdit(false, modifiedFieldNames);
}
} else {
field = fieldMap[fieldName];
convert = field && field.getConvert();
if (convert) {
value = convert.call(field, value, me);
}
currentValue = me.data[fieldName];
me.data[fieldName] = value;
if (field && !me.isEqual(currentValue, value)) {
if (modified.hasOwnProperty(fieldName)) {
if (me.isEqual(modified[fieldName], value)) {
// the original value in me.modified equals the new value, so the
// field is no longer modified
delete modified[fieldName];
// we might have removed the last modified field, so check to see if
// there are any modified fields remaining and correct me.dirty:
me.dirty = false;
for (key in modified) {
if (modified.hasOwnProperty(key)) {
me.dirty = true;
break;
}
}
}
} else {
me.dirty = true;
// We only go one level back?
modified[fieldName] = currentValue;
}
}
if (notEditing) {
me.afterEdit([fieldName], modified);
}
}
},
/**
* Checks if two values are equal, taking into account certain
* special factors, for example dates.
* @private
* @param {Object} a The first value.
* @param {Object} b The second value.
* @return {Boolean} `true` if the values are equal.
*/
isEqual: function(a, b){
if (Ext.isDate(a) && Ext.isDate(b)) {
return a.getTime() === b.getTime();
}
return a === b;
},
/**
* Begins an edit. While in edit mode, no events (e.g. the `update` event) are relayed to the containing store.
* When an edit has begun, it must be followed by either {@link #endEdit} or {@link #cancelEdit}.
*/
beginEdit: function() {
var me = this;
if (!me.editing) {
me.editing = true;
// We save the current states of dirty, data and modified so that when we
// cancel the edit, we can put it back to this state
me.dirtySave = me.dirty;
me.dataSave = Ext.apply({}, me.data);
me.modifiedSave = Ext.apply({}, me.modified);
}
},
/**
* Cancels all changes made in the current edit operation.
*/
cancelEdit: function() {
var me = this;
if (me.editing) {
me.editing = false;
// Reset the modified state, nothing changed since the edit began
me.modified = me.modifiedSave;
me.data = me.dataSave;
me.dirty = me.dirtySave;
// Delete the saved states
delete me.modifiedSave;
delete me.dataSave;
delete me.dirtySave;
}
},
/**
* Ends an edit. If any data was modified, the containing store is notified (ie, the store's `update` event will
* fire).
* @param {Boolean} silent `true` to not notify the store of the change.
* @param {String[]} modifiedFieldNames Array of field names changed during edit.
*/
endEdit: function(silent, modifiedFieldNames) {
var me = this;
if (me.editing) {
me.editing = false;
if (silent !== true && (me.changedWhileEditing())) {
me.afterEdit(modifiedFieldNames || Ext.Object.getKeys(this.modified), this.modified);
}
delete me.modifiedSave;
delete me.dataSave;
delete me.dirtySave;
}
},
/**
* Checks if the underlying data has changed during an edit. This doesn't necessarily
* mean the record is dirty, however we still need to notify the store since it may need
* to update any views.
* @private
* @return {Boolean} `true` if the underlying data has changed during an edit.
*/
changedWhileEditing: function() {
var me = this,
saved = me.dataSave,
data = me.data,
key;
for (key in data) {
if (data.hasOwnProperty(key)) {
if (!me.isEqual(data[key], saved[key])) {
return true;
}
}
}
return false;
},
/**
* Gets a hash of only the fields that have been modified since this Model was created or committed.
* @return {Object}
*/
getChanges : function() {
var modified = this.modified,
changes = {},
field;
for (field in modified) {
if (modified.hasOwnProperty(field)) {
changes[field] = this.get(field);
}
}
return changes;
},
/**
* Returns `true` if the passed field name has been `{@link #modified}` since the load or last commit.
* @param {String} fieldName {@link Ext.data.Field#name}
* @return {Boolean}
*/
isModified : function(fieldName) {
return this.modified.hasOwnProperty(fieldName);
},
/**
* Saves the model instance using the configured proxy.
*
* @param {Object/Function} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
* If you pass a function, this will automatically become the callback method. For convenience the config
* object may also contain `success` and `failure` methods in addition to `callback` - they will all be invoked
* with the Model and Operation as arguments.
* @param {Object} scope The scope to run your callback method in. This is only used if you passed a function
* as the first argument.
* @return {Ext.data.Model} The Model instance
*/
save: function(options, scope) {
var me = this,
action = me.phantom ? 'create' : 'update',
proxy = me.getProxy(),
operation,
callback;
if (!proxy) {
Ext.Logger.error('You are trying to save a model instance that doesn\'t have a Proxy specified');
}
options = options || {};
scope = scope || me;
if (Ext.isFunction(options)) {
options = {
callback: options,
scope: scope
};
}
Ext.applyIf(options, {
records: [me],
action : action,
model: me.self
});
operation = Ext.create('Ext.data.Operation', options);
callback = function(operation) {
if (operation.wasSuccessful()) {
Ext.callback(options.success, scope, [me, operation]);
} else {
Ext.callback(options.failure, scope, [me, operation]);
}
Ext.callback(options.callback, scope, [me, operation]);
};
proxy[action](operation, callback, me);
return me;
},
/**
* Destroys the record using the configured proxy. This will create a 'destroy' operation.
* Note that this doesn't destroy this instance after the server comes back with a response.
* It will however call `afterErase` on any Stores it is joined to. Stores by default will
* automatically remove this instance from their data collection.
*
* @param {Object/Function} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
* If you pass a function, this will automatically become the callback method. For convenience the config
* object may also contain `success` and `failure` methods in addition to `callback` - they will all be invoked
* with the Model and Operation as arguments.
* @param {Object} scope The scope to run your callback method in. This is only used if you passed a function
* as the first argument.
* @return {Ext.data.Model} The Model instance.
*/
erase: function(options, scope) {
var me = this,
proxy = this.getProxy(),
operation,
callback;
if (!proxy) {
Ext.Logger.error('You are trying to erase a model instance that doesn\'t have a Proxy specified');
}
options = options || {};
scope = scope || me;
if (Ext.isFunction(options)) {
options = {
callback: options,
scope: scope
};
}
Ext.applyIf(options, {
records: [me],
action : 'destroy',
model: this.self
});
operation = Ext.create('Ext.data.Operation', options);
callback = function(operation) {
if (operation.wasSuccessful()) {
Ext.callback(options.success, scope, [me, operation]);
} else {
Ext.callback(options.failure, scope, [me, operation]);
}
Ext.callback(options.callback, scope, [me, operation]);
};
proxy.destroy(operation, callback, me);
return me;
},
/**
* Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. Rejects
* all changes made to the model instance since either creation, or the last commit operation. Modified fields are
* reverted to their original values.
*
* Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of reject
* operations.
*
* @param {Boolean} [silent=false] (optional) `true` to skip notification of the owning store of the change.
*/
reject: function(silent) {
var me = this,
modified = me.modified,
field;
for (field in modified) {
if (modified.hasOwnProperty(field)) {
if (typeof modified[field] != "function") {
me.data[field] = modified[field];
}
}
}
me.dirty = false;
me.editing = false;
me.modified = {};
if (silent !== true) {
me.afterReject();
}
},
/**
* Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all changes made to the
* instance since either creation or the last commit operation.
*
* Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of commit
* operations.
*
* @param {Boolean} [silent=false] (optional) `true` to skip notification of the owning store of the change.
*/
commit: function(silent) {
var me = this,
modified = this.modified;
me.phantom = me.dirty = me.editing = false;
me.modified = {};
if (silent !== true) {
me.afterCommit(modified);
}
},
/**
* @private
* If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
* `afterEdit` method is called.
* @param {String[]} modifiedFieldNames Array of field names changed during edit.
*/
afterEdit : function(modifiedFieldNames, modified) {
this.notifyStores('afterEdit', modifiedFieldNames, modified);
},
/**
* @private
* If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
* `afterReject` method is called.
*/
afterReject : function() {
this.notifyStores("afterReject");
},
/**
* @private
* If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
* `afterCommit` method is called.
*/
afterCommit: function(modified) {
this.notifyStores('afterCommit', Ext.Object.getKeys(modified || {}), modified);
},
/**
* @private
* Helper function used by {@link #afterEdit}, {@link #afterReject}, and {@link #afterCommit}. Calls the given method on the
* {@link Ext.data.Store store} that this instance has {@link #join join}ed, if any. The store function
* will always be called with the model instance as its single argument.
* @param {String} fn The function to call on the store.
*/
notifyStores: function(fn) {
var args = Ext.Array.clone(arguments),
stores = this.stores,
ln = stores.length,
i, store;
args[0] = this;
for (i = 0; i < ln; ++i) {
store = stores[i];
if (store !== undefined && typeof store[fn] == "function") {
store[fn].apply(store, args);
}
}
},
/**
* Creates a copy (clone) of this Model instance.
*
* @param {String} id A new `id`. If you don't specify this a new `id` will be generated for you.
* To generate a phantom instance with a new `id` use:
*
* var rec = record.copy(); // clone the record with a new id
*
* @return {Ext.data.Model}
*/
copy: function(newId) {
var me = this,
idProperty = me.getIdProperty(),
raw = Ext.apply({}, me.raw),
data = Ext.apply({}, me.data);
delete raw[idProperty];
delete data[idProperty];
return new me.self(null, newId, raw, data);
},
/**
* Returns an object containing the data set on this record. This method also allows you to
* retrieve all the associated data. Note that if you should always use this method if you
* need all the associated data, since the data property on the record instance is not
* ensured to be updated at all times.
* @param {Boolean} includeAssociated `true` to include the associated data.
* @return {Object} The data.
*/
getData: function(includeAssociated) {
var data = this.data;
if (includeAssociated === true) {
Ext.apply(data, this.getAssociatedData());
}
return data;
},
/**
* Gets all of the data from this Models *loaded* associations. It does this recursively - for example if we have a
* User which `hasMany` Orders, and each Order `hasMany` OrderItems, it will return an object like this:
*
* {
* orders: [
* {
* id: 123,
* status: 'shipped',
* orderItems: [
* // ...
* ]
* }
* ]
* }
*
* @return {Object} The nested data set for the Model's loaded associations.
*/
getAssociatedData: function() {
return this.prepareAssociatedData(this, [], null);
},
/**
* @private
* This complex-looking method takes a given Model instance and returns an object containing all data from
* all of that Model's *loaded* associations. See {@link #getAssociatedData}
* @param {Ext.data.Model} record The Model instance
* @param {String[]} ids PRIVATE. The set of Model instance `internalIds` that have already been loaded
* @param {String} associationType (optional) The name of the type of association to limit to.
* @return {Object} The nested data set for the Model's loaded associations.
*/
prepareAssociatedData: function(record, ids, associationType) {
//we keep track of all of the internalIds of the models that we have loaded so far in here
var associations = record.associations.items,
associationCount = associations.length,
associationData = {},
associatedStore, associationName, associatedRecords, associatedRecord,
associatedRecordCount, association, id, i, j, type, allow;
for (i = 0; i < associationCount; i++) {
association = associations[i];
associationName = association.getName();
type = association.getType();
allow = true;
if (associationType) {
allow = type == associationType;
}
if (allow && type.toLowerCase() == 'hasmany') {
//this is the hasMany store filled with the associated data
associatedStore = record[association.getStoreName()];
//we will use this to contain each associated record's data
associationData[associationName] = [];
//if it's loaded, put it into the association data
if (associatedStore && associatedStore.getCount() > 0) {
associatedRecords = associatedStore.data.items;
associatedRecordCount = associatedRecords.length;
//now we're finally iterating over the records in the association. We do this recursively
for (j = 0; j < associatedRecordCount; j++) {
associatedRecord = associatedRecords[j];
// Use the id, since it is prefixed with the model name, guaranteed to be unique
id = associatedRecord.id;
//when we load the associations for a specific model instance we add it to the set of loaded ids so that
//we don't load it twice. If we don't do this, we can fall into endless recursive loading failures.
if (Ext.Array.indexOf(ids, id) == -1) {
ids.push(id);
associationData[associationName][j] = associatedRecord.getData();
Ext.apply(associationData[associationName][j], this.prepareAssociatedData(associatedRecord, ids, associationType));
}
}
}
} else if (allow && (type.toLowerCase() == 'belongsto' || type.toLowerCase() == 'hasone')) {
associatedRecord = record[association.getInstanceName()];
if (associatedRecord !== undefined) {
id = associatedRecord.id;
if (Ext.Array.indexOf(ids, id) === -1) {
ids.push(id);
associationData[associationName] = associatedRecord.getData();
Ext.apply(associationData[associationName], this.prepareAssociatedData(associatedRecord, ids, associationType));
}
}
}
}
return associationData;
},
/**
* By joining this model to an instance of a class, this model will automatically try to
* call certain template methods on that instance ({@link #afterEdit}, {@link #afterCommit}, {@link Ext.data.Store#afterErase}).
* For example, a Store calls join and unjoin whenever you add or remove a record to it's data collection.
* This way a Store can get notified of any changes made to this record.
* This functionality is usually only required when creating custom components.
* @param {Ext.data.Store} store The store to which this model has been added.
*/
join: function(store) {
Ext.Array.include(this.stores, store);
},
/**
* This un-joins this record from an instance of a class. Look at the documentation for {@link #join}
* for more information about joining records to class instances.
* @param {Ext.data.Store} store The store from which this model has been removed.
*/
unjoin: function(store) {
Ext.Array.remove(this.stores, store);
},
/**
* Marks this **Record** as `{@link #dirty}`. This method is used internally when adding `{@link #phantom}` records
* to a {@link Ext.data.proxy.Server#writer writer enabled store}.
*
* Marking a record `{@link #dirty}` causes the phantom to be returned by {@link Ext.data.Store#getUpdatedRecords}
* where it will have a create action composed for it during {@link Ext.data.Model#save model save} operations.
*/
setDirty : function() {
var me = this,
name;
me.dirty = true;
me.fields.each(function(field) {
if (field.getPersist()) {
name = field.getName();
me.modified[name] = me.get(name);
}
});
},
/**
* Validates the current data against all of its configured {@link #cfg-validations}.
* @return {Ext.data.Errors} The errors object.
*/
validate: function() {
var errors = Ext.create('Ext.data.Errors'),
validations = this.getValidations().items,
validators = Ext.data.Validations,
length, validation, field, valid, type, i;
if (validations) {
length = validations.length;
for (i = 0; i < length; i++) {
validation = validations[i];
field = validation.field || validation.name;
type = validation.type;
valid = validators[type](validation, this.get(field));
if (!valid) {
errors.add(Ext.create('Ext.data.Error', {
field : field,
message: validation.message || validators.getMessage(type)
}));
}
}
}
return errors;
},
/**
* Checks if the model is valid. See {@link #validate}.
* @return {Boolean} `true` if the model is valid.
*/
isValid: function(){
return this.validate().isValid();
},
/**
* Returns a url-suitable string for this model instance. By default this just returns the name of the Model class
* followed by the instance ID - for example an instance of MyApp.model.User with ID 123 will return 'user/123'.
* @return {String} The url string for this model instance.
*/
toUrl: function() {
var pieces = this.$className.split('.'),
name = pieces[pieces.length - 1].toLowerCase();
return name + '/' + this.getId();
},
/**
* Destroys this model instance. Note that this doesn't do a 'destroy' operation. If you want to destroy
* the record in your localStorage or on the server you should use the {@link #erase} method.
*/
destroy: function() {
var me = this;
me.notifyStores('afterErase', me);
if (me.getUseCache()) {
delete Ext.data.Model.cache[Ext.data.Model.generateCacheId(me)];
}
me.raw = me.stores = me.modified = null;
me.callParent(arguments);
},
//<debug>
markDirty : function() {
if (Ext.isDefined(Ext.Logger)) {
Ext.Logger.deprecate('Ext.data.Model: markDirty has been deprecated. Use setDirty instead.');
}
return this.setDirty.apply(this, arguments);
},
//</debug>
applyProxy: function(proxy, currentProxy) {
return Ext.factory(proxy, Ext.data.Proxy, currentProxy, 'proxy');
},
updateProxy: function(proxy) {
if (proxy) {
proxy.setModel(this.self);
}
},
applyAssociations: function(associations) {
if (associations) {
this.addAssociations(associations, 'hasMany');
}
},
applyBelongsTo: function(belongsTo) {
if (belongsTo) {
this.addAssociations(belongsTo, 'belongsTo');
}
},
applyHasMany: function(hasMany) {
if (hasMany) {
this.addAssociations(hasMany, 'hasMany');
}
},
applyHasOne: function(hasOne) {
if (hasOne) {
this.addAssociations(hasOne, 'hasOne');
}
},
addAssociations: function(associations, defaultType) {
var ln, i, association,
name = this.self.modelName,
associationsCollection = this.self.associations,
onCreatedFn;
associations = Ext.Array.from(associations);
for (i = 0, ln = associations.length; i < ln; i++) {
association = associations[i];
if (!Ext.isObject(association)) {
association = {model: association};
}
Ext.applyIf(association, {
type: defaultType,
ownerModel: name,
associatedModel: association.model
});
delete association.model;
onCreatedFn = Ext.Function.bind(function(associationName) {
associationsCollection.add(Ext.data.association.Association.create(this));
}, association);
Ext.ClassManager.onCreated(onCreatedFn, this, (typeof association.associatedModel === 'string') ? association.associatedModel : Ext.getClassName(association.associatedModel));
}
},
applyValidations: function(validations) {
if (validations) {
if (!Ext.isArray(validations)) {
validations = [validations];
}
this.addValidations(validations);
}
},
addValidations: function(validations) {
this.self.validations.addAll(validations);
},
/**
* @method setFields
* Updates the collection of Fields that all instances of this Model use. **Does not** update field values in a Model
* instance (use {@link #set} for that), instead this updates which fields are available on the Model class. This
* is normally used when creating or updating Model definitions dynamically, for example if you allow your users to
* define their own Models and save the fields configuration to a database, this method allows you to change those
* fields later.
* @return {Array}
*/
applyFields: function(fields) {
var superFields = this.superclass.fields;
if (superFields) {
fields = superFields.items.concat(fields || []);
}
return fields || [];
},
updateFields: function(fields) {
var ln = fields.length,
me = this,
prototype = me.self.prototype,
idProperty = this.getIdProperty(),
idField, fieldsCollection, field, i;
/**
* @property {Ext.util.MixedCollection} fields
* The fields defined on this model.
*/
fieldsCollection = me._fields = me.fields = new Ext.util.Collection(prototype.getFieldName);
for (i = 0; i < ln; i++) {
field = fields[i];
if (!field.isField) {
field = new Ext.data.Field(fields[i]);
}
fieldsCollection.add(field);
}
// We want every Model to have an id property field
idField = fieldsCollection.get(idProperty);
if (!idField) {
fieldsCollection.add(new Ext.data.Field(idProperty));
} else {
idField.setType('auto');
}
fieldsCollection.addSorter(prototype.sortConvertFields);
},
applyIdentifier: function(identifier) {
if (typeof identifier === 'string') {
identifier = {
type: identifier
};
}
return Ext.factory(identifier, Ext.data.identifier.Simple, this.getIdentifier(), 'data.identifier');
},
/**
* This method is used by the fields collection to retrieve the key for a field
* based on it's name.
* @param field
* @return {String}
* @private
*/
getFieldName: function(field) {
return field.getName();
},
/**
* This method is being used to sort the fields based on their convert method. If
* a field has a custom convert method, we ensure its more to the bottom of the collection.
* @param field1
* @param field2
* @return {Number}
* @private
*/
sortConvertFields: function(field1, field2) {
var f1SpecialConvert = field1.hasCustomConvert(),
f2SpecialConvert = field2.hasCustomConvert();
if (f1SpecialConvert && !f2SpecialConvert) {
return 1;
}
if (!f1SpecialConvert && f2SpecialConvert) {
return -1;
}
return 0;
},
/**
* @private
*/
onClassExtended: function(cls, data, hooks) {
var onBeforeClassCreated = hooks.onBeforeCreated,
Model = this,
prototype = Model.prototype,
configNameCache = Ext.Class.configNameCache,
staticConfigs = prototype.staticConfigs.concat(data.staticConfigs || []),
defaultConfig = prototype.config,
config = data.config || {},
key;
// Convert old properties in data into a config object
data.config = config;
hooks.onBeforeCreated = function(cls, data) {
var dependencies = [],
prototype = cls.prototype,
statics = {},
config = prototype.config,
staticConfigsLn = staticConfigs.length,
copyMethods = ['set', 'get'],
copyMethodsLn = copyMethods.length,
associations = config.associations || [],
name = Ext.getClassName(cls),
key, methodName, i, j, ln;
// Create static setters and getters for each config option
for (i = 0; i < staticConfigsLn; i++) {
key = staticConfigs[i];
for (j = 0; j < copyMethodsLn; j++) {
methodName = configNameCache[key][copyMethods[j]];
if (methodName in prototype) {
statics[methodName] = Model.generateProxyMethod(methodName);
}
}
}
cls.addStatics(statics);
// Save modelName on class and its prototype
cls.modelName = name;
prototype.modelName = name;
// Take out dependencies on other associations and the proxy type
if (config.belongsTo) {
dependencies.push('association.belongsto');
}
if (config.hasMany) {
dependencies.push('association.hasmany');
}
if (config.hasOne) {
dependencies.push('association.hasone');
}
for (i = 0,ln = associations.length; i < ln; ++i) {
dependencies.push('association.' + associations[i].type.toLowerCase());
}
if (config.identifier) {
if (typeof config.identifier === 'string') {
dependencies.push('data.identifier.' + config.identifier);
}
else if (typeof config.identifier.type === 'string') {
dependencies.push('data.identifier.' + config.identifier.type);
}
}
if (config.proxy) {
if (typeof config.proxy === 'string') {
dependencies.push('proxy.' + config.proxy);
}
else if (typeof config.proxy.type === 'string') {
dependencies.push('proxy.' + config.proxy.type);
}
}
if (config.validations) {
dependencies.push('Ext.data.Validations');
}
Ext.require(dependencies, function() {
Ext.Function.interceptBefore(hooks, 'onCreated', function() {
Ext.data.ModelManager.registerType(name, cls);
var superCls = cls.prototype.superclass;
/**
* @property {Ext.util.Collection} associations
* The associations defined on this model.
*/
cls.prototype.associations = cls.associations = cls.prototype._associations = (superCls && superCls.associations)
? superCls.associations.clone()
: new Ext.util.Collection(function(association) {
return association.getName();
});
/**
* @property {Ext.util.Collection} validations
* The validations defined on this model.
*/
cls.prototype.validations = cls.validations = cls.prototype._validations = (superCls && superCls.validations)
? superCls.validations.clone()
: new Ext.util.Collection(function(validation) {
return validation.field ? (validation.field + '-' + validation.type) : (validation.name + '-' + validation.type);
});
cls.prototype = Ext.Object.chain(cls.prototype);
cls.prototype.initConfig.call(cls.prototype, config);
delete cls.prototype.initConfig;
});
onBeforeClassCreated.call(Model, cls, data, hooks);
});
};
}
});
/**
* @private
*/
Ext.define('Ext.util.Grouper', {
/* Begin Definitions */
extend: 'Ext.util.Sorter',
isGrouper: true,
config: {
/**
* @cfg {Function} groupFn This function will be called for each item in the collection to
* determine the group to which it belongs.
* @cfg {Object} groupFn.item The current item from the collection
* @cfg {String} groupFn.return The group identifier for the item
*/
groupFn: null,
/**
* @cfg {String} sortProperty You can define this configuration if you want the groups to be sorted
* on something other then the group string returned by the `groupFn`.
*/
sortProperty: null,
/**
* @cfg {Function} sorterFn
* Grouper has a custom sorterFn that cannot be overridden by the user. If a property has been defined
* on this grouper, we use the default `sorterFn`, else we sort based on the returned group string.
*/
sorterFn: function(item1, item2) {
var property = this.getSortProperty(),
groupFn, group1, group2, modifier;
groupFn = this.getGroupFn();
group1 = groupFn.call(this, item1);
group2 = groupFn.call(this, item2);
if (property) {
if (group1 !== group2) {
return this.defaultSortFn.call(this, item1, item2);
} else {
return 0;
}
}
return (group1 > group2) ? 1 : ((group1 < group2) ? -1 : 0);
}
},
/**
* @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._sortProperty;
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);
},
updateProperty: function(property) {
this.setGroupFn(this.standardGroupFn);
},
standardGroupFn: function(item) {
var root = this.getRoot(),
property = this.getProperty(),
data = item;
if (root) {
data = item[root];
}
return data[property];
},
getGroupString: function(item) {
var group = this.getGroupFn().call(this, item);
return typeof group != 'undefined' ? group.toString() : '';
}
});
/**
* @author Ed Spencer
* @aside guide stores
*
* The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load
* data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting},
* {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.
*
* Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:
*
* // Set up a {@link Ext.data.Model model} to use in our Store
* Ext.define("User", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* {name: "firstName", type: "string"},
* {name: "lastName", type: "string"},
* {name: "age", type: "int"},
* {name: "eyeColor", type: "string"}
* ]
* }
* });
*
* var myStore = Ext.create("Ext.data.Store", {
* model: "User",
* proxy: {
* type: "ajax",
* url : "/users.json",
* reader: {
* type: "json",
* rootProperty: "users"
* }
* },
* autoLoad: true
* });
*
* Ext.create("Ext.List", {
* fullscreen: true,
* store: myStore,
* itemTpl: "{lastName}, {firstName} ({age})"
* });
*
* In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy
* to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object -
* {@link Ext.data.reader.Json see the docs on JsonReader} for details.
*
* The external data file, _/users.json_, is as follows:
*
* {
* "success": true,
* "users": [
* {
* "firstName": "Tommy",
* "lastName": "Maintz",
* "age": 24,
* "eyeColor": "green"
* },
* {
* "firstName": "Aaron",
* "lastName": "Conran",
* "age": 26,
* "eyeColor": "blue"
* },
* {
* "firstName": "Jamie",
* "lastName": "Avins",
* "age": 37,
* "eyeColor": "brown"
* }
* ]
* }
*
* ## Inline data
*
* Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #cfg-data}
* into Model instances:
*
* @example
* // Set up a model to use in our Store
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* {name: 'firstName', type: 'string'},
* {name: 'lastName', type: 'string'},
* {name: 'age', type: 'int'},
* {name: 'eyeColor', type: 'string'}
* ]
* }
* });
*
* Ext.create("Ext.data.Store", {
* storeId: "usersStore",
* model: "User",
* data : [
* {firstName: "Ed", lastName: "Spencer"},
* {firstName: "Tommy", lastName: "Maintz"},
* {firstName: "Aaron", lastName: "Conran"},
* {firstName: "Jamie", lastName: "Avins"}
* ]
* });
*
* Ext.create("Ext.List", {
* fullscreen: true,
* store: "usersStore",
* itemTpl: "{lastName}, {firstName}"
* });
*
* Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need
* to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure,
* use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).
*
* Additional data can also be loaded locally using {@link #method-add}.
*
* ## Loading Nested Data
*
* Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders.
* Instead of issuing an AJAX request for the User and a series of additional AJAX requests for each Order, we can load a nested dataset
* and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro
* documentation for a full explanation:
*
* // Set up a model to use in our Store
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* {name: 'name', type: 'string'},
* {name: 'id', type: 'int'}
* ]
* }
* });
*
* var store = Ext.create('Ext.data.Store', {
* autoLoad: true,
* model: "User",
* proxy: {
* type: 'ajax',
* url : 'users.json',
* reader: {
* type: 'json',
* rootProperty: 'users'
* }
* }
* });
*
* Ext.create("Ext.List", {
* fullscreen: true,
* store: store,
* itemTpl: "{name} (id: {id})"
* });
*
* Which would consume a response like this:
*
* {
* "users": [
* {
* "id": 1,
* "name": "Ed",
* "orders": [
* {
* "id": 10,
* "total": 10.76,
* "status": "invoiced"
* },
* {
* "id": 11,
* "total": 13.45,
* "status": "shipped"
* }
* ]
* },
* {
* "id": 3,
* "name": "Tommy",
* "orders": [
* ]
* },
* {
* "id": 4,
* "name": "Jamie",
* "orders": [
* {
* "id": 12,
* "total": 17.76,
* "status": "shipped"
* }
* ]
* }
* ]
* }
*
* See the {@link Ext.data.reader.Reader} intro docs for a full explanation.
*
* ## Filtering and Sorting
*
* Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are
* held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to
* either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}:
*
* // Set up a model to use in our Store
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* {name: 'firstName', type: 'string'},
* {name: 'lastName', type: 'string'},
* {name: 'age', type: 'int'}
* ]
* }
* });
*
* var store = Ext.create("Ext.data.Store", {
* autoLoad: true,
* model: "User",
* proxy: {
* type: "ajax",
* url : "users.json",
* reader: {
* type: "json",
* rootProperty: "users"
* }
* },
* sorters: [
* {
* property : "age",
* direction: "DESC"
* },
* {
* property : "firstName",
* direction: "ASC"
* }
* ],
* filters: [
* {
* property: "firstName",
* value: /Jamie/
* }
* ]
* });
*
* Ext.create("Ext.List", {
* fullscreen: true,
* store: store,
* itemTpl: "{lastName}, {firstName} ({age})"
* });
*
* And the data file, _users.json_, is as follows:
*
* {
* "success": true,
* "users": [
* {
* "firstName": "Tommy",
* "lastName": "Maintz",
* "age": 24
* },
* {
* "firstName": "Aaron",
* "lastName": "Conran",
* "age": 26
* },
* {
* "firstName": "Jamie",
* "lastName": "Avins",
* "age": 37
* }
* ]
* }
*
* The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting
* and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to
* perform these operations instead.
*
* Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store
* and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by
* default your sorters are automatically reapplied if using local sorting.
*
* store.filter('eyeColor', 'Brown');
*
* Change the sorting at any time by calling {@link #sort}:
*
* store.sort('height', 'ASC');
*
* Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments,
* the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them
* to the MixedCollection:
*
* store.sorters.add(new Ext.util.Sorter({
* property : 'shoeSize',
* direction: 'ASC'
* }));
*
* store.sort();
*
* ## Registering with StoreManager
*
* Any Store that is instantiated with a {@link #storeId} will automatically be registered with the {@link Ext.data.StoreManager StoreManager}.
* This makes it easy to reuse the same store in multiple views:
*
* // this store can be used several times
* Ext.create('Ext.data.Store', {
* model: 'User',
* storeId: 'usersStore'
* });
*
* Ext.create('Ext.List', {
* store: 'usersStore'
* // other config goes here
* });
*
* Ext.create('Ext.view.View', {
* store: 'usersStore'
* // other config goes here
* });
*
* ## Further Reading
*
* Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these
* pieces and how they fit together, see:
*
* - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
* - {@link Ext.data.Model Model} - the core class in the data package
* - {@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response
*/
Ext.define('Ext.data.Store', {
alias: 'store.store',
extend: 'Ext.Evented',
requires: [
'Ext.util.Collection',
'Ext.data.Operation',
'Ext.data.proxy.Memory',
'Ext.data.Model',
'Ext.data.StoreManager',
'Ext.util.Grouper'
],
/**
* @event addrecords
* Fired when one or more new Model instances have been added to this Store. You should listen
* for this event if you have to update a representation of the records in this store in your UI.
* If you need the indices of the records that were added please use the store.indexOf(record) method.
* @param {Ext.data.Store} store The store
* @param {Ext.data.Model[]} records The Model instances that were added
*/
/**
* @event removerecords
* Fired when one or more Model instances have been removed from this Store. You should listen
* for this event if you have to update a representation of the records in this store in your UI.
* @param {Ext.data.Store} store The Store object
* @param {Ext.data.Model[]} records The Model instances that was removed
* @param {Number[]} indices The indices of the records that were removed. These indices already
* take into account any potential earlier records that you remove. This means that if you loop
* over the records, you can get its current index in your data representation from this array.
*/
/**
* @event updaterecord
* Fires when a Model instance has been updated
* @param {Ext.data.Store} this
* @param {Ext.data.Model} record The Model instance that was updated
* @param {Number} newIndex If the update changed the index of the record (due to sorting for example), then
* this gives you the new index in the store.
* @param {Number} oldIndex If the update changed the index of the record (due to sorting for example), then
* this gives you the old index in the store.
* @param {Array} modifiedFieldNames An array containing the field names that have been modified since the
* record was committed or created
* @param {Object} modifiedValues An object where each key represents a field name that had it's value modified,
* and where the value represents the old value for that field. To get the new value in a listener
* you should use the {@link Ext.data.Model#get get} method.
*/
/**
* @event update
* @inheritdoc Ext.data.Store#updaterecord
* @removed 2.0 Listen to #updaterecord instead.
*/
/**
* @event refresh
* Fires whenever the records in the Store have changed in a way that your representation of the records
* need to be entirely refreshed.
* @param {Ext.data.Store} this The data store
* @param {Ext.util.Collection} data The data collection containing all the records
*/
/**
* @event beforeload
* Fires before a request is made for a new data object. If the beforeload handler returns false the load
* action will be canceled. Note that you should not listen for this event in order to refresh the
* data view. Use the {@link #refresh} event for this instead.
* @param {Ext.data.Store} store This Store
* @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
* load the Store
*/
/**
* @event load
* Fires whenever records have been loaded into the store. Note that you should not listen
* for this event in order to refresh the data view. Use the {@link #refresh} event for this instead.
* @param {Ext.data.Store} this
* @param {Ext.data.Model[]} records An array of records
* @param {Boolean} successful `true` if the operation was successful.
* @param {Ext.data.Operation} operation The associated operation.
*/
/**
* @event write
* Fires whenever a successful write has been made via the configured {@link #proxy Proxy}
* @param {Ext.data.Store} store This Store
* @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object that was used in
* the write
*/
/**
* @event beforesync
* Fired before a call to {@link #sync} is executed. Return `false` from any listener to cancel the sync
* @param {Object} options Hash of all records to be synchronized, broken down into create, update and destroy
*/
/**
* @event clear
* Fired after the {@link #removeAll} method is called. Note that you should not listen for this event in order
* to refresh the data view. Use the {@link #refresh} event for this instead.
* @param {Ext.data.Store} this
* @return {Ext.data.Store}
*/
statics: {
create: function(store) {
if (!store.isStore) {
if (!store.type) {
store.type = 'store';
}
store = Ext.createByAlias('store.' + store.type, store);
}
return store;
}
},
isStore: true,
config: {
/**
* @cfg {String} storeId
* Unique identifier for this store. If present, this Store will be registered with the {@link Ext.data.StoreManager},
* making it easy to reuse elsewhere.
* @accessor
*/
storeId: undefined,
/**
* @cfg {Object[]/Ext.data.Model[]} data
* Array of Model instances or data objects to load locally. See "Inline data" above for details.
* @accessor
*/
data: null,
/**
* @cfg {Boolean/Object} [autoLoad=false]
* If data is not specified, and if `autoLoad` is `true` or an Object, this store's load method is automatically called
* after creation. If the value of `autoLoad` is an Object, this Object will be passed to the store's `load()` method.
* @accessor
*/
autoLoad: null,
/**
* @cfg {Boolean} autoSync
* `true` to automatically sync the Store with its Proxy after every edit to one of its Records.
* @accessor
*/
autoSync: false,
/**
* @cfg {String} model
* Name of the {@link Ext.data.Model Model} associated with this store.
* The string is used as an argument for {@link Ext.ModelManager#getModel}.
* @accessor
*/
model: undefined,
/**
* @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
* object or a Proxy instance - see {@link #setProxy} for details.
* @accessor
*/
proxy: undefined,
/**
* @cfg {Object[]} fields
* This may be used in place of specifying a {@link #model} configuration. The fields should be a
* set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model}
* with these fields. In general this configuration option should be avoided, it exists for the purposes of
* backwards compatibility. For anything more complicated, such as specifying a particular id property or
* associations, a {@link Ext.data.Model} should be defined and specified for the {@link #model}
* config.
* @accessor
*/
fields: null,
/**
* @cfg {Boolean} remoteSort
* `true` to defer any sorting operation to the server. If `false`, sorting is done locally on the client.
*
* If this is set to `true`, you will have to manually call the {@link #method-load} method after you {@link #method-sort}, to retrieve the sorted
* data from the server.
* @accessor
*/
remoteSort: false,
/**
* @cfg {Boolean} remoteFilter
* `true` to defer any filtering operation to the server. If `false`, filtering is done locally on the client.
*
* If this is set to `true`, you will have to manually call the {@link #method-load} method after you {@link #method-filter} to retrieve the filtered
* data from the server.
* @accessor
*/
remoteFilter: false,
/**
* @cfg {Boolean} remoteGroup
* `true` to defer any grouping operation to the server. If `false`, grouping is done locally on the client.
* @accessor
*/
remoteGroup: false,
/**
* @cfg {Object[]} filters
* Array of {@link Ext.util.Filter Filters} for this store. This configuration is handled by the
* {@link Ext.mixin.Filterable Filterable} mixin of the {@link Ext.util.Collection data} collection.
* @accessor
*/
filters: null,
/**
* @cfg {Object[]} sorters
* Array of {@link Ext.util.Sorter Sorters} for this store. This configuration is handled by the
* {@link Ext.mixin.Sortable Sortable} mixin of the {@link Ext.util.Collection data} collection.
* See also the {@link #sort} method.
* @accessor
*/
sorters: null,
/**
* @cfg {Object[]} grouper
* A configuration object for this Store's {@link Ext.util.Grouper grouper}.
*
* For example, to group a store's items by the first letter of the last name:
*
* Ext.define('People', {
* extend: 'Ext.data.Store',
*
* config: {
* fields: ['first_name', 'last_name'],
*
* grouper: {
* groupFn: function(record) {
* return record.get('last_name').substr(0, 1);
* },
* sortProperty: 'last_name'
* }
* }
* });
*
* @accessor
*/
grouper: null,
/**
* @cfg {String} groupField
* The (optional) field by which to group data in the store. Internally, grouping is very similar to sorting - the
* groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
* level of grouping, and groups can be fetched via the {@link #getGroups} method.
* @accessor
*/
groupField: null,
/**
* @cfg {String} groupDir
* The direction in which sorting should be applied when grouping. If you specify a grouper by using the {@link #groupField}
* configuration, this will automatically default to "ASC" - the other supported value is "DESC"
* @accessor
*/
groupDir: null,
/**
* @cfg {Function} getGroupString This function will be passed to the {@link #grouper} configuration as it's `groupFn`.
* Note that this configuration is deprecated and grouper: `{groupFn: yourFunction}}` is preferred.
* @deprecated
* @accessor
*/
getGroupString: null,
/**
* @cfg {Number} pageSize
* The number of records considered to form a 'page'. This is used to power the built-in
* paging using the nextPage and previousPage functions.
* @accessor
*/
pageSize: 25,
/**
* @cfg {Number} totalCount The total number of records in the full dataset, as indicated by a server. If the
* server-side dataset contains 5000 records but only returns pages of 50 at a time, `totalCount` will be set to
* 5000 and {@link #getCount} will return 50
*/
totalCount: null,
/**
* @cfg {Boolean} clearOnPageLoad `true` to empty the store when loading another page via {@link #loadPage},
* {@link #nextPage} or {@link #previousPage}. Setting to `false` keeps existing records, allowing
* large data sets to be loaded one page at a time but rendered all together.
* @accessor
*/
clearOnPageLoad: true,
modelDefaults: {},
/**
* @cfg {Boolean} autoDestroy This is a private configuration used in the framework whether this Store
* can be destroyed.
* @private
*/
autoDestroy: false,
/**
* @cfg {Boolean} syncRemovedRecords This configuration allows you to disable the synchronization of
* removed records on this Store. By default, when you call `removeAll()` or `remove()`, records will be added
* to an internal removed array. When you then sync the Store, we send a destroy request for these records.
* If you don't want this to happen, you can set this configuration to `false`.
*/
syncRemovedRecords: true,
/**
* @cfg {Boolean} destroyRemovedRecords This configuration allows you to prevent destroying record
* instances when they are removed from this store and are not in any other store.
*/
destroyRemovedRecords: true
},
/**
* @property {Number} currentPage
* The page that the Store has most recently loaded (see {@link #loadPage})
*/
currentPage: 1,
constructor: function(config) {
config = config || {};
this.data = this._data = this.createDataCollection();
this.data.setSortRoot('data');
this.data.setFilterRoot('data');
this.removed = [];
if (config.id && !config.storeId) {
config.storeId = config.id;
delete config.id;
}
this.initConfig(config);
this.callParent(arguments);
},
/**
* @private
* @return {Ext.util.Collection}
*/
createDataCollection: function() {
return new Ext.util.Collection(function(record) {
return record.getId();
});
},
applyStoreId: function(storeId) {
if (storeId === undefined || storeId === null) {
storeId = this.getUniqueId();
}
return storeId;
},
updateStoreId: function(storeId, oldStoreId) {
if (oldStoreId) {
Ext.data.StoreManager.unregister(this);
}
if (storeId) {
Ext.data.StoreManager.register(this);
}
},
applyModel: function(model) {
if (typeof model == 'string') {
var registeredModel = Ext.data.ModelManager.getModel(model);
if (!registeredModel) {
Ext.Logger.error('Model with name "' + model + '" does not exist.');
}
model = registeredModel;
}
if (model && !model.prototype.isModel && Ext.isObject(model)) {
model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
}
if (!model) {
var fields = this.getFields(),
data = this.config.data;
if (!fields && data && data.length) {
fields = Ext.Object.getKeys(data[0]);
}
if (fields) {
model = Ext.define('Ext.data.Store.ImplicitModel-' + (this.getStoreId() || Ext.id()), {
extend: 'Ext.data.Model',
config: {
fields: fields,
proxy: this.getProxy()
}
});
this.implicitModel = true;
}
}
if (!model && this.getProxy()) {
model = this.getProxy().getModel();
}
// <debug>
if (!model) {
Ext.Logger.warn('Unless you define your model through metadata, a store needs to have a model defined on either itself or on its proxy');
}
// </debug>
return model;
},
updateModel: function(model) {
var proxy = this.getProxy();
if (proxy && !proxy.getModel()) {
proxy.setModel(model);
}
},
applyProxy: function(proxy, currentProxy) {
proxy = Ext.factory(proxy, Ext.data.Proxy, currentProxy, 'proxy');
if (!proxy && this.getModel()) {
proxy = this.getModel().getProxy();
}
if (!proxy) {
proxy = new Ext.data.proxy.Memory({
model: this.getModel()
});
}
if (proxy.isMemoryProxy) {
this.setSyncRemovedRecords(false);
}
return proxy;
},
updateProxy: function(proxy) {
if (proxy) {
if (!proxy.getModel()) {
proxy.setModel(this.getModel());
}
proxy.on('metachange', this.onMetaChange, this);
}
},
/**
* We are using applyData so that we can return nothing and prevent the `this.data`
* property to be overridden.
* @param data
*/
applyData: function(data) {
var me = this,
proxy;
if (data) {
proxy = me.getProxy();
if (proxy instanceof Ext.data.proxy.Memory) {
proxy.setData(data);
me.load();
return;
} else {
// We make it silent because we don't want to fire a refresh event
me.removeAll(true);
// This means we have to fire a clear event though
me.fireEvent('clear', me);
// We don't want to fire addrecords event since we will be firing
// a refresh event later which will already take care of updating
// any views bound to this store
me.suspendEvents();
me.add(data);
me.resumeEvents();
// We set this to true so isAutoLoading to try
me.dataLoaded = true;
}
} else {
me.removeAll(true);
// This means we have to fire a clear event though
me.fireEvent('clear', me);
}
me.fireEvent('refresh', me, me.data);
},
clearData: function() {
this.setData(null);
},
addData: function(data) {
var reader = this.getProxy().getReader(),
resultSet = reader.read(data),
records = resultSet.getRecords();
this.add(records);
},
updateAutoLoad: function(autoLoad) {
var proxy = this.getProxy();
if (autoLoad && (proxy && !proxy.isMemoryProxy)) {
this.load(Ext.isObject(autoLoad) ? autoLoad : null);
}
},
/**
* Returns `true` if the Store is set to {@link #autoLoad} or is a type which loads upon instantiation.
* @return {Boolean}
*/
isAutoLoading: function() {
var proxy = this.getProxy();
return (this.getAutoLoad() || (proxy && proxy.isMemoryProxy) || this.dataLoaded);
},
updateGroupField: function(groupField) {
var grouper = this.getGrouper();
if (groupField) {
if (!grouper) {
this.setGrouper({
property: groupField,
direction: this.getGroupDir() || 'ASC'
});
} else {
grouper.setProperty(groupField);
}
} else if (grouper) {
this.setGrouper(null);
}
},
updateGroupDir: function(groupDir) {
var grouper = this.getGrouper();
if (grouper) {
grouper.setDirection(groupDir);
}
},
applyGetGroupString: function(getGroupStringFn) {
var grouper = this.getGrouper();
if (getGroupStringFn) {
// <debug>
Ext.Logger.warn('Specifying getGroupString on a store has been deprecated. Please use grouper: {groupFn: yourFunction}');
// </debug>
if (grouper) {
grouper.setGroupFn(getGroupStringFn);
} else {
this.setGrouper({
groupFn: getGroupStringFn
});
}
} else if (grouper) {
this.setGrouper(null);
}
},
applyGrouper: function(grouper) {
if (typeof grouper == 'string') {
grouper = {
property: grouper
};
}
else if (typeof grouper == 'function') {
grouper = {
groupFn: grouper
};
}
grouper = Ext.factory(grouper, Ext.util.Grouper, this.getGrouper());
return grouper;
},
updateGrouper: function(grouper, oldGrouper) {
var data = this.data;
if (oldGrouper) {
data.removeSorter(oldGrouper);
if (!grouper) {
data.getSorters().removeSorter('isGrouper');
}
}
if (grouper) {
data.insertSorter(0, grouper);
if (!oldGrouper) {
data.getSorters().addSorter({
direction: 'DESC',
property: 'isGrouper',
transform: function(value) {
return (value === true) ? 1 : -1;
}
});
}
}
},
/**
* This method tells you if this store has a grouper defined on it.
* @return {Boolean} `true` if this store has a grouper defined.
*/
isGrouped: function() {
return !!this.getGrouper();
},
updateSorters: function(sorters) {
var grouper = this.getGrouper(),
data = this.data,
autoSort = data.getAutoSort();
// While we remove/add sorters we don't want to automatically sort because we still need
// to apply any field sortTypes as transforms on the Sorters after we have added them.
data.setAutoSort(false);
data.setSorters(sorters);
if (grouper) {
data.insertSorter(0, grouper);
}
this.updateSortTypes();
// Now we put back autoSort on the Collection to the value it had before. If it was
// auto sorted, setting this back will cause it to sort right away.
data.setAutoSort(autoSort);
},
updateSortTypes: function() {
var model = this.getModel(),
fields = model && model.getFields(),
data = this.data;
// We loop over each sorter and set it's transform method to the every field's sortType.
if (fields) {
data.getSorters().each(function(sorter) {
var property = sorter.getProperty(),
field;
if (!sorter.isGrouper && property && !sorter.getTransform()) {
field = fields.get(property);
if (field) {
sorter.setTransform(field.getSortType());
}
}
});
}
},
updateFilters: function(filters) {
this.data.setFilters(filters);
},
/**
* Adds Model instance to the Store. This method accepts either:
*
* - An array of Model instances or Model configuration objects.
* - Any number of Model instance or Model configuration object arguments.
*
* The new Model instances will be added at the end of the existing collection.
*
* Sample usage:
*
* myStore.add({some: 'data2'}, {some: 'other data2'});
*
* @param {Ext.data.Model[]/Ext.data.Model...} model An array of Model instances
* or Model configuration objects, or variable number of Model instance or config arguments.
* @return {Ext.data.Model[]} The model instances that were added.
*/
add: function(records) {
if (!Ext.isArray(records)) {
records = Array.prototype.slice.call(arguments);
}
return this.insert(this.data.length, records);
},
/**
* Inserts Model instances into the Store at the given index and fires the {@link #add} event.
* See also `{@link #add}`.
* @param {Number} index The start index at which to insert the passed Records.
* @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
* @return {Object}
*/
insert: function(index, records) {
if (!Ext.isArray(records)) {
records = Array.prototype.slice.call(arguments, 1);
}
var me = this,
sync = false,
data = this.data,
ln = records.length,
Model = this.getModel(),
modelDefaults = me.getModelDefaults(),
added = false,
i, record;
records = records.slice();
for (i = 0; i < ln; i++) {
record = records[i];
if (!record.isModel) {
record = new Model(record);
}
// If we are adding a record that is already an instance which was still in the
// removed array, then we remove it from the removed array
else if (this.removed.indexOf(record) != -1) {
Ext.Array.remove(this.removed, record);
}
record.set(modelDefaults);
record.join(me);
records[i] = record;
// If this is a newly created record, then we might want to sync it later
sync = sync || (record.phantom === true);
}
// Now we insert all these records in one go to the collection. Saves many function
// calls to data.insert. Does however create two loops over the records we are adding.
if (records.length === 1) {
added = data.insert(index, records[0]);
if (added) {
added = [added];
}
} else {
added = data.insertAll(index, records);
}
if (added) {
me.fireEvent('addrecords', me, added);
}
if (me.getAutoSync() && sync) {
me.sync();
}
return records;
},
/**
* Removes the given record from the Store, firing the `removerecords` event passing all the instances that are removed.
* @param {Ext.data.Model/Ext.data.Model[]} records Model instance or array of instances to remove.
*/
remove: function (records) {
if (records.isModel) {
records = [records];
}
var me = this,
sync = false,
i = 0,
autoSync = this.getAutoSync(),
syncRemovedRecords = me.getSyncRemovedRecords(),
destroyRemovedRecords = this.getDestroyRemovedRecords(),
ln = records.length,
indices = [],
removed = [],
isPhantom,
items = me.data.items,
record, index, j;
for (; i < ln; i++) {
record = records[i];
if (me.data.contains(record)) {
isPhantom = (record.phantom === true);
index = items.indexOf(record);
if (index !== -1) {
removed.push(record);
indices.push(index);
}
record.unjoin(me);
me.data.remove(record);
if (destroyRemovedRecords && !syncRemovedRecords && !record.stores.length) {
record.destroy();
}
else if (!isPhantom && syncRemovedRecords) {
// don't push phantom records onto removed
me.removed.push(record);
}
sync = sync || !isPhantom;
}
}
me.fireEvent('removerecords', me, removed, indices);
if (autoSync && sync) {
me.sync();
}
},
/**
* Removes the model instance at the given index.
* @param {Number} index The record index.
*/
removeAt: function(index) {
var record = this.getAt(index);
if (record) {
this.remove(record);
}
},
/**
* Remove all items from the store.
* @param {Boolean} silent Prevent the `clear` event from being fired.
*/
removeAll: function(silent) {
if (silent !== true && this.eventFiringSuspended !== true) {
this.fireAction('clear', [this], 'doRemoveAll');
} else {
this.doRemoveAll.call(this, true);
}
},
doRemoveAll: function (silent) {
var me = this,
destroyRemovedRecords = this.getDestroyRemovedRecords(),
syncRemovedRecords = this.getSyncRemovedRecords(),
records = me.data.all.slice(),
ln = records.length,
i, record;
for (i = 0; i < ln; i++) {
record = records[i];
record.unjoin(me);
if (destroyRemovedRecords && !syncRemovedRecords && !record.stores.length) {
record.destroy();
}
else if (record.phantom !== true && syncRemovedRecords) {
me.removed.push(record);
}
}
me.data.clear();
if (silent !== true) {
me.fireEvent('refresh', me, me.data);
}
if (me.getAutoSync()) {
this.sync();
}
},
/**
* Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
*
* // Set up a model to use in our Store
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* {name: 'firstName', type: 'string'},
* {name: 'lastName', type: 'string'}
* ]
* }
* });
*
* var store = Ext.create('Ext.data.Store', {
* model: 'User',
* data : [
* {firstName: 'Ed', lastName: 'Spencer'},
* {firstName: 'Tommy', lastName: 'Maintz'},
* {firstName: 'Aaron', lastName: 'Conran'},
* {firstName: 'Jamie', lastName: 'Avins'}
* ]
* });
*
* store.each(function (item, index, length) {
* console.log(item.get('firstName'), index);
* });
*
* @param {Function} fn The function to call. Returning `false` aborts and exits the iteration.
* @param {Ext.data.Model} fn.item
* @param {Number} fn.index
* @param {Number} fn.length
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
* Defaults to the current {@link Ext.data.Model Record} in the iteration.
*/
each: function(fn, scope) {
this.data.each(fn, scope);
},
/**
* Gets the number of cached records. Note that filtered records are not included in this count.
* If using paging, this may not be the total size of the dataset.
* @return {Number} The number of Records in the Store's cache.
*/
getCount: function() {
return this.data.items.length || 0;
},
/**
* Gets the number of all cached records including the ones currently filtered.
* If using paging, this may not be the total size of the dataset.
* @return {Number} The number of all Records in the Store's cache.
*/
getAllCount: function () {
return this.data.all.length || 0;
},
/**
* Get the Record at the specified index.
* @param {Number} index The index of the Record to find.
* @return {Ext.data.Model/undefined} The Record at the passed index. Returns `undefined` if not found.
*/
getAt: function(index) {
return this.data.getAt(index);
},
/**
* Returns a range of Records between specified indices.
* @param {Number} [startIndex=0] (optional) The starting index.
* @param {Number} [endIndex=-1] (optional) The ending index (defaults to the last Record in the Store).
* @return {Ext.data.Model[]} An array of Records.
*/
getRange: function(start, end) {
return this.data.getRange(start, end);
},
/**
* Get the Record with the specified id.
* @param {String} id The id of the Record to find.
* @return {Ext.data.Model/undefined} The Record with the passed id. Returns `undefined` if not found.
*/
getById: function(id) {
return this.data.findBy(function(record) {
return record.getId() == id;
});
},
/**
* Get the index within the cache of the passed Record.
* @param {Ext.data.Model} record The Ext.data.Model object to find.
* @return {Number} The index of the passed Record. Returns -1 if not found.
*/
indexOf: function(record) {
return this.data.indexOf(record);
},
/**
* Get the index within the cache of the Record with the passed id.
* @param {String} id The id of the Record to find.
* @return {Number} The index of the Record. Returns -1 if not found.
*/
indexOfId: function(id) {
return this.data.indexOfKey(id);
},
/**
* @private
* A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
* @param {Ext.data.Model} record The model instance that was edited.
* @param {String[]} modifiedFieldNames Array of field names changed during edit.
*/
afterEdit: function(record, modifiedFieldNames, modified) {
var me = this,
data = me.data,
currentId = modified[record.getIdProperty()] || record.getId(),
currentIndex = data.keys.indexOf(currentId),
newIndex;
if (currentIndex === -1 && data.map[currentId] === undefined) {
return;
}
if (me.getAutoSync()) {
me.sync();
}
if (currentId !== record.getId()) {
data.replace(currentId, record);
} else {
data.replace(record);
}
newIndex = data.indexOf(record);
if (currentIndex === -1 && newIndex !== -1) {
me.fireEvent('addrecords', me, [record]);
}
else if (currentIndex !== -1 && newIndex === -1) {
me.fireEvent('removerecords', me, [record], [currentIndex]);
}
else if (newIndex !== -1) {
me.fireEvent('updaterecord', me, record, newIndex, currentIndex, modifiedFieldNames, modified);
}
},
/**
* @private
* A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
* @param {Ext.data.Model} record The model instance that was edited.
*/
afterReject: function(record) {
var index = this.data.indexOf(record);
this.fireEvent('updaterecord', this, record, index, index, [], {});
},
/**
* @private
* A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
* @param {Ext.data.Model} record The model instance that was edited.
*/
afterCommit: function(record, modifiedFieldNames, modified) {
var me = this,
data = me.data,
currentId = modified[record.getIdProperty()] || record.getId(),
currentIndex = data.keys.indexOf(currentId),
newIndex;
if (currentIndex === -1 && data.map[currentId] === undefined) {
return;
}
if (currentId !== record.getId()) {
data.replace(currentId, record);
} else {
data.replace(record);
}
newIndex = data.indexOf(record);
if (currentIndex === -1 && newIndex !== -1) {
me.fireEvent('addrecords', me, [record]);
}
else if (currentIndex !== -1 && newIndex === -1) {
me.fireEvent('removerecords', me, [record], [currentIndex]);
}
else if (newIndex !== -1) {
me.fireEvent('updaterecord', me, record, newIndex, currentIndex, modifiedFieldNames, modified);
}
},
/**
* This gets called by a record after is gets erased from the server.
* @param record
* @private
*/
afterErase: function(record) {
var me = this,
data = me.data,
index = data.indexOf(record);
if (index !== -1) {
data.remove(record);
me.fireEvent('removerecords', me, [record], [index]);
}
},
updateRemoteFilter: function(remoteFilter) {
this.data.setAutoFilter(!remoteFilter);
},
updateRemoteSort: function(remoteSort) {
this.data.setAutoSort(!remoteSort);
},
/**
* Sorts the data in the Store by one or more of its properties. Example usage:
*
* // sort by a single field
* myStore.sort('myField', 'DESC');
*
* // sorting by multiple fields
* myStore.sort([
* {
* property : 'age',
* direction: 'ASC'
* },
* {
* property : 'name',
* direction: 'DESC'
* }
* ]);
*
* Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter} instances, and delegates
* the actual sorting to its internal {@link Ext.util.Collection}.
*
* When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field, so this code:
*
* store.sort('myField');
* store.sort('myField');
*
* is equivalent to this code:
*
* store.sort('myField', 'ASC');
* store.sort('myField', 'DESC');
*
* because Store handles the toggling automatically.
*
* If the {@link #remoteSort} configuration has been set to `true`, you will have to manually call the {@link #method-load}
* method after you sort to retrieve the sorted data from the server.
*
* @param {String/Ext.util.Sorter[]} sorters Either a string name of one of the fields in this Store's configured
* {@link Ext.data.Model Model}, or an array of sorter configurations.
* @param {String} [defaultDirection=ASC] The default overall direction to sort the data by.
* @param {String} where (Optional) This can be either `'prepend'` or `'append'`. If you leave this undefined
* it will clear the current sorters.
*/
sort: function(sorters, defaultDirection, where) {
var data = this.data,
grouper = this.getGrouper(),
autoSort = data.getAutoSort();
if (sorters) {
// While we are adding sorters we don't want to sort right away
// since we need to update sortTypes on the sorters.
data.setAutoSort(false);
if (typeof where === 'string') {
if (where == 'prepend') {
data.insertSorters(grouper ? 1 : 0, sorters, defaultDirection);
} else {
data.addSorters(sorters, defaultDirection);
}
} else {
data.setSorters(null);
if (grouper) {
data.addSorters(grouper);
}
data.addSorters(sorters, defaultDirection);
}
this.updateSortTypes();
// Setting back autoSort to true (if it was like that before) will
// instantly sort the data again.
data.setAutoSort(autoSort);
}
if (!this.getRemoteSort()) {
// If we haven't added any new sorters we have to manually call sort
if (!sorters) {
this.data.sort();
}
this.fireEvent('sort', this, this.data, this.data.getSorters());
if (data.length) {
this.fireEvent('refresh', this, this.data);
}
}
},
/**
* Filters the loaded set of records by a given set of filters.
*
* Filtering by single field:
*
* store.filter("email", /\.com$/);
*
* Using multiple filters:
*
* store.filter([
* {property: "email", value: /\.com$/},
* {filterFn: function(item) { return item.get("age") > 10; }}
* ]);
*
* Using Ext.util.Filter instances instead of config objects
* (note that we need to specify the {@link Ext.util.Filter#root root} config option in this case):
*
* store.filter([
* Ext.create('Ext.util.Filter', {property: "email", value: /\.com$/, root: 'data'}),
* Ext.create('Ext.util.Filter', {filterFn: function(item) { return item.get("age") > 10; }, root: 'data'})
* ]);
*
* If the {@link #remoteFilter} configuration has been set to `true`, you will have to manually call the {@link #method-load}
* method after you filter to retrieve the filtered data from the server.
*
* @param {Object[]/Ext.util.Filter[]/String} filters The set of filters to apply to the data.
* These are stored internally on the store, but the filtering itself is done on the Store's
* {@link Ext.util.MixedCollection MixedCollection}. See MixedCollection's
* {@link Ext.util.MixedCollection#filter filter} method for filter syntax.
* Alternatively, pass in a property string.
* @param {String} [value] value to filter by (only if using a property string as the first argument).
* @param {Boolean} [anyMatch=false] `true` to allow any match, false to anchor regex beginning with `^`.
* @param {Boolean} [caseSensitive=false] `true` to make the filtering regex case sensitive.
*/
filter: function(property, value, anyMatch, caseSensitive) {
var data = this.data,
filter = null;
if (property) {
if (Ext.isFunction(property)) {
filter = {filterFn: property};
}
else if (Ext.isArray(property) || property.isFilter) {
filter = property;
}
else {
filter = {
property : property,
value : value,
anyMatch : anyMatch,
caseSensitive: caseSensitive,
id : property
}
}
}
if (this.getRemoteFilter()) {
data.addFilters(filter);
} else {
data.filter(filter);
this.fireEvent('filter', this, data, data.getFilters());
this.fireEvent('refresh', this, data);
}
},
/**
* Filter by a function. The specified function will be called for each
* Record in this Store. If the function returns `true` the Record is included,
* otherwise it is filtered out.
* @param {Function} fn The function to be called. It will be passed the following parameters:
* @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
* to test for filtering. Access field values using {@link Ext.data.Model#get}.
* @param {Object} fn.id The ID of the Record passed.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
*/
filterBy: function(fn, scope) {
var me = this,
data = me.data,
ln = data.length;
data.filter({
filterFn: function(record) {
return fn.call(scope || me, record, record.getId())
}
});
this.fireEvent('filter', this, data, data.getFilters());
if (data.length !== ln) {
this.fireEvent('refresh', this, data);
}
},
/**
* Query the cached records in this Store using a filtering function. The specified function
* will be called with each record in this Store. If the function returns `true` the record is
* included in the results.
* @param {Function} fn The function to be called. It will be passed the following parameters:
* @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
* to test for filtering. Access field values using {@link Ext.data.Model#get}.
* @param {Object} fn.id The ID of the Record passed.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
* @return {Ext.util.MixedCollection} Returns an Ext.util.MixedCollection of the matched records.
*/
queryBy: function(fn, scope) {
return this.data.filterBy(fn, scope || this);
},
/**
* Reverts to a view of the Record cache with no filtering applied.
* @param {Boolean} [suppressEvent=false] `true` to clear silently without firing the `refresh` event.
*/
clearFilter: function(suppressEvent) {
var ln = this.data.length;
if (suppressEvent) {
this.suspendEvents();
}
this.data.setFilters(null);
if (suppressEvent) {
this.resumeEvents();
} else if (ln !== this.data.length) {
this.fireEvent('refresh', this, this.data);
}
},
/**
* Returns `true` if this store is currently filtered.
* @return {Boolean}
*/
isFiltered : function () {
return this.data.filtered;
},
/**
* Returns `true` if this store is currently sorted.
* @return {Boolean}
*/
isSorted : function () {
return this.data.sorted;
},
getSorters: function() {
var sorters = this.data.getSorters();
return (sorters) ? sorters.items : [];
},
getFilters: function() {
var filters = this.data.getFilters();
return (filters) ? filters.items : [];
},
/**
* Returns an array containing the result of applying the grouper to the records in this store. See {@link #groupField},
* {@link #groupDir} and {@link #grouper}. Example for a store containing records with a color field:
*
* var myStore = Ext.create('Ext.data.Store', {
* groupField: 'color',
* groupDir : 'DESC'
* });
*
* myStore.getGroups(); //returns:
* [
* {
* name: 'yellow',
* children: [
* //all records where the color field is 'yellow'
* ]
* },
* {
* name: 'red',
* children: [
* //all records where the color field is 'red'
* ]
* }
* ]
*
* @param {String} groupName (Optional) Pass in an optional `groupName` argument to access a specific group as defined by {@link #grouper}.
* @return {Object/Object[]} The grouped data.
*/
getGroups: function(requestGroupString) {
var records = this.data.items,
length = records.length,
grouper = this.getGrouper(),
groups = [],
pointers = {},
record,
groupStr,
group,
i;
// <debug>
if (!grouper) {
Ext.Logger.error('Trying to get groups for a store that has no grouper');
}
// </debug>
for (i = 0; i < length; i++) {
record = records[i];
groupStr = grouper.getGroupString(record);
group = pointers[groupStr];
if (group === undefined) {
group = {
name: groupStr,
children: []
};
groups.push(group);
pointers[groupStr] = group;
}
group.children.push(record);
}
return requestGroupString ? pointers[requestGroupString] : groups;
},
/**
* @param record
* @return {null}
*/
getGroupString: function(record) {
var grouper = this.getGrouper();
if (grouper) {
return grouper.getGroupString(record);
}
return null;
},
/**
* Finds the index of the first matching Record in this store by a specific field value.
* @param {String} fieldName The name of the Record field to test.
* @param {String/RegExp} value Either a string that the field value
* should begin with, or a RegExp to test against the field.
* @param {Number} startIndex (optional) The index to start searching at.
* @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning.
* @param {Boolean} caseSensitive (optional) `true` for case sensitive comparison.
* @param {Boolean} [exactMatch=false] (optional) `true` to force exact match (^ and $ characters added to the regex).
* @return {Number} The matched index or -1
*/
find: function(fieldName, value, startIndex, anyMatch, caseSensitive, exactMatch) {
var filter = Ext.create('Ext.util.Filter', {
property: fieldName,
value: value,
anyMatch: anyMatch,
caseSensitive: caseSensitive,
exactMatch: exactMatch,
root: 'data'
});
return this.data.findIndexBy(filter.getFilterFn(), null, startIndex);
},
/**
* Finds the first matching Record in this store by a specific field value.
* @param {String} fieldName The name of the Record field to test.
* @param {String/RegExp} value Either a string that the field value
* should begin with, or a RegExp to test against the field.
* @param {Number} startIndex (optional) The index to start searching at.
* @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning.
* @param {Boolean} caseSensitive (optional) `true` for case sensitive comparison.
* @param {Boolean} [exactMatch=false] (optional) `true` to force exact match (^ and $ characters added to the regex).
* @return {Ext.data.Model} The matched record or `null`.
*/
findRecord: function() {
var me = this,
index = me.find.apply(me, arguments);
return index !== -1 ? me.getAt(index) : null;
},
/**
* Finds the index of the first matching Record in this store by a specific field value.
* @param {String} fieldName The name of the Record field to test.
* @param {Object} value The value to match the field against.
* @param {Number} startIndex (optional) The index to start searching at.
* @return {Number} The matched index or -1.
*/
findExact: function(fieldName, value, startIndex) {
return this.data.findIndexBy(function(record) {
return record.get(fieldName) === value;
}, this, startIndex);
},
/**
* Find the index of the first matching Record in this Store by a function.
* If the function returns `true` it is considered a match.
* @param {Function} fn The function to be called. It will be passed the following parameters:
* @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
* to test for filtering. Access field values using {@link Ext.data.Model#get}.
* @param {Object} fn.id The ID of the Record passed.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
* @param {Number} startIndex (optional) The index to start searching at.
* @return {Number} The matched index or -1.
*/
findBy: function(fn, scope, startIndex) {
return this.data.findIndexBy(fn, scope, startIndex);
},
/**
* Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
* asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
* instances into the Store and calling an optional callback if required. Example usage:
*
* store.load({
* callback: function(records, operation, success) {
* // the {@link Ext.data.Operation operation} object contains all of the details of the load operation
* console.log(records);
* },
* scope: this
* });
*
* If only the callback and scope options need to be specified, then one can call it simply like so:
*
* store.load(function(records, operation, success) {
* console.log('loaded records');
* }, this);
*
* @param {Object/Function} [options] config object, passed into the {@link Ext.data.Operation} object before loading.
* @param {Object} [scope] Scope for the function.
* @return {Object}
*/
load: function(options, scope) {
var me = this,
operation,
currentPage = me.currentPage,
pageSize = me.getPageSize();
options = options || {};
if (Ext.isFunction(options)) {
options = {
callback: options,
scope: scope || this
};
}
if (me.getRemoteSort()) {
options.sorters = options.sorters || this.getSorters();
}
if (me.getRemoteFilter()) {
options.filters = options.filters || this.getFilters();
}
if (me.getRemoteGroup()) {
options.grouper = options.grouper || this.getGrouper();
}
Ext.applyIf(options, {
page: currentPage,
start: (currentPage - 1) * pageSize,
limit: pageSize,
addRecords: false,
action: 'read',
model: this.getModel()
});
operation = Ext.create('Ext.data.Operation', options);
if (me.fireEvent('beforeload', me, operation) !== false) {
me.loading = true;
me.getProxy().read(operation, me.onProxyLoad, me);
}
return me;
},
/**
* Returns `true` if the Store is currently performing a load operation.
* @return {Boolean} `true` if the Store is currently loading.
*/
isLoading: function() {
return Boolean(this.loading);
},
/**
* Returns `true` if the Store has been loaded.
* @return {Boolean} `true` if the Store has been loaded.
*/
isLoaded: function() {
return Boolean(this.loaded);
},
/**
* Synchronizes the Store with its Proxy. This asks the Proxy to batch together any new, updated
* and deleted records in the store, updating the Store's internal representation of the records
* as each operation completes.
* @return {Object}
* @return {Object} return.added
* @return {Object} return.updated
* @return {Object} return.removed
*/
sync: function() {
var me = this,
operations = {},
toCreate = me.getNewRecords(),
toUpdate = me.getUpdatedRecords(),
toDestroy = me.getRemovedRecords(),
needsSync = false;
if (toCreate.length > 0) {
operations.create = toCreate;
needsSync = true;
}
if (toUpdate.length > 0) {
operations.update = toUpdate;
needsSync = true;
}
if (toDestroy.length > 0) {
operations.destroy = toDestroy;
needsSync = true;
}
if (needsSync && me.fireEvent('beforesync', this, operations) !== false) {
me.getProxy().batch({
operations: operations,
listeners: me.getBatchListeners()
});
}
return {
added: toCreate,
updated: toUpdate,
removed: toDestroy
};
},
/**
* Convenience function for getting the first model instance in the store.
* @return {Ext.data.Model/undefined} The first model instance in the store, or `undefined`.
*/
first: function() {
return this.data.first();
},
/**
* Convenience function for getting the last model instance in the store.
* @return {Ext.data.Model/undefined} The last model instance in the store, or `undefined`.
*/
last: function() {
return this.data.last();
},
/**
* Sums the value of `property` for each {@link Ext.data.Model record} between `start`
* and `end` and returns the result.
* @param {String} field The field in each record.
* @return {Number} The sum.
*/
sum: function(field) {
var total = 0,
i = 0,
records = this.data.items,
len = records.length;
for (; i < len; ++i) {
total += records[i].get(field);
}
return total;
},
/**
* Gets the minimum value in the store.
* @param {String} field The field in each record.
* @return {Object/undefined} The minimum value, if no items exist, `undefined`.
*/
min: function(field) {
var i = 1,
records = this.data.items,
len = records.length,
value, min;
if (len > 0) {
min = records[0].get(field);
}
for (; i < len; ++i) {
value = records[i].get(field);
if (value < min) {
min = value;
}
}
return min;
},
/**
* Gets the maximum value in the store.
* @param {String} field The field in each record.
* @return {Object/undefined} The maximum value, if no items exist, `undefined`.
*/
max: function(field) {
var i = 1,
records = this.data.items,
len = records.length,
value,
max;
if (len > 0) {
max = records[0].get(field);
}
for (; i < len; ++i) {
value = records[i].get(field);
if (value > max) {
max = value;
}
}
return max;
},
/**
* Gets the average value in the store.
* @param {String} field The field in each record you want to get the average for.
* @return {Number} The average value, if no items exist, 0.
*/
average: function(field) {
var i = 0,
records = this.data.items,
len = records.length,
sum = 0;
if (records.length > 0) {
for (; i < len; ++i) {
sum += records[i].get(field);
}
return sum / len;
}
return 0;
},
/**
* @private
* Returns an object which is passed in as the listeners argument to `proxy.batch` inside `this.sync`.
* This is broken out into a separate function to allow for customization of the listeners.
* @return {Object} The listeners object.
* @return {Object} return.scope
* @return {Object} return.exception
* @return {Object} return.complete
*/
getBatchListeners: function() {
return {
scope: this,
exception: this.onBatchException,
complete: this.onBatchComplete
};
},
/**
* @private
* Attached as the `complete` event listener to a proxy's Batch object. Iterates over the batch operations
* and updates the Store's internal data MixedCollection.
*/
onBatchComplete: function(batch) {
var me = this,
operations = batch.operations,
length = operations.length,
i;
for (i = 0; i < length; i++) {
me.onProxyWrite(operations[i]);
}
},
onBatchException: function(batch, operation) {
// //decide what to do... could continue with the next operation
// batch.start();
//
// //or retry the last operation
// batch.retry();
},
/**
* @private
* Called internally when a Proxy has completed a load request.
*/
onProxyLoad: function(operation) {
var me = this,
records = operation.getRecords(),
resultSet = operation.getResultSet(),
successful = operation.wasSuccessful();
if (resultSet) {
me.setTotalCount(resultSet.getTotal());
}
if (successful) {
this.fireAction('datarefresh', [this, this.data, operation], 'doDataRefresh');
}
me.loaded = true;
me.loading = false;
me.fireEvent('load', this, records, successful, operation);
//this is a callback that would have been passed to the 'read' function and is optional
Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, successful]);
},
doDataRefresh: function(store, data, operation) {
var records = operation.getRecords(),
me = this,
destroyRemovedRecords = me.getDestroyRemovedRecords(),
currentRecords = data.all.slice(),
ln = currentRecords.length,
ln2 = records.length,
ids = {},
i, record;
if (operation.getAddRecords() !== true) {
for (i = 0; i < ln2; i++) {
ids[records[i].id] = true;
}
for (i = 0; i < ln; i++) {
record = currentRecords[i];
record.unjoin(me);
// If the record we are removing is not part of the records we are about to add to the store then handle
// the destroying or removing of the record to avoid memory leaks.
if (ids[record.id] !== true && destroyRemovedRecords && !record.stores.length) {
record.destroy();
}
}
data.clear();
// This means we have to fire a clear event though
me.fireEvent('clear', me);
}
if (records && records.length) {
// Now lets add the records without firing an addrecords event
me.suspendEvents();
me.add(records);
me.resumeEvents();
}
me.fireEvent('refresh', me, data);
},
/**
* @private
* Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect
* the updates provided by the Proxy.
*/
onProxyWrite: function(operation) {
var me = this,
success = operation.wasSuccessful(),
records = operation.getRecords();
switch (operation.getAction()) {
case 'create':
me.onCreateRecords(records, operation, success);
break;
case 'update':
me.onUpdateRecords(records, operation, success);
break;
case 'destroy':
me.onDestroyRecords(records, operation, success);
break;
}
if (success) {
me.fireEvent('write', me, operation);
}
//this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional
Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, success]);
},
// These methods are now just template methods since updating the records etc is all taken care of
// by the operation itself.
onCreateRecords: function(records, operation, success) {},
onUpdateRecords: function(records, operation, success) {},
onDestroyRecords: function(records, operation, success) {
this.removed = [];
},
onMetaChange: function(data) {
var model = this.getProxy().getModel();
if (!this.getModel() && model) {
this.setModel(model);
}
/**
* @event metachange
* Fires whenever the server has sent back new metadata to reconfigure the Reader.
* @param {Ext.data.Store} this
* @param {Object} data The metadata sent back from the server.
*/
this.fireEvent('metachange', this, data);
},
/**
* Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not
* yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one).
* @return {Ext.data.Model[]} The Model instances.
*/
getNewRecords: function() {
return this.data.filterBy(function(item) {
// only want phantom records that are valid
return item.phantom === true && item.isValid();
}).items;
},
/**
* Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy.
* @return {Ext.data.Model[]} The updated Model instances.
*/
getUpdatedRecords: function() {
return this.data.filterBy(function(item) {
// only want dirty records, not phantoms that are valid
return item.dirty === true && item.phantom !== true && item.isValid();
}).items;
},
/**
* Returns any records that have been removed from the store but not yet destroyed on the proxy.
* @return {Ext.data.Model[]} The removed Model instances.
*/
getRemovedRecords: function() {
return this.removed;
},
// PAGING METHODS
/**
* Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
* load operation, passing in calculated `start` and `limit` params.
* @param {Number} page The number of the page to load.
* @param {Object} options See options for {@link #method-load}.
* @param scope
*/
loadPage: function(page, options, scope) {
if (typeof options === 'function') {
options = {
callback: options,
scope: scope || this
};
}
var me = this,
pageSize = me.getPageSize(),
clearOnPageLoad = me.getClearOnPageLoad();
options = Ext.apply({}, options);
me.currentPage = page;
me.load(Ext.applyIf(options, {
page: page,
start: (page - 1) * pageSize,
limit: pageSize,
addRecords: !clearOnPageLoad
}));
},
/**
* Loads the next 'page' in the current data set.
* @param {Object} options See options for {@link #method-load}.
*/
nextPage: function(options) {
this.loadPage(this.currentPage + 1, options);
},
/**
* Loads the previous 'page' in the current data set.
* @param {Object} options See options for {@link #method-load}.
*/
previousPage: function(options) {
this.loadPage(this.currentPage - 1, options);
},
destroy: function() {
Ext.data.StoreManager.unregister(this);
this.callParent(arguments);
}
});
/**
* The Ext.chart package provides the capability to visualize data.
* Each chart binds directly to an {@link Ext.data.Store} enabling automatic updates of the chart.
* A chart configuration object has some overall styling options as well as an array of axes
* and series. A chart instance example could look like:
*
* new Ext.chart.CartesianChart({
* width: 800,
* height: 600,
* animate: true,
* store: store1,
* legend: {
* position: 'right'
* },
* axes: [
* // ...some axes options...
* ],
* series: [
* // ...some series options...
* ]
* });
*
* In this example we set the `width` and `height` of a cartesian chart; We decide whether our series are
* animated or not and we select a store to be bound to the chart; We also set the legend to the right part of the
* chart.
*
* You can register certain interactions such as {@link Ext.chart.interactions.PanZoom} on the chart by specify an
* array of names or more specific config objects. All the events will be wired automatically.
*
* You can also listen to `itemXXX` events directly on charts. That case all the contained series will relay this event to the
* chart.
*
* For more information about the axes and series configurations please check the documentation of
* each series (Line, Bar, Pie, etc).
*
*/
Ext.define('Ext.chart.AbstractChart', {
extend: 'Ext.draw.Component',
requires: [
'Ext.chart.series.Series',
'Ext.chart.interactions.Abstract',
'Ext.chart.axis.Axis',
'Ext.data.StoreManager',
'Ext.chart.Legend',
'Ext.data.Store'
],
/**
* @event beforerefresh
* Fires before a refresh to the chart data is called. If the `beforerefresh` handler returns
* `false` the {@link #refresh} action will be canceled.
* @param {Ext.chart.AbstractChart} this
*/
/**
* @event refresh
* Fires after the chart data has been refreshed.
* @param {Ext.chart.AbstractChart} this
*/
/**
* @event redraw
* Fires after the chart is redrawn.
* @param {Ext.chart.AbstractChart} this
*/
/**
* @event itemmousemove
* Fires when the mouse is moved on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemmouseup
* Fires when a mouseup event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemmousedown
* Fires when a mousedown event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemmouseover
* Fires when the mouse enters a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemmouseout
* Fires when the mouse exits a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemclick
* Fires when a click event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemdoubleclick
* Fires when a doubleclick event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemtap
* Fires when a tap event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemtapstart
* Fires when a tapstart event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemtapend
* Fires when a tapend event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemtapcancel
* Fires when a tapcancel event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemtaphold
* Fires when a taphold event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemdoubletap
* Fires when a doubletap event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemsingletap
* Fires when a singletap event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemtouchstart
* Fires when a touchstart event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemtouchmove
* Fires when a touchmove event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemtouchend
* Fires when a touchend event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemdragstart
* Fires when a dragstart event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemdrag
* Fires when a drag event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemdragend
* Fires when a dragend event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itempinchstart
* Fires when a pinchstart event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itempinch
* Fires when a pinch event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itempinchend
* Fires when a pinchend event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @event itemswipe
* Fires when a swipe event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
/**
* @property version Current Version of Touch Charts
* @type {String}
*/
version: '2.0.0',
// @ignore
viewBox: false,
delegationRegex: /^item([a-z]+)$/i,
domEvents: /click|focus|blur|paste|input|mousemove|mousedown|mouseup|mouseover|mouseout|keyup|keydown|keypress|submit|pinch|pinchstart|pinchend|touchstart|touchend|rotate|rotatestart|rotateend|drag|dragstart|dragend|tap|doubletap|singletap/,
config: {
/**
* @cfg {Ext.data.Store} store
* The store that supplies data to this chart.
*/
store: null,
/**
* @cfg {Boolean/Object} shadow (optional) `true` for the default shadow configuration `{shadowOffsetX: 2, shadowOffsetY: 2, shadowBlur: 3, shadowColor: '#444'}`
* or a standard shadow config object to be used for default chart shadows.
*/
shadow: false,
/**
* @cfg {Boolean/Object} animate (optional) `true` for the default animation (easing: 'ease' and duration: 500)
* or a standard animation config object to be used for default chart animations.
*/
animate: true,
/**
* @cfg {Ext.chart.series.Series/Array} series
* Array of {@link Ext.chart.series.Series Series} instances or config objects. For example:
*
* series: [{
* type: 'column',
* axis: 'left',
* listeners: {
* 'afterrender': function() {
* console.log('afterrender');
* }
* },
* xField: 'category',
* yField: 'data1'
* }]
*/
series: [],
/**
* @cfg {Ext.chart.axis.Axis/Array/Object} axes
* Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. For example:
*
* axes: [{
* type: 'Numeric',
* position: 'left',
* fields: ['data1'],
* title: 'Number of Hits',
* minimum: 0,
* // one minor tick between two major ticks
* minorTickSteps: 1
* }, {
* type: 'Category',
* position: 'bottom',
* fields: ['name'],
* title: 'Month of the Year'
* }]
*/
axes: [],
/**
* @cfg {Ext.chart.Legend/Object} legend
*/
legend: null,
/**
* @cfg {Boolean/Array} colors Array of colors/gradients to override the color of items and legends.
*/
colors: null,
/**
* @cfg {Object|Number} insetPadding The amount of inset padding in pixels for the chart. Inset padding is
* the padding from the boundary of the chart to any of its contents.
* @cfg {Number} insetPadding.top
*/
insetPadding: {
top: 10,
left: 10,
right: 10,
bottom: 10
},
/**
* @cfg {Object} innerPadding The amount of inner padding in pixel. Inner padding is the padding from
* axis to the series.
*/
innerPadding: {
top: 0,
left: 0,
right: 0,
bottom: 0
},
/**
* @cfg {Object|Boolean} background (optional) Set the chart background. This can be a gradient object, image, or color.
*
* For example, if `background` were to be a color we could set the object as
*
* background: {
* //color string
* fill: '#ccc'
* }
*
* You can specify an image by using:
*
* background: {
* image: 'http://path.to.image/'
* }
*
* Also you can specify a gradient by using the gradient object syntax:
*
* background: {
* gradient: {
* id: 'gradientId',
* angle: 45,
* stops: {
* 0: {
* color: '#555'
* },
* 100: {
* color: '#ddd'
* }
* }
* }
* }
*/
background: null,
/**
* @cfg {Array} interactions
* Interactions are optional modules that can be plugged in to a chart to allow the user to interact
* with the chart and its data in special ways. The `interactions` config takes an Array of Object
* configurations, each one corresponding to a particular interaction class identified by a `type` property:
*
* new Ext.chart.AbstractChart({
* renderTo: Ext.getBody(),
* width: 800,
* height: 600,
* store: store1,
* axes: [
* // ...some axes options...
* ],
* series: [
* // ...some series options...
* ],
* interactions: [{
* type: 'interactiontype'
* // ...additional configs for the interaction...
* }]
* });
*
* When adding an interaction which uses only its default configuration (no extra properties other than `type`),
* you can alternately specify only the type as a String rather than the full Object:
*
* interactions: ['reset', 'rotate']
*
* The current supported interaction types include:
*
* - {@link Ext.chart.interactions.PanZoom panzoom} - allows pan and zoom of axes
* - {@link Ext.chart.interactions.ItemHighlight itemhighlight} - allows highlighting of series data points
* - {@link Ext.chart.interactions.ItemInfo iteminfo} - allows displaying details of a data point in a popup panel
* - {@link Ext.chart.interactions.Rotate rotate} - allows rotation of pie and radar series
*
* See the documentation for each of those interaction classes to see how they can be configured.
*
* Additional custom interactions can be registered using `'interactions.'` alias prefix.
*/
interactions: [],
/**
* @private
* The main region of the chart.
*/
mainRegion: null,
/**
* @private
* Override value
*/
autoSize: false,
/**
* @private
* Override value
*/
viewBox: false,
/**
* @private
* Override value
*/
fitSurface: false,
/**
* @private
* Override value
*/
resizeHandler: null,
/**
* @readonly
* @cfg {Object} highlightItem
* The current highlight item in the chart.
* The object must be the one that you get from item events.
*
* Note that series can also own highlight items.
* This notion is separate from this one and should not be used at the same time.
*/
highlightItem: null
},
/**
* @private
*/
resizing: 0,
/**
* @private The z-indexes to use for the various surfaces
*/
surfaceZIndexes: {
main: 0,
grid: 1,
series: 2,
axis: 3,
overlay: 4,
events: 5
},
animating: 0,
applyAnimate: function (newAnimate, oldAnimate) {
if (!newAnimate) {
newAnimate = {
duration: 0
};
} else if (newAnimate === true) {
newAnimate = {
easing: 'easeInOut',
duration: 500
};
}
if (!oldAnimate) {
return newAnimate;
} else {
oldAnimate = Ext.apply({}, newAnimate, oldAnimate);
}
return oldAnimate;
},
applyInsetPadding: function (padding, oldPadding) {
if (Ext.isNumber(padding)) {
return {
top: padding,
left: padding,
right: padding,
bottom: padding
};
} else if (!oldPadding) {
return padding;
} else {
return Ext.apply(oldPadding, padding);
}
},
applyInnerPadding: function (padding, oldPadding) {
if (Ext.isNumber(padding)) {
return {
top: padding,
left: padding,
right: padding,
bottom: padding
};
} else if (!oldPadding) {
return padding;
} else {
return Ext.apply(oldPadding, padding);
}
},
scheduleLayout: function () {
if (!this.scheduledLayoutId) {
this.scheduledLayoutId = Ext.draw.Animator.schedule('doScheduleLayout', this);
}
},
doScheduleLayout: function () {
this.scheduledLayoutId = null;
this.performLayout();
},
getAnimate: function () {
if (this.resizing) {
return {
duration: 0
};
} else {
return this._animate;
}
},
constructor: function () {
var me = this;
me.itemListeners = {};
me.surfaceMap = {};
me.legendStore = new Ext.data.Store({
storeId: this.getId() + '-legendStore',
fields: [
'id', 'name', 'mark', 'disabled', 'series', 'index'
]
});
me.callSuper(arguments);
me.refreshLegendStore();
me.getLegendStore().on('updaterecord', 'onUpdateLegendStore', me);
},
/**
* Return the legend store that contains all the legend information. These
* information are collected from all the series.
* @return {Ext.data.Store}
*/
getLegendStore: function () {
return this.legendStore;
},
refreshLegendStore: function () {
if (this.getLegendStore()) {
var i, ln,
series = this.getSeries(), seriesItem,
legendData = [];
if (series) {
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
if (seriesItem.getShowInLegend()) {
seriesItem.provideLegendInfo(legendData);
}
}
}
this.getLegendStore().setData(legendData);
}
},
onUpdateLegendStore: function (store, record) {
var series = this.getSeries(), seriesItem;
if (record && series) {
seriesItem = series.map[record.get('series')];
if (seriesItem) {
seriesItem.setHiddenByIndex(record.get('index'), record.get('disabled'));
this.redraw();
}
}
},
initialized: function () {
var me = this;
me.callSuper();
me.getSurface('main');
me.getSurface('overlay');
me.applyStyles();
},
resizeHandler: function (size) {
var me = this;
me.getSurface('overlay').setRegion([0, 0, size.width, size.height]);
me.performLayout();
},
applyMainRegion: function (newRegion, region) {
if (!region) {
return newRegion;
}
this.getSeries();
this.getAxes();
if (newRegion[0] === region[0] &&
newRegion[1] === region[1] &&
newRegion[2] === region[2] &&
newRegion[3] === region[3]) {
return region;
} else {
return newRegion;
}
},
getSurface: function (name, type) {
name = name || 'main';
type = type || name;
var me = this,
surface = this.callSuper([name]),
zIndexes = me.surfaceZIndexes;
if (type in zIndexes) {
surface.element.setStyle('zIndex', zIndexes[type]);
}
if (!me.surfaceMap[type]) {
me.surfaceMap[type] = [];
}
surface.type = type;
me.surfaceMap[type].push(surface);
return surface;
},
updateColors: function (colors) {
var series = this.getSeries(),
seriesItem;
for (var i = 0; i < series.length; i++) {
seriesItem = series[i];
if (!seriesItem.getColors()) {
seriesItem.updateColors(colors);
}
}
},
applyAxes: function (newAxes, oldAxes) {
this.resizing++;
try {
if (!oldAxes) {
oldAxes = [];
oldAxes.map = {};
}
var result = [], i, ln, axis, oldAxis, oldMap = oldAxes.map;
result.map = {};
newAxes = Ext.Array.from(newAxes, true);
for (i = 0, ln = newAxes.length; i < ln; i++) {
axis = newAxes[i];
if (!axis) {
continue;
}
axis = Ext.factory(axis, null, oldAxis = oldMap[axis.getId && axis.getId() || axis.id], 'axis');
axis.setChart(this);
if (axis) {
result.push(axis);
result.map[axis.getId()] = axis;
if (!oldAxis) {
axis.on('animationstart', 'onAnimationStart', this);
axis.on('animationend', 'onAnimationEnd', this);
}
}
}
for (i in oldMap) {
if (!result.map[oldMap[i]]) {
oldMap[i].destroy();
}
}
return result;
} finally {
this.resizing--;
}
},
updateAxes: function (newAxes) {
var i, ln, axis;
for (i = 0, ln = newAxes.length; i < ln; i++) {
axis = newAxes[i];
axis.setChart(this);
}
},
applySeries: function (newSeries, oldSeries) {
this.resizing++;
try {
this.getAxes();
if (!oldSeries) {
oldSeries = [];
oldSeries.map = {};
}
var me = this,
result = [],
i, ln, series, oldMap = oldSeries.map, oldSeriesItem;
result.map = {};
newSeries = Ext.Array.from(newSeries, true);
for (i = 0, ln = newSeries.length; i < ln; i++) {
series = newSeries[i];
if (!series) {
continue;
}
oldSeriesItem = oldSeries.map[series.getId && series.getId() || series.id];
if (series instanceof Ext.chart.series.Series) {
if (oldSeriesItem !== series) {
// Replacing
if (oldSeriesItem) {
oldSeriesItem.destroy();
}
me.addItemListenersToSeries(series);
}
series.setChart(this);
} else if (Ext.isObject(series)) {
if (oldSeriesItem) {
// Update
oldSeriesItem.setConfig(series);
series = oldSeriesItem;
} else {
if (Ext.isString(series)) {
series = Ext.create(series.xclass || ("series." + series), {chart: this});
} else {
series.chart = this;
series = Ext.create(series.xclass || ("series." + series.type), series);
}
series.on('animationstart', 'onAnimationStart', this);
series.on('animationend', 'onAnimationEnd', this);
me.addItemListenersToSeries(series);
}
}
result.push(series);
result.map[series.getId()] = series;
}
for (i in oldMap) {
if (!result.map[oldMap[i]]) {
oldMap[i].destroy();
}
}
return result;
} finally {
this.resizing--;
}
},
applyLegend: function (newLegend, oldLegend) {
return Ext.factory(newLegend, Ext.chart.Legend, oldLegend);
},
updateLegend: function (legend) {
if (legend) {
// On create
legend.setStore(this.getLegendStore());
if (!legend.getDocked()) {
legend.setDocked('bottom');
}
if (this.getParent()) {
this.getParent().add(legend);
}
}
},
setParent: function (parent) {
this.callSuper(arguments);
if (parent && this.getLegend()) {
parent.add(this.getLegend());
}
},
updateSeries: function (newSeries, oldSeries) {
this.resizing++;
try {
this.fireEvent('serieschanged', this, newSeries, oldSeries);
var i, ln, seriesItem;
for (i = 0, ln = newSeries.length; i < ln; i++) {
seriesItem = newSeries[i];
}
this.refreshLegendStore();
} finally {
this.resizing--;
}
},
applyInteractions: function (interations, oldInterations) {
if (!oldInterations) {
oldInterations = [];
oldInterations.map = {};
}
var me = this,
result = [], oldMap = oldInterations.map;
result.map = {};
interations = Ext.Array.from(interations, true);
for (var i = 0, ln = interations.length; i < ln; i++) {
var interation = interations[i];
if (!interation) {
continue;
}
interation = Ext.factory(interation, null, oldMap[interation.getId && interation.getId() || interation.id], 'interaction');
interation.setChart(me);
if (interation) {
result.push(interation);
result.map[interation.getId()] = interation;
}
}
for (i in oldMap) {
if (!result.map[oldMap[i]]) {
oldMap[i].destroy();
}
}
return result;
},
applyStore: function (store) {
return Ext.StoreManager.lookup(store);
},
updateStore: function (newStore, oldStore) {
var me = this;
if (oldStore) {
oldStore.un('refresh', 'onRefresh', me, null, 'after');
if (oldStore.autoDestroy) {
oldStore.destroy();
}
}
if (newStore) {
newStore.on('refresh', 'onRefresh', me, null, 'after');
me.fireEvent('storechanged', newStore, oldStore);
me.onRefresh();
}
},
/**
* Redraw the chart. If animations are set this will animate the chart too.
*/
redraw: function () {
this.fireEvent('redraw');
},
getEventXY: function (e) {
e = (e.changedTouches && e.changedTouches[0]) || e.event || e.browserEvent || e;
var me = this,
xy = me.element.getXY(),
region = me.getMainRegion();
return [e.pageX - xy[0] - region[0], e.pageY - xy[1] - region[1]];
},
/**
* Given an x/y point relative to the chart, find and return the first series item that
* matches that point.
* @param {Number} x
* @param {Number} y
* @return {Object} An object with `series` and `item` properties, or `false` if no item found.
*/
getItemForPoint: function (x, y) {
var me = this,
i = 0,
items = me.getSeries(),
l = items.length,
series, item;
for (; i < l; i++) {
series = items[i];
item = series.getItemForPoint(x, y);
if (item) {
return item;
}
}
return null;
},
/**
* Given an x/y point relative to the chart, find and return all series items that match that point.
* @param {Number} x
* @param {Number} y
* @return {Array} An array of objects with `series` and `item` properties.
*/
getItemsForPoint: function (x, y) {
var me = this,
series = me.getSeries(),
seriesItem,
items = [];
for (var i = 0; i < series.length; i++) {
seriesItem = series[i];
var item = seriesItem.getItemForPoint(x, y);
if (item) {
items.push(item);
}
}
return items;
},
/**
* @private
*/
delayThicknessChanged: 0,
/**
* @private
*/
thicknessChanged: false,
/**
* Suspend the layout initialized by thickness change
*/
suspendThicknessChanged: function () {
this.delayThicknessChanged++;
},
/**
* Resume the layout initialized by thickness change
*/
resumeThicknessChanged: function () {
this.delayThicknessChanged--;
if (this.delayThicknessChanged === 0 && this.thicknessChanged) {
this.onThicknessChanged();
}
},
onAnimationStart: function () {
this.fireEvent("animationstart", this);
},
onAnimationEnd: function () {
this.fireEvent("animationend", this);
},
onThicknessChanged: function () {
if (this.delayThicknessChanged === 0) {
this.thicknessChanged = false;
this.performLayout();
} else {
this.thicknessChanged = true;
}
},
/**
* @private
*/
onRefresh: function () {
var region = this.getMainRegion(),
axes = this.getAxes(),
store = this.getStore(),
series = this.getSeries();
if (!store || !axes || !series || !region) {
return;
}
this.redraw();
},
/**
* Changes the data store bound to this chart and refreshes it.
* @param {Ext.data.Store} store The store to bind to this chart.
*/
bindStore: function (store) {
this.setStore(store);
},
applyHighlightItem: function (newHighlightItem, oldHighlightItem) {
if (newHighlightItem === oldHighlightItem) {
return;
}
if (Ext.isObject(newHighlightItem) && Ext.isObject(oldHighlightItem)) {
if (newHighlightItem.sprite === oldHighlightItem.sprite &&
newHighlightItem.index === oldHighlightItem.index
) {
return;
}
}
return newHighlightItem;
},
updateHighlightItem: function (newHighlightItem, oldHighlightItem) {
if (oldHighlightItem) {
oldHighlightItem.series.setAttributesForItem(oldHighlightItem, {highlighted: false});
}
if (newHighlightItem) {
newHighlightItem.series.setAttributesForItem(newHighlightItem, {highlighted: true});
}
},
/**
* Reset the chart back to its initial state, before any user interaction.
* @param {Boolean} skipRedraw If `true`, redrawing of the chart will be skipped.
*/
reset: function (skipRedraw) {
var me = this,
i, ln,
axes = me.getAxes(), axis,
series = me.getSeries(), seriesItem;
for (i = 0, ln = axes.length; i < ln; i++) {
axis = axes[i];
if (axis.reset) {
axis.reset();
}
}
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
if (seriesItem.reset) {
seriesItem.reset();
}
}
if (!skipRedraw) {
me.redraw();
}
},
addItemListenersToSeries: function (series) {
for (var name in this.itemListeners) {
var listenerMap = this.itemListeners[name], i, ln;
for (i = 0, ln = listenerMap.length; i < ln; i++) {
series.addListener.apply(series, listenerMap[i]);
}
}
},
addItemListener: function (name, fn, scope, options, order) {
var listenerMap = this.itemListeners[name] || (this.itemListeners[name] = []),
series = this.getSeries(), seriesItem,
i, ln;
listenerMap.push([name, fn, scope, options, order]);
if (series) {
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
seriesItem.addListener(name, fn, scope, options, order);
}
}
},
remoteItemListener: function (name, fn, scope, options, order) {
var listenerMap = this.itemListeners[name],
series = this.getSeries(), seriesItem,
i, ln;
if (listenerMap) {
for (i = 0, ln = listenerMap.length; i < ln; i++) {
if (listenerMap[i].fn === fn) {
listenerMap.splice(i, 1);
if (series) {
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
seriesItem.removeListener(name, fn, scope, options, order);
}
}
break;
}
}
}
},
doAddListener: function (name, fn, scope, options, order) {
if (name.match(this.delegationRegex)) {
return this.addItemListener(name, fn, scope || this, options, order);
} else if (name.match(this.domEvents)) {
return this.element.doAddListener.apply(this.element, arguments);
} else {
return this.callSuper(arguments);
}
},
doRemoveListener: function (name, fn, scope, options, order) {
if (name.match(this.delegationRegex)) {
return this.remoteItemListener(name, fn, scope || this, options, order);
} else if (name.match(this.domEvents)) {
return this.element.doRemoveListener.apply(this.element, arguments);
} else {
return this.callSuper(arguments);
}
},
onItemRemove: function (item) {
this.callSuper(arguments);
if (this.surfaceMap) {
Ext.Array.remove(this.surfaceMap[item.type], item);
if (this.surfaceMap[item.type].length === 0) {
delete this.surfaceMap[item.type];
}
}
},
// @private remove gently.
destroy: function () {
var me = this,
emptyArray = [];
me.surfaceMap = null;
me.setHighlightItem(null);
me.setSeries(emptyArray);
me.setAxes(emptyArray);
me.setInteractions(emptyArray);
me.setStore(null);
Ext.Viewport.un('orientationchange', me.redraw, me);
this.callSuper(arguments);
},
/* ---------------------------------
Methods needed for ComponentQuery
----------------------------------*/
/**
* @private
* @param deep
* @return {Array}
*/
getRefItems: function (deep) {
var me = this,
series = me.getSeries(),
axes = me.getAxes(),
interaction = me.getInteractions(),
ans = [], i, ln;
for (i = 0, ln = series.length; i < ln; i++) {
ans.push(series[i]);
if (series[i].getRefItems) {
ans.push.apply(ans, series[i].getRefItems(deep));
}
}
for (i = 0, ln = axes.length; i < ln; i++) {
ans.push(axes[i]);
if (axes[i].getRefItems) {
ans.push.apply(ans, axes[i].getRefItems(deep));
}
}
for (i = 0, ln = interaction.length; i < ln; i++) {
ans.push(interaction[i]);
if (interaction[i].getRefItems) {
ans.push.apply(ans, interaction[i].getRefItems(deep));
}
}
return ans;
}
});
/**
* @class Ext.chart.grid.HorizontalGrid
* @extends Ext.draw.sprite.Sprite
*
* Horizontal Grid sprite. Used in Cartesian Charts.
*/
Ext.define("Ext.chart.grid.HorizontalGrid", {
extend: 'Ext.draw.sprite.Sprite',
alias: 'grid.horizontal',
inheritableStatics: {
def: {
processors: {
x: 'number',
y: 'number',
width: 'number',
height: 'number'
},
defaults: {
x: 0,
y: 0,
width: 1,
height: 1,
strokeStyle: '#DDD'
}
}
},
render: function (surface, ctx, clipRegion) {
var attr = this.attr,
x = attr.x,
y = surface.roundPixel(attr.y),
w = attr.width,
h = attr.height,
halfLineWidth = ctx.lineWidth * 0.5;
ctx.beginPath();
ctx.rect(clipRegion[0] - surface.matrix.getDX(), y + halfLineWidth, +clipRegion[2], attr.height);
ctx.fill();
ctx.beginPath();
ctx.moveTo(clipRegion[0] - surface.matrix.getDX(), y + halfLineWidth);
ctx.lineTo(clipRegion[0] + clipRegion[2] - surface.matrix.getDX(), y + halfLineWidth);
ctx.stroke();
}
});
/**
* @class Ext.chart.grid.VerticalGrid
* @extends Ext.draw.sprite.Sprite
*
* Vertical Grid sprite. Used in Cartesian Charts.
*/
Ext.define("Ext.chart.grid.VerticalGrid", {
extend: 'Ext.draw.sprite.Sprite',
alias: 'grid.vertical',
inheritableStatics: {
def: {
processors: {
x: 'number',
y: 'number',
width: 'number',
height: 'number'
},
defaults: {
x: 0,
y: 0,
width: 1,
height: 1,
strokeStyle: '#DDD'
}
}
},
render: function (surface, ctx, clipRegion) {
var attr = this.attr,
x = surface.roundPixel(attr.x),
halfLineWidth = ctx.lineWidth * 0.5;
ctx.beginPath();
ctx.rect(x - halfLineWidth, clipRegion[1] - surface.matrix.getDY(), attr.width, clipRegion[3]);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - halfLineWidth, clipRegion[1] - surface.matrix.getDY());
ctx.lineTo(x - halfLineWidth, clipRegion[1] + clipRegion[3] - surface.matrix.getDY());
ctx.stroke();
}
});
/**
* @class Ext.chart.CartesianChart
* @extends Ext.chart.AbstractChart
*
* Represents a chart that uses cartesian coordinates.
* A cartesian chart have two directions, X direction and Y direction.
* The series and axes are coordinated along these directions.
* By default the x direction is horizontal and y direction is vertical,
* You can swap the by setting {@link #flipXY} config to `true`.
*
* Cartesian series often treats x direction an y direction differently.
* In most cases, data on x direction are assumed to be monotonically increasing.
* Based on this property, cartesian series can be trimmed and summarized properly
* to gain a better performance.
*
* @xtype chart
*/
Ext.define('Ext.chart.CartesianChart', {
extend: 'Ext.chart.AbstractChart',
alternateClassName: 'Ext.chart.Chart',
requires: ['Ext.chart.grid.HorizontalGrid', 'Ext.chart.grid.VerticalGrid'],
config: {
/**
* @cfg {Boolean} flipXY Flip the direction of X and Y axis.
* If flipXY is true, the X axes will be vertical and Y axes will be horizontal.
*/
flipXY: false,
innerRegion: [0, 0, 1, 1]
},
xtype: 'chart',
alias: 'Ext.chart.Chart',
getDirectionForAxis: function (position) {
var flipXY = this.getFlipXY();
if (position === 'left' || position === 'right') {
if (flipXY) {
return 'X';
} else {
return 'Y';
}
} else {
if (flipXY) {
return 'Y';
} else {
return 'X';
}
}
},
/**
* Layout the axes and series.
*/
performLayout: function () {
try {
this.resizing++;
this.suspendThicknessChanged();
var me = this,
axes = me.getAxes(), axis,
serieses = me.getSeries(), series,
axisSurface, thickness,
size = me.element.getSize(),
width = size.width,
height = size.height,
insetPadding = me.getInsetPadding(),
innerPadding = me.getInnerPadding(),
surface,
shrinkBox = {
top: insetPadding.top,
left: insetPadding.left,
right: insetPadding.right,
bottom: insetPadding.bottom
},
gridSurface,
mainRegion, innerWidth, innerHeight,
elements, floating, matrix, i, ln,
flipXY = me.getFlipXY();
if (width <= 0 || height <= 0) {
return;
}
for (i = 0; i < axes.length; i++) {
axis = axes[i];
axisSurface = axis.getSurface();
floating = axis.getStyle && axis.getStyle() && axis.getStyle().floating;
thickness = axis.getThickness();
switch (axis.getPosition()) {
case 'top':
axisSurface.setRegion([0, shrinkBox.top, width, thickness]);
break;
case 'bottom':
axisSurface.setRegion([0, height - (shrinkBox.bottom + thickness), width, thickness]);
break;
case 'left':
axisSurface.setRegion([shrinkBox.left, 0, thickness, height]);
break;
case 'right':
axisSurface.setRegion([width - (shrinkBox.right + thickness), 0, thickness, height]);
break;
}
if (!floating) {
shrinkBox[axis.getPosition()] += thickness;
}
}
width -= shrinkBox.left + shrinkBox.right;
height -= shrinkBox.top + shrinkBox.bottom;
mainRegion = [shrinkBox.left, shrinkBox.top, width, height];
shrinkBox.left += innerPadding.left;
shrinkBox.top += innerPadding.top;
shrinkBox.right += innerPadding.right;
shrinkBox.bottom += innerPadding.bottom;
innerWidth = width - innerPadding.left - innerPadding.right;
innerHeight = height - innerPadding.top - innerPadding.bottom;
me.setInnerRegion([shrinkBox.left, shrinkBox.top, innerWidth, innerHeight]);
if (innerWidth <= 0 || innerHeight <= 0) {
return;
}
me.setMainRegion(mainRegion);
me.getSurface('main').setRegion(mainRegion);
for (i = 0, ln = me.surfaceMap.grid && me.surfaceMap.grid.length; i < ln; i++) {
gridSurface = me.surfaceMap.grid[i];
gridSurface.setRegion(mainRegion);
gridSurface.matrix.set(1, 0, 0, 1, innerPadding.left, innerPadding.top);
gridSurface.matrix.inverse(gridSurface.inverseMatrix);
}
for (i = 0; i < axes.length; i++) {
axis = axes[i];
axisSurface = axis.getSurface();
matrix = axisSurface.matrix;
elements = matrix.elements;
switch (axis.getPosition()) {
case 'top':
case 'bottom':
elements[4] = shrinkBox.left;
axis.setLength(innerWidth);
break;
case 'left':
case 'right':
elements[5] = shrinkBox.top;
axis.setLength(innerHeight);
break;
}
axis.updateTitleSprite();
matrix.inverse(axisSurface.inverseMatrix);
}
for (i = 0, ln = serieses.length; i < ln; i++) {
series = serieses[i];
surface = series.getSurface();
surface.setRegion(mainRegion);
if (flipXY) {
surface.matrix.set(0, -1, 1, 0, innerPadding.left, innerHeight + innerPadding.top);
} else {
surface.matrix.set(1, 0, 0, -1, innerPadding.left, innerHeight + innerPadding.top);
}
surface.matrix.inverse(surface.inverseMatrix);
series.getOverlaySurface().setRegion(mainRegion);
}
me.redraw();
me.onPlaceWatermark();
} finally {
this.resizing--;
this.resumeThicknessChanged();
}
},
redraw: function () {
var me = this,
series = me.getSeries(),
axes = me.getAxes(),
region = me.getMainRegion(),
innerWidth, innerHeight,
innerPadding = me.getInnerPadding(),
left, right, top, bottom, i, j,
sprites, xRange, yRange, isSide, attr,
axisX, axisY, range, visibleRange,
flipXY = me.getFlipXY();
if (!region) {
return;
}
innerWidth = region[2] - innerPadding.left - innerPadding.right;
innerHeight = region[3] - innerPadding.top - innerPadding.bottom;
for (i = 0; i < series.length; i++) {
if ((axisX = series[i].getXAxis())) {
visibleRange = axisX.getVisibleRange();
xRange = axisX.getRange();
xRange = [xRange[0] + (xRange[1] - xRange[0]) * visibleRange[0], xRange[0] + (xRange[1] - xRange[0]) * visibleRange[1]];
} else {
xRange = series[i].getXRange();
}
if ((axisY = series[i].getYAxis())) {
visibleRange = axisY.getVisibleRange();
yRange = axisY.getRange();
yRange = [yRange[0] + (yRange[1] - yRange[0]) * visibleRange[0], yRange[0] + (yRange[1] - yRange[0]) * visibleRange[1]];
} else {
yRange = series[i].getYRange();
}
left = xRange[0];
right = xRange[1];
top = yRange[0];
bottom = yRange[1];
attr = {
visibleMinX: xRange[0],
visibleMaxX: xRange[1],
visibleMinY: yRange[0],
visibleMaxY: yRange[1],
innerWidth: innerWidth,
innerHeight: innerHeight,
flipXY: flipXY
};
sprites = series[i].getSprites();
for (j = 0; j < sprites.length; j++) {
sprites[j].setAttributes(attr, true);
}
}
for (i = 0; i < axes.length; i++) {
isSide = axes[i].isSide();
sprites = axes[i].getSprites();
range = axes[i].getRange();
visibleRange = axes[i].getVisibleRange();
attr = {
dataMin: range[0],
dataMax: range[1],
visibleMin: visibleRange[0],
visibleMax: visibleRange[1]
};
if (isSide) {
attr.length = innerHeight;
attr.startGap = innerPadding.bottom;
attr.endGap = innerPadding.top;
} else {
attr.length = innerWidth;
attr.startGap = innerPadding.left;
attr.endGap = innerPadding.right;
}
for (j = 0; j < sprites.length; j++) {
sprites[j].setAttributes(attr, true);
}
}
me.renderFrame();
me.callSuper(arguments);
},
onPlaceWatermark: function () {
var region0 = this.element.getBox(),
region = this.getSurface ? this.getSurface('main').getRegion() : this.getItems().get(0).getRegion();
if (region) {
this.watermarkElement.setStyle({
right: Math.round(region0.width - (region[2] + region[0])) + 'px',
bottom: Math.round(region0.height - (region[3] + region[1])) + 'px'
});
}
}
});
/**
* @class Ext.chart.grid.CircularGrid
* @extends Ext.draw.sprite.Circle
*
* Circular Grid sprite.
*/
Ext.define("Ext.chart.grid.CircularGrid", {
extend: 'Ext.draw.sprite.Circle',
alias: 'grid.circular',
inheritableStatics: {
def: {
defaults: {
r: 1,
strokeStyle: '#DDD'
}
}
}
});
/**
* @class Ext.chart.grid.RadialGrid
* @extends Ext.draw.sprite.Path
*
* Radial Grid sprite. Used by Radar to render a series of concentric circles.
* Represents the scale of the radar chart on the yField.
*/
Ext.define("Ext.chart.grid.RadialGrid", {
extend: 'Ext.draw.sprite.Path',
alias: 'grid.radial',
inheritableStatics: {
def: {
processors: {
startRadius: 'number',
endRadius: 'number'
},
defaults: {
startRadius: 0,
endRadius: 1,
scalingCenterX: 0,
scalingCenterY: 0,
strokeStyle: '#DDD'
},
dirtyTriggers: {
startRadius: 'path,bbox',
endRadius: 'path,bbox'
}
}
},
render: function () {
this.callSuper(arguments);
},
updatePath: function (path, attr) {
var startRadius = attr.startRadius,
endRadius = attr.endRadius;
path.moveTo(startRadius, 0);
path.lineTo(endRadius, 0);
}
});
/**
* @class Ext.chart.PolarChart
* @extends Ext.chart.AbstractChart
*
* Creates a chart that uses polar coordinates.
*/
Ext.define('Ext.chart.PolarChart', {
requires: [
'Ext.chart.grid.CircularGrid',
'Ext.chart.grid.RadialGrid'
],
extend: 'Ext.chart.AbstractChart',
xtype: 'polar',
config: {
/**
* @cfg {Array} center Determines the center of the polar chart.
* Updated when the chart performs layout.
*/
center: [0, 0],
/**
* @cfg {Number} radius Determines the radius of the polar chart.
* Updated when the chart performs layout.
*/
radius: 0
},
getDirectionForAxis: function (position) {
if (position === 'radial') {
return 'Y';
} else {
return 'X';
}
},
applyCenter: function (center, oldCenter) {
if (oldCenter && center[0] === oldCenter[0] && center[1] === oldCenter[1]) {
return;
}
return [+center[0], +center[1]];
},
updateCenter: function (center) {
var me = this,
axes = me.getAxes(), axis,
series = me.getSeries(), seriesItem,
i, ln;
for (i = 0, ln = axes.length; i < ln; i++) {
axis = axes[i];
axis.setCenter(center);
}
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
seriesItem.setCenter(center);
}
},
updateRadius: function (radius) {
var me = this,
axes = me.getAxes(), axis,
series = me.getSeries(), seriesItem,
i, ln;
for (i = 0, ln = axes.length; i < ln; i++) {
axis = axes[i];
axis.setMinimum(0);
axis.setLength(radius);
axis.getSprites();
}
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
seriesItem.setRadius(radius);
}
},
doSetSurfaceRegion: function (surface, region) {
var mainRegion = this.getMainRegion();
surface.setRegion(region);
surface.matrix.set(1, 0, 0, 1, mainRegion[0] - region[0], mainRegion[1] - region[1]);
surface.inverseMatrix.set(1, 0, 0, 1, region[0] - mainRegion[0], region[1] - mainRegion[1]);
},
performLayout: function () {
try {
this.resizing++;
var me = this,
size = me.element.getSize(),
fullRegion = [0, 0, size.width, size.height],
inset = me.getInsetPadding(),
inner = me.getInnerPadding(),
left = inset.left,
top = inset.top,
width = size.width - left - inset.right,
height = size.height - top - inset.bottom,
region = [inset.left, inset.top, width, height],
innerWidth = width - inner.left - inner.right,
innerHeight = height - inner.top - inner.bottom,
center = [innerWidth * 0.5 + inner.left, innerHeight * 0.5 + inner.top],
radius = Math.min(innerWidth, innerHeight) * 0.5,
axes = me.getAxes(), axis,
series = me.getSeries(), seriesItem,
i, ln;
me.setMainRegion(region);
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
me.doSetSurfaceRegion(seriesItem.getSurface(), region);
me.doSetSurfaceRegion(seriesItem.getOverlaySurface(), fullRegion);
}
me.doSetSurfaceRegion(me.getSurface(), fullRegion);
for (i = 0, ln = me.surfaceMap.grid && me.surfaceMap.grid.length; i < ln; i++) {
me.doSetSurfaceRegion(me.surfaceMap.grid[i], fullRegion);
}
for (i = 0, ln = axes.length; i < ln; i++) {
axis = axes[i];
me.doSetSurfaceRegion(axis.getSurface(), fullRegion);
}
me.setRadius(radius);
me.setCenter(center);
me.redraw();
} finally {
this.resizing--;
}
},
getEventXY: function (e) {
e = (e.changedTouches && e.changedTouches[0]) || e.event || e.browserEvent || e;
var me = this,
xy = me.element.getXY(),
padding = me.getInsetPadding();
return [e.pageX - xy[0] - padding.left, e.pageY - xy[1] - padding.top];
},
redraw: function () {
var me = this,
axes = me.getAxes(), axis,
series = me.getSeries(), seriesItem,
i, ln;
for (i = 0, ln = axes.length; i < ln; i++) {
axis = axes[i];
axis.getSprites();
}
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
seriesItem.getSprites();
}
this.renderFrame();
}
});
/**
* @class Ext.chart.SpaceFillingChart
* @extends Ext.chart.AbstractChart
*
* Creates a chart that fills the entire area of the chart.
* e.g. Treemap
*/
Ext.define('Ext.chart.SpaceFillingChart', {
extend: 'Ext.chart.AbstractChart',
xtype: 'spacefilling',
config: {
},
performLayout: function () {
try {
this.resizing++;
var me = this,
size = me.element.getSize(),
series = me.getSeries(), seriesItem,
padding = me.getInsetPadding(),
width = size.width - padding.left - padding.right,
height = size.height - padding.top - padding.bottom,
region = [padding.left, padding.top, width, height],
i, ln;
me.getSurface().setRegion(region);
me.setMainRegion(region);
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
seriesItem.getSurface().setRegion(region);
seriesItem.setRegion(region);
}
me.redraw();
} finally {
this.resizing--;
}
},
redraw: function () {
var me = this,
series = me.getSeries(), seriesItem,
i, ln;
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
seriesItem.getSprites();
}
this.renderFrame();
}
});
/**
* @class Ext.chart.axis.Category
* @extends Ext.chart.axis.Axis
*
* A type of axis that displays items in categories. This axis is generally used to
* display categorical information like names of items, month names, quarters, etc.
* but no quantitative values. For that other type of information {@link Ext.chart.axis.Numeric Numeric}
* axis are more suitable.
*
* As with other axis you can set the position of the axis and its title. For example:
*
* @example preview
* var chart = new Ext.chart.CartesianChart({
* animate: true,
* innerPadding: {
* left: 40,
* right: 40,
* },
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'area',
* subStyle: {
* fill: ['blue', 'green', 'red']
* },
* xField: 'name',
* yField: ['data1', 'data2', 'data3']
*
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*
* In this example with set the category axis to the bottom of the surface, bound the axis to
* the `name` property and set as title "Sample Values".
*/
Ext.define('Ext.chart.axis.Category', {
requires: [
'Ext.chart.axis.layout.CombineDuplicate',
'Ext.chart.axis.segmenter.Names'
],
extend: 'Ext.chart.axis.Axis',
alias: 'axis.category',
type: 'category',
config: {
layout: 'combineDuplicate',
segmenter: 'names'
}
});
/**
* @class Ext.chart.axis.Numeric
* @extends Ext.chart.axis.Axis
*
* An axis to handle numeric values. This axis is used for quantitative data as
* opposed to the category axis. You can set minimum and maximum values to the
* axis so that the values are bound to that. If no values are set, then the
* scale will auto-adjust to the values.
*
* @example preview
* var chart = new Ext.chart.CartesianChart({
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':1, 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':2, 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':3, 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':4, 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':5, 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'numeric',
* grid: true,
* position: 'left',
* fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
* title: 'Sample Values',
* grid: {
* odd: {
* opacity: 1,
* fill: '#ddd',
* stroke: '#bbb',
* 'lineWidth': 1
* }
* },
* minimum: 0,
* adjustMinimumByMajorUnit: 0
* }],
* series: [{
* type: 'area',
* subStyle: {
* fill: ['blue', 'green', 'red']
* },
* xField: 'name',
* yField: ['data1', 'data2', 'data3']
*
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
* In this example we create an axis of Numeric type. We set a minimum value so that
* even if all series have values greater than zero, the grid starts at zero. We bind
* the axis onto the left part of the surface by setting _position_ to _left_.
* We bind three different store fields to this axis by setting _fields_ to an array.
* We set the title of the axis to _Number of Hits_ by using the _title_ property.
* We use a _grid_ configuration to set odd background rows to a certain style and even rows
* to be transparent/ignored.
*
*/
Ext.define('Ext.chart.axis.Numeric', {
extend: 'Ext.chart.axis.Axis',
alias: 'axis.numeric',
type: 'numeric',
requires: ['Ext.chart.axis.layout.Continuous', 'Ext.chart.axis.segmenter.Numeric'],
config: {
layout: 'continuous',
segmenter: 'numeric',
aggregator: 'double'
}
});
/**
* @class Ext.chart.axis.Time
* @extends Ext.chart.axis.Numeric
*
* A type of axis whose units are measured in time values. Use this axis
* for listing dates that you will want to group or dynamically change.
* If you just want to display dates as categories then use the
* Category class for axis instead.
*
* @example preview
* var chart = new Ext.chart.CartesianChart({
* animate: true,
* store: {
* fields: ['time', 'open', 'high', 'low', 'close'],
* data: [
* {'time':new Date('Jan 1 2010').getTime(), 'open':600, 'high':614, 'low':578, 'close':590},
* {'time':new Date('Jan 2 2010').getTime(), 'open':590, 'high':609, 'low':580, 'close':580},
* {'time':new Date('Jan 3 2010').getTime(), 'open':580, 'high':602, 'low':578, 'close':602},
* {'time':new Date('Jan 4 2010').getTime(), 'open':602, 'high':614, 'low':586, 'close':586},
* {'time':new Date('Jan 5 2010').getTime(), 'open':586, 'high':602, 'low':565, 'close':565}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['open', 'high', 'low', 'close'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 560,
* maximum: 640
* }, {
* type: 'time',
* position: 'bottom',
* fields: ['time'],
* fromDate: new Date('Dec 31 2009'),
* toDate: new Date('Jan 6 2010'),
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* style: {
* axisLine: false
* }
* }],
* series: [{
* type: 'candlestick',
* xField: 'time',
* openField: 'open',
* highField: 'high',
* lowField: 'low',
* closeField: 'close',
* style: {
* ohlcType: 'ohlc',
* dropStyle: {
* fill: 'rgb(237, 123, 43)',
* stroke: 'rgb(237, 123, 43)'
* },
* raiseStyle: {
* fill: 'rgb(55, 153, 19)',
* stroke: 'rgb(55, 153, 19)'
* }
* },
* aggregator: {
* strategy: 'time'
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define('Ext.chart.axis.Time', {
extend: 'Ext.chart.axis.Numeric',
alias: 'axis.time',
type: 'time',
requires: ['Ext.chart.axis.layout.Continuous', 'Ext.chart.axis.segmenter.Time', 'Ext.DateExtras'],
config: {
/**
* @cfg {Boolean} calculateByLabelSize
* The minimum value drawn by the axis. If not set explicitly, the axis
* minimum will be calculated automatically.
*/
calculateByLabelSize: true,
/**
* @cfg {String/Boolean} dateFormat
* Indicates the format the date will be rendered on.
* For example: 'M d' will render the dates as 'Jan 30', etc.
*/
dateFormat: null,
/**
* @cfg {Date} fromDate The starting date for the time axis.
*/
fromDate: null,
/**
* @cfg {Date} toDate The ending date for the time axis.
*/
toDate: null,
/**
* @cfg {Array} [step=[Ext.Date.DAY, 1]] An array with two components:
*
* - The unit of the step (Ext.Date.DAY, Ext.Date.MONTH, etc).
* - The number of units for the step (1, 2, etc).
*
*/
step: [Ext.Date.DAY, 1],
layout: 'continuous',
segmenter: 'time',
aggregator: 'time'
},
updateDateFormat: function (format) {
this.setRenderer(function (date) {
return Ext.Date.format(new Date(date), format);
});
},
updateFromDate: function (date) {
this.setMinimum(+date);
},
updateToDate: function (date) {
this.setMaximum(+date);
},
getCoordFor: function (value) {
if (Ext.isString(value)) {
value = new Date(value);
}
return +value;
}
});
/**
* @class Ext.chart.interactions.CrossZoom
* @extends Ext.chart.interactions.Abstract
*
* The CrossZoom interaction allows the user to zoom in on a selected area of the chart.
*
* @example preview
* var lineChart = new Ext.chart.CartesianChart({
* interactions: [{
* type: 'crosszoom'
* }],
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data1'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 0
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'line',
* highlight: {
* size: 7,
* radius: 7
* },
* style: {
* stroke: 'rgb(143,203,203)'
* },
* xField: 'name',
* yField: 'data1',
* marker: {
* type: 'path',
* path: ['M', -2, 0, 0, 2, 2, 0, 0, -2, 'Z'],
* stroke: 'blue',
* lineWidth: 0
* }
* }, {
* type: 'line',
* highlight: {
* size: 7,
* radius: 7
* },
* fill: true,
* xField: 'name',
* yField: 'data3',
* marker: {
* type: 'circle',
* radius: 4,
* lineWidth: 0
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(lineChart);
*/
Ext.define('Ext.chart.interactions.CrossZoom', {
extend: 'Ext.chart.interactions.Abstract',
type: 'crosszoom',
alias: 'interaction.crosszoom',
config: {
/**
* @cfg {Object/Array} axes
* Specifies which axes should be made navigable. The config value can take the following formats:
*
* - An Object whose keys correspond to the {@link Ext.chart.axis.Axis#position position} of each
* axis that should be made navigable. Each key's value can either be an Object with further
* configuration options for each axis or simply `true` for a default set of options.
* {
* type: 'crosszoom',
* axes: {
* left: {
* maxZoom: 5,
* allowPan: false
* },
* bottom: true
* }
* }
*
* If using the full Object form, the following options can be specified for each axis:
*
* - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its natural size.
* - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`.
* - startZoom (Number) A starting zoom level for the axis. Defaults to `1`.
* - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`.
* - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`.
* - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`.
*
* - An Array of strings, each one corresponding to the {@link Ext.chart.axis.Axis#position position}
* of an axis that should be made navigable. The default options will be used for each named axis.
*
* {
* type: 'crosszoom',
* axes: ['left', 'bottom']
* }
*
* If the `axes` config is not specified, it will default to making all axes navigable with the
* default axis options.
*/
axes: true,
gesture: 'drag',
undoButton: {}
},
stopAnimationBeforeSync: false,
constructor: function () {
this.callSuper(arguments);
this.zoomHistory = [];
},
applyAxes: function (axesConfig) {
var result = {};
if (axesConfig === true) {
return {
top: {},
right: {},
bottom: {},
left: {}
};
} else if (Ext.isArray(axesConfig)) {
// array of axis names - translate to full object form
result = {};
Ext.each(axesConfig, function (axis) {
result[axis] = {};
});
} else if (Ext.isObject(axesConfig)) {
Ext.iterate(axesConfig, function (key, val) {
// axis name with `true` value -> translate to object
if (val === true) {
result[key] = {};
} else if (val !== false) {
result[key] = val;
}
});
}
return result;
},
applyUndoButton: function (button, oldButton) {
var me = this;
if (button) {
if (oldButton) {
oldButton.destroy();
}
return Ext.create('Ext.Button', Ext.apply({
cls: [],
iconCls: 'refresh',
text: 'Undo Zoom',
iconMask: true,
disabled: true,
handler: function () {
me.undoZoom();
}
}, button));
} else if (oldButton) {
oldButton.destroy();
}
},
getGestures: function () {
var me = this,
gestures = {};
gestures[me.getGesture()] = 'onGesture';
gestures[me.getGesture() + 'start'] = 'onGestureStart';
gestures[me.getGesture() + 'end'] = 'onGestureEnd';
gestures.doubletap = 'onDoubleTap';
return gestures;
},
getSurface: function () {
return this.getChart() && this.getChart().getSurface("overlay");
},
onGestureStart: function (e) {
var me = this,
chart = me.getChart(),
region = chart.getInnerRegion(),
xy = chart.element.getXY(),
x = e.pageX - xy[0],
y = e.pageY - xy[1],
surface = this.getSurface();
if (region[0] < x && x < region[0] + region[2] && region[1] < y && y < region[1] + region[3]) {
me.lockEvents(me.getGesture());
me.startX = x;
me.startY = y;
me.selectionRect = surface.add({
type: 'rect',
globalAlpha: 0.3,
fillStyle: 'rgba(80,80,140,0.3)',
strokeStyle: 'rgba(80,80,140,1)',
lineWidth: 2,
x: x,
y: y,
width: 0,
height: 0,
zIndex: 1000
});
}
},
onGesture: function (e) {
var me = this;
if (me.getLocks()[me.getGesture()] === me) {
var chart = me.getChart(),
surface = me.getSurface(),
region = chart.getInnerRegion(),
xy = chart.element.getXY(),
x = e.pageX - xy[0],
y = e.pageY - xy[1];
if (x < region[0]) {
x = region[0];
} else if (x > region[0] + region[2]) {
x = region[0] + region[2];
}
if (y < region[1]) {
y = region[1];
} else if (y > region[1] + region[3]) {
y = region[1] + region[3];
}
me.selectionRect.setAttributes({
width: x - me.startX,
height: y - me.startY
});
if (Math.abs(me.startX - x) < 11 || Math.abs(me.startY - y) < 11) {
me.selectionRect.setAttributes({globalAlpha: 0.3});
} else {
me.selectionRect.setAttributes({globalAlpha: 1});
}
surface.renderFrame();
}
},
onGestureEnd: function (e) {
var me = this;
if (me.getLocks()[me.getGesture()] === me) {
var chart = me.getChart(),
surface = me.getSurface(),
region = chart.getInnerRegion(),
selectionRect = me.selectionRect,
xy = chart.element.getXY(),
x = e.pageX - xy[0],
y = e.pageY - xy[1];
if (x < region[0]) {
x = region[0];
} else if (x > region[0] + region[2]) {
x = region[0] + region[2];
}
if (y < region[1]) {
y = region[1];
} else if (y > region[1] + region[3]) {
y = region[1] + region[3];
}
if (Math.abs(me.startX - x) < 11 || Math.abs(me.startY - y) < 11) {
surface.remove(me.selectionRect);
} else {
me.zoomBy([
(Math.min(me.startX, x) - region[0]) / region[2],
1 - (Math.max(me.startY, y) - region[1]) / region[3],
(Math.max(me.startX, x) - region[0]) / region[2],
1 - (Math.min(me.startY, y) - region[1]) / region[3]
]);
selectionRect.setAttributes({
x: Math.min(me.startX, x),
y: Math.min(me.startY, y),
width: Math.abs(me.startX - x),
height: Math.abs(me.startY - y)
});
selectionRect.fx.setConfig(chart.getAnimate() || {duration: 0});
selectionRect.setAttributes({
globalAlpha: 0,
x: region[0],
y: region[1],
width: region[2],
height: region[3]
});
chart.suspendThicknessChanged();
selectionRect.fx.on('animationend', function () {
chart.resumeThicknessChanged();
surface.remove(me.selectionRect);
});
}
this.selectionRect = null;
surface.renderFrame();
me.sync();
me.unlockEvents(me.getGesture());
}
},
zoomBy: function (region) {
var me = this,
axisConfigs = me.getAxes(),
axes = me.getChart().getAxes(),
config,
zoomMap = {};
for (var i = 0; i < axes.length; i++) {
var axis = axes[i];
config = axisConfigs[axis.getPosition()];
if (config && config.allowZoom !== false) {
var isSide = axis.isSide(),
oldRange = axis.getVisibleRange();
zoomMap[axis.getId()] = oldRange.slice(0);
if (!isSide) {
axis.setVisibleRange([
(oldRange[1] - oldRange[0]) * region[0] + oldRange[0],
(oldRange[1] - oldRange[0]) * region[2] + oldRange[0]
]);
} else {
axis.setVisibleRange([
(oldRange[1] - oldRange[0]) * region[1] + oldRange[0],
(oldRange[1] - oldRange[0]) * region[3] + oldRange[0]
]);
}
}
}
me.zoomHistory.push(zoomMap);
me.getUndoButton().setDisabled(false);
},
undoZoom: function () {
var zoomMap = this.zoomHistory.pop(),
axes = this.getChart().getAxes();
if (zoomMap) {
for (var i = 0; i < axes.length; i++) {
var axis = axes[i];
if (zoomMap[axis.getId()]) {
axis.setVisibleRange(zoomMap[axis.getId()]);
}
}
}
this.getUndoButton().setDisabled(this.zoomHistory.length === 0);
this.sync();
},
onDoubleTap: function (e) {
this.undoZoom();
}
});
/**
* @class Ext.chart.interactions.ItemHighlight
* @extends Ext.chart.interactions.Abstract
*
* The ItemHighlight interaction allows the user to highlight series items in the chart.
*/
Ext.define('Ext.chart.interactions.ItemHighlight', {
extend: 'Ext.chart.interactions.Abstract',
type: 'itemhighlight',
alias: 'interaction.itemhighlight',
config: {
/**
* @cfg {String} gesture
* Defines the gesture type that should trigger item highlighting.
*/
gesture: 'tap'
},
getGestures: function () {
var gestures = {};
gestures.itemtap = 'onGesture';
gestures.tap = 'onFailedGesture';
return gestures;
},
onGesture: function (series, item, e) {
e.highlightItem = item;
},
onFailedGesture: function (e) {
this.getChart().setHighlightItem(e.highlightItem || null);
this.sync();
}
});
/**
* The ItemInfo interaction allows displaying detailed information about a series data
* point in a popup panel.
*
* To attach this interaction to a chart, include an entry in the chart's
* {@link Ext.chart.AbstractChart#interactions interactions} config with the `iteminfo` type:
*
* new Ext.chart.AbstractChart({
* renderTo: Ext.getBody(),
* width: 800,
* height: 600,
* store: store1,
* axes: [ ...some axes options... ],
* series: [ ...some series options... ],
* interactions: [{
* type: 'iteminfo',
* listeners: {
* show: function(me, item, panel) {
* panel.setHtml('Stock Price: $' + item.record.get('price'));
* }
* }
* }]
* });
*/
Ext.define('Ext.chart.interactions.ItemInfo', {
extend: 'Ext.chart.interactions.Abstract',
type: 'iteminfo',
alias: 'interaction.iteminfo',
/**
* @event show
* Fires when the info panel is shown.
* @param {Ext.chart.interactions.ItemInfo} this The interaction instance
* @param {Object} item The item whose info is being displayed
* @param {Ext.Panel} panel The panel for displaying the info
*/
config: {
/**
* @cfg {String} gesture
* Defines the gesture type that should trigger the item info panel to be displayed.
*/
gesture: 'itemtap',
/**
* @cfg {Object} panel
* An optional set of configuration overrides for the {@link Ext.Panel} that gets
* displayed. This object will be merged with the default panel configuration.
*/
panel: {
modal: true,
centered: true,
width: 250,
height: 300,
styleHtmlContent: true,
scrollable: 'vertical',
hideOnMaskTap: true,
fullscreen: false,
hidden: true,
zIndex: 30,
items: [
{
docked: 'top',
xtype: 'toolbar',
title: 'Item Detail'
}
]
}
},
applyPanel: function (panel, oldPanel) {
return Ext.factory(panel, 'Ext.Panel', oldPanel);
},
updatePanel: function (panel, oldPanel) {
if (panel) {
panel.on('hide', "reset", this);
}
if (oldPanel) {
oldPanel.un('hide', "reset", this);
}
},
onGesture: function (series, item) {
var me = this,
panel = me.getPanel();
me.item = item;
me.fireEvent('show', me, item, panel);
Ext.Viewport.add(panel);
panel.show('pop');
series.setAttributesForItem(item, { highlighted: true });
me.sync();
},
reset: function () {
var me = this,
item = me.item;
if (item) {
item.series.setAttributesForItem(item, { highlighted: false });
delete me.item;
me.sync();
}
}
});
/**
* @private
*/
Ext.define('Ext.util.Offset', {
/* Begin Definitions */
statics: {
fromObject: function(obj) {
return new this(obj.x, obj.y);
}
},
/* End Definitions */
constructor: function(x, y) {
this.x = (x != null && !isNaN(x)) ? x : 0;
this.y = (y != null && !isNaN(y)) ? y : 0;
return this;
},
copy: function() {
return new Ext.util.Offset(this.x, this.y);
},
copyFrom: function(p) {
this.x = p.x;
this.y = p.y;
},
toString: function() {
return "Offset[" + this.x + "," + this.y + "]";
},
equals: function(offset) {
//<debug>
if(!(offset instanceof this.statics())) {
Ext.Error.raise('Offset must be an instance of Ext.util.Offset');
}
//</debug>
return (this.x == offset.x && this.y == offset.y);
},
round: function(to) {
if (!isNaN(to)) {
var factor = Math.pow(10, to);
this.x = Math.round(this.x * factor) / factor;
this.y = Math.round(this.y * factor) / factor;
} else {
this.x = Math.round(this.x);
this.y = Math.round(this.y);
}
},
isZero: function() {
return this.x == 0 && this.y == 0;
}
});
/**
* Represents a rectangular region and provides a number of utility methods
* to compare regions.
*/
Ext.define('Ext.util.Region', {
requires: ['Ext.util.Offset'],
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`, and `left` properties.
* @param {Number} o.top
* @param {Number} o.right
* @param {Number} o.bottom
* @param {Number} o.left
* @return {Ext.util.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(top, right, bottom, left) {
var me = this;
me.top = top;
me[1] = top;
me.right = right;
me.bottom = bottom;
me.left = left;
me[0] = left;
},
/**
* Checks if this region completely contains the region that is passed in.
* @param {Ext.util.Region} region
* @return {Boolean}
*/
contains: function(region) {
var me = this;
return (region.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
* @return {Ext.util.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
* @return {Ext.util.Region} this
*/
constrainTo: function(targetRegion) {
var me = this,
constrain = Ext.util.Numbers.constrain;
me.top = constrain(me.top, targetRegion.top, targetRegion.bottom);
me.bottom = constrain(me.bottom, targetRegion.top, targetRegion.bottom);
me.left = constrain(me.left, targetRegion.left, targetRegion.right);
me.right = constrain(me.right, targetRegion.left, targetRegion.right);
return me;
},
/**
* Modifies the current region to be adjusted by offsets.
* @param {Number} top Top offset
* @param {Number} right Right offset
* @param {Number} bottom Bottom offset
* @param {Number} left Left offset
* @return {Ext.util.Region} this
* @chainable
*/
adjust: function(top, right, bottom, left) {
var me = this;
me.top += top;
me.left += left;
me.right += right;
me.bottom += bottom;
return me;
},
/**
* Get the offset amount of a point outside the region.
* @param {String/Object} 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 {
var d = new Ext.util.Offset();
d.x = this.getOutOfBoundOffsetX(axis.x);
d.y = this.getOutOfBoundOffsetY(axis.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 bounds.
* @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=1] (optional) The factor.
* @return {Number}
*/
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=1] (optional) The factor.
* @return {Number}
*/
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.
* @return {Number} return.width
* @return {Number} return.height
*/
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 Region.
* @chainable
*/
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 Region.
* @chainable
*/
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)
}
});
/**
* The PanZoom interaction allows the user to navigate the data for one or more chart
* axes by panning and/or zooming. Navigation can be limited to particular axes. Zooming is
* performed by pinching on the chart or axis area; panning is performed by single-touch dragging.
*
* For devices which do not support multiple-touch events, zooming can not be done via pinch gestures; in this case the
* interaction will allow the user to perform both zooming and panning using the same single-touch drag gesture.
* {@link #modeToggleButton} provides a button to indicate and toggle between two modes.
*
* @example preview
* var lineChart = new Ext.chart.CartesianChart({
* interactions: [{
* type: 'panzoom',
* zoomOnPanGesture: true
* }],
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data1'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 0
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'line',
* highlight: {
* size: 7,
* radius: 7
* },
* style: {
* stroke: 'rgb(143,203,203)'
* },
* xField: 'name',
* yField: 'data1',
* marker: {
* type: 'path',
* path: ['M', -2, 0, 0, 2, 2, 0, 0, -2, 'Z'],
* stroke: 'blue',
* lineWidth: 0
* }
* }, {
* type: 'line',
* highlight: {
* size: 7,
* radius: 7
* },
* fill: true,
* xField: 'name',
* yField: 'data3',
* marker: {
* type: 'circle',
* radius: 4,
* lineWidth: 0
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(lineChart);
*
* The configuration object for the `panzoom` interaction type should specify which axes
* will be made navigable via the `axes` config. See the {@link #axes} config documentation
* for details on the allowed formats. If the `axes` config is not specified, it will default
* to making all axes navigable with the default axis options.
*
*/
Ext.define('Ext.chart.interactions.PanZoom', {
extend: 'Ext.chart.interactions.Abstract',
type: 'panzoom',
alias: 'interaction.panzoom',
requires: [
'Ext.util.Region',
'Ext.draw.Animator'
],
config: {
/**
* @cfg {Object/Array} axes
* Specifies which axes should be made navigable. The config value can take the following formats:
*
* - An Object whose keys correspond to the {@link Ext.chart.axis.Axis#position position} of each
* axis that should be made navigable. Each key's value can either be an Object with further
* configuration options for each axis or simply `true` for a default set of options.
* {
* type: 'panzoom',
* axes: {
* left: {
* maxZoom: 5,
* allowPan: false
* },
* bottom: true
* }
* }
*
* If using the full Object form, the following options can be specified for each axis:
*
* - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its natural size.
* - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`.
* - startZoom (Number) A starting zoom level for the axis. Defaults to `1`.
* - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`.
* - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`.
* - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`.
*
* - An Array of strings, each one corresponding to the {@link Ext.chart.axis.Axis#position position}
* of an axis that should be made navigable. The default options will be used for each named axis.
*
* {
* type: 'panzoom',
* axes: ['left', 'bottom']
* }
*
* If the `axes` config is not specified, it will default to making all axes navigable with the
* default axis options.
*/
axes: {
top: {},
right: {},
bottom: {},
left: {}
},
minZoom: 1,
maxZoom: 10000,
/**
* @cfg {Boolean} showOverflowArrows
* If `true`, arrows will be conditionally shown at either end of each axis to indicate that the
* axis is overflowing and can therefore be panned in that direction. Set this to `false` to
* prevent the arrows from being displayed.
*/
showOverflowArrows: true,
/**
* @cfg {Object} overflowArrowOptions
* A set of optional overrides for the overflow arrow sprites' options. Only relevant when
* {@link #showOverflowArrows} is `true`.
*/
gesture: 'pinch',
panGesture: 'drag',
zoomOnPanGesture: false,
modeToggleButton: {
cls: ['x-panzoom-toggle', 'x-zooming'],
iconCls: 'x-panzoom-toggle-icon',
iconMask: true
},
hideLabelInGesture: false //Ext.os.is.Android
},
stopAnimationBeforeSync: true,
applyAxes: function (axesConfig, oldAxesConfig) {
return Ext.merge(oldAxesConfig || {}, axesConfig);
},
applyZoomOnPanGesture: function (zoomOnPanGesture) {
this.getChart();
if (this.isMultiTouch()) {
return false;
}
return zoomOnPanGesture;
},
updateZoomOnPanGesture: function (zoomOnPanGesture) {
if (!this.isMultiTouch()) {
var button = this.getModeToggleButton(),
zoomModeCls = Ext.baseCSSPrefix + 'zooming';
if (zoomOnPanGesture) {
button.addCls(zoomModeCls);
if (!button.config.hideText) {
button.setText('&nbsp;Zoom');
}
} else {
button.removeCls(zoomModeCls);
if (!button.config.hideText) {
button.setText('&nbsp;Pan');
}
}
}
},
toggleMode: function () {
var me = this;
if (!me.isMultiTouch()) {
me.setZoomOnPanGesture(!me.getZoomOnPanGesture());
}
},
applyModeToggleButton: function (button, oldButton) {
var me = this,
result = Ext.factory(button, "Ext.Button", oldButton);
if (result && !oldButton) {
result.setHandler(function () {
me.toggleMode();
});
}
return result;
},
getGestures: function () {
var me = this,
gestures = {};
gestures[me.getGesture()] = 'onGesture';
gestures[me.getGesture() + 'start'] = 'onGestureStart';
gestures[me.getGesture() + 'end'] = 'onGestureEnd';
gestures[me.getPanGesture()] = 'onPanGesture';
gestures[me.getPanGesture() + 'start'] = 'onPanGestureStart';
gestures[me.getPanGesture() + 'end'] = 'onPanGestureEnd';
gestures.doubletap = 'onDoubleTap';
return gestures;
},
onDoubleTap: function (e) {
},
onPanGestureStart: function (e) {
if (!e || !e.touches || e.touches.length < 2) { //Limit drags to single touch
var me = this,
region = me.getChart().getInnerRegion(),
xy = me.getChart().element.getXY();
me.startX = e.pageX - xy[0] - region[0];
me.startY = e.pageY - xy[1] - region[1];
me.oldVisibleRanges = null;
me.hideLabels();
me.getChart().suspendThicknessChanged();
}
},
onPanGesture: function (e) {
if (!e.touches || e.touches.length < 2) { //Limit drags to single touch
var me = this,
region = me.getChart().getInnerRegion(),
xy = me.getChart().element.getXY();
if (me.getZoomOnPanGesture()) {
me.transformAxesBy(me.getZoomableAxes(e), 0, 0, (e.pageX - xy[0] - region[0]) / me.startX, me.startY / (e.pageY - xy[1] - region[1]));
} else {
me.transformAxesBy(me.getPannableAxes(e), e.pageX - xy[0] - region[0] - me.startX, e.pageY - xy[1] - region[1] - me.startY, 1, 1);
}
me.sync();
}
},
onPanGestureEnd: function (e) {
var me = this;
me.getChart().resumeThicknessChanged();
me.showLabels();
me.sync();
},
onGestureStart: function (e) {
if (e.touches && e.touches.length === 2) {
var me = this,
xy = me.getChart().element.getXY(),
region = me.getChart().getInnerRegion(),
x = xy[0] + region[0],
y = xy[1] + region[1],
newPoints = [e.touches[0].point.x - x, e.touches[0].point.y - y, e.touches[1].point.x - x, e.touches[1].point.y - y],
xDistance = Math.max(44, Math.abs(newPoints[2] - newPoints[0])),
yDistance = Math.max(44, Math.abs(newPoints[3] - newPoints[1]));
me.getChart().suspendThicknessChanged();
me.lastZoomDistances = [xDistance, yDistance];
me.lastPoints = newPoints;
me.oldVisibleRanges = null;
me.hideLabels();
}
},
onGesture: function (e) {
if (e.touches && e.touches.length === 2) {
var me = this,
region = me.getChart().getInnerRegion(),
xy = me.getChart().element.getXY(),
x = xy[0] + region[0],
y = xy[1] + region[1],
abs = Math.abs,
lastPoints = me.lastPoints,
newPoints = [e.touches[0].point.x - x, e.touches[0].point.y - y, e.touches[1].point.x - x, e.touches[1].point.y - y],
xDistance = Math.max(44, abs(newPoints[2] - newPoints[0])),
yDistance = Math.max(44, abs(newPoints[3] - newPoints[1])),
lastDistances = this.lastZoomDistances || [xDistance, yDistance],
zoomX = xDistance / lastDistances[0],
zoomY = yDistance / lastDistances[1];
me.transformAxesBy(me.getZoomableAxes(e),
region[2] * (zoomX - 1) / 2 + newPoints[2] - lastPoints[2] * zoomX,
region[3] * (zoomY - 1) / 2 + newPoints[3] - lastPoints[3] * zoomY,
zoomX,
zoomY);
me.sync();
}
},
onGestureEnd: function (e) {
var me = this;
me.showLabels();
me.sync();
},
hideLabels: function () {
if (this.getHideLabelInGesture()) {
this.eachInteractiveAxes(function (axis) {
axis.hideLabels();
});
}
},
showLabels: function () {
if (this.getHideLabelInGesture()) {
this.eachInteractiveAxes(function (axis) {
axis.showLabels();
});
}
},
isEventOnAxis: function (e, axis) {
// TODO: right now this uses the current event position but really we want to only
// use the gesture's start event. Pinch does not give that to us though.
var region = axis.getSurface().getRegion();
return region[0] <= e.pageX && e.pageX <= region[0] + region[2] && region[1] <= e.pageY && e.pageY <= region[1] + region[3];
},
getPannableAxes: function (e) {
var me = this,
axisConfigs = me.getAxes(),
axes = me.getChart().getAxes(),
i, ln = axes.length,
result = [], isEventOnAxis = false,
config;
if (e) {
for (i = 0; i < ln; i++) {
if (this.isEventOnAxis(e, axes[i])) {
isEventOnAxis = true;
break;
}
}
}
for (i = 0; i < ln; i++) {
config = axisConfigs[axes[i].getPosition()];
if (config && config.allowPan !== false && (!isEventOnAxis || this.isEventOnAxis(e, axes[i]))) {
result.push(axes[i]);
}
}
return result;
},
getZoomableAxes: function (e) {
var me = this,
axisConfigs = me.getAxes(),
axes = me.getChart().getAxes(),
result = [],
i, ln = axes.length, axis,
isEventOnAxis = false, config;
if (e) {
for (i = 0; i < ln; i++) {
if (this.isEventOnAxis(e, axes[i])) {
isEventOnAxis = true;
break;
}
}
}
for (i = 0; i < ln; i++) {
axis = axes[i];
config = axisConfigs[axis.getPosition()];
if (config && config.allowZoom !== false && (!isEventOnAxis || this.isEventOnAxis(e, axis))) {
result.push(axis);
}
}
return result;
},
eachInteractiveAxes: function (fn) {
var me = this,
axisConfigs = me.getAxes(),
axes = me.getChart().getAxes();
for (var i = 0; i < axes.length; i++) {
if (axisConfigs[axes[i].getPosition()]) {
if (false === fn.call(this, axes[i])) {
return;
}
}
}
},
transformAxesBy: function (axes, panX, panY, sx, sy) {
var region = this.getChart().getInnerRegion(),
axesCfg = this.getAxes(), axisCfg,
oldVisibleRanges = this.oldVisibleRanges;
if (!oldVisibleRanges) {
this.oldVisibleRanges = oldVisibleRanges = {};
this.eachInteractiveAxes(function (axis) {
oldVisibleRanges[axis.getId()] = axis.getVisibleRange();
});
}
if (!region) {
return;
}
for (var i = 0; i < axes.length; i++) {
axisCfg = axesCfg[axes[i].getPosition()];
this.transformAxisBy(axes[i], oldVisibleRanges[axes[i].getId()], panX, panY, sx, sy, axisCfg.minZoom, axisCfg.maxZoom);
}
},
transformAxisBy: function (axis, oldVisibleRange, panX, panY, sx, sy, minZoom, maxZoom) {
var me = this,
visibleLength = oldVisibleRange[1] - oldVisibleRange[0],
actualMinZoom = axis.config.minZoom || minZoom || me.getMinZoom(),
actualMaxZoom = axis.config.maxZoom || maxZoom || me.getMaxZoom(),
region = me.getChart().getInnerRegion();
if (!region) {
return;
}
var isSide = axis.isSide(),
length = isSide ? region[3] : region[2],
pan = isSide ? -panY : panX;
visibleLength /= isSide ? sy : sx;
if (visibleLength < 0) {
visibleLength = -visibleLength;
}
if (visibleLength * actualMinZoom > 1) {
visibleLength = 1;
}
if (visibleLength * actualMaxZoom < 1) {
visibleLength = 1 / actualMaxZoom;
}
axis.setVisibleRange([
(oldVisibleRange[0] + oldVisibleRange[1] - visibleLength) * 0.5 - pan / length * visibleLength,
(oldVisibleRange[0] + oldVisibleRange[1] + visibleLength) * 0.5 - pan / length * visibleLength
]);
},
destroy: function () {
this.setModeToggleButton(null);
this.callSuper();
}
});
/**
* @class Ext.chart.interactions.Rotate
* @extends Ext.chart.interactions.Abstract
*
* The Rotate interaction allows the user to rotate a polar chart about its central point.
*
* @example preview
* var chart = new Ext.chart.PolarChart({
* animate: true,
* interactions: ['rotate'],
* colors: ["#115fa6", "#94ae0a", "#a61120", "#ff8809", "#ffd13e"],
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* series: [{
* type: 'pie',
* labelField: 'name',
* xField: 'data3',
* donut: 30
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define('Ext.chart.interactions.Rotate', {
extend: 'Ext.chart.interactions.Abstract',
type: 'rotate',
alias: 'interaction.rotate',
config: {
/**
* @cfg {String} gesture
* Defines the gesture type that will be used to rotate the chart. Currently only
* supports `pinch` for two-finger rotation and `drag` for single-finger rotation.
*/
gesture: 'rotate'
},
oldRotations: null,
getGestures: function () {
var gestures = {};
gestures.rotate = 'onRotate';
gestures.rotateend = 'onRotate';
gestures.dragstart = 'onGestureStart';
gestures.drag = 'onGesture';
gestures.dragend = 'onGesture';
return gestures;
},
getAngle: function (e) {
var me = this,
chart = me.getChart(),
xy = chart.getEventXY(e),
center = chart.getCenter();
return Math.atan2(xy[1] - center[1],
xy[0] - center[0]);
},
onGestureStart: function (e) {
this.angle = this.getAngle(e);
this.oldRotations = {};
},
onGesture: function (e) {
var me = this,
chart = me.getChart(),
angle = this.getAngle(e) - this.angle,
axes = chart.getAxes(), axis,
series = chart.getSeries(), seriesItem,
center = chart.getCenter(),
oldRotations = this.oldRotations,
oldRotation, i, ln;
for (i = 0, ln = axes.length; i < ln; i++) {
axis = axes[i];
oldRotation = oldRotations[axis.getId()] || (oldRotations[axis.getId()] = axis.getRotation());
axis.setRotation(angle + oldRotation);
}
for (i = 0, ln = series.length; i < ln; i++) {
seriesItem = series[i];
oldRotation = oldRotations[seriesItem.getId()] || (oldRotations[seriesItem.getId()] = seriesItem.getRotation());
seriesItem.setRotation(angle + oldRotation);
}
me.sync();
},
onRotate: function (e) {
}
});
/**
* @class Ext.chart.interactions.RotatePie3D
* @extends Ext.chart.interactions.Rotate
*
* A special version of the Rotate interaction used by Pie3D Chart.
*/
Ext.define('Ext.chart.interactions.RotatePie3D', {
extend: 'Ext.chart.interactions.Rotate',
type: 'rotatePie3d',
alias: 'interaction.rotatePie3d',
getAngle: function (e) {
var me = this,
chart = me.getChart(),
xy = chart.element.getXY(),
region = chart.getMainRegion();
return Math.atan2(e.pageY - xy[1] - region[3] * 0.5, e.pageX - xy[0] - region[2] * 0.5);
}
});
/**
* @abstract
* @class Ext.chart.series.Cartesian
* @extends Ext.chart.series.Series
*
* Common base class for series implementations which plot values using x/y coordinates.
*
* @constructor
*/
Ext.define('Ext.chart.series.Cartesian', {
extend: 'Ext.chart.series.Series',
config: {
/**
* The field used to access the x axis value from the items from the data
* source.
*
* @cfg {String} xField
*/
xField: null,
/**
* The field used to access the y-axis value from the items from the data
* source.
*
* @cfg {String} yField
*/
yField: null,
/**
* @cfg {Ext.chart.axis.Axis} xAxis The chart axis bound to the series on the x-axis.
*/
xAxis: null,
/**
* @cfg {Ext.chart.axis.Axis} yAxis The chart axis bound to the series on the y-axis.
*/
yAxis: null
},
directions: ['X', 'Y'],
fieldCategoryX: ['X'],
fieldCategoryY: ['Y'],
updateXAxis: function (axis) {
axis.processData(this);
},
updateYAxis: function (axis) {
axis.processData(this);
},
coordinateX: function () {
return this.coordinate('X', 0, 2);
},
coordinateY: function () {
return this.coordinate('Y', 1, 2);
},
getItemForPoint: function (x, y) {
if (this.getSprites()) {
var me = this,
sprite = me.getSprites()[0],
store = me.getStore(),
item;
if (sprite) {
var index = sprite.getIndexNearPoint(x, y);
if (index !== -1) {
item = {
series: this,
category: this.getItemInstancing() ? 'items' : 'markers',
index: index,
record: store.getData().items[index],
field: this.getYField(),
sprite: sprite
};
return item;
}
}
}
},
createSprite: function () {
var sprite = this.callSuper(),
xAxis = this.getXAxis();
sprite.setFlipXY(this.getChart().getFlipXY());
if (sprite.setAggregator && xAxis && xAxis.getAggregator) {
if (xAxis.getAggregator) {
sprite.setAggregator({strategy: xAxis.getAggregator()});
} else {
sprite.setAggregator({});
}
}
return sprite;
},
getSprites: function () {
var me = this,
chart = this.getChart(),
animation = chart && chart.getAnimate(),
itemInstancing = me.getItemInstancing(),
sprites = me.sprites, sprite;
if (!chart) {
return [];
}
if (!sprites.length) {
sprite = me.createSprite();
} else {
sprite = sprites[0];
}
if (animation) {
me.getLabel().getTemplate().fx.setConfig(animation);
if (itemInstancing) {
sprite.itemsMarker.getTemplate().fx.setConfig(animation);
}
sprite.fx.setConfig(animation);
}
return sprites;
},
provideLegendInfo: function (target) {
var style = this.getStyle();
target.push({
name: this.getTitle() || this.getYField() || this.getId(),
mark: style.fillStyle || style.strokeStyle || 'black',
disabled: false,
series: this.getId(),
index: 0
});
},
getXRange: function () {
return [this.dataRange[0], this.dataRange[2]];
},
getYRange: function () {
return [this.dataRange[1], this.dataRange[3]];
}
})
;
/**
* @abstract
* @extends Ext.chart.series.Cartesian
* Abstract class for all the stacked cartesian series including area series
* and bar series.
*/
Ext.define('Ext.chart.series.StackedCartesian', {
extend: 'Ext.chart.series.Cartesian',
config: {
/**
* @cfg {Boolean}
* 'true' to display the series in its stacked configuration.
*/
stacked: true,
/**
* @cfg {Array} hidden
*/
hidden: []
},
animatingSprites: 0,
updateStacked: function () {
this.processData();
},
coordinateY: function () {
return this.coordinateStacked('Y', 1, 2);
},
getFields: function (fieldCategory) {
var me = this,
fields = [], fieldsItem,
i, ln;
for (i = 0, ln = fieldCategory.length; i < ln; i++) {
fieldsItem = me['get' + fieldCategory[i] + 'Field']();
if (Ext.isArray(fieldsItem)) {
fields.push.apply(fields, fieldsItem);
} else {
fields.push(fieldsItem);
}
}
return fields;
},
updateLabelOverflowPadding: function (labelOverflowPadding) {
this.getLabel().setAttributes({labelOverflowPadding: labelOverflowPadding});
},
getSprites: function () {
var me = this,
chart = this.getChart(),
animation = chart && chart.getAnimate(),
fields = me.getFields(me.fieldCategoryY),
itemInstancing = me.getItemInstancing(),
sprites = me.sprites, sprite,
hidden = me.getHidden(),
spritesCreated = false,
i, length = fields.length;
if (!chart) {
return [];
}
for (i = 0; i < length; i++) {
sprite = sprites[i];
if (!sprite) {
sprite = me.createSprite();
if (chart.getFlipXY()) {
sprite.setAttributes({zIndex: i});
} else {
sprite.setAttributes({zIndex: -i});
}
sprite.setField(fields[i]);
spritesCreated = true;
hidden.push(false);
if (itemInstancing) {
sprite.itemsMarker.getTemplate().setAttributes(me.getOverriddenStyleByIndex(i));
} else {
sprite.setAttributes(me.getStyleByIndex(i));
}
}
if (animation) {
if (itemInstancing) {
sprite.itemsMarker.getTemplate().fx.setConfig(animation);
}
sprite.fx.setConfig(animation);
}
}
if (spritesCreated) {
me.updateHidden(hidden);
}
return sprites;
},
getItemForPoint: function (x, y) {
if (this.getSprites()) {
var me = this,
i, ln, sprite,
itemInstancing = me.getItemInstancing(),
sprites = me.getSprites(),
store = me.getStore(),
item;
for (i = 0, ln = sprites.length; i < ln; i++) {
sprite = sprites[i];
var index = sprite.getIndexNearPoint(x, y);
if (index !== -1) {
item = {
series: me,
index: index,
category: itemInstancing ? 'items' : 'markers',
record: store.getData().items[index],
field: this.getYField()[i],
sprite: sprite
};
return item;
}
}
}
},
provideLegendInfo: function (target) {
var sprites = this.getSprites(),
title = this.getTitle(),
field = this.getYField(),
hidden = this.getHidden();
for (var i = 0; i < sprites.length; i++) {
target.push({
name: this.getTitle() ? this.getTitle()[i] : (field && field[i]) || this.getId(),
mark: this.getStyleByIndex(i).fillStyle || this.getStyleByIndex(i).strokeStyle || 'black',
disabled: hidden[i],
series: this.getId(),
index: i
});
}
},
onSpriteAnimationStart: function (sprite) {
this.animatingSprites++;
if (this.animatingSprites === 1) {
this.fireEvent('animationstart');
}
},
onSpriteAnimationEnd: function (sprite) {
this.animatingSprites--;
if (this.animatingSprites === 0) {
this.fireEvent('animationend');
}
}
});
/**
* @class Ext.chart.series.sprite.Cartesian
* @extends Ext.draw.sprite.Sprite
*
* Cartesian sprite.
*/
Ext.define("Ext.chart.series.sprite.Cartesian", {
extend: 'Ext.draw.sprite.Sprite',
mixins: {
markerHolder: "Ext.chart.MarkerHolder"
},
homogeneous: true,
ascending: true,
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [dataMinX=0] Data minimum on the x-axis.
*/
dataMinX: 'number',
/**
* @cfg {Number} [dataMaxX=1] Data maximum on the x-axis.
*/
dataMaxX: 'number',
/**
* @cfg {Number} [dataMinY=0] Data minimum on the y-axis.
*/
dataMinY: 'number',
/**
* @cfg {Number} [dataMaxY=2] Data maximum on the y-axis.
*/
dataMaxY: 'number',
/**
* @cfg {Object} [dataY=null] Data items on the y-axis.
*/
dataY: 'data',
/**
* @cfg {Object} [dataX=null] Data items on the x-axis.
*/
dataX: 'data',
/**
* @cfg {Object} [labels=null] Labels used in the series.
*/
labels: 'default',
/**
* @cfg {Number} [labelOverflowPadding=10] Padding around labels to determine overlap.
*/
labelOverflowPadding: 'number',
/**
* @cfg {Boolean} [flipXY=true] 'true' if the series is flipped
*/
flipXY: 'bool',
renderer: 'default',
// PanZoom information
visibleMinX: 'number',
visibleMinY: 'number',
visibleMaxX: 'number',
visibleMaxY: 'number',
innerWidth: 'number',
innerHeight: 'number'
},
defaults: {
dataY: null,
dataX: null,
dataMinX: 0,
dataMaxX: 1,
dataMinY: 0,
dataMaxY: 1,
labels: null,
labelOverflowPadding: 10,
flipXY: false,
renderer: null,
transformFillStroke: false,
visibleMinX: 0,
visibleMinY: 0,
visibleMaxX: 1,
visibleMaxY: 1,
innerWidth: 1,
innerHeight: 1
},
dirtyTriggers: {
dataX: 'dataX,bbox',
dataY: 'dataY,bbox',
dataMinX: 'bbox',
dataMaxX: 'bbox',
dataMinY: 'bbox',
dataMaxY: 'bbox',
visibleMinX: 'panzoom',
visibleMinY: 'panzoom',
visibleMaxX: 'panzoom',
visibleMaxY: 'panzoom',
innerWidth: 'panzoom',
innerHeight: 'panzoom'
},
updaters: {
'dataX': function (attrs) {
this.processDataX();
if (!attrs.dirtyFlags.dataY) {
attrs.dirtyFlags.dataY = [];
}
attrs.dirtyFlags.dataY.push('dataY');
},
'dataY': function () {
this.processDataY();
},
'panzoom': function (attrs) {
var dx = attrs.visibleMaxX - attrs.visibleMinX,
dy = attrs.visibleMaxY - attrs.visibleMinY,
innerWidth = attrs.flipXY ? attrs.innerHeight : attrs.innerWidth,
innerHeight = !attrs.flipXY ? attrs.innerHeight : attrs.innerWidth;
attrs.translationX = -attrs.visibleMinX * innerWidth / dx;
attrs.translationY = -attrs.visibleMinY * innerHeight / dy;
attrs.scalingX = innerWidth / dx;
attrs.scalingY = innerHeight / dy;
attrs.scalingCenterX = 0;
attrs.scalingCenterY = 0;
this.applyTransformations(true);
}
}
}
},
config: {
/**
* @cfg {Boolean} flipXY 'true' if the series is flipped
*/
flipXY: false,
/**
* @private
* @cfg {Object} dataItems Store items that are passed to the renderer.
*/
dataItems: null,
/**
* @cfg {String} field The store field used by the series.
*/
field: null
},
processDataY: Ext.emptyFn,
processDataX: Ext.emptyFn,
updatePlainBBox: function (plain) {
var attr = this.attr;
plain.x = attr.dataMinX;
plain.y = attr.dataMinY;
plain.width = attr.dataMaxX - attr.dataMinX;
plain.height = attr.dataMaxY - attr.dataMinY;
},
/**
* Does a binary search of the data on the x-axis using the given key.
* @param key
* @return {*}
*/
binarySearch: function (key) {
var dx = this.attr.dataX,
start = 0,
end = dx.length;
if (key <= dx[0]) {
return start;
}
if (key >= dx[end - 1]) {
return end - 1;
}
while (start + 1 < end) {
var mid = (start + end) >> 1,
val = dx[mid];
if (val === key) {
return mid;
} else if (val < key) {
start = mid;
} else {
end = mid;
}
}
return start;
},
render: function (surface, ctx, region) {
var me = this,
flipXY = me.getFlipXY(),
attr = me.attr,
inverseMatrix = attr.inverseMatrix.clone();
inverseMatrix.appendMatrix(surface.inverseMatrix);
if (attr.dataX === null) {
return;
}
if (attr.dataY === null) {
return;
}
if (inverseMatrix.getXX() * inverseMatrix.getYX() || inverseMatrix.getXY() * inverseMatrix.getYY()) {
console.log('Cartesian Series sprite does not support rotation/sheering');
return;
}
var clip = inverseMatrix.transformList([
[region[0] - 1, region[3] + 1],
[region[0] + region[2] + 1, -1]
]);
clip = clip[0].concat(clip[1]);
if (clip[2] < clip[0]) {
console.log('Cartesian Series sprite does not supports flipped X.');
// TODO: support it
return;
}
me.renderClipped(surface, ctx, clip, region);
},
/**
* Render the given visible clip range.
* @param surface
* @param ctx
* @param clip
* @param region
*/
renderClipped: Ext.emptyFn,
/**
* Get the nearest item index from point (x, y). -1 as not found.
* @param {Number} x
* @param {Number} y
* @return {Number} The index
*/
getIndexNearPoint: function (x, y) {
var sprite = this,
mat = sprite.attr.matrix,
dataX = sprite.attr.dataX,
dataY = sprite.attr.dataY,
minX, minY, index = -1,
imat = mat.clone().prependMatrix(this.surfaceMatrix).inverse(),
center = imat.transformPoint([x, y]),
positionLB = imat.transformPoint([x - 22, y - 22]),
positionTR = imat.transformPoint([x + 22, y + 22]),
left = Math.min(positionLB[0], positionTR[0]),
right = Math.max(positionLB[0], positionTR[0]),
top = Math.min(positionLB[1], positionTR[1]),
bottom = Math.max(positionLB[1], positionTR[1]);
for (var i = 0; i < dataX.length; i++) {
if (left < dataX[i] && dataX[i] < right && top < dataY[i] && dataY[i] < bottom) {
if (index === -1 || Math.abs(dataX[i] - center[0]) < minX &&
Math.abs(dataY[i] - center[1]) < minY) {
minX = Math.abs(dataX[i] - center[0]);
minY = Math.abs(dataY[i] - center[1]);
index = i;
}
}
}
return index;
}
});
/**
* @class Ext.chart.series.sprite.StackedCartesian
* @extends Ext.chart.series.sprite.Cartesian
*
* Stacked cartesian sprite.
*/
Ext.define("Ext.chart.series.sprite.StackedCartesian", {
extend: 'Ext.chart.series.sprite.Cartesian',
inheritableStatics: {
def: {
processors: {
/**
* @private
* @cfg {Number} [groupCount=1] The number of groups in the series.
*/
groupCount: 'number',
/**
* @private
* @cfg {Number} [groupOffset=0] The group index of the series sprite.
*/
groupOffset: 'number',
/**
* @private
* @cfg {Object} [dataStartY=null] The starting point of the data used in the series.
*/
dataStartY: 'data'
},
defaults: {
groupCount: 1,
groupOffset: 0,
dataStartY: null
},
dirtyTriggers: {
dataStartY: 'dataY,bbox'
}
}
},
//@inheritdoc
getIndexNearPoint: function (x, y) {
var sprite = this,
mat = sprite.attr.matrix,
dataX = sprite.attr.dataX,
dataY = sprite.attr.dataY,
dataStartY = sprite.attr.dataStartY,
minX, minY, index = -1,
imat = mat.clone().prependMatrix(this.surfaceMatrix).inverse(),
center = imat.transformPoint([x, y]),
positionLB = imat.transformPoint([x - 22, y - 22]),
positionTR = imat.transformPoint([x + 22, y + 22]),
dx, dy,
left = Math.min(positionLB[0], positionTR[0]),
right = Math.max(positionLB[0], positionTR[0]);
for (var i = 0; i < dataX.length; i++) {
if (left <= dataX[i] && dataX[i] <= right && dataStartY[i] <= center[1] && center[1] <= dataY[i]) {
dx = Math.abs(dataX[i] - center[0]);
dy = Math.max(-Math.min(dataY[i] - center[1], center[1] - dataStartY[i]), 0);
if (index === -1 || dx < minX && dy <= minY) {
minX = dx;
minY = dy;
index = i;
}
}
}
return index;
}
});
/**
* @class Ext.chart.series.sprite.Area
* @extends Ext.chart.series.sprite.StackedCartesian
*
* Area series sprite.
*/
Ext.define("Ext.chart.series.sprite.Area", {
alias: 'sprite.areaSeries',
extend: "Ext.chart.series.sprite.StackedCartesian",
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Boolean} [step=false] 'true' if the area is represented with steps instead of lines.
*/
step: 'bool'
},
defaults: {
step: false
}
}
},
renderClipped: function (surface, ctx, clip, clipRegion) {
var me = this,
attr = me.attr,
dataX = attr.dataX,
dataY = attr.dataY,
dataStartY = attr.dataStartY,
matrix = attr.matrix,
x, y, i, lastX, lastY,
xx = matrix.elements[0],
dx = matrix.elements[4],
yy = matrix.elements[3],
dy = matrix.elements[5],
surfaceMatrix = me.surfaceMatrix,
markerCfg = {},
start = Math.max(0, this.binarySearch(clip[0])),
end = Math.min(dataX.length - 1, this.binarySearch(clip[2]) + 1);
ctx.beginPath();
if (attr.step) {
lastY = dataY[start] * yy + dy;
for (i = start; i <= end; i++) {
x = dataX[i] * xx + dx;
y = dataY[i] * yy + dy;
ctx.lineTo(x, lastY);
ctx.lineTo(x, lastY = y);
}
} else {
for (i = start; i <= end; i++) {
x = dataX[i] * xx + dx;
y = dataY[i] * yy + dy;
ctx.lineTo(x, y);
}
}
if (dataStartY) {
if (attr.step) {
lastX = dataX[end] * xx + dx;
for (i = end; i >= start; i--) {
x = dataX[i] * xx + dx;
y = dataStartY[i] * yy + dy;
ctx.lineTo(lastX, y);
ctx.lineTo(lastX = x, y);
}
} else {
for (i = end; i >= start; i--) {
x = dataX[i] * xx + dx;
y = dataStartY[i] * yy + dy;
ctx.lineTo(x, y);
}
}
} else {
// dataStartY[i] == 0;
ctx.lineTo(dataX[end] * xx + dx, y);
ctx.lineTo(dataX[end] * xx + dx, dy);
ctx.lineTo(dataX[start] * xx + dx, dy);
ctx.lineTo(dataX[start] * xx + dx, dataY[i] * yy + dy);
}
if (attr.transformFillStroke) {
attr.matrix.toContext(ctx);
}
ctx.fill();
if (attr.transformFillStroke) {
attr.inverseMatrix.toContext(ctx);
}
ctx.beginPath();
if (attr.step) {
for (i = start; i <= end; i++) {
x = dataX[i] * xx + dx;
y = dataY[i] * yy + dy;
ctx.lineTo(x, lastY);
ctx.lineTo(x, lastY = y);
markerCfg.translationX = surfaceMatrix.x(x, y);
markerCfg.translationY = surfaceMatrix.y(x, y);
me.putMarker("markers", markerCfg, i, !attr.renderer);
}
} else {
for (i = start; i <= end; i++) {
x = dataX[i] * xx + dx;
y = dataY[i] * yy + dy;
ctx.lineTo(x, y);
markerCfg.translationX = surfaceMatrix.x(x, y);
markerCfg.translationY = surfaceMatrix.y(x, y);
me.putMarker("markers", markerCfg, i, !attr.renderer);
}
}
if (attr.transformFillStroke) {
attr.matrix.toContext(ctx);
}
ctx.stroke();
}
});
/**
* @class Ext.chart.series.Area
* @extends Ext.chart.series.StackedCartesian
*
* Creates an Area Chart.
*
* @example preview
* var chart = new Ext.chart.CartesianChart({
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data1'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 0
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'area',
* subStyle: {
* fill: ['blue', 'green', 'red']
* },
* xField: 'name',
* yField: ['data1', 'data2', 'data3']
*
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define('Ext.chart.series.Area', {
extend: 'Ext.chart.series.StackedCartesian',
alias: 'series.area',
type: 'area',
seriesType: 'areaSeries',
requires: ['Ext.chart.series.sprite.Area']
});
/**
* @class Ext.chart.series.sprite.Bar
* @extends Ext.chart.series.sprite.StackedCartesian
*
* Draws a sprite used in the bar series.
*/
Ext.define("Ext.chart.series.sprite.Bar", {
alias: 'sprite.barSeries',
extend: 'Ext.chart.series.sprite.StackedCartesian',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [minBarWidth=2] The minimum bar width.
*/
minBarWidth: 'number',
/**
* @cfg {Number} [maxBarWidth=100] The maximum bar width.
*/
maxBarWidth: 'number',
/**
* @cfg {Number} [minGapWidth=5] The minimum gap between bars.
*/
minGapWidth: 'number',
/**
* @cfg {Number} [radius=0] The degree of rounding for rounded bars.
*/
radius: 'number',
/**
* @cfg {Number} [inGroupGapWidth=3] The gap between grouped bars.
*/
inGroupGapWidth: 'number',
renderer: 'default'
},
defaults: {
minBarWidth: 2,
maxBarWidth: 100,
minGapWidth: 5,
inGroupGapWidth: 3,
radius: 0,
renderer: null
}
}
},
// TODO: design this more carefully
drawLabel: function (text, dataX, dataStartY, dataY, labelId) {
var me = this,
attr = me.attr,
labelCfg = me.labelCfg || (me.labelCfg = {}),
surfaceMatrix = me.surfaceMatrix,
labelX, labelY,
labelOverflowPadding = attr.labelOverflowPadding,
halfWidth,
labelBox;
labelBox = this.getMarkerBBox('labels', labelId, true);
labelCfg.text = text;
if (!labelBox) {
me.putMarker('labels', labelCfg, labelId);
labelBox = this.getMarkerBBox('labels', labelId, true);
}
if (!attr.flipXY) {
labelCfg.rotationRads = Math.PI * 0.5;
} else {
labelCfg.rotationRads = 0;
}
labelCfg.calloutVertical = !attr.flipXY;
halfWidth = (labelBox.width / 2 + labelOverflowPadding);
if (dataStartY > dataY) {
halfWidth = -halfWidth;
}
labelX = dataX;
labelY = dataY - halfWidth;
labelCfg.x = surfaceMatrix.x(labelX, labelY);
labelCfg.y = surfaceMatrix.y(labelX, labelY);
labelX = dataX;
labelY = dataY + halfWidth;
labelCfg.calloutPlaceX = surfaceMatrix.x(labelX, labelY);
labelCfg.calloutPlaceY = surfaceMatrix.y(labelX, labelY);
labelX = dataX;
labelY = dataY;
labelCfg.calloutStartX = surfaceMatrix.x(labelX, labelY);
labelCfg.calloutStartY = surfaceMatrix.y(labelX, labelY);
if (dataStartY > dataY) {
halfWidth = -halfWidth;
}
if (Math.abs(dataY - dataStartY) > halfWidth * 2) {
labelCfg.callout = 0;
} else {
labelCfg.callout = 1;
}
me.putMarker('labels', labelCfg, labelId);
},
drawBar: function (ctx, surface, clip, left, top, right, bottom, index) {
var itemCfg = this.itemCfg || (this.itemCfg = {});
itemCfg.x = left;
itemCfg.y = top;
itemCfg.width = right - left;
itemCfg.height = bottom - top;
itemCfg.radius = this.attr.radius;
if (this.attr.renderer) {
this.attr.renderer.call(this, itemCfg, this, index, this.getDataItems().items[index]);
}
this.putMarker("items", itemCfg, index, !this.attr.renderer);
},
//@inheritdoc
renderClipped: function (surface, ctx, clip) {
if (this.cleanRedraw) {
return;
}
var me = this,
attr = me.attr,
dataX = attr.dataX,
dataY = attr.dataY,
dataText = attr.labels,
dataStartY = attr.dataStartY,
groupCount = attr.groupCount,
groupOffset = attr.groupOffset - (groupCount - 1) * 0.5,
inGroupGapWidth = attr.inGroupGapWidth,
yLow, yHi,
lineWidth = ctx.lineWidth,
matrix = attr.matrix,
maxBarWidth = matrix.getXX() - attr.minGapWidth,
barWidth = surface.roundPixel(Math.max(attr.minBarWidth, (Math.min(maxBarWidth, attr.maxBarWidth) - inGroupGapWidth * (groupCount - 1)) / groupCount)),
surfaceMatrix = this.surfaceMatrix,
left, right, bottom, top, i, center,
halfLineWidth = 0.5 * attr.lineWidth,
xx = matrix.elements[0],
dx = matrix.elements[4],
yy = matrix.elements[3],
dy = surface.roundPixel(matrix.elements[5]) - 1,
start = Math.max(0, Math.floor(clip[0])),
end = Math.min(dataX.length - 1, Math.ceil(clip[2])),
drawMarkers = dataText && !!this.getBoundMarker("labels");
for (i = start; i <= end; i++) {
yLow = dataStartY ? dataStartY[i] : 0;
yHi = dataY[i];
center = dataX[i] * xx + dx + groupOffset * (barWidth + inGroupGapWidth);
left = surface.roundPixel(center - barWidth / 2) + halfLineWidth;
top = surface.roundPixel(yHi * yy + lineWidth + dy);
right = surface.roundPixel(center + barWidth / 2) - halfLineWidth;
bottom = surface.roundPixel(yLow * yy + lineWidth + dy);
me.drawBar(ctx, surface, clip, left, top - halfLineWidth, right, bottom - halfLineWidth, i);
if (drawMarkers && dataText[i]) {
this.drawLabel(dataText[i], center, bottom, top, i);
}
me.putMarker("markers", {
translationX: surfaceMatrix.x(center, top),
translationY: surfaceMatrix.y(center, top)
}, i, true);
}
}
});
/**
* @class Ext.chart.series.Bar
* @extends Ext.chart.series.StackedCartesian
*
* Creates a Bar Chart.
*
* @example preview
* var chart = new Ext.chart.Chart({
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* fields: 'data1'
* }, {
* type: 'category',
* position: 'bottom',
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* fields: 'name'
* }],
* series: [{
* type: 'bar',
* xField: 'name',
* yField: 'data1',
* style: {
* fill: 'blue'
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define('Ext.chart.series.Bar', {
extend: 'Ext.chart.series.StackedCartesian',
alias: 'series.bar',
type: 'bar',
seriesType: 'barSeries',
requires: [
'Ext.chart.series.sprite.Bar',
'Ext.draw.sprite.Rect'
],
config: {
/**
* @private
* @cfg {Object} itemInstancing Sprite template used for series.
*/
itemInstancing: {
type: 'rect',
fx: {
customDuration: {
x: 0,
y: 0,
width: 0,
height: 0,
radius: 0
}
}
}
},
updateXAxis: function (axis) {
axis.setLabelInSpan(true);
this.callSuper(arguments);
},
updateStacked: function (stacked) {
var sprites = this.getSprites(),
attrs = {}, i, ln = sprites.length;
if (this.getStacked()) {
attrs.groupCount = 1;
attrs.groupOffset = 0;
for (i = 0; i < ln; i++) {
sprites[i].setAttributes(attrs);
}
} else {
attrs.groupCount = this.getYField().length;
for (i = 0; i < ln; i++) {
attrs.groupOffset = i;
sprites[i].setAttributes(attrs);
}
}
this.callSuper(arguments);
}
});
/**
* This class we summarize the data and returns it when required.
*/
Ext.define("Ext.draw.SegmentTree", {
config: {
strategy: "double"
},
/**
* @private
* @param result
* @param last
* @param dataX
* @param dataOpen
* @param dataHigh
* @param dataLow
* @param dataClose
*/
"time": function (result, last, dataX, dataOpen, dataHigh, dataLow, dataClose) {
var start = 0, lastOffset, lastOffsetEnd,
minimum = new Date(dataX[result.startIdx[0]]),
maximum = new Date(dataX[result.endIdx[last - 1]]),
extDate = Ext.Date,
units = [
[extDate.MILLI, 1, 'ms1', null],
[extDate.MILLI, 2, 'ms2', 'ms1'],
[extDate.MILLI, 5, 'ms5', 'ms1'],
[extDate.MILLI, 10, 'ms10', 'ms5'],
[extDate.MILLI, 50, 'ms50', 'ms10'],
[extDate.MILLI, 100, 'ms100', 'ms50'],
[extDate.MILLI, 500, 'ms500', 'ms100'],
[extDate.SECOND, 1, 's1', 'ms500'],
[extDate.SECOND, 10, 's10', 's1'],
[extDate.SECOND, 30, 's30', 's10'],
[extDate.MINUTE, 1, 'mi1', 's10'],
[extDate.MINUTE, 5, 'mi5', 'mi1'],
[extDate.MINUTE, 10, 'mi10', 'mi5'],
[extDate.MINUTE, 30, 'mi30', 'mi10'],
[extDate.HOUR, 1, 'h1', 'mi30'],
[extDate.HOUR, 6, 'h6', 'h1'],
[extDate.HOUR, 12, 'h12', 'h6'],
[extDate.DAY, 1, 'd1', 'h12'],
[extDate.DAY, 7, 'd7', 'd1'],
[extDate.MONTH, 1, 'mo1', 'd1'],
[extDate.MONTH, 3, 'mo3', 'mo1'],
[extDate.MONTH, 6, 'mo6', 'mo3'],
[extDate.YEAR, 1, 'y1', 'mo3'],
[extDate.YEAR, 5, 'y5', 'y1'],
[extDate.YEAR, 10, 'y10', 'y5'],
[extDate.YEAR, 100, 'y100', 'y10']
], unitIdx, currentUnit,
plainStart = start,
plainEnd = last,
first = false,
startIdxs = result.startIdx,
endIdxs = result.endIdx,
minIdxs = result.minIdx,
maxIdxs = result.maxIdx,
opens = result.open,
closes = result.close,
minXs = result.minX,
minYs = result.minY,
maxXs = result.maxX,
maxYs = result.maxY,
i, current;
for (unitIdx = 0; last > start + 1 && unitIdx < units.length; unitIdx++) {
minimum = new Date(dataX[startIdxs[0]]);
currentUnit = units[unitIdx];
minimum = extDate.align(minimum, currentUnit[0], currentUnit[1]);
if (extDate.diff(minimum, maximum, currentUnit[0]) > dataX.length * 2 * currentUnit[1]) {
continue;
}
if (currentUnit[3] && result.map['time_' + currentUnit[3]]) {
lastOffset = result.map['time_' + currentUnit[3]][0];
lastOffsetEnd = result.map['time_' + currentUnit[3]][1];
} else {
lastOffset = plainStart;
lastOffsetEnd = plainEnd;
}
start = last;
current = minimum;
first = true;
startIdxs[last] = startIdxs[lastOffset];
endIdxs[last] = endIdxs[lastOffset];
minIdxs[last] = minIdxs[lastOffset];
maxIdxs[last] = maxIdxs[lastOffset];
opens[last] = opens[lastOffset];
closes[last] = closes[lastOffset];
minXs[last] = minXs[lastOffset];
minYs[last] = minYs[lastOffset];
maxXs[last] = maxXs[lastOffset];
maxYs[last] = maxYs[lastOffset];
current = Ext.Date.add(current, currentUnit[0], currentUnit[1]);
for (i = lastOffset + 1; i < lastOffsetEnd; i++) {
if (dataX[endIdxs[i]] < +current) {
endIdxs[last] = endIdxs[i];
closes[last] = closes[i];
if (maxYs[i] > maxYs[last]) {
maxYs[last] = maxYs[i];
maxXs[last] = maxXs[i];
maxIdxs[last] = maxIdxs[i];
}
if (minYs[i] < minYs[last]) {
minYs[last] = minYs[i];
minXs[last] = minXs[i];
minIdxs[last] = minIdxs[i];
}
} else {
last++;
startIdxs[last] = startIdxs[i];
endIdxs[last] = endIdxs[i];
minIdxs[last] = minIdxs[i];
maxIdxs[last] = maxIdxs[i];
opens[last] = opens[i];
closes[last] = closes[i];
minXs[last] = minXs[i];
minYs[last] = minYs[i];
maxXs[last] = maxXs[i];
maxYs[last] = maxYs[i];
current = Ext.Date.add(current, currentUnit[0], currentUnit[1]);
}
}
if (last > start) {
result.map['time_' + currentUnit[2]] = [start, last];
}
}
},
/**
* @private
* @param result
* @param position
* @param dataX
* @param dataOpen
* @param dataHigh
* @param dataLow
* @param dataClose
*/
"double": function (result, position, dataX, dataOpen, dataHigh, dataLow, dataClose) {
var offset = 0, lastOffset, step = 1,
i,
startIdx,
endIdx,
minIdx,
maxIdx,
open,
close,
minX,
minY,
maxX,
maxY;
while (position > offset + 1) {
lastOffset = offset;
offset = position;
step += step;
for (i = lastOffset; i < offset; i += 2) {
if (i === offset - 1) {
startIdx = result.startIdx[i];
endIdx = result.endIdx[i];
minIdx = result.minIdx[i];
maxIdx = result.maxIdx[i];
open = result.open[i];
close = result.close[i];
minX = result.minX[i];
minY = result.minY[i];
maxX = result.maxX[i];
maxY = result.maxY[i];
} else {
startIdx = result.startIdx[i];
endIdx = result.endIdx[i + 1];
open = result.open[i];
close = result.close[i];
if (result.minY[i] <= result.minY[i + 1]) {
minIdx = result.minIdx[i];
minX = result.minX[i];
minY = result.minY[i];
} else {
minIdx = result.minIdx[i + 1];
minX = result.minX[i + 1];
minY = result.minY[i + 1];
}
if (result.maxY[i] >= result.maxY[i + 1]) {
maxIdx = result.maxIdx[i];
maxX = result.maxX[i];
maxY = result.maxY[i];
} else {
maxIdx = result.maxIdx[i + 1];
maxX = result.maxX[i + 1];
maxY = result.maxY[i + 1];
}
}
result.startIdx[position] = startIdx;
result.endIdx[position] = endIdx;
result.minIdx[position] = minIdx;
result.maxIdx[position] = maxIdx;
result.open[position] = open;
result.close[position] = close;
result.minX[position] = minX;
result.minY[position] = minY;
result.maxX[position] = maxX;
result.maxY[position] = maxY;
position++;
}
result.map['double_' + step] = [offset, position];
}
},
/**
* @private
*/
"none": Ext.emptyFn,
/**
* @private
*
* @param dataX
* @param dataOpen
* @param dataHigh
* @param dataLow
* @param dataClose
* @return {Object}
*/
aggregateData: function (dataX, dataOpen, dataHigh, dataLow, dataClose) {
var length = dataX.length,
startIdx = [],
endIdx = [],
minIdx = [],
maxIdx = [],
open = [],
minX = [],
minY = [],
maxX = [],
maxY = [],
close = [],
result = {
startIdx: startIdx,
endIdx: endIdx,
minIdx: minIdx,
maxIdx: maxIdx,
open: open,
minX: minX,
minY: minY,
maxX: maxX,
maxY: maxY,
close: close
},
i;
for (i = 0; i < length; i++) {
startIdx[i] = i;
endIdx[i] = i;
minIdx[i] = i;
maxIdx[i] = i;
open[i] = dataOpen[i];
minX[i] = dataX[i];
minY[i] = dataLow[i];
maxX[i] = dataX[i];
maxY[i] = dataHigh[i];
close[i] = dataClose[i];
}
result.map = {
original: [0, length]
};
if (length) {
this[this.getStrategy()](result, length, dataX, dataOpen, dataHigh, dataLow, dataClose);
}
return result;
},
/**
* @private
* @param items
* @param start
* @param end
* @param key
* @return {*}
*/
binarySearchMin: function (items, start, end, key) {
var dx = this.dataX;
if (key <= dx[items.startIdx[0]]) {
return start;
}
if (key >= dx[items.startIdx[end - 1]]) {
return end - 1;
}
while (start + 1 < end) {
var mid = (start + end) >> 1,
val = dx[items.startIdx[mid]];
if (val === key) {
return mid;
} else if (val < key) {
start = mid;
} else {
end = mid;
}
}
return start;
},
/**
* @private
* @param items
* @param start
* @param end
* @param key
* @return {*}
*/
binarySearchMax: function (items, start, end, key) {
var dx = this.dataX;
if (key <= dx[items.endIdx[0]]) {
return start;
}
if (key >= dx[items.endIdx[end - 1]]) {
return end - 1;
}
while (start + 1 < end) {
var mid = (start + end) >> 1,
val = dx[items.endIdx[mid]];
if (val === key) {
return mid;
} else if (val < key) {
start = mid;
} else {
end = mid;
}
}
return end;
},
constructor: function (config) {
this.initConfig(config);
},
/**
* Sets the data of the segment tree.
* @param dataX
* @param dataOpen
* @param dataHigh
* @param dataLow
* @param dataClose
*/
setData: function (dataX, dataOpen, dataHigh, dataLow, dataClose) {
if (!dataHigh) {
dataClose = dataLow = dataHigh = dataOpen;
}
this.dataX = dataX;
this.dataOpen = dataOpen;
this.dataHigh = dataHigh;
this.dataLow = dataLow;
this.dataClose = dataClose;
if (dataX.length === dataHigh.length &&
dataX.length === dataLow.length) {
this.cache = this.aggregateData(dataX, dataOpen, dataHigh, dataLow, dataClose);
}
},
/**
* Returns the minimum range of data that fits the given range and step size.
*
* @param {Number} min
* @param {Number} max
* @param {Number} estStep
* @return {Object} The aggregation information.
* @return {Number} return.start
* @return {Number} return.end
* @return {Object} return.data The aggregated data
*/
getAggregation: function (min, max, estStep) {
if (!this.cache) {
return null;
}
var minStep = Infinity,
range = this.dataX[this.dataX.length - 1] - this.dataX[0],
cacheMap = this.cache.map,
result = cacheMap.original,
name, positions, ln, step, minIdx, maxIdx;
for (name in cacheMap) {
positions = cacheMap[name];
ln = positions[1] - positions[0] - 1;
step = range / ln;
if (estStep <= step && step < minStep) {
result = positions;
minStep = step;
}
}
minIdx = Math.max(this.binarySearchMin(this.cache, result[0], result[1], min), result[0]);
maxIdx = Math.min(this.binarySearchMax(this.cache, result[0], result[1], max) + 1, result[1]);
return {
data: this.cache,
start: minIdx,
end: maxIdx
};
}
});
/**
*
*/
Ext.define("Ext.chart.series.sprite.Aggregative", {
extend: 'Ext.chart.series.sprite.Cartesian',
requires: ['Ext.draw.LimitedCache', 'Ext.draw.SegmentTree'],
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Object} [dataHigh=null] Data items representing the high values of the aggregated data.
*/
dataHigh: 'data',
/**
* @cfg {Object} [dataLow=null] Data items representing the low values of the aggregated data.
*/
dataLow: 'data',
/**
* @cfg {Object} [dataClose=null] Data items representing the closing values of the aggregated data.
*/
dataClose: 'data'
},
aliases: {
/**
* @cfg {Object} [dataOpen=null] Data items representing the opening values of the aggregated data.
*/
dataOpen: 'dataY'
},
defaults: {
dataHigh: null,
dataLow: null,
dataClose: null
}
}
},
config: {
aggregator: {}
},
applyAggregator: function (aggregator, oldAggr) {
return Ext.factory(aggregator, Ext.draw.SegmentTree, oldAggr);
},
constructor: function () {
this.callSuper(arguments);
},
processDataY: function () {
var me = this,
attr = me.attr,
high = attr.dataHigh,
low = attr.dataLow,
close = attr.dataClose,
open = attr.dataY;
me.callSuper(arguments);
if (attr.dataX && open && open.length > 0) {
if (high) {
me.getAggregator().setData(attr.dataX, attr.dataY, high, low, close);
} else {
me.getAggregator().setData(attr.dataX, attr.dataY);
}
}
},
getGapWidth: function () {
return 1;
},
renderClipped: function (surface, ctx, clip, region) {
var me = this,
aggregates = me.getAggregator() && me.getAggregator().getAggregation(
clip[0],
clip[2],
(clip[2] - clip[0]) / region[2] * me.getGapWidth()
);
if (aggregates) {
me.dataStart = aggregates.data.startIdx[aggregates.start];
me.dataEnd = aggregates.data.endIdx[aggregates.end - 1];
me.renderAggregates(aggregates.data, aggregates.start, aggregates.end, surface, ctx, clip, region);
}
}
});
/**
* @class Ext.chart.series.sprite.CandleStick
* @extends Ext.chart.series.sprite.Aggregative
*
* CandleStick series sprite.
*/
Ext.define("Ext.chart.series.sprite.CandleStick", {
alias: 'sprite.candlestickSeries',
extend: 'Ext.chart.series.sprite.Aggregative',
inheritableStatics: {
def: {
processors: {
raiseStyle: function (n, o) {
return Ext.merge({}, o || {}, n);
},
dropStyle: function (n, o) {
return Ext.merge({}, o || {}, n);
},
/**
* @cfg {Number} [barWidth=15] The bar width of the candles.
*/
barWidth: 'number',
/**
* @cfg {Number} [padding=3] The amount of padding between candles.
*/
padding: 'number',
/**
* @cfg {String} [ohlcType='candlestick'] Determines whether candlestick or ohlc is used.
*/
ohlcType: 'enums(candlestick,ohlc)'
},
defaults: {
raiseStyle: {
strokeStyle: 'green',
fillStyle: 'green'
},
dropStyle: {
strokeStyle: 'red',
fillStyle: 'red'
},
planar: false,
barWidth: 15,
padding: 3,
lineJoin: 'miter',
miterLimit: 5,
ohlcType: 'candlestick'
},
dirtyTriggers: {
raiseStyle: 'raiseStyle',
dropStyle: 'dropStyle'
},
updaters: {
raiseStyle: function () {
this.raiseTemplate && this.raiseTemplate.setAttributes(this.attr.raiseStyle);
},
dropStyle: function () {
this.dropTemplate && this.dropTemplate.setAttributes(this.attr.dropStyle);
}
}
}
},
"candlestick": function (ctx, open, high, low, close, mid, halfWidth) {
var minOC = Math.min(open, close),
maxOC = Math.max(open, close);
ctx.moveTo(mid, low);
ctx.lineTo(mid, maxOC);
ctx.moveTo(mid + halfWidth, maxOC);
ctx.lineTo(mid + halfWidth, minOC);
ctx.lineTo(mid - halfWidth, minOC);
ctx.lineTo(mid - halfWidth, maxOC);
ctx.closePath();
ctx.moveTo(mid, high);
ctx.lineTo(mid, minOC);
},
"ohlc": function (ctx, open, high, low, close, mid, halfWidth) {
ctx.moveTo(mid, high);
ctx.lineTo(mid, low);
ctx.moveTo(mid, open);
ctx.lineTo(mid - halfWidth, open);
ctx.moveTo(mid, close);
ctx.lineTo(mid + halfWidth, close);
},
constructor: function () {
this.callSuper(arguments);
this.raiseTemplate = new Ext.draw.sprite.Rect({parent: this});
this.dropTemplate = new Ext.draw.sprite.Rect({parent: this});
},
getGapWidth: function () {
var attr = this.attr,
barWidth = attr.barWidth,
padding = attr.padding;
return barWidth + padding;
},
renderAggregates: function (aggregates, start, end, surface, ctx, clip, region) {
var me = this,
attr = this.attr,
dataX = attr.dataX,
matrix = attr.matrix,
xx = matrix.getXX(),
yy = matrix.getYY(),
dx = matrix.getDX(),
dy = matrix.getDY(),
barWidth = attr.barWidth / xx,
template,
ohlcType = attr.ohlcType,
halfWidth = Math.round(barWidth * 0.5 * xx),
opens = aggregates.open,
highs = aggregates.high,
lows = aggregates.low,
closes = aggregates.close,
maxYs = aggregates.maxY,
minYs = aggregates.minY,
startIdxs = aggregates.startIdx,
open, high, low, close, mid,
i,
pixelAdjust = attr.lineWidth * surface.devicePixelRatio / 2;
pixelAdjust -= Math.floor(pixelAdjust);
ctx.save();
template = this.raiseTemplate;
template.useAttributes(ctx);
ctx.beginPath();
for (i = start; i < end; i++) {
if (opens[i] <= closes[i]) {
open = Math.round(opens[i] * yy + dy) + pixelAdjust;
high = Math.round(maxYs[i] * yy + dy) + pixelAdjust;
low = Math.round(minYs[i] * yy + dy) + pixelAdjust;
close = Math.round(closes[i] * yy + dy) + pixelAdjust;
mid = Math.round(dataX[startIdxs[i]] * xx + dx) + pixelAdjust;
me[ohlcType](ctx, open, high, low, close, mid, halfWidth);
}
}
ctx.fillStroke(template.attr);
ctx.restore();
ctx.save();
template = this.dropTemplate;
template.useAttributes(ctx);
ctx.beginPath();
for (i = start; i < end; i++) {
if (opens[i] > closes[i]) {
open = Math.round(opens[i] * yy + dy) + pixelAdjust;
high = Math.round(maxYs[i] * yy + dy) + pixelAdjust;
low = Math.round(minYs[i] * yy + dy) + pixelAdjust;
close = Math.round(closes[i] * yy + dy) + pixelAdjust;
mid = Math.round(dataX[startIdxs[i]] * xx + dx) + pixelAdjust;
me[ohlcType](ctx, open, high, low, close, mid, halfWidth);
}
}
ctx.fillStroke(template.attr);
ctx.restore();
}
});
/**
* @class Ext.chart.series.CandleStick
* @extends Ext.chart.series.Cartesian
*
* Creates a candlestick or OHLC Chart.
*
* @example preview
* var chart = new Ext.chart.CartesianChart({
* animate: true,
* store: {
* fields: ['time', 'open', 'high', 'low', 'close'],
* data: [
* {'time':new Date('Jan 1 2010').getTime(), 'open':600, 'high':614, 'low':578, 'close':590},
* {'time':new Date('Jan 2 2010').getTime(), 'open':590, 'high':609, 'low':580, 'close':580},
* {'time':new Date('Jan 3 2010').getTime(), 'open':580, 'high':602, 'low':578, 'close':602},
* {'time':new Date('Jan 4 2010').getTime(), 'open':602, 'high':614, 'low':586, 'close':586},
* {'time':new Date('Jan 5 2010').getTime(), 'open':586, 'high':602, 'low':565, 'close':565}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['open', 'high', 'low', 'close'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 560,
* maximum: 640
* }, {
* type: 'time',
* position: 'bottom',
* fields: ['time'],
* fromDate: new Date('Dec 31 2009'),
* toDate: new Date('Jan 6 2010'),
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* style: {
* axisLine: false
* }
* }],
* series: [{
* type: 'candlestick',
* xField: 'time',
* openField: 'open',
* highField: 'high',
* lowField: 'low',
* closeField: 'close',
* style: {
* dropStyle: {
* fill: 'rgb(237, 123, 43)',
* stroke: 'rgb(237, 123, 43)'
* },
* raiseStyle: {
* fill: 'rgb(55, 153, 19)',
* stroke: 'rgb(55, 153, 19)'
* }
* },
* aggregator: {
* strategy: 'time'
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define("Ext.chart.series.CandleStick", {
extend: "Ext.chart.series.Cartesian",
requires: ['Ext.chart.series.sprite.CandleStick'],
alias: 'series.candlestick',
type: 'candlestick',
seriesType: 'candlestickSeries',
config: {
/**
* @cfg {String} openField
* The store record field name that represents the opening value of the given period.
*/
openField: null,
/**
* @cfg {String} highField
* The store record field name that represents the highest value of the time interval represented.
*/
highField: null,
/**
* @cfg {String} lowField
* The store record field name that represents the lowest value of the time interval represented.
*/
lowField: null,
/**
* @cfg {String} closeField
* The store record field name that represents the closing value of the given period.
*/
closeField: null
},
fieldCategoryY: ['Open', 'High', 'Low', 'Close']
});
/**
* @class Ext.chart.series.Gauge
* @extends Ext.chart.series.Series
*
* Creates a Gauge Chart.
*
* @example preview
* var chart = new Ext.chart.SpaceFillingChart({
* series: [{
* type: 'gauge',
* minimum: 100,
* maximum: 800,
* value: 400,
* donut: 30,
* subStyle: {
* fillStyle: ["#115fa6", "lightgrey"]
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define('Ext.chart.series.Gauge', {
alias: 'series.gauge',
extend: 'Ext.chart.series.Series',
type: "gauge",
seriesType: 'sector',
requires: [
'Ext.draw.sprite.Sector'
],
config: {
/**
* @cfg {String} angleField
* @deprecated Use field directly
* The store record field name to be used for the gauge angles.
* The values bound to this field name must be positive real numbers.
*/
angleField: null,
/**
* @cfg {String} field
* The store record field name to be used for the gauge angles.
* The values bound to this field name must be positive real numbers.
*/
field: null,
/**
* @cfg {Boolean} needle
* Use the Gauge Series as an area series or add a needle to it.
*/
needle: false,
/**
* @cfg {Number} needleLengthRatio
* The length ratio between the length of needle and the radius of background section.
*/
needleLengthRatio: 0.8,
/**
* @cfg {Boolean/Number} donut
* Use the entire disk or just a fraction of it for the gauge.
*/
donut: 30,
/**
* @cfg {Boolean} showInLegend
* Whether to add the gauge chart elements as legend items.
*/
showInLegend: false,
/**
* @cfg {Number} value
* Directly sets the displayed value of the gauge.
*/
value: null,
/**
* @cfg {Number} minimum
* The minimum value of the gauge.
*/
minimum: 0,
/**
* @cfg {Number} maximum
* The maximum value of the gauge.
*/
maximum: 100,
rotation: 0,
totalAngle: Math.PI / 2,
region: [0, 0, 1, 1],
center: [0.5, 0.75],
radius: 0.5,
/**
* @cfg {Boolean} wholeDisk Indicates whether to show the whole disk or only the marked part.
*/
wholeDisk: false
},
updateAngleField: function (angleField) {
this.setField(angleField);
},
updateRegion: function (region) {
var wholeDisk = this.getWholeDisk(),
halfTotalAngle = wholeDisk ? Math.PI : this.getTotalAngle() / 2,
donut = this.getDonut() / 100,
width, height, radius;
if (halfTotalAngle <= Math.PI / 2) {
width = 2 * Math.sin(halfTotalAngle);
height = 1 - donut * Math.cos(halfTotalAngle);
} else {
width = 2;
height = 1 - Math.cos(halfTotalAngle);
}
radius = Math.min(region[2] / width, region[3] / height);
this.setRadius(radius);
this.setCenter([region[2] / 2, radius + (region[3] - height * radius) / 2]);
},
updateCenter: function (center) {
this.setStyle({
centerX: center[0],
centerY: center[1],
rotationCenterX: center[0],
rotationCenterY: center[1]
});
this.doUpdateStyles();
},
updateRotation: function (rotation) {
this.setStyle({
rotationRads: rotation - (this.getTotalAngle() + Math.PI) / 2
});
this.doUpdateStyles();
},
updateRadius: function (radius) {
var donut = this.getDonut(),
needle = this.getNeedle(),
needleLengthRatio = needle ? this.getNeedleLengthRatio() : 1;
this.setSubStyle({
endRho: [radius * needleLengthRatio, radius],
startRho: radius / 100 * donut
});
this.doUpdateStyles();
},
updateDonut: function (donut) {
var radius = this.getRadius(),
needle = this.getNeedle(),
needleLengthRatio = needle ? this.getNeedleLengthRatio() : 1;
this.setSubStyle({
endRho: [radius * needleLengthRatio, radius],
startRho: radius / 100 * donut
});
this.doUpdateStyles();
},
applyValue: function (value) {
return Math.min(this.getMaximum(), Math.max(value, this.getMinimum()));
},
updateValue: function (value) {
var needle = this.getNeedle(),
pos = (value - this.getMinimum()) / (this.getMaximum() - this.getMinimum()),
total = this.getTotalAngle(),
angle = pos * total,
sprites = this.getSprites();
if (needle) {
sprites[0].setAttributes({
startAngle: angle,
endAngle: angle
});
} else {
sprites[0].setAttributes({
endAngle: angle
});
}
this.doUpdateStyles();
},
processData: function () {
var store = this.getStore();
if (!store) {
return;
}
var field = this.getField();
if (!field) {
return;
}
if (!store.getData().items.length) {
return;
}
this.setValue(store.getData().items[0].get(field));
},
getDefaultSpriteConfig: function () {
return {
type: 'sector',
fx: {
customDuration: {
translationX: 0,
translationY: 0,
rotationCenterX: 0,
rotationCenterY: 0,
centerX: 0,
centerY: 0,
startRho: 0,
endRho: 0,
baseRotation: 0
}
}
};
},
getSprites: function () {
//initialize store
if(!this.getStore() && !Ext.isNumber(this.getValue())) {
return null;
}
var me = this,
sprite,
animate = this.getChart().getAnimate(),
sprites = me.sprites;
if (sprites && sprites.length) {
sprites[0].fx.setConfig(animate);
return sprites;
}
// The needle
sprite = me.createSprite();
sprite.setAttributes({
zIndex: 10
});
// The background
sprite = me.createSprite();
sprite.setAttributes({
startAngle: 0,
endAngle: me.getTotalAngle()
});
me.doUpdateStyles();
return sprites;
}
});
/**
* @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);
}
});
/**
* @private
*/
Ext.define('Ext.chart.series.ItemPublisher', {
extend: 'Ext.event.publisher.Publisher',
targetType: 'series',
handledEvents: [
/**
* @event itemmousemove
* Fires when the mouse is moved on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemmousemove',
/**
* @event itemmouseup
* Fires when a mouseup event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemmouseup',
/**
* @event itemmousedown
* Fires when a mousedown event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemmousedown',
/**
* @event itemmouseover
* Fires when the mouse enters a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemmouseover',
/**
* @event itemmouseout
* Fires when the mouse exits a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemmouseout',
/**
* @event itemclick
* Fires when a click event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemclick',
/**
* @event itemdoubleclick
* Fires when a doubleclick event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemdoubleclick',
/**
* @event itemtap
* Fires when a tap event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemtap',
/**
* @event itemtapstart
* Fires when a tapstart event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemtapstart',
/**
* @event itemtapend
* Fires when a tapend event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemtapend',
/**
* @event itemtapcancel
* Fires when a tapcancel event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemtapcancel',
/**
* @event itemtaphold
* Fires when a taphold event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemtaphold',
/**
* @event itemdoubletap
* Fires when a doubletap event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemdoubletap',
/**
* @event itemsingletap
* Fires when a singletap event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemsingletap',
/**
* @event itemtouchstart
* Fires when a touchstart event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemtouchstart',
/**
* @event itemtouchmove
* Fires when a touchmove event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemtouchmove',
/**
* @event itemtouchend
* Fires when a touchend event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemtouchend',
/**
* @event itemdragstart
* Fires when a dragstart event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemdragstart',
/**
* @event itemdrag
* Fires when a drag event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemdrag',
/**
* @event itemdragend
* Fires when a dragend event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemdragend',
/**
* @event itempinchstart
* Fires when a pinchstart event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itempinchstart',
/**
* @event itempinch
* Fires when a pinch event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itempinch',
/**
* @event itempinchend
* Fires when a pinchend event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itempinchend',
/**
* @event itemswipe
* Fires when a swipe event occurs on a series item.
* @param {Ext.chart.series.Series} series
* @param {Object} item
* @param {Event} event
*/
'itemswipe'
],
delegationRegex: /^item([a-z]+)$/i,
getSubscribers: function (chartId) {
var subscribers = this.subscribers;
if (!subscribers.hasOwnProperty(chartId)) {
subscribers[chartId] = {};
}
return subscribers[chartId];
},
subscribe: function (target, eventName) {
var match = target.match(this.idSelectorRegex),
dispatcher = this.dispatcher,
targetType = this.targetType,
subscribers, series, id;
if (!match) {
return false;
}
id = match[1];
series = Ext.ComponentManager.get(id);
if (!series) {
return false;
}
if (!series.getChart()) {
dispatcher.addListener(targetType, target, 'chartattached', 'attachChart', this, [series, eventName], 'before');
} else {
this.attachChart(series.getChart(), [series, eventName]);
}
return true;
},
unsubscribe: function (target, eventName, all) {
var match = target.match(this.idSelectorRegex),
dispatcher = this.dispatcher,
targetType = this.targetType,
subscribers, series, id;
if (!match) {
return false;
}
id = match[1];
series = Ext.ComponentManager.get(id);
if (!series) {
return false;
}
subscribers = this.getSubscribers(target, false);
if (!subscribers) {
return false;
}
subscribers.$length--;
if (subscribers.hasOwnProperty(eventName)) {
subscribers[eventName]--;
if (series.getChart()) {
this.detachChart(series.getChart(), [series, eventName, subscribers]);
}
}
return true;
},
relayMethod: function (e, sender, args) {
var chart = args[0],
eventName = args[1],
dispatcher = this.dispatcher,
targetType = this.targetType,
chartXY = chart.getEventXY(e),
x = chartXY[0],
y = chartXY[1],
subscriber = this.getSubscribers(chart.getId())[eventName],
i, ln;
if (subscriber) {
for (i = 0, ln = subscriber.length; i < ln; i++) {
var series = subscriber[i],
item = series.getItemForPoint(x, y);
if (item) {
dispatcher.doDispatchEvent(targetType, '#' + series.getId(), eventName, [series, item, e]);
return;
}
}
}
},
detachChart: function (chart, args) {
var dispatcher = this.dispatcher,
targetType = this.targetType,
series = args[0],
eventName = args[1],
subscribers = args[2],
match = eventName.match(this.delegationRegex);
if (match) {
var chartEventName = match[1];
if (subscribers.hasOwnProperty(eventName)) {
Ext.remove(subscribers[eventName], series);
if (subscribers[eventName].length === 0) {
chart.element.un(chartEventName, "relayMethod", this, [chart, series, eventName]);
}
}
dispatcher.removeListener(targetType, '#' + series.getId(), 'chartdetached', 'detachChart', this, [series, eventName, subscribers], 'after');
}
},
attachChart: function (chart, args) {
var dispatcher = this.dispatcher,
targetType = this.targetType,
series = args[0],
eventName = args[1],
subscribers = this.getSubscribers(chart.getId()),
match = eventName.match(this.delegationRegex);
if (match) {
var chartEventName = match[1];
if (!subscribers.hasOwnProperty(eventName)) {
subscribers[eventName] = [];
dispatcher.addListener(targetType, '#' + series.getId(), 'chartdetached', 'detachChart', this, [series, eventName, subscribers], 'after');
chart.element.on(chartEventName, "relayMethod", this, [chart, eventName]);
}
subscribers[eventName].push(series);
return true;
} else {
return false;
}
}
}, function () {
});
/**
* @class Ext.chart.series.sprite.Line
* @extends Ext.chart.series.sprite.Aggregative
*
* Line series sprite.
*/
Ext.define("Ext.chart.series.sprite.Line", {
alias: 'sprite.lineSeries',
extend: 'Ext.chart.series.sprite.Aggregative',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Boolean} [smooth=false] 'true' if the sprite uses line smoothing.
*/
smooth: 'bool',
/**
* @cfg {Boolean} [step=false] 'true' if the line uses step.
*/
step: 'bool',
/**
* @cfg {Boolean} [preciseStroke=false] 'true' if the line uses precise stroke.
*/
preciseStroke: 'bool'
},
defaults: {
smooth: false,
step: false,
preciseStroke: false
},
dirtyTriggers: {
dataX: 'dataX,bbox,smooth',
dataY: 'dataY,bbox,smooth',
smooth: 'smooth'
},
updaters: {
"smooth": function (attr) {
if (attr.smooth && attr.dataX && attr.dataY) {
this.smoothX = Ext.draw.Draw.spline(attr.dataX);
this.smoothY = Ext.draw.Draw.spline(attr.dataY);
} else {
delete this.smoothX;
delete this.smoothY;
}
}
}
}
},
list: null,
updatePlainBBox: function (plain) {
var attr = this.attr,
ymin = Math.min(0, attr.dataMinY),
ymax = Math.max(0, attr.dataMaxY);
plain.x = attr.dataMinX;
plain.y = ymin;
plain.width = attr.dataMaxX - attr.dataMinX;
plain.height = ymax - ymin;
},
drawStroke: function (surface, ctx, list) {
var attr = this.attr,
matrix = attr.matrix,
xx = matrix.getXX(),
yy = matrix.getYY(),
dx = matrix.getDX(),
dy = matrix.getDY(),
smooth = attr.smooth,
step = attr.step,
start = list[2],
smoothX = this.smoothX,
smoothY = this.smoothY,
i, j;
ctx.beginPath();
if (smooth) {
ctx.moveTo(smoothX[start * 3] * xx + dx, smoothY[start * 3] * yy + dy);
for (i = 0, j = start * 3 + 1; i < list.length - 3; i += 3, j += 3) {
ctx.bezierCurveTo(
smoothX[j] * xx + dx, smoothY[j] * yy + dy,
smoothX[j + 1] * xx + dx, smoothY[j + 1] * yy + dy,
list[i + 3], list[i + 4]
);
}
} else if (step) {
ctx.moveTo(list[0], list[1]);
for (i = 3; i < list.length; i += 3) {
ctx.lineTo(list[i], list[i - 2]);
ctx.lineTo(list[i], list[i + 1]);
}
} else {
ctx.moveTo(list[0], list[1]);
for (i = 3; i < list.length; i += 3) {
ctx.lineTo(list[i], list[i + 1]);
}
}
},
renderAggregates: function (aggregates, start, end, surface, ctx, clip, region) {
var me = this,
attr = me.attr,
dataX = attr.dataX,
matrix = attr.matrix,
first = true,
dataY = attr.dataY,
pixel = surface.devicePixelRatio,
xx = matrix.getXX(),
yy = matrix.getYY(),
dx = matrix.getDX(),
dy = matrix.getDY(),
markerCfg = {},
list = this.list || (this.list = []),
x, y,
minXs = aggregates.minX,
maxXs = aggregates.maxX,
minYs = aggregates.minY,
maxYs = aggregates.maxY,
idx = aggregates.startIdx,
surfaceMatrix = surface.matrix;
list.length = 0;
for (var i = start; i < end; i++) {
var minX = minXs[i],
maxX = maxXs[i],
minY = minYs[i],
maxY = maxYs[i];
if (minX < maxX) {
list.push(minX * xx + dx, minY * yy + dy, idx[i]);
list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
} else if (minX > maxX) {
list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
list.push(minX * xx + dx, minY * yy + dy, idx[i]);
} else {
list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
}
first = false;
}
if (list.length) {
for (i = 0; i < list.length; i += 3) {
x = list[i];
y = list[i + 1];
if (attr.renderer) {
attr.renderer.call(this, markerCfg, this, i, this.getDataItems().items[i]);
}
markerCfg.translationX = surfaceMatrix.x(x, y);
markerCfg.translationY = surfaceMatrix.y(x, y);
me.putMarker("markers", markerCfg, list[i + 2], !attr.renderer);
}
me.drawStroke(surface, ctx, list);
ctx.lineTo(dataX[dataX.length - 1] * xx + dx + pixel, dataY[dataY.length - 1] * yy + dy);
ctx.lineTo(dataX[dataX.length - 1] * xx + dx + pixel, region[1] - pixel);
ctx.lineTo(dataX[0] * xx + dx - pixel, region[1] - pixel);
ctx.lineTo(dataX[0] * xx + dx - pixel, dataY[0] * yy + dy);
ctx.closePath();
if (attr.transformFillStroke) {
attr.matrix.toContext(ctx);
}
if (attr.preciseStroke) {
ctx.fill();
if (attr.transformFillStroke) {
attr.inverseMatrix.toContext(ctx);
}
me.drawStroke(surface, ctx, list);
if (attr.transformFillStroke) {
attr.matrix.toContext(ctx);
}
ctx.stroke();
} else {
// Prevent the reverse transform to fix floating point err.
ctx.fillStroke(attr, true);
}
}
}
});
/**
* @class Ext.chart.series.Line
* @extends Ext.chart.series.Cartesian
*
* Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different
* categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset.
* As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart
* documentation for more information. A typical configuration object for the line series could be:
*
* @example preview
* var lineChart = new Ext.chart.CartesianChart({
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data1'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 0
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'line',
* highlight: {
* size: 7,
* radius: 7
* },
* style: {
* stroke: 'rgb(143,203,203)'
* },
* xField: 'name',
* yField: 'data1',
* marker: {
* type: 'path',
* path: ['M', -2, 0, 0, 2, 2, 0, 0, -2, 'Z'],
* stroke: 'blue',
* lineWidth: 0
* }
* }, {
* type: 'line',
* highlight: {
* size: 7,
* radius: 7
* },
* fill: true,
* xField: 'name',
* yField: 'data3',
* marker: {
* type: 'circle',
* radius: 4,
* lineWidth: 0
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(lineChart);
*
* In this configuration we're adding two series (or lines), one bound to the `data1`
* property of the store and the other to `data3`. The type for both configurations is
* `line`. The `xField` for both series is the same, the `name` property of the store.
* Both line series share the same axis, the left axis. You can set particular marker
* configuration by adding properties onto the markerConfig object. Both series have
* an object as highlight so that markers animate smoothly to the properties in highlight
* when hovered. The second series has `fill = true` which means that the line will also
* have an area below it of the same color.
*
* **Note:** In the series definition remember to explicitly set the axis to bind the
* values of the line series to. This can be done by using the `axis` configuration property.
*/
Ext.define('Ext.chart.series.Line', {
extend: 'Ext.chart.series.Cartesian',
alias: 'series.line',
type: 'line',
seriesType: 'lineSeries',
requires: [
'Ext.chart.series.sprite.Line'
],
config: {
/**
* @cfg {Number} selectionTolerance
* The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc).
*/
selectionTolerance: 20,
/**
* @cfg {Object} style
* An object containing styles for the visualization lines. These styles will override the theme styles.
* Some options contained within the style object will are described next.
*/
/**
* @cfg {Boolean/Number} smooth
* If set to `true` or a non-zero number, the line will be smoothed/rounded around its points; otherwise
* straight line segments will be drawn.
*
* A numeric value is interpreted as a divisor of the horizontal distance between consecutive points in
* the line; larger numbers result in sharper curves while smaller numbers result in smoother curves.
*
* If set to `true` then a default numeric value of 3 will be used.
*/
smooth: false,
aggregator: { strategy: 'double' }
},
/**
* @private Default numeric smoothing value to be used when `{@link #smooth} = true`.
*/
defaultSmoothness: 3,
/**
* @private Size of the buffer area on either side of the viewport to provide seamless zoom/pan
* transforms. Expressed as a multiple of the viewport length, e.g. 1 will make the buffer on
* each side equal to the length of the visible axis viewport.
*/
overflowBuffer: 1
});
/**
* Polar series.
*/
Ext.define('Ext.chart.series.Polar', {
extend: 'Ext.chart.series.Series',
config: {
/**
* @cfg {Number} rotation
* The angle in degrees at which the first polar series item should start.
*/
rotation: 0,
/**
* @cfg {Number} radius
* The radius of the polar series. Set to `null` will fit the polar series to the boundary.
*/
radius: null,
/**
* @cfg {Array} center for the polar series.
*/
center: [0, 0],
/**
* @cfg {Number} offsetX
* The x-offset of center of the polar series related to the center of the boundary.
*/
offsetX: 0,
/**
* @cfg {Number} offsetY
* The y-offset of center of the polar series related to the center of the boundary.
*/
offsetY: 0,
/**
* @cfg {Boolean} showInLegend
* Whether to add the series elements as legend items.
*/
showInLegend: true,
/**
* @cfg {String} xField
* The store record field name for the labels used in the radar series.
*/
xField: null,
/**
* @cfg {String} yField
* The store record field name for the deflection of the graph in the radar series.
*/
yField: null,
xAxis: null,
yAxis: null
},
directions: ['X', 'Y'],
fieldCategoryX: ['X'],
fieldCategoryY: ['Y'],
getDefaultSpriteConfig: function () {
return {
type: this.seriesType,
centerX: 0,
centerY: 0,
rotationCenterX: 0,
rotationCenterY: 0,
fx: {
customDuration: {
translationX: 0,
translationY: 0,
centerX: 0,
centerY: 0,
startRho: 0,
endRho: 0,
baseRotation: 0,
rotationCenterX: 0,
rotationCenterY: 0,
rotationRads: 0
}
}
};
},
applyRotation: function (rotation) {
var twoPie = Math.PI * 2;
return (rotation % twoPie + Math.PI) % twoPie - Math.PI;
},
updateRotation: function (rotation) {
var sprites = this.getSprites();
if (sprites && sprites[0]) {
sprites[0].setAttributes({
baseRotation: rotation
});
}
}
});
/**
* @class Ext.chart.series.sprite.PieSlice
*
* Pie slice sprite.
*/
Ext.define("Ext.chart.series.sprite.PieSlice", {
alias: 'sprite.pieslice',
mixins: {
markerHolder: "Ext.chart.MarkerHolder"
},
extend: "Ext.draw.sprite.Sector",
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Boolean} [doCallout=true] 'true' if the pie series uses label callouts.
*/
doCallout: 'bool',
/**
* @cfg {String} [label=''] Label associated with the Pie sprite.
*/
label: 'string',
/**
* @cfg {Number} [labelOverflowPadding=10] Padding around labels to determine overlap.
*/
labelOverflowPadding: 'number'
},
defaults: {
doCallout: true,
label: '',
labelOverflowPadding: 10
}
}
},
render: function (ctx, surface, clipRegion) {
this.callSuper(arguments);
if (this.attr.label && this.getBoundMarker('labels')) {
this.placeLabel();
}
},
placeLabel: function () {
var attr = this.attr,
startAngle = Math.min(attr.startAngle, attr.endAngle),
endAngle = Math.max(attr.startAngle, attr.endAngle),
midAngle = (startAngle + endAngle) * 0.5,
margin = attr.margin,
centerX = attr.centerX,
centerY = attr.centerY,
startRho = Math.min(attr.startRho, attr.endRho) + margin,
endRho = Math.max(attr.startRho, attr.endRho) + margin,
midRho = (startRho + endRho) * 0.5,
surfaceMatrix = this.surfaceMatrix,
labelCfg = this.labelCfg || (this.labelCfg = {}),
labelBox, x, y;
surfaceMatrix.appendMatrix(attr.matrix);
x = centerX + Math.cos(midAngle) * midRho;
y = centerY + Math.sin(midAngle) * midRho;
labelCfg.x = surfaceMatrix.x(x, y);
labelCfg.y = surfaceMatrix.y(x, y);
x = centerX + Math.cos(midAngle) * endRho;
y = centerY + Math.sin(midAngle) * endRho;
labelCfg.calloutStartX = surfaceMatrix.x(x, y);
labelCfg.calloutStartY = surfaceMatrix.y(x, y);
x = centerX + Math.cos(midAngle) * (endRho + 40);
y = centerY + Math.sin(midAngle) * (endRho + 40);
labelCfg.calloutPlaceX = surfaceMatrix.x(x, y);
labelCfg.calloutPlaceY = surfaceMatrix.y(x, y);
labelCfg.rotationRads = midAngle + Math.atan2(surfaceMatrix.y(1, 0) - surfaceMatrix.y(0, 0), surfaceMatrix.x(1, 0) - surfaceMatrix.x(0, 0));
labelCfg.text = attr.label;
labelCfg.calloutColor = this.attr.fillStyle;
labelCfg.globalAlpha = attr.globalAlpha * attr.fillOpacity;
this.putMarker('labels', labelCfg, this.attr.attributeId);
labelBox = this.getMarkerBBox('labels', this.attr.attributeId, true);
if (labelBox) {
if (attr.doCallout) {
this.putMarker('labels', {callout: 1 - +this.sliceContainsLabel(attr, labelBox)}, this.attr.attributeId);
} else {
this.putMarker('labels', {globalAlpha: +this.sliceContainsLabel(attr, labelBox)}, this.attr.attributeId);
}
}
},
sliceContainsLabel: function (attr, bbox) {
var padding = attr.labelOverflowPadding,
middle = (attr.endRho + attr.startRho) / 2,
outer = middle + (bbox.width + padding) / 2,
inner = middle - (bbox.width + padding) / 2,
l1, l2, l3;
if (bbox.width + padding * 2 > (attr.endRho - attr.startRho)) {
return 0;
}
l1 = Math.sqrt(attr.endRho * attr.endRho - outer * outer);
l2 = Math.sqrt(attr.endRho * attr.endRho - inner * inner);
l3 = Math.abs(Math.tan(Math.abs(attr.endAngle - attr.startAngle) / 2)) * inner;
if (bbox.height + padding * 2 > Math.min(l1, l2, l3) * 2) {
return 0;
}
return 1;
}
});
/**
* @class Ext.chart.series.Pie
* @extends Ext.chart.series.Polar
*
* Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different
* categories that also have a meaning as a whole.
* As with all other series, the Pie Series must be appended in the *series* Chart array configuration. See the Chart
* documentation for more information. A typical configuration object for the pie series could be:
*
* @example preview
* var chart = new Ext.chart.PolarChart({
* animate: true,
* interactions: ['rotate'],
* colors: ["#115fa6", "#94ae0a", "#a61120", "#ff8809", "#ffd13e"],
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* series: [{
* type: 'pie',
* labelField: 'name',
* xField: 'data3',
* donut: 30
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*
* In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options
* (triggered when hovering elements). We also set true to `showInLegend` so all the pie slices can be represented by a legend item.
* We set `data1` as the value of the field to determine the angle span for each pie slice. We also set a label configuration object
* where we set the field name of the store field to be renderer as text for the label. The labels will also be displayed rotated.
* We set `contrast` to `true` to flip the color of the label if it is to similar to the background color. Finally, we set the font family
* and size through the `font` parameter.
*
*/
Ext.define('Ext.chart.series.Pie', {
extend: 'Ext.chart.series.Polar',
requires: [
"Ext.chart.series.sprite.PieSlice"
],
type: 'pie',
alias: 'series.pie',
seriesType: 'pieslice',
config: {
/**
* @cfg {String} labelField
* The store record field name to be used for the pie slice labels.
*/
labelField: false,
/**
* @cfg {Boolean/Number} donut Whether to set the pie chart as donut chart.
* Can be set to a particular percentage to set the radius of the donut chart.
*/
donut: false,
/**
* @cfg {String} field
* @deprecated Use xField directly
*/
field: null,
/**
* @cfg {Number} rotation The starting angle of the pie slices.
*/
rotation: 0,
/**
* @cfg {Number} [totalAngle=2*PI] The total angle of the pie series.
*/
totalAngle: Math.PI * 2,
/**
* @cfg {Array} hidden Determines which pie slices are hidden.
*/
hidden: [],
style: {
}
},
directions: ['X'],
setField: function (f) {
return this.setXField(f);
},
getField: function () {
return this.getXField();
},
updateLabelData: function () {
var me = this,
store = me.getStore(),
items = store.getData().items,
sprites = me.getSprites(),
labelField = me.getLabelField(),
i, ln, labels;
if (sprites.length > 0 && labelField) {
labels = [];
for (i = 0, ln = items.length; i < ln; i++) {
labels.push(items[i].get(labelField));
}
for (i = 0, ln = sprites.length; i < ln; i++) {
sprites[i].setAttributes({label: labels[i]});
}
}
},
coordinateX: function () {
var me = this,
store = me.getStore(),
items = store.getData().items,
length = items.length,
field = me.getXField(),
value, sum = 0,
hidden = me.getHidden(),
summation = [], i,
lastAngle = 0,
totalAngle = me.getTotalAngle(),
sprites = me.getSprites();
if (!sprites) {
return;
}
for (i = 0; i < length; i++) {
value = items[i].get(field);
if (!hidden[i]) {
sum += value;
}
summation[i] = sum;
if (i >= hidden.length) {
hidden[i] = false;
}
}
if (sum === 0) {
return;
}
sum = totalAngle / sum;
for (i = 0; i < length; i++) {
sprites[i].setAttributes({
startAngle: lastAngle,
endAngle: lastAngle = summation[i] * sum,
globalAlpha: 1
});
}
for (; i < me.sprites.length; i++) {
sprites[i].setAttributes({
startAngle: totalAngle,
endAngle: totalAngle,
globalAlpha: 0
});
}
me.getChart().refreshLegendStore();
},
updateCenter: function (center) {
this.setStyle({
translationX: center[0] + this.getOffsetX(),
translationY: center[1] + this.getOffsetY()
});
this.doUpdateStyles();
},
updateRadius: function (radius) {
this.setStyle({
startRho: radius * this.getDonut() * 0.01, // Percentage
endRho: radius
});
this.doUpdateStyles();
},
updateDonut: function (donut) {
var radius = this.getRadius();
this.setStyle({
startRho: radius * donut * 0.01, // Percentage
endRho: radius
});
this.doUpdateStyles();
},
updateRotation: function (rotation) {
this.setStyle({
rotationRads: rotation
});
this.doUpdateStyles();
},
updateTotalAngle: function (totalAngle) {
this.processData();
},
getSprites: function () {
var me = this,
chart = this.getChart(),
store = me.getStore();
if (!chart || !store) {
return[];
}
me.getColors();
me.getSubStyle();
var items = store.getData().items,
length = items.length,
animation = chart && chart.getAnimate(),
center = me.getCenter(),
offsetX = me.getOffsetX(),
offsetY = me.getOffsetY(),
sprites = me.sprites, sprite,
i, spriteCreated = false;
for (i = 0; i < length; i++) {
sprite = sprites[i];
if (!sprite) {
sprite = me.createSprite();
if (me.getHighlightCfg()) {
sprite.config.highlightCfg = me.getHighlightCfg();
sprite.addModifier('highlight', true);
}
if (me.getLabelField()) {
me.getLabel().getTemplate().setAttributes({
labelOverflowPadding: this.getLabelOverflowPadding()
});
me.getLabel().getTemplate().fx.setCustomDuration({'callout': 200});
sprite.bindMarker('labels', me.getLabel());
}
sprite.setAttributes(this.getStyleByIndex(i));
spriteCreated = true;
}
sprite.fx.setConfig(animation);
}
if (spriteCreated) {
me.doUpdateStyles();
}
return me.sprites;
},
betweenAngle: function (x, a, b) {
b -= a;
x -= a;
x %= Math.PI * 2;
b %= Math.PI * 2;
x += Math.PI * 2;
b += Math.PI * 2;
x %= Math.PI * 2;
b %= Math.PI * 2;
return x < b;
},
getItemForPoint: function (x, y) {
var me = this,
sprites = me.getSprites();
if (sprites) {
var center = me.getCenter(),
offsetX = me.getOffsetX(),
offsetY = me.getOffsetY(),
originalX = x - center[0] + offsetX,
originalY = y - center[1] + offsetY,
store = me.getStore(),
donut = me.getDonut(),
items = store.getData().items,
direction = Math.atan2(originalY, originalX) - me.getRotation(),
donutLimit = Math.sqrt(originalX * originalX + originalY * originalY),
endRadius = me.getRadius(),
startRadius = donut / 100 * endRadius,
i, ln, attr;
for (i = 0, ln = items.length; i < ln; i++) {
// Fortunately, the id of items equals the index of it in instances list.
attr = sprites[i].attr;
if (startRadius + attr.margin <= donutLimit && donutLimit + attr.margin <= endRadius) {
if (this.betweenAngle(direction, attr.startAngle, attr.endAngle)) {
return {
series: this,
sprite: sprites[i],
index: i,
record: items[i],
field: this.getXField()
};
}
}
}
}
},
provideLegendInfo: function (target) {
var store = this.getStore();
if (store) {
var items = store.getData().items,
labelField = this.getLabelField(),
field = this.getField(),
hidden = this.getHidden();
for (var i = 0; i < items.length; i++) {
target.push({
name: labelField ? String(items[i].get(labelField)) : (field && field[i]) || this.getId(),
mark: this.getStyleByIndex(i).fillStyle || this.getStyleByIndex(i).strokeStyle || 'black',
disabled: hidden[i],
series: this.getId(),
index: i
});
}
}
}
});
/**
* @class Ext.chart.series.sprite.Pie3DPart
* @extends Ext.draw.sprite.Path
*
* Pie3D series sprite.
*/
Ext.define("Ext.chart.series.sprite.Pie3DPart", {
extend: 'Ext.draw.sprite.Path',
mixins: {
markerHolder: "Ext.chart.MarkerHolder"
},
alias: 'sprite.pie3dPart',
type: 'pie3dPart',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [centerX=0] The central point of the series on the x-axis.
*/
centerX: "number",
/**
* @cfg {Number} [centerY=0] The central point of the series on the x-axis.
*/
centerY: "number",
/**
* @cfg {Number} [startAngle=0] The starting angle of the polar series.
*/
startAngle: "number",
/**
* @cfg {Number} [endAngle=Math.PI] The ending angle of the polar series.
*/
endAngle: "number",
/**
* @cfg {Number} [startRho=0] The starting radius of the polar series.
*/
startRho: "number",
/**
* @cfg {Number} [endRho=150] The ending radius of the polar series.
*/
endRho: "number",
/**
* @cfg {Number} [margin=0] Margin from the center of the pie. Used for donut.
*/
margin: "number",
/**
* @cfg {Number} [thickness=0] The thickness of the 3D pie part.
*/
thickness: "number",
/**
* @cfg {Number} [thickness=0] The distortion of the 3D pie part.
*/
distortion: "number",
/**
* @cfg {Object} [baseColor='white'] The color of the 3D pie part before adding the 3D effect.
*/
baseColor: "color",
/**
* @cfg {Number} [baseRotation=0] The starting rotation of the polar series.
*/
baseRotation: "number",
/**
* @cfg {String} [part=0] The part of the 3D Pie represented by the sprite.
*/
part: "enums(top,start,end,inner,outer)"
},
aliases: {
rho: 'endRho'
},
dirtyTriggers: {
centerX: "path,bbox",
centerY: "path,bbox",
startAngle: "path,partZIndex",
endAngle: "path,partZIndex",
startRho: "path",
endRho: "path,bbox",
margin: "path,bbox",
thickness: "path",
baseRotation: "path,partZIndex",
baseColor: 'partZIndex,partColor',
part: "path,partZIndex"
},
defaults: {
centerX: 0,
centerY: 0,
startAngle: 0,
endAngle: 0,
startRho: 0,
endRho: 150,
margin: 0,
distortion: 1,
baseRotation: 0,
baseColor: 'white',
part: "top"
},
updaters: {
"partColor": function (attrs) {
var color = Ext.draw.Color.fly(attrs.baseColor),
fillStyle;
switch (attrs.part) {
case 'top':
fillStyle = color.toString();
break;
case 'outer':
fillStyle = Ext.create("Ext.draw.gradient.Linear", {
type: 'linear',
stops: [
{
offset: 0,
color: color.createDarker(0.3).toString()
},
{
offset: 0.3,
color: color.toString()
},
{
offset: 0.8,
color: color.createLighter(0.2).toString()
},
{
offset: 1,
color: color.createDarker(0.4).toString()
}
]
});
break;
case 'start':
fillStyle = color.createDarker(0.3).toString();
break;
case 'end':
fillStyle = color.createDarker(0.3).toString();
break;
case 'inner':
fillStyle = Ext.create("Ext.draw.gradient.Linear", {
type: 'linear',
stops: [
{
offset: 0,
color: color.createDarker(0.4).toString()
},
{
offset: 0.2,
color: color.createLighter(0.2).toString()
},
{
offset: 0.7,
color: color.toString()
},
{
offset: 1,
color: color.createDarker(0.3).toString()
}
]
});
break;
}
attrs.fillStyle = fillStyle;
attrs.canvasAttributes.fillStyle = fillStyle;
},
"partZIndex": function (attrs) {
var rotation = attrs.baseRotation;
switch (attrs.part) {
case 'top':
attrs.zIndex = 5;
break;
case 'outer':
attrs.zIndex = 4;
break;
case 'start':
attrs.zIndex = 1 + Math.sin(attrs.startAngle + rotation);
break;
case 'end':
attrs.zIndex = 1 + Math.sin(attrs.endAngle + rotation);
break;
case 'inner':
attrs.zIndex = 1;
break;
}
attrs.dirtyZIndex = true;
}
}
}
},
updatePlainBBox: function (plain) {
var attr = this.attr,
rho = attr.part === 'inner' ? attr.startRho : attr.endRho;
plain.width = rho * 2;
plain.height = rho * attr.distortion * 2 + attr.thickness;
plain.x = attr.centerX - rho;
plain.y = attr.centerY - rho * attr.distortion;
},
updateTransformedBBox: function (transform) {
return this.updatePlainBBox(transform);
},
updatePath: function (path) {
if (this.attr.endAngle < this.attr.startAngle) {
return;
}
this[this.attr.part + 'Renderer'](path);
},
topRenderer: function (path) {
var attr = this.attr,
margin = attr.margin,
distortion = attr.distortion,
centerX = attr.centerX,
centerY = attr.centerY,
baseRotation = attr.baseRotation,
startAngle = attr.startAngle + baseRotation ,
endAngle = attr.endAngle + baseRotation ,
startRho = attr.startRho,
endRho = attr.endRho,
midAngle,
sinEnd = Math.sin(endAngle),
cosEnd = Math.cos(endAngle);
midAngle = (startAngle + endAngle) * 0.5;
centerX += Math.cos(midAngle) * margin;
centerY += Math.sin(midAngle) * margin * distortion;
path.ellipse(centerX, centerY, startRho, startRho * distortion, 0, startAngle, endAngle, false);
path.lineTo(centerX + cosEnd * endRho, centerY + sinEnd * endRho * distortion);
path.ellipse(centerX, centerY, endRho, endRho * distortion, 0, endAngle, startAngle, true);
path.closePath();
},
startRenderer: function (path) {
var attr = this.attr,
margin = attr.margin,
centerX = attr.centerX,
centerY = attr.centerY,
distortion = attr.distortion,
baseRotation = attr.baseRotation,
startAngle = attr.startAngle + baseRotation ,
endAngle = attr.endAngle + baseRotation,
thickness = attr.thickness,
startRho = attr.startRho,
endRho = attr.endRho,
sinStart = Math.sin(startAngle),
cosStart = Math.cos(startAngle),
midAngle;
if (cosStart < 0) {
midAngle = (startAngle + endAngle) * 0.5;
centerX += Math.cos(midAngle) * margin;
centerY += Math.sin(midAngle) * margin * distortion;
path.moveTo(centerX + cosStart * startRho, centerY + sinStart * startRho * distortion);
path.lineTo(centerX + cosStart * endRho, centerY + sinStart * endRho * distortion);
path.lineTo(centerX + cosStart * endRho, centerY + sinStart * endRho * distortion + thickness);
path.lineTo(centerX + cosStart * startRho, centerY + sinStart * startRho * distortion + thickness);
path.closePath();
}
},
endRenderer: function (path) {
var attr = this.attr,
margin = attr.margin,
centerX = attr.centerX,
centerY = attr.centerY,
distortion = attr.distortion,
baseRotation = attr.baseRotation,
startAngle = attr.startAngle + baseRotation ,
endAngle = attr.endAngle + baseRotation,
thickness = attr.thickness,
startRho = attr.startRho,
endRho = attr.endRho,
sin = Math.sin(endAngle),
cos = Math.cos(endAngle), midAngle;
if (cos > 0) {
midAngle = (startAngle + endAngle) * 0.5;
centerX += Math.cos(midAngle) * margin;
centerY += Math.sin(midAngle) * margin * distortion;
path.moveTo(centerX + cos * startRho, centerY + sin * startRho * distortion);
path.lineTo(centerX + cos * endRho, centerY + sin * endRho * distortion);
path.lineTo(centerX + cos * endRho, centerY + sin * endRho * distortion + thickness);
path.lineTo(centerX + cos * startRho, centerY + sin * startRho * distortion + thickness);
path.closePath();
}
},
innerRenderer: function (path) {
var attr = this.attr,
margin = attr.margin,
centerX = attr.centerX,
centerY = attr.centerY,
distortion = attr.distortion,
baseRotation = attr.baseRotation,
startAngle = attr.startAngle + baseRotation ,
endAngle = attr.endAngle + baseRotation,
thickness = attr.thickness,
startRho = attr.startRho,
sinEnd, cosEnd,
tempStart, tempEnd, midAngle;
midAngle = (startAngle + endAngle) * 0.5;
centerX += Math.cos(midAngle) * margin;
centerY += Math.sin(midAngle) * margin * distortion;
if (startAngle >= Math.PI * 2) {
startAngle -= Math.PI * 2;
endAngle -= Math.PI * 2;
}
if (endAngle > Math.PI && endAngle < Math.PI * 3) {
tempStart = startAngle;
tempEnd = Math.min(endAngle, Math.PI * 2);
sinEnd = Math.sin(tempEnd);
cosEnd = Math.cos(tempEnd);
path.ellipse(centerX, centerY, startRho, startRho * distortion, 0, tempStart, tempEnd, false);
path.lineTo(centerX + cosEnd * startRho, centerY + sinEnd * startRho * distortion + thickness);
path.ellipse(centerX, centerY + thickness, startRho, startRho * distortion, 0, tempEnd, tempStart, true);
path.closePath();
}
if (endAngle > Math.PI * 3) {
tempStart = Math.PI;
tempEnd = endAngle;
sinEnd = Math.sin(tempEnd);
cosEnd = Math.cos(tempEnd);
path.ellipse(centerX, centerY, startRho, startRho * distortion, 0, tempStart, tempEnd, false);
path.lineTo(centerX + cosEnd * startRho, centerY + sinEnd * startRho * distortion + thickness);
path.ellipse(centerX, centerY + thickness, startRho, startRho * distortion, 0, tempEnd, tempStart, true);
path.closePath();
}
},
outerRenderer: function (path) {
var attr = this.attr,
margin = attr.margin,
centerX = attr.centerX,
centerY = attr.centerY,
distortion = attr.distortion,
baseRotation = attr.baseRotation,
startAngle = attr.startAngle + baseRotation ,
endAngle = attr.endAngle + baseRotation,
thickness = attr.thickness,
endRho = attr.endRho,
sinEnd, cosEnd,
tempStart, tempEnd, midAngle;
midAngle = (startAngle + endAngle) * 0.5;
centerX += Math.cos(midAngle) * margin;
centerY += Math.sin(midAngle) * margin * distortion;
if (startAngle >= Math.PI * 2) {
startAngle -= Math.PI * 2;
endAngle -= Math.PI * 2;
}
if (startAngle < Math.PI) {
tempStart = startAngle;
tempEnd = Math.min(endAngle, Math.PI);
sinEnd = Math.sin(tempEnd);
cosEnd = Math.cos(tempEnd);
path.ellipse(centerX, centerY, endRho, endRho * distortion, 0, tempStart, tempEnd, false);
path.lineTo(centerX + cosEnd * endRho, centerY + sinEnd * endRho * distortion + thickness);
path.ellipse(centerX, centerY + thickness, endRho, endRho * distortion, 0, tempEnd, tempStart, true);
path.closePath();
}
if (endAngle > Math.PI * 2) {
tempStart = Math.max(startAngle, Math.PI * 2);
tempEnd = endAngle;
sinEnd = Math.sin(tempEnd);
cosEnd = Math.cos(tempEnd);
path.ellipse(centerX, centerY, endRho, endRho * distortion, 0, tempStart, tempEnd, false);
path.lineTo(centerX + cosEnd * endRho, centerY + sinEnd * endRho * distortion + thickness);
path.ellipse(centerX, centerY + thickness, endRho, endRho * distortion, 0, tempEnd, tempStart, true);
path.closePath();
}
}
});
/**
* @class Ext.chart.series.Pie3D
* @extends Ext.chart.series.sprite.Polar
*
* Creates a 3D Pie Chart.
*
* @example preview
* var chart = new Ext.chart.PolarChart({
* animate: true,
* interactions: ['rotate'],
* colors: ["#115fa6", "#94ae0a", "#a61120", "#ff8809", "#ffd13e"],
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* series: [{
* type: 'pie3d',
* field: 'data3',
* donut: 30
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define('Ext.chart.series.Pie3D', {
requires: ['Ext.chart.series.sprite.Pie3DPart'],
extend: 'Ext.chart.series.Polar',
type: 'pie3d',
seriesType: 'pie3d',
alias: 'series.pie3d',
config: {
region: [0, 0, 0, 0],
thickness: 35,
distortion: 0.5,
/**
* @cfg {String} field (required)
* The store record field name to be used for the pie angles.
* The values bound to this field name must be positive real numbers.
*/
field: false,
/**
* @private
* @cfg {String} lengthField
* Not supported.
*/
lengthField: false,
/**
* @cfg {Boolean/Number} donut
* Whether to set the pie chart as donut chart.
* Can be set to a particular percentage to set the radius
* of the donut chart.
*/
donut: false,
rotation: 0
},
applyRotation: function (rotation) {
var twoPie = Math.PI * 2;
return (rotation % twoPie + twoPie) % twoPie;
},
updateRotation: function (rotation) {
var sprites = this.getSprites(),
i, ln;
for (i = 0, ln = sprites.length; i < ln; i++) {
sprites[i].setAttributes({
baseRotation: rotation
});
}
},
updateColors: function (colorSet) {
this.setSubStyle({baseColor: colorSet});
},
doUpdateStyles: function () {
var sprites = this.getSprites(),
i = 0, j = 0, ln = sprites && sprites.length;
for (; i < ln; i += 5, j++) {
sprites[i].setAttributes(this.getStyleByIndex(j));
sprites[i + 1].setAttributes(this.getStyleByIndex(j));
sprites[i + 2].setAttributes(this.getStyleByIndex(j));
sprites[i + 3].setAttributes(this.getStyleByIndex(j));
sprites[i + 4].setAttributes(this.getStyleByIndex(j));
}
},
processData: function () {
var me = this,
chart = me.getChart(),
animation = chart && chart.getAnimate(),
store = me.getStore(),
items = store.getData().items,
length = items.length,
field = me.getField(),
value, sum = 0, ratio,
summation = [],
i,
sprites = this.getSprites(),
lastAngle;
for (i = 0; i < length; i++) {
value = items[i].get(field);
sum += value;
summation[i] = sum;
}
if (sum === 0) {
return;
}
ratio = 2 * Math.PI / sum;
for (i = 0; i < length; i++) {
summation[i] *= ratio;
}
for (i = 0; i < sprites.length; i++) {
sprites[i].fx.setConfig(animation);
}
for (i = 0, lastAngle = 0; i < length; i++) {
var commonAttributes = {opacity: 1, startAngle: lastAngle, endAngle: summation[i]};
sprites[i * 5].setAttributes(commonAttributes);
sprites[i * 5 + 1].setAttributes(commonAttributes);
sprites[i * 5 + 2].setAttributes(commonAttributes);
sprites[i * 5 + 3].setAttributes(commonAttributes);
sprites[i * 5 + 4].setAttributes(commonAttributes);
lastAngle = summation[i];
}
},
getSprites: function () {
var me = this,
chart = this.getChart(),
surface = me.getSurface(),
store = me.getStore();
if (!store) {
return [];
}
var items = store.getData().items,
length = items.length,
animation = chart && chart.getAnimate(),
region = chart.getMainRegion() || [0, 0, 1, 1],
rotation = me.getRotation(),
center = me.getCenter(),
offsetX = me.getOffsetX(),
offsetY = me.getOffsetY(),
radius = Math.min((region[3] - me.getThickness() * 2) / me.getDistortion(), region[2]) / 2,
commonAttributes = {
centerX: center[0] + offsetX,
centerY: center[1] + offsetY - me.getThickness() / 2,
endRho: radius,
startRho: radius * me.getDonut() / 100,
thickness: me.getThickness(),
distortion: me.getDistortion()
}, sliceAttributes, twoPie = Math.PI * 2,
topSprite, startSprite, endSprite, innerSideSprite, outerSideSprite,
i;
for (i = 0; i < length; i++) {
sliceAttributes = Ext.apply({}, this.getStyleByIndex(i), commonAttributes);
topSprite = me.sprites[i * 5];
if (!topSprite) {
topSprite = surface.add({
type: 'pie3dPart',
part: 'top',
startAngle: twoPie,
endAngle: twoPie
});
startSprite = surface.add({
type: 'pie3dPart',
part: 'start',
startAngle: twoPie,
endAngle: twoPie
});
endSprite = surface.add({
type: 'pie3dPart',
part: 'end',
startAngle: twoPie,
endAngle: twoPie
});
innerSideSprite = surface.add({
type: 'pie3dPart',
part: 'inner',
startAngle: twoPie,
endAngle: twoPie,
thickness: 0
});
outerSideSprite = surface.add({
type: 'pie3dPart',
part: 'outer',
startAngle: twoPie,
endAngle: twoPie,
thickness: 0
});
topSprite.fx.setDurationOn('baseRotation', 0);
startSprite.fx.setDurationOn('baseRotation', 0);
endSprite.fx.setDurationOn('baseRotation', 0);
innerSideSprite.fx.setDurationOn('baseRotation', 0);
outerSideSprite.fx.setDurationOn('baseRotation', 0);
topSprite.setAttributes(sliceAttributes);
startSprite.setAttributes(sliceAttributes);
endSprite.setAttributes(sliceAttributes);
innerSideSprite.setAttributes(sliceAttributes);
outerSideSprite.setAttributes(sliceAttributes);
me.sprites.push(topSprite, startSprite, endSprite, innerSideSprite, outerSideSprite);
} else {
startSprite = me.sprites[i * 5 + 1];
endSprite = me.sprites[i * 5 + 2];
innerSideSprite = me.sprites[i * 5 + 3];
outerSideSprite = me.sprites[i * 5 + 4];
if (animation) {
topSprite.fx.setConfig(animation);
startSprite.fx.setConfig(animation);
endSprite.fx.setConfig(animation);
innerSideSprite.fx.setConfig(animation);
outerSideSprite.fx.setConfig(animation);
}
topSprite.setAttributes(sliceAttributes);
startSprite.setAttributes(sliceAttributes);
endSprite.setAttributes(sliceAttributes);
innerSideSprite.setAttributes(sliceAttributes);
outerSideSprite.setAttributes(sliceAttributes);
}
}
for (i *= 5; i < me.sprites.length; i++) {
me.sprites[i].fx.setConfig(animation);
me.sprites[i].setAttributes({
opacity: 0,
startAngle: twoPie,
endAngle: twoPie,
baseRotation: rotation
});
}
return me.sprites;
}
});
/**
* @class Ext.chart.series.sprite.Polar
* @extends Ext.draw.sprite.Sprite
*
* Polar sprite.
*/
Ext.define("Ext.chart.series.sprite.Polar", {
mixins: {
markerHolder: "Ext.chart.MarkerHolder"
},
extend: 'Ext.draw.sprite.Sprite',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [dataMinX=0] Data minimum on the x-axis.
*/
dataMinX: 'number',
/**
* @cfg {Number} [dataMaxX=1] Data maximum on the x-axis.
*/
dataMaxX: 'number',
/**
* @cfg {Number} [dataMinY=0] Data minimum on the y-axis.
*/
dataMinY: 'number',
/**
* @cfg {Number} [dataMaxY=2] Data maximum on the y-axis.
*/
dataMaxY: 'number',
/**
* @cfg {Object} [dataY=null] Data items on the y-axis.
*/
dataY: 'data',
/**
* @cfg {Object} [dataX=null] Data items on the x-axis.
*/
dataX: 'data',
/**
* @cfg {Number} [centerX=0] The central point of the series on the x-axis.
*/
centerX: 'number',
/**
* @cfg {Number} [centerY=0] The central point of the series on the y-axis.
*/
centerY: 'number',
/**
* @cfg {Number} [startAngle=0] The starting angle of the polar series.
*/
startAngle: "number",
/**
* @cfg {Number} [endAngle=Math.PI] The ending angle of the polar series.
*/
endAngle: "number",
/**
* @cfg {Number} [startRho=0] The starting radius of the polar series.
*/
startRho: "number",
/**
* @cfg {Number} [endRho=150] The ending radius of the polar series.
*/
endRho: "number",
/**
* @cfg {Number} [baseRotation=0] The starting rotation of the polar series.
*/
baseRotation: "number",
/**
* @cfg {Object} [labels=null] Labels used in the series.
*/
labels: 'default',
/**
* @cfg {Number} [labelOverflowPadding=10] Padding around labels to determine overlap.
*/
labelOverflowPadding: 'number'
},
defaults: {
dataY: null,
dataX: null,
dataMinX: 0,
dataMaxX: 1,
dataMinY: 0,
dataMaxY: 1,
centerX: 0,
centerY: 0,
startAngle: 0,
endAngle: Math.PI,
startRho: 0,
endRho: 150,
baseRotation: 0,
labels: null,
labelOverflowPadding: 10
},
dirtyTriggers: {
dataX: 'bbox',
dataY: 'bbox',
dataMinX: 'bbox',
dataMaxX: 'bbox',
dataMinY: 'bbox',
dataMaxY: 'bbox',
centerX: "bbox",
centerY: "bbox",
startAngle: "bbox",
endAngle: "bbox",
startRho: "bbox",
endRho: "bbox",
baseRotation: "bbox"
}
}
},
config: {
dataItems: null,
field: null
},
updatePlainBBox: function (plain) {
var attr = this.attr;
plain.x = attr.centerX - attr.endRho;
plain.y = attr.centerY + attr.endRho;
plain.width = attr.endRho * 2;
plain.height = attr.endRho * 2;
}
});
/**
* @class Ext.chart.series.sprite.Radar
* @extends Ext.chart.series.sprite.Polar
*
* Radar series sprite.
*/
Ext.define("Ext.chart.series.sprite.Radar", {
alias: 'sprite.radar',
extend: 'Ext.chart.series.sprite.Polar',
render: function (surface, ctx) {
var me = this,
attr = me.attr,
centerX = attr.centerX,
centerY = attr.centerY,
matrix = attr.matrix,
minX = attr.dataMinX,
maxX = attr.dataMaxX,
maxY = attr.dataMaxY,
dataX = attr.dataX,
dataY = attr.dataY,
endRho = attr.endRho,
startRho = attr.startRho,
baseRotation = attr.baseRotation,
i, length = dataX.length,
markerCfg = {},
surfaceMatrix = me.surfaceMatrix,
x, y, r, th;
ctx.beginPath();
for (i = 0; i < length; i++) {
th = (dataX[i] - minX) / (maxX - minX + 1) * 2 * Math.PI + baseRotation;
r = dataY[i] / maxY * (endRho - startRho) + startRho;
x = matrix.x(centerX + Math.cos(th) * r, centerY + Math.sin(th) * r);
y = matrix.y(centerX + Math.cos(th) * r, centerY + Math.sin(th) * r);
ctx.lineTo(x, y);
markerCfg.translationX = surfaceMatrix.x(x, y);
markerCfg.translationY = surfaceMatrix.y(x, y);
me.putMarker("markers", markerCfg, i, true);
}
ctx.closePath();
ctx.fillStroke(attr);
}
});
/**
* @class Ext.chart.series.Radar
* @extends Ext.chart.series.Polar
*
* Creates a Radar Chart. A Radar Chart is a useful visualization technique for comparing different quantitative values for
* a constrained number of categories.
* As with all other series, the Radar series must be appended in the *series* Chart array configuration. See the Chart
* documentation for more information. A typical configuration object for the radar series could be:
*
* @example preview
* var chart = new Ext.chart.PolarChart({
* animate: true,
* interactions: ['rotate'],
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* series: [{
* type: 'radar',
* xField: 'name',
* yField: 'data4',
* style: {
* fillStyle: 'rgba(0, 0, 255, 0.1)',
* strokeStyle: 'rgba(0, 0, 0, 0.8)',
* lineWidth: 1
* }
* }],
* axes: [
* {
* type: 'numeric',
* position: 'radial',
* fields: 'data4',
* style: {
* estStepSize: 10
* },
* grid: true
* },
* {
* type: 'category',
* position: 'angular',
* fields: 'name',
* style: {
* estStepSize: 1
* },
* grid: true
* }
* ]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*
*
*/
Ext.define('Ext.chart.series.Radar', {
extend: 'Ext.chart.series.Polar',
type: "radar",
seriesType: 'radar',
alias: 'series.radar',
requires: ['Ext.chart.series.Cartesian', 'Ext.chart.series.sprite.Radar'],
/**
* @cfg {Object} style
* An object containing styles for overriding series styles from theming.
*/
config: {
},
updateAngularAxis: function (axis) {
axis.processData(this);
},
updateRadialAxis: function (axis) {
axis.processData(this);
},
coordinateX: function () {
return this.coordinate('X', 0, 2);
},
coordinateY: function () {
return this.coordinate('Y', 1, 2);
},
updateCenter: function (center) {
this.setStyle({
translationX: center[0] + this.getOffsetX(),
translationY: center[1] + this.getOffsetY()
});
this.doUpdateStyles();
},
updateRadius: function (radius) {
this.setStyle({
endRho: radius
});
this.doUpdateStyles();
},
updateRotation: function (rotation) {
this.setStyle({
rotationRads: rotation
});
this.doUpdateStyles();
},
updateTotalAngle: function (totalAngle) {
this.processData();
},
getItemForPoint: function (x, y) {
var me = this,
sprite = me.sprites && me.sprites[0],
attr = sprite.attr,
dataX = attr.dataX,
dataY = attr.dataY,
centerX = attr.centerX,
centerY = attr.centerY,
minX = attr.dataMinX,
maxX = attr.dataMaxX,
maxY = attr.dataMaxY,
endRho = attr.endRho,
startRho = attr.startRho,
baseRotation = attr.baseRotation,
i, length = dataX.length,
store = me.getStore(),
marker = me.getMarker(),
item, th, r;
if (sprite && marker) {
for (i = 0; i < length; i++) {
th = (dataX[i] - minX) / (maxX - minX + 1) * 2 * Math.PI + baseRotation;
r = dataY[i] / maxY * (endRho - startRho) + startRho;
if (Math.abs(centerX + Math.cos(th) * r - x) < 22 && Math.abs(centerY + Math.sin(th) * r - y) < 22) {
item = {
series: this,
sprite: sprite,
index: i,
record: store.getData().items[i],
field: store.getFields().items[i]
};
return item;
}
}
}
return this.callSuper(arguments);
},
getXRange: function () {
return [this.dataRange[0], this.dataRange[2]];
},
getYRange: function () {
return [this.dataRange[1], this.dataRange[3]];
}
}, function () {
var klass = this;
// TODO: [HACK] Steal from cartesian series.
klass.prototype.onAxesChanged = Ext.chart.series.Cartesian.prototype.onAxesChanged;
klass.prototype.getSprites = Ext.chart.series.Cartesian.prototype.getSprites;
});
/**
* @class Ext.chart.series.sprite.Scatter
* @extends Ext.chart.series.sprite.Cartesian
*
* Scatter series sprite.
*/
Ext.define("Ext.chart.series.sprite.Scatter", {
alias: 'sprite.scatterSeries',
extend: 'Ext.chart.series.sprite.Cartesian',
renderClipped: function (surface, ctx, clip, clipRegion) {
if (this.cleanRedraw) {
return;
}
var attr = this.attr,
dataX = attr.dataX,
dataY = attr.dataY,
matrix = this.attr.matrix,
xx = matrix.getXX(),
yy = matrix.getYY(),
dx = matrix.getDX(),
dy = matrix.getDY(),
markerCfg = {},
left = clipRegion[0] - xx,
right = clipRegion[0] + clipRegion[2] + xx,
top = clipRegion[1] - yy,
bottom = clipRegion[1] + clipRegion[3] + yy,
x, y;
for (var i = 0; i < dataX.length; i++) {
x = dataX[i];
y = dataY[i];
x = x * xx + dx;
y = y * yy + dy;
if (left <= x && x <= right && top <= y && y <= bottom) {
if (attr.renderer) {
attr.renderer.call(this, markerCfg, this, i, this.getDataItems().items[i]);
}
markerCfg.translationX = x;
markerCfg.translationY = y;
this.putMarker("items", markerCfg, i, !attr.renderer);
}
}
}
});
/**
* @class Ext.chart.series.Scatter
* @extends Ext.chart.series.Cartesian
*
* Creates a Scatter Chart. The scatter plot is useful when trying to display more than two variables in the same visualization.
* These variables can be mapped into x, y coordinates and also to an element's radius/size, color, etc.
* As with all other series, the Scatter Series must be appended in the *series* Chart array configuration. See the Chart
* documentation for more information on creating charts. A typical configuration object for the scatter could be:
*
* @example preview
* var chart = new Ext.chart.CartesianChart({
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data1'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 0
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'scatter',
* highlight: {
* size: 7,
* radius: 7
* },
* fill: true,
* xField: 'name',
* yField: 'data3',
* marker: {
* type: 'circle',
* fillStyle: 'blue',
* radius: 10,
* lineWidth: 0
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*
* In this configuration we add three different categories of scatter series. Each of them is bound to a different field of the same data store,
* `data1`, `data2` and `data3` respectively. All x-fields for the series must be the same field, in this case `name`.
* Each scatter series has a different styling configuration for markers, specified by the `marker` object. Finally we set the left axis as
* axis to show the current values of the elements.
*
*/
Ext.define('Ext.chart.series.Scatter', {
extend: 'Ext.chart.series.Cartesian',
alias: 'series.scatter',
type: 'scatter',
seriesType: 'scatterSeries',
requires: [
'Ext.chart.series.sprite.Scatter'
],
config: {
itemInstancing: {
fx: {
customDuration: {
translationX: 0,
translationY: 0
}
}
}
},
applyMarker: function (marker) {
this.getItemInstancing();
this.setItemInstancing(marker);
}
});
Ext.define("Ext.chart.series.sprite.AbstractRadial", {
extend: 'Ext.draw.sprite.Sprite',
inheritableStatics: {
def: {
processors: {
rotation: 'number',
x: 'number',
y: 'number'
},
defaults: {
rotation: 0,
x: 0,
y: 0
}
}
}
});
/**
* @author Ed Spencer
* @class Ext.data.reader.Array
*
* Data reader class to create an Array of {@link Ext.data.Model} objects from an Array.
* Each element of that Array represents a row of data fields. The
* fields are pulled into a Record object using as a subscript, the `mapping` property
* of the field definition if it exists, or the field's ordinal position in the definition.
*
* Example code:
*
* Employee = Ext.define('Employee', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* 'id',
* {name: 'name', mapping: 1}, // "mapping" only needed if an "id" field is present which
* {name: 'occupation', mapping: 2} // precludes using the ordinal position as the index.
* ]
* }
* });
*
* var myReader = new Ext.data.reader.Array({
* model: 'Employee'
* }, Employee);
*
* This would consume an Array like this:
*
* [ [1, 'Bill', 'Gardener'], [2, 'Ben', 'Horticulturalist'] ]
*
* @constructor
* Create a new ArrayReader
* @param {Object} meta Metadata configuration options.
*/
Ext.define('Ext.data.reader.Array', {
extend: 'Ext.data.reader.Json',
alternateClassName: 'Ext.data.ArrayReader',
alias : 'reader.array',
// For Array Reader, methods in the base which use these properties must not see the defaults
config: {
totalProperty: undefined,
successProperty: undefined
},
/**
* @private
* Returns an accessor expression for the passed Field from an Array using either the Field's mapping, or
* its ordinal position in the fields collection as the index.
* This is used by buildExtractors to create optimized on extractor function which converts raw data into model instances.
*/
createFieldAccessExpression: function(field, fieldVarName, dataName) {
var me = this,
mapping = field.getMapping(),
index = (mapping == null) ? me.getModel().getFields().indexOf(field) : mapping,
result;
if (typeof index === 'function') {
result = fieldVarName + '.getMapping()(' + dataName + ', this)';
} else {
if (isNaN(index)) {
index = '"' + index + '"';
}
result = dataName + "[" + index + "]";
}
return result;
}
});
/**
* @author Ed Spencer
* @aside guide stores
*
* Small helper class to make creating {@link Ext.data.Store}s from Array data easier. An ArrayStore will be
* automatically configured with a {@link Ext.data.reader.Array}.
*
* A store configuration would be something like:
*
* var store = Ext.create('Ext.data.ArrayStore', {
* // store configs
* autoDestroy: true,
* storeId: 'myStore',
* // reader configs
* idIndex: 0,
* fields: [
* 'company',
* {name: 'price', type: 'float'},
* {name: 'change', type: 'float'},
* {name: 'pctChange', type: 'float'},
* {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
* ]
* });
*
* This store is configured to consume a returned object of the form:
*
* var myData = [
* ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
* ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
* ['Boeing Co.',75.43,0.53,0.71,'9/1 12:00am'],
* ['Hewlett-Packard Co.',36.53,-0.03,-0.08,'9/1 12:00am'],
* ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
* ];
*
* An object literal of this form could also be used as the {@link #data} config option.
*
* **Note:** Although not listed here, this class accepts all of the configuration options of
* **{@link Ext.data.reader.Array ArrayReader}**.
*/
Ext.define('Ext.data.ArrayStore', {
extend: 'Ext.data.Store',
alias: 'store.array',
uses: ['Ext.data.reader.Array'],
config: {
proxy: {
type: 'memory',
reader: 'array'
}
},
loadData: function(data, append) {
// if (this.expandData === true) {
// var r = [],
// i = 0,
// ln = data.length;
//
// for (; i < ln; i++) {
// r[r.length] = [data[i]];
// }
//
// data = r;
// }
this.callParent([data, append]);
}
}, function() {
// backwards compat
Ext.data.SimpleStore = Ext.data.ArrayStore;
// Ext.reg('simplestore', Ext.data.SimpleStore);
});
/**
* Ext.Direct aims to streamline communication between the client and server by providing a single interface that
* reduces the amount of common code typically required to validate data and handle returned data packets (reading data,
* error conditions, etc).
*
* The Ext.direct namespace includes several classes for a closer integration with the server-side. The Ext.data
* namespace also includes classes for working with Ext.data.Stores which are backed by data from an Ext.Direct method.
*
* # Specification
*
* For additional information consult the [Ext.Direct Specification](http://sencha.com/products/extjs/extdirect).
*
* # Providers
*
* Ext.Direct uses a provider architecture, where one or more providers are used to transport data to and from the
* server. There are several providers that exist in the core at the moment:
*
* - {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
* - {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
* - {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side on the client.
*
* A provider does not need to be invoked directly, providers are added via {@link Ext.direct.Manager}.{@link #addProvider}.
*
* # Router
*
* Ext.Direct utilizes a "router" on the server to direct requests from the client to the appropriate server-side
* method. Because the Ext.Direct API is completely platform-agnostic, you could completely swap out a Java based server
* solution and replace it with one that uses C# without changing the client side JavaScript at all.
*
* # Server side events
*
* Custom events from the server may be handled by the client by adding listeners, for example:
*
* {"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
*
* // add a handler for a 'message' event sent by the server
* Ext.direct.Manager.on('message', function(e){
* out.append(String.format('<p><i>{0}</i></p>', e.data));
* out.el.scrollTo('t', 100000, true);
* });
*
* @singleton
* @alternateClassName Ext.Direct
*/
Ext.define('Ext.direct.Manager', {
singleton: true,
mixins: {
observable: 'Ext.mixin.Observable'
},
requires: ['Ext.util.Collection'],
alternateClassName: 'Ext.Direct',
exceptions: {
TRANSPORT: 'xhr',
PARSE: 'parse',
LOGIN: 'login',
SERVER: 'exception'
},
/**
* @event event
* Fires after an event.
* @param {Ext.direct.Event} e The Ext.direct.Event type that occurred.
* @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
*/
/**
* @event exception
* Fires after an event exception.
* @param {Ext.direct.Event} e The event type that occurred.
*/
constructor: function() {
var me = this;
me.transactions = Ext.create('Ext.util.Collection', this.getKey);
me.providers = Ext.create('Ext.util.Collection', this.getKey);
},
getKey: function(item) {
return item.getId();
},
/**
* Adds an Ext.Direct Provider and creates the proxy or stub methods to execute server-side methods. If the provider
* is not already connected, it will auto-connect.
*
* Ext.direct.Manager.addProvider({
* type: "remoting", // create a {@link Ext.direct.RemotingProvider}
* url: "php/router.php", // url to connect to the Ext.Direct server-side router.
* actions: { // each property within the actions object represents a Class
* TestAction: [ // array of methods within each server side Class
* {
* name: "doEcho", // name of method
* len: 1
* },{
* name: "multiply",
* len: 1
* },{
* name: "doForm",
* formHandler: true, // handle form on server with Ext.Direct.Transaction
* len: 1
* }]
* },
* namespace: "myApplication" // namespace to create the Remoting Provider in
* });
*
* @param {Ext.direct.Provider/Object...} provider
* Accepts any number of Provider descriptions (an instance or config object for
* a Provider). Each Provider description instructs Ext.Direct how to create
* client-side stub methods.
* @return {Object}
*/
addProvider : function(provider) {
var me = this,
args = Ext.toArray(arguments),
i = 0, ln;
if (args.length > 1) {
for (ln = args.length; i < ln; ++i) {
me.addProvider(args[i]);
}
return;
}
// if provider has not already been instantiated
if (!provider.isProvider) {
provider = Ext.create('direct.' + provider.type + 'provider', provider);
}
me.providers.add(provider);
provider.on('data', me.onProviderData, me);
if (!provider.isConnected()) {
provider.connect();
}
return provider;
},
/**
* Retrieves a {@link Ext.direct.Provider provider} by the **{@link Ext.direct.Provider#id id}** specified when the
* provider is {@link #addProvider added}.
* @param {String/Ext.direct.Provider} id The id of the provider, or the provider instance.
* @return {Object}
*/
getProvider : function(id){
return id.isProvider ? id : this.providers.get(id);
},
/**
* Removes the provider.
* @param {String/Ext.direct.Provider} provider The provider instance or the id of the provider.
* @return {Ext.direct.Provider/null} The provider, `null` if not found.
*/
removeProvider : function(provider) {
var me = this,
providers = me.providers;
provider = provider.isProvider ? provider : providers.get(provider);
if (provider) {
provider.un('data', me.onProviderData, me);
providers.remove(provider);
return provider;
}
return null;
},
/**
* Adds a transaction to the manager.
* @private
* @param {Ext.direct.Transaction} transaction The transaction to add
* @return {Ext.direct.Transaction} transaction
*/
addTransaction: function(transaction) {
this.transactions.add(transaction);
return transaction;
},
/**
* Removes a transaction from the manager.
* @private
* @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to remove
* @return {Ext.direct.Transaction} transaction
*/
removeTransaction: function(transaction) {
transaction = this.getTransaction(transaction);
this.transactions.remove(transaction);
return transaction;
},
/**
* Gets a transaction
* @private
* @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to get
* @return {Ext.direct.Transaction}
*/
getTransaction: function(transaction) {
return Ext.isObject(transaction) ? transaction : this.transactions.get(transaction);
},
onProviderData : function(provider, event) {
var me = this,
i = 0, ln,
name;
if (Ext.isArray(event)) {
for (ln = event.length; i < ln; ++i) {
me.onProviderData(provider, event[i]);
}
return;
}
name = event.getName();
if (name && name != 'event' && name != 'exception') {
me.fireEvent(name, event);
} else if (event.getStatus() === false) {
me.fireEvent('exception', event);
}
me.fireEvent('event', event, provider);
},
/**
* Parses a direct function. It may be passed in a string format, for example:
* "MyApp.Person.read".
* @protected
* @param {String/Function} fn The direct function
* @return {Function} The function to use in the direct call. Null if not found
*/
parseMethod: function(fn) {
if (Ext.isString(fn)) {
var parts = fn.split('.'),
i = 0,
ln = parts.length,
current = window;
while (current && i < ln) {
current = current[parts[i]];
++i;
}
fn = Ext.isFunction(current) ? current : null;
}
return fn || null;
}
});
/**
* @aside guide proxies
*
* This class is used to send requests to the server using {@link Ext.direct.Manager Ext.Direct}. When a
* request is made, the transport mechanism is handed off to the appropriate
* {@link Ext.direct.RemotingProvider Provider} to complete the call.
*
* # Specifying the function
*
* This proxy expects a Direct remoting method to be passed in order to be able to complete requests.
* This can be done by specifying the {@link #directFn} configuration. This will use the same direct
* method for all requests. Alternatively, you can provide an {@link #api} configuration. This
* allows you to specify a different remoting method for each CRUD action.
*
* # Parameters
*
* This proxy provides options to help configure which parameters will be sent to the server.
* By specifying the {@link #paramsAsHash} option, it will send an object literal containing each
* of the passed parameters. The {@link #paramOrder} option can be used to specify the order in which
* the remoting method parameters are passed.
*
* # Example Usage
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['firstName', 'lastName'],
* proxy: {
* type: 'direct',
* directFn: MyApp.getUsers,
* paramOrder: 'id' // Tells the proxy to pass the id as the first parameter to the remoting method.
* }
* }
* });
* User.load(1);
*/
Ext.define('Ext.data.proxy.Direct', {
extend: 'Ext.data.proxy.Server',
alternateClassName: 'Ext.data.DirectProxy',
alias: 'proxy.direct',
requires: ['Ext.direct.Manager'],
config: {
/**
* @cfg {String/String[]} paramOrder
* Defaults to undefined. A list of params to be executed server side. Specify the params in the order in
* which they must be executed on the server-side as either (1) an Array of String values, or (2) a String
* of params delimited by either whitespace, comma, or pipe. For example, any of the following would be
* acceptable:
*
* paramOrder: ['param1','param2','param3']
* paramOrder: 'param1 param2 param3'
* paramOrder: 'param1,param2,param3'
* paramOrder: 'param1|param2|param'
*/
paramOrder: undefined,
/**
* @cfg {Boolean} paramsAsHash
* Send parameters as a collection of named arguments.
* Providing a {@link #paramOrder} nullifies this configuration.
*/
paramsAsHash: true,
/**
* @cfg {Function/String} directFn
* Function to call when executing a request. directFn is a simple alternative to defining the api configuration-parameter
* for Store's which will not implement a full CRUD api. The directFn may also be a string reference to the fully qualified
* name of the function, for example: 'MyApp.company.GetProfile'. This can be useful when using dynamic loading. The string
* will be looked up when the proxy is created.
*/
directFn : undefined,
/**
* @cfg {Object} api
* The same as {@link Ext.data.proxy.Server#api}, however instead of providing urls, you should provide a direct
* function call. See {@link #directFn}.
*/
api: null,
/**
* @cfg {Object} extraParams
* Extra parameters that will be included on every read request. Individual requests with params
* of the same name will override these params when they are in conflict.
*/
extraParams: null
},
// @private
paramOrderRe: /[\s,|]/,
applyParamOrder: function(paramOrder) {
if (Ext.isString(paramOrder)) {
paramOrder = paramOrder.split(this.paramOrderRe);
}
return paramOrder;
},
applyDirectFn: function(directFn) {
return Ext.direct.Manager.parseMethod(directFn);
},
applyApi: function(api) {
var fn;
if (api && Ext.isObject(api)) {
for (fn in api) {
if (api.hasOwnProperty(fn)) {
api[fn] = Ext.direct.Manager.parseMethod(api[fn]);
}
}
}
return api;
},
doRequest: function(operation, callback, scope) {
var me = this,
writer = me.getWriter(),
request = me.buildRequest(operation, callback, scope),
api = me.getApi(),
fn = api && api[request.getAction()] || me.getDirectFn(),
params = request.getParams(),
args = [],
method;
//<debug>
if (!fn) {
Ext.Logger.error('No direct function specified for this proxy');
}
//</debug>
request = writer.write(request);
if (operation.getAction() == 'read') {
// We need to pass params
method = fn.directCfg.method;
args = method.getArgs(params, me.getParamOrder(), me.getParamsAsHash());
} else {
args.push(request.getJsonData());
}
args.push(me.createRequestCallback(request, operation, callback, scope), me);
request.setConfig({
args: args,
directFn: fn
});
fn.apply(window, args);
},
/*
* Inherit docs. We don't apply any encoding here because
* all of the direct requests go out as jsonData
*/
applyEncoding: function(value) {
return value;
},
createRequestCallback: function(request, operation, callback, scope) {
var me = this;
return function(data, event) {
me.processResponse(event.getStatus(), operation, request, event.getResult(), callback, scope);
};
},
// @inheritdoc
extractResponseData: function(response) {
var result = response.getResult();
return Ext.isDefined(result) ? result : response.getData();
},
// @inheritdoc
setException: function(operation, response) {
operation.setException(response.getMessage());
},
// @inheritdoc
buildUrl: function() {
return '';
}
});
/**
* @aside guide stores
*
* Small helper class to create an {@link Ext.data.Store} configured with an {@link Ext.data.proxy.Direct}
* and {@link Ext.data.reader.Json} to make interacting with an {@link Ext.direct.Manager} server-side
* {@link Ext.direct.Provider Provider} easier. To create a different proxy/reader combination create a basic
* {@link Ext.data.Store} configured as needed.
*
* Since configurations are deeply merged with the standard configuration, you can override certain proxy and
* reader configurations like this:
*
* Ext.create('Ext.data.DirectStore', {
* proxy: {
* paramsAsHash: true,
* directFn: someDirectFn,
* simpleSortMode: true,
* reader: {
* rootProperty: 'results',
* idProperty: '_id'
* }
* }
* });
*
*/
Ext.define('Ext.data.DirectStore', {
extend: 'Ext.data.Store',
alias: 'store.direct',
requires: ['Ext.data.proxy.Direct'],
config: {
proxy: {
type: 'direct',
reader: {
type: 'json'
}
}
}
});
/**
* @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, request) {
* // 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,
/* End Definitions */
/**
* Number of requests done so far.
* @private
*/
requestCount: 0,
/**
* Hash of pending requests.
* @private
*/
requests: {},
/**
* @property {Number} [timeout=30000]
* A default timeout (in milliseconds) for any JsonP requests. If the request has not completed in this time the failure callback will
* be fired.
*/
timeout: 30000,
/**
* @property {Boolean} disableCaching
* `true` to add a unique cache-buster param to requests.
*/
disableCaching: true,
/**
* @property {String} disableCachingParam
* Change the parameter which is sent went disabling caching through a cache buster.
*/
disableCachingParam: '_dc',
/**
* @property {String} callbackKey
* Specifies the GET parameter that will be sent to the server containing the function name to be executed when the
* request completes. 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);
//<debug>
if (!options.url) {
Ext.Logger.error('A url must be specified for a JSONP request.');
}
//</debug>
var me = this,
disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching,
cacheParam = options.disableCachingParam || me.disableCachingParam,
id = ++me.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.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.requests,
key;
if (request) {
if (!request.id) {
request = requests[request];
}
this.handleAbort(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 any 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.requests[request.id];
this.cleanupErrorHandling(request);
Ext.fly(request.script).destroy();
if (request.errorType) {
success = false;
Ext.callback(request.failure, request.scope, [request.errorType, request]);
} else {
Ext.callback(request.success, request.scope, [result, request]);
}
Ext.callback(request.callback, request.scope, [success, result, request.errorType, request]);
},
/**
* 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
* @class Ext.data.JsonStore
* @extends Ext.data.Store
* @private
*
* Small helper class to make creating {@link Ext.data.Store}s from JSON data easier.
* A JsonStore will be automatically configured with a {@link Ext.data.reader.Json}.
*
* A store configuration would be something like:
*
* var store = new Ext.data.JsonStore({
* // store configs
* autoDestroy: true,
* storeId: 'myStore',
*
* proxy: {
* type: 'ajax',
* url: 'get-images.php',
* reader: {
* type: 'json',
* root: 'images',
* idProperty: 'name'
* }
* },
*
* // alternatively, a {@link Ext.data.Model} name can be given (see {@link Ext.data.Store} for an example)
* fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
* });
*
* This store is configured to consume a returned object of the form:
*
* {
* images: [
* {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
* {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
* ]
* }
*
* An object literal of this form could also be used as the {@link #data} config option.
*
* @xtype jsonstore
*/
Ext.define('Ext.data.JsonStore', {
extend: 'Ext.data.Store',
alias: 'store.json',
config: {
proxy: {
type: 'ajax',
reader: 'json',
writer: 'json'
}
}
});
/**
* @class Ext.data.NodeInterface
* This class is meant to be used as a set of methods that are applied to the prototype of a
* Record to decorate it with a Node API. This means that models used in conjunction with a tree
* will have all of the tree related methods available on the model. In general this class will
* not be used directly by the developer. This class also creates extra fields on the model if
* they do not exist, to help maintain the tree state and UI. These fields are:
*
* - parentId
* - index
* - depth
* - expanded
* - expandable
* - checked
* - leaf
* - cls
* - iconCls
* - root
* - isLast
* - isFirst
* - allowDrop
* - allowDrag
* - loaded
* - loading
* - href
* - hrefTarget
* - qtip
* - qtitle
*/
Ext.define('Ext.data.NodeInterface', {
requires: ['Ext.data.Field', 'Ext.data.ModelManager'],
alternateClassName: 'Ext.data.Node',
/**
* @property nextSibling
* A reference to this node's next sibling node. `null` if this node does not have a next sibling.
*/
/**
* @property previousSibling
* A reference to this node's previous sibling node. `null` if this node does not have a previous sibling.
*/
/**
* @property parentNode
* A reference to this node's parent node. `null` if this node is the root node.
*/
/**
* @property lastChild
* A reference to this node's last child node. `null` if this node has no children.
*/
/**
* @property firstChild
* A reference to this node's first child node. `null` if this node has no children.
*/
/**
* @property childNodes
* An array of this nodes children. Array will be empty if this node has no children.
*/
statics: {
/**
* This method allows you to decorate a Record's prototype to implement the NodeInterface.
* This adds a set of methods, new events, new properties and new fields on every Record
* with the same Model as the passed Record.
* @param {Ext.data.Model} record The Record you want to decorate the prototype of.
* @static
*/
decorate: function(record) {
if (!record.isNode) {
// Apply the methods and fields to the prototype
var mgr = Ext.data.ModelManager,
modelName = record.modelName,
modelClass = mgr.getModel(modelName),
newFields = [],
i, newField, len;
// Start by adding the NodeInterface methods to the Model's prototype
modelClass.override(this.getPrototypeBody());
newFields = this.applyFields(modelClass, [
{name: 'parentId', type: 'string', defaultValue: null},
{name: 'index', type: 'int', defaultValue: 0},
{name: 'depth', type: 'int', defaultValue: 0, persist: false},
{name: 'expanded', type: 'bool', defaultValue: false, persist: false},
{name: 'expandable', type: 'bool', defaultValue: true, persist: false},
{name: 'checked', type: 'auto', defaultValue: null},
{name: 'leaf', type: 'bool', defaultValue: false, persist: false},
{name: 'cls', type: 'string', defaultValue: null, persist: false},
{name: 'iconCls', type: 'string', defaultValue: null, persist: false},
{name: 'root', type: 'boolean', defaultValue: false, persist: false},
{name: 'isLast', type: 'boolean', defaultValue: false, persist: false},
{name: 'isFirst', type: 'boolean', defaultValue: false, persist: false},
{name: 'allowDrop', type: 'boolean', defaultValue: true, persist: false},
{name: 'allowDrag', type: 'boolean', defaultValue: true, persist: false},
{name: 'loaded', type: 'boolean', defaultValue: false, persist: false},
{name: 'loading', type: 'boolean', defaultValue: false, persist: false},
{name: 'href', type: 'string', defaultValue: null, persist: false},
{name: 'hrefTarget', type: 'string', defaultValue: null, persist: false},
{name: 'qtip', type: 'string', defaultValue: null, persist: false},
{name: 'qtitle', type: 'string', defaultValue: null, persist: false}
]);
len = newFields.length;
// We set a dirty flag on the fields collection of the model. Any reader that
// will read in data for this model will update their extractor functions.
modelClass.getFields().isDirty = true;
// Set default values
for (i = 0; i < len; ++i) {
newField = newFields[i];
if (record.get(newField.getName()) === undefined) {
record.data[newField.getName()] = newField.getDefaultValue();
}
}
}
if (!record.isDecorated) {
record.isDecorated = true;
Ext.applyIf(record, {
firstChild: null,
lastChild: null,
parentNode: null,
previousSibling: null,
nextSibling: null,
childNodes: []
});
record.enableBubble([
/**
* @event append
* Fires when a new child node is appended.
* @param {Ext.data.NodeInterface} this This node.
* @param {Ext.data.NodeInterface} node The newly appended node.
* @param {Number} index The index of the newly appended node.
*/
"append",
/**
* @event remove
* Fires when a child node is removed.
* @param {Ext.data.NodeInterface} this This node.
* @param {Ext.data.NodeInterface} node The removed node.
*/
"remove",
/**
* @event move
* Fires when this node is moved to a new location in the tree.
* @param {Ext.data.NodeInterface} this This node.
* @param {Ext.data.NodeInterface} oldParent The old parent of this node.
* @param {Ext.data.NodeInterface} newParent The new parent of this node.
* @param {Number} index The index it was moved to.
*/
"move",
/**
* @event insert
* Fires when a new child node is inserted.
* @param {Ext.data.NodeInterface} this This node.
* @param {Ext.data.NodeInterface} node The child node inserted.
* @param {Ext.data.NodeInterface} refNode The child node the node was inserted before.
*/
"insert",
/**
* @event beforeappend
* Fires before a new child is appended, return `false` to cancel the append.
* @param {Ext.data.NodeInterface} this This node.
* @param {Ext.data.NodeInterface} node The child node to be appended.
*/
"beforeappend",
/**
* @event beforeremove
* Fires before a child is removed, return `false` to cancel the remove.
* @param {Ext.data.NodeInterface} this This node.
* @param {Ext.data.NodeInterface} node The child node to be removed.
*/
"beforeremove",
/**
* @event beforemove
* Fires before this node is moved to a new location in the tree. Return `false` to cancel the move.
* @param {Ext.data.NodeInterface} this This node.
* @param {Ext.data.NodeInterface} oldParent The parent of this node.
* @param {Ext.data.NodeInterface} newParent The new parent this node is moving to.
* @param {Number} index The index it is being moved to.
*/
"beforemove",
/**
* @event beforeinsert
* Fires before a new child is inserted, return false to cancel the insert.
* @param {Ext.data.NodeInterface} this This node
* @param {Ext.data.NodeInterface} node The child node to be inserted
* @param {Ext.data.NodeInterface} refNode The child node the node is being inserted before
*/
"beforeinsert",
/**
* @event expand
* Fires when this node is expanded.
* @param {Ext.data.NodeInterface} this The expanding node.
*/
"expand",
/**
* @event collapse
* Fires when this node is collapsed.
* @param {Ext.data.NodeInterface} this The collapsing node.
*/
"collapse",
/**
* @event beforeexpand
* Fires before this node is expanded.
* @param {Ext.data.NodeInterface} this The expanding node.
*/
"beforeexpand",
/**
* @event beforecollapse
* Fires before this node is collapsed.
* @param {Ext.data.NodeInterface} this The collapsing node.
*/
"beforecollapse",
/**
* @event sort
* Fires when this node's childNodes are sorted.
* @param {Ext.data.NodeInterface} this This node.
* @param {Ext.data.NodeInterface[]} childNodes The childNodes of this node.
*/
"sort",
'load'
]);
}
return record;
},
applyFields: function(modelClass, addFields) {
var modelPrototype = modelClass.prototype,
fields = modelPrototype.fields,
keys = fields.keys,
ln = addFields.length,
addField, i,
newFields = [];
for (i = 0; i < ln; i++) {
addField = addFields[i];
if (!Ext.Array.contains(keys, addField.name)) {
addField = Ext.create('Ext.data.Field', addField);
newFields.push(addField);
fields.add(addField);
}
}
return newFields;
},
getPrototypeBody: function() {
return {
isNode: true,
/**
* Ensures that the passed object is an instance of a Record with the NodeInterface applied
* @return {Boolean}
* @private
*/
createNode: function(node) {
if (Ext.isObject(node) && !node.isModel) {
node = Ext.data.ModelManager.create(node, this.modelName);
}
// Make sure the node implements the node interface
return Ext.data.NodeInterface.decorate(node);
},
/**
* Returns true if this node is a leaf
* @return {Boolean}
*/
isLeaf : function() {
return this.get('leaf') === true;
},
/**
* Sets the first child of this node
* @private
* @param {Ext.data.NodeInterface} node
*/
setFirstChild : function(node) {
this.firstChild = node;
},
/**
* Sets the last child of this node
* @private
* @param {Ext.data.NodeInterface} node
*/
setLastChild : function(node) {
this.lastChild = node;
},
/**
* Updates general data of this node like isFirst, isLast, depth. This
* method is internally called after a node is moved. This shouldn't
* have to be called by the developer unless they are creating custom
* Tree plugins.
* @return {Boolean}
*/
updateInfo: function(silent) {
var me = this,
parentNode = me.parentNode,
isFirst = (!parentNode ? true : parentNode.firstChild == me),
isLast = (!parentNode ? true : parentNode.lastChild == me),
depth = 0,
parent = me,
children = me.childNodes,
ln = children.length,
i;
while (parent.parentNode) {
++depth;
parent = parent.parentNode;
}
me.beginEdit();
me.set({
isFirst: isFirst,
isLast: isLast,
depth: depth,
index: parentNode ? parentNode.indexOf(me) : 0,
parentId: parentNode ? parentNode.getId() : null
});
me.endEdit(silent);
if (silent) {
me.commit(silent);
}
for (i = 0; i < ln; i++) {
children[i].updateInfo(silent);
}
},
/**
* Returns `true` if this node is the last child of its parent.
* @return {Boolean}
*/
isLast : function() {
return this.get('isLast');
},
/**
* Returns `true` if this node is the first child of its parent.
* @return {Boolean}
*/
isFirst : function() {
return this.get('isFirst');
},
/**
* Returns `true` if this node has one or more child nodes, else `false`.
* @return {Boolean}
*/
hasChildNodes : function() {
return !this.isLeaf() && this.childNodes.length > 0;
},
/**
* Returns `true` if this node has one or more child nodes, or if the `expandable`
* node attribute is explicitly specified as `true`, otherwise returns `false`.
* @return {Boolean}
*/
isExpandable : function() {
var me = this;
if (me.get('expandable')) {
return !(me.isLeaf() || (me.isLoaded() && !me.hasChildNodes()));
}
return false;
},
/**
* Insert node(s) as the last child node of this node.
*
* If the node was previously a child node of another parent node, it will be removed from that node first.
*
* @param {Ext.data.NodeInterface/Ext.data.NodeInterface[]} node The node or Array of nodes to append.
* @return {Ext.data.NodeInterface} The appended node if single append, or `null` if an array was passed.
*/
appendChild : function(node, suppressEvents, suppressNodeUpdate) {
var me = this,
i, ln,
index,
oldParent,
ps;
// if passed an array or multiple args do them one by one
if (Ext.isArray(node)) {
for (i = 0, ln = node.length; i < ln; i++) {
me.appendChild(node[i]);
}
} else {
// Make sure it is a record
node = me.createNode(node);
if (suppressEvents !== true && me.fireEvent("beforeappend", me, node) === false) {
return false;
}
index = me.childNodes.length;
oldParent = node.parentNode;
// it's a move, make sure we move it cleanly
if (oldParent) {
if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index) === false) {
return false;
}
oldParent.removeChild(node, null, false, true);
}
index = me.childNodes.length;
if (index === 0) {
me.setFirstChild(node);
}
me.childNodes.push(node);
node.parentNode = me;
node.nextSibling = null;
me.setLastChild(node);
ps = me.childNodes[index - 1];
if (ps) {
node.previousSibling = ps;
ps.nextSibling = node;
ps.updateInfo(suppressNodeUpdate);
} else {
node.previousSibling = null;
}
node.updateInfo(suppressNodeUpdate);
// As soon as we append a child to this node, we are loaded
if (!me.isLoaded()) {
me.set('loaded', true);
}
// If this node didn't have any childnodes before, update myself
else if (me.childNodes.length === 1) {
me.set('loaded', me.isLoaded());
}
if (suppressEvents !== true) {
me.fireEvent("append", me, node, index);
if (oldParent) {
node.fireEvent("move", node, oldParent, me, index);
}
}
return node;
}
},
/**
* Returns the bubble target for this node.
* @private
* @return {Object} The bubble target.
*/
getBubbleTarget: function() {
return this.parentNode;
},
/**
* Removes a child node from this node.
* @param {Ext.data.NodeInterface} node The node to remove.
* @param {Boolean} [destroy=false] `true` to destroy the node upon removal.
* @return {Ext.data.NodeInterface} The removed node.
*/
removeChild : function(node, destroy, suppressEvents, suppressNodeUpdate) {
var me = this,
index = me.indexOf(node);
if (index == -1 || (suppressEvents !== true && me.fireEvent("beforeremove", me, node) === false)) {
return false;
}
// remove it from childNodes collection
Ext.Array.erase(me.childNodes, index, 1);
// update child refs
if (me.firstChild == node) {
me.setFirstChild(node.nextSibling);
}
if (me.lastChild == node) {
me.setLastChild(node.previousSibling);
}
if (suppressEvents !== true) {
me.fireEvent("remove", me, node);
}
// update siblings
if (node.previousSibling) {
node.previousSibling.nextSibling = node.nextSibling;
node.previousSibling.updateInfo(suppressNodeUpdate);
}
if (node.nextSibling) {
node.nextSibling.previousSibling = node.previousSibling;
node.nextSibling.updateInfo(suppressNodeUpdate);
}
// If this node suddenly doesn't have childnodes anymore, update myself
if (!me.childNodes.length) {
me.set('loaded', me.isLoaded());
}
if (destroy) {
node.destroy(true);
} else {
node.clear();
}
return node;
},
/**
* Creates a copy (clone) of this Node.
* @param {String} id (optional) A new id, defaults to this Node's id.
* @param {Boolean} deep (optional) If passed as `true`, all child Nodes are recursively copied into the new Node.
* If omitted or `false`, the copy will have no child Nodes.
* @return {Ext.data.NodeInterface} A copy of this Node.
*/
copy: function(newId, deep) {
var me = this,
result = me.callOverridden(arguments),
len = me.childNodes ? me.childNodes.length : 0,
i;
// Move child nodes across to the copy if required
if (deep) {
for (i = 0; i < len; i++) {
result.appendChild(me.childNodes[i].copy(true));
}
}
return result;
},
/**
* Clear the node.
* @private
* @param {Boolean} destroy `true` to destroy the node.
*/
clear : function(destroy) {
var me = this;
// clear any references from the node
me.parentNode = me.previousSibling = me.nextSibling = null;
if (destroy) {
me.firstChild = me.lastChild = null;
}
},
/**
* Destroys the node.
*/
destroy : function(silent) {
/*
* Silent is to be used in a number of cases
* 1) When setRoot is called.
* 2) When destroy on the tree is called
* 3) For destroying child nodes on a node
*/
var me = this,
options = me.destroyOptions;
if (silent === true) {
me.clear(true);
Ext.each(me.childNodes, function(n) {
n.destroy(true);
});
me.childNodes = null;
delete me.destroyOptions;
me.callOverridden([options]);
} else {
me.destroyOptions = silent;
// overridden method will be called, since remove will end up calling destroy(true);
me.remove(true);
}
},
/**
* Inserts the first node before the second node in this nodes `childNodes` collection.
* @param {Ext.data.NodeInterface} node The node to insert.
* @param {Ext.data.NodeInterface} refNode The node to insert before (if `null` the node is appended).
* @return {Ext.data.NodeInterface} The inserted node.
*/
insertBefore : function(node, refNode, suppressEvents) {
var me = this,
index = me.indexOf(refNode),
oldParent = node.parentNode,
refIndex = index,
ps;
if (!refNode) { // like standard Dom, refNode can be null for append
return me.appendChild(node);
}
// nothing to do
if (node == refNode) {
return false;
}
// Make sure it is a record with the NodeInterface
node = me.createNode(node);
if (suppressEvents !== true && me.fireEvent("beforeinsert", me, node, refNode) === false) {
return false;
}
// when moving internally, indexes will change after remove
if (oldParent == me && me.indexOf(node) < index) {
refIndex--;
}
// it's a move, make sure we move it cleanly
if (oldParent) {
if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index, refNode) === false) {
return false;
}
oldParent.removeChild(node);
}
if (refIndex === 0) {
me.setFirstChild(node);
}
Ext.Array.splice(me.childNodes, refIndex, 0, node);
node.parentNode = me;
node.nextSibling = refNode;
refNode.previousSibling = node;
ps = me.childNodes[refIndex - 1];
if (ps) {
node.previousSibling = ps;
ps.nextSibling = node;
ps.updateInfo();
} else {
node.previousSibling = null;
}
node.updateInfo();
if (!me.isLoaded()) {
me.set('loaded', true);
}
// If this node didn't have any childnodes before, update myself
else if (me.childNodes.length === 1) {
me.set('loaded', me.isLoaded());
}
if (suppressEvents !== true) {
me.fireEvent("insert", me, node, refNode);
if (oldParent) {
node.fireEvent("move", node, oldParent, me, refIndex, refNode);
}
}
return node;
},
/**
* Insert a node into this node.
* @param {Number} index The zero-based index to insert the node at.
* @param {Ext.data.Model} node The node to insert.
* @return {Ext.data.Model} The record you just inserted.
*/
insertChild: function(index, node) {
var sibling = this.childNodes[index];
if (sibling) {
return this.insertBefore(node, sibling);
}
else {
return this.appendChild(node);
}
},
/**
* Removes this node from its parent.
* @param {Boolean} [destroy=false] `true` to destroy the node upon removal.
* @return {Ext.data.NodeInterface} this
*/
remove : function(destroy, suppressEvents) {
var parentNode = this.parentNode;
if (parentNode) {
parentNode.removeChild(this, destroy, suppressEvents, true);
}
return this;
},
/**
* Removes all child nodes from this node.
* @param {Boolean} [destroy=false] `true` to destroy the node upon removal.
* @return {Ext.data.NodeInterface} this
*/
removeAll : function(destroy, suppressEvents) {
var cn = this.childNodes,
n;
while ((n = cn[0])) {
this.removeChild(n, destroy, suppressEvents);
}
return this;
},
/**
* Returns the child node at the specified index.
* @param {Number} index
* @return {Ext.data.NodeInterface}
*/
getChildAt : function(index) {
return this.childNodes[index];
},
/**
* Replaces one child node in this node with another.
* @param {Ext.data.NodeInterface} newChild The replacement node.
* @param {Ext.data.NodeInterface} oldChild The node to replace.
* @return {Ext.data.NodeInterface} The replaced node.
*/
replaceChild : function(newChild, oldChild, suppressEvents) {
var s = oldChild ? oldChild.nextSibling : null;
this.removeChild(oldChild, suppressEvents);
this.insertBefore(newChild, s, suppressEvents);
return oldChild;
},
/**
* Returns the index of a child node.
* @param {Ext.data.NodeInterface} node
* @return {Number} The index of the node or -1 if it was not found.
*/
indexOf : function(child) {
return Ext.Array.indexOf(this.childNodes, child);
},
/**
* Gets the hierarchical path from the root of the current node.
* @param {String} field (optional) The field to construct the path from. Defaults to the model `idProperty`.
* @param {String} [separator=/] (optional) A separator to use.
* @return {String} The node path
*/
getPath: function(field, separator) {
field = field || this.idProperty;
separator = separator || '/';
var path = [this.get(field)],
parent = this.parentNode;
while (parent) {
path.unshift(parent.get(field));
parent = parent.parentNode;
}
return separator + path.join(separator);
},
/**
* Returns depth of this node (the root node has a depth of 0).
* @return {Number}
*/
getDepth : function() {
return this.get('depth');
},
/**
* Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function
* will be the args provided or the current node. If the function returns `false` at any point,
* the bubble is stopped.
* @param {Function} fn The function to call.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the current Node.
* @param {Array} args (optional) The args to call the function with (default to passing the current Node).
*/
bubble : function(fn, scope, args) {
var p = this;
while (p) {
if (fn.apply(scope || p, args || [p]) === false) {
break;
}
p = p.parentNode;
}
},
/**
* Cascades down the tree from this node, calling the specified function with each node. The arguments to the function
* will be the args provided or the current node. If the function returns false at any point,
* the cascade is stopped on that branch.
* @param {Function} fn The function to call
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the current Node.
* @param {Array} args (optional) The args to call the function with (default to passing the current Node).
*/
cascadeBy : function(fn, scope, args) {
if (fn.apply(scope || this, args || [this]) !== false) {
var childNodes = this.childNodes,
length = childNodes.length,
i;
for (i = 0; i < length; i++) {
childNodes[i].cascadeBy(fn, scope, args);
}
}
},
/**
* Iterates the child nodes of this node, calling the specified function with each node. The arguments to the function
* will be the args provided or the current node. If the function returns false at any point,
* the iteration stops.
* @param {Function} fn The function to call.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the current Node in the iteration.
* @param {Array} args (optional) The args to call the function with (default to passing the current Node).
*/
eachChild : function(fn, scope, args) {
var childNodes = this.childNodes,
length = childNodes.length,
i;
for (i = 0; i < length; i++) {
if (fn.apply(scope || this, args || [childNodes[i]]) === false) {
break;
}
}
},
/**
* Finds the first child that has the attribute with the specified value.
* @param {String} attribute The attribute name.
* @param {Object} value The value to search for.
* @param {Boolean} deep (Optional) `true` to search through nodes deeper than the immediate children.
* @return {Ext.data.NodeInterface} The found child or `null` if none was found.
*/
findChild : function(attribute, value, deep) {
return this.findChildBy(function() {
return this.get(attribute) == value;
}, null, deep);
},
/**
* Finds the first child by a custom function. The child matches if the function passed returns `true`.
* @param {Function} fn A function which must return `true` if the passed Node is the required Node.
* @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to the Node being tested.
* @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children.
* @return {Ext.data.NodeInterface} The found child or null if `none` was found.
*/
findChildBy : function(fn, scope, deep) {
var cs = this.childNodes,
len = cs.length,
i = 0, n, res;
for (; i < len; i++) {
n = cs[i];
if (fn.call(scope || n, n) === true) {
return n;
}
else if (deep) {
res = n.findChildBy(fn, scope, deep);
if (res !== null) {
return res;
}
}
}
return null;
},
/**
* Returns `true` if this node is an ancestor (at any point) of the passed node.
* @param {Ext.data.NodeInterface} node
* @return {Boolean}
*/
contains : function(node) {
return node.isAncestor(this);
},
/**
* Returns `true` if the passed node is an ancestor (at any point) of this node.
* @param {Ext.data.NodeInterface} node
* @return {Boolean}
*/
isAncestor : function(node) {
var p = this.parentNode;
while (p) {
if (p == node) {
return true;
}
p = p.parentNode;
}
return false;
},
/**
* Sorts this nodes children using the supplied sort function.
* @param {Function} sortFn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order.
* @param {Boolean} recursive Whether or not to apply this sort recursively.
* @param {Boolean} suppressEvent Set to true to not fire a sort event.
*/
sort: function(sortFn, recursive, suppressEvent) {
var cs = this.childNodes,
ln = cs.length,
i, n;
if (ln > 0) {
Ext.Array.sort(cs, sortFn);
for (i = 0; i < ln; i++) {
n = cs[i];
n.previousSibling = cs[i-1];
n.nextSibling = cs[i+1];
if (i === 0) {
this.setFirstChild(n);
}
if (i == ln - 1) {
this.setLastChild(n);
}
n.updateInfo(suppressEvent);
if (recursive && !n.isLeaf()) {
n.sort(sortFn, true, true);
}
}
this.notifyStores('afterEdit', ['sorted'], {sorted: 'sorted'});
if (suppressEvent !== true) {
this.fireEvent('sort', this, cs);
}
}
},
/**
* Returns `true` if this node is expanded.
* @return {Boolean}
*/
isExpanded: function() {
return this.get('expanded');
},
/**
* Returns `true` if this node is loaded.
* @return {Boolean}
*/
isLoaded: function() {
return this.get('loaded');
},
/**
* Returns `true` if this node is loading.
* @return {Boolean}
*/
isLoading: function() {
return this.get('loading');
},
/**
* Returns `true` if this node is the root node.
* @return {Boolean}
*/
isRoot: function() {
return !this.parentNode;
},
/**
* Returns `true` if this node is visible.
* @return {Boolean}
*/
isVisible: function() {
var parent = this.parentNode;
while (parent) {
if (!parent.isExpanded()) {
return false;
}
parent = parent.parentNode;
}
return true;
},
/**
* Expand this node.
* @param {Function} recursive (Optional) `true` to recursively expand all the children.
* @param {Function} callback (Optional) The function to execute once the expand completes.
* @param {Object} scope (Optional) The scope to run the callback in.
*/
expand: function(recursive, callback, scope) {
var me = this;
if (!me.isLeaf()) {
if (me.isLoading()) {
me.on('expand', function() {
me.expand(recursive, callback, scope);
}, me, {single: true});
}
else {
if (!me.isExpanded()) {
// The TreeStore actually listens for the beforeexpand method and checks
// whether we have to asynchronously load the children from the server
// first. Thats why we pass a callback function to the event that the
// store can call once it has loaded and parsed all the children.
me.fireAction('expand', [this], function() {
me.set('expanded', true);
Ext.callback(callback, scope || me, [me.childNodes]);
});
}
else {
Ext.callback(callback, scope || me, [me.childNodes]);
}
}
} else {
Ext.callback(callback, scope || me);
}
},
/**
* Collapse this node.
* @param {Function} recursive (Optional) `true` to recursively collapse all the children.
* @param {Function} callback (Optional) The function to execute once the collapse completes.
* @param {Object} scope (Optional) The scope to run the callback in.
*/
collapse: function(recursive, callback, scope) {
var me = this;
// First we start by checking if this node is a parent
if (!me.isLeaf() && me.isExpanded()) {
this.fireAction('collapse', [me], function() {
me.set('expanded', false);
Ext.callback(callback, scope || me, [me.childNodes]);
});
} else {
Ext.callback(callback, scope || me, [me.childNodes]);
}
}
};
}
}
});
/**
* @private
*/
Ext.define('Ext.data.NodeStore', {
extend: 'Ext.data.Store',
alias: 'store.node',
requires: ['Ext.data.NodeInterface'],
config: {
/**
* @cfg {Ext.data.Model} node The Record you want to bind this Store to. Note that
* this record will be decorated with the {@link Ext.data.NodeInterface} if this is not the
* case yet.
* @accessor
*/
node: null,
/**
* @cfg {Boolean} recursive Set this to `true` if you want this NodeStore to represent
* all the descendants of the node in its flat data collection. This is useful for
* rendering a tree structure to a DataView and is being used internally by
* the TreeView. Any records that are moved, removed, inserted or appended to the
* node at any depth below the node this store is bound to will be automatically
* updated in this Store's internal flat data structure.
* @accessor
*/
recursive: false,
/**
* @cfg {Boolean} rootVisible `false` to not include the root node in this Stores collection.
* @accessor
*/
rootVisible: false,
sorters: undefined,
filters: undefined,
/**
* @cfg {Boolean} folderSort
* Set to `true` to automatically prepend a leaf sorter.
*/
folderSort: false
},
afterEdit: function(record, modifiedFields) {
if (modifiedFields) {
if (modifiedFields.indexOf('loaded') !== -1) {
return this.add(this.retrieveChildNodes(record));
}
if (modifiedFields.indexOf('expanded') !== -1) {
return this.filter();
}
if (modifiedFields.indexOf('sorted') !== -1) {
return this.sort();
}
}
this.callParent(arguments);
},
onNodeAppend: function(parent, node) {
this.add([node].concat(this.retrieveChildNodes(node)));
},
onNodeInsert: function(parent, node) {
this.add([node].concat(this.retrieveChildNodes(node)));
},
onNodeRemove: function(parent, node) {
this.remove([node].concat(this.retrieveChildNodes(node)));
},
onNodeSort: function() {
this.sort();
},
updateFolderSort: function(folderSort) {
if (folderSort) {
this.setGrouper(function(node) {
if (node.isLeaf()) {
return 1;
}
return 0;
});
} else {
this.setGrouper(null);
}
},
createDataCollection: function() {
var collection = this.callParent();
collection.handleSort = Ext.Function.bind(this.handleTreeSort, this, [collection], true);
collection.findInsertionIndex = Ext.Function.bind(this.handleTreeInsertionIndex, this, [collection, collection.findInsertionIndex], true);
return collection;
},
handleTreeInsertionIndex: function(items, item, collection, originalFn) {
return originalFn.call(collection, items, item, this.treeSortFn);
},
handleTreeSort: function(data) {
Ext.Array.sort(data, this.treeSortFn);
return data;
},
/**
* This is a custom tree sorting algorithm. It uses the index property on each node to determine
* how to sort siblings. It uses the depth property plus the index to create a weight for each node.
* This weight algorithm has the limitation of not being able to go more then 80 levels in depth, or
* more then 10k nodes per parent. The end result is a flat collection being correctly sorted based
* on this one single sort function.
* @param node1
* @param node2
* @return {Number}
* @private
*/
treeSortFn: function(node1, node2) {
// A shortcut for siblings
if (node1.parentNode === node2.parentNode) {
return (node1.data.index < node2.data.index) ? -1 : 1;
}
// @NOTE: with the following algorithm we can only go 80 levels deep in the tree
// and each node can contain 10000 direct children max
var weight1 = 0,
weight2 = 0,
parent1 = node1,
parent2 = node2;
while (parent1) {
weight1 += (Math.pow(10, (parent1.data.depth+1) * -4) * (parent1.data.index+1));
parent1 = parent1.parentNode;
}
while (parent2) {
weight2 += (Math.pow(10, (parent2.data.depth+1) * -4) * (parent2.data.index+1));
parent2 = parent2.parentNode;
}
if (weight1 > weight2) {
return 1;
} else if (weight1 < weight2) {
return -1;
}
return (node1.data.index > node2.data.index) ? 1 : -1;
},
applyFilters: function(filters) {
var me = this;
return function(item) {
return me.isVisible(item);
};
},
applyProxy: function(proxy) {
//<debug>
if (proxy) {
Ext.Logger.warn("A NodeStore cannot be bound to a proxy. Instead bind it to a record " +
"decorated with the NodeInterface by setting the node config.");
}
//</debug>
},
applyNode: function(node) {
if (node) {
node = Ext.data.NodeInterface.decorate(node);
}
return node;
},
updateNode: function(node, oldNode) {
if (oldNode && !oldNode.isDestroyed) {
oldNode.un({
append : 'onNodeAppend',
insert : 'onNodeInsert',
remove : 'onNodeRemove',
load : 'onNodeLoad',
scope: this
});
oldNode.unjoin(this);
}
if (node) {
node.on({
scope : this,
append : 'onNodeAppend',
insert : 'onNodeInsert',
remove : 'onNodeRemove',
load : 'onNodeLoad'
});
node.join(this);
var data = [];
if (node.childNodes.length) {
data = data.concat(this.retrieveChildNodes(node));
}
if (this.getRootVisible()) {
data.push(node);
} else if (node.isLoaded() || node.isLoading()) {
node.set('expanded', true);
}
this.data.clear();
this.fireEvent('clear', this);
this.suspendEvents();
this.add(data);
this.resumeEvents();
this.fireEvent('refresh', this, this.data);
}
},
/**
* Private method used to deeply retrieve the children of a record without recursion.
* @private
* @param root
* @return {Array}
*/
retrieveChildNodes: function(root) {
var node = this.getNode(),
recursive = this.getRecursive(),
added = [],
child = root;
if (!root.childNodes.length || (!recursive && root !== node)) {
return added;
}
if (!recursive) {
return root.childNodes;
}
while (child) {
if (child._added) {
delete child._added;
if (child === root) {
break;
} else {
child = child.nextSibling || child.parentNode;
}
} else {
if (child !== root) {
added.push(child);
}
if (child.firstChild) {
child._added = true;
child = child.firstChild;
} else {
child = child.nextSibling || child.parentNode;
}
}
}
return added;
},
/**
* @param {Object} node
* @return {Boolean}
*/
isVisible: function(node) {
var parent = node.parentNode;
if (!this.getRecursive() && parent !== this.getNode()) {
return false;
}
while (parent) {
if (!parent.isExpanded()) {
return false;
}
//we need to check this because for a nodestore the node is not likely to be the root
//so we stop going up the chain when we hit the original node as we don't care about any
//ancestors above the configured node
if (parent === this.getNode()) {
break;
}
parent = parent.parentNode;
}
return true;
}
});
/**
* @aside guide stores
*
* The TreeStore is a store implementation that allows for nested data.
*
* It provides convenience methods for loading nodes, as well as the ability to use
* the hierarchical tree structure combined with a store. This class also relays many events from
* the Tree for convenience.
*
* # Using Models
*
* If no Model is specified, an implicit model will be created that implements {@link Ext.data.NodeInterface}.
* The standard Tree fields will also be copied onto the Model for maintaining their state. These fields are listed
* in the {@link Ext.data.NodeInterface} documentation.
*
* # Reading Nested Data
*
* For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured with a root property,
* so the reader can find nested data for each node. If a root is not specified, it will default to
* 'children'.
*/
Ext.define('Ext.data.TreeStore', {
extend: 'Ext.data.NodeStore',
alias: 'store.tree',
config: {
/**
* @cfg {Ext.data.Model/Ext.data.NodeInterface/Object} root
* The root node for this store. For example:
*
* root: {
* expanded: true,
* text: "My Root",
* children: [
* { text: "Child 1", leaf: true },
* { text: "Child 2", expanded: true, children: [
* { text: "GrandChild", leaf: true }
* ] }
* ]
* }
*
* Setting the `root` config option is the same as calling {@link #setRootNode}.
* @accessor
*/
root: undefined,
/**
* @cfg {Boolean} clearOnLoad
* Remove previously existing child nodes before loading. Default to true.
* @accessor
*/
clearOnLoad : true,
/**
* @cfg {String} nodeParam
* The name of the parameter sent to the server which contains the identifier of the node.
* Defaults to 'node'.
* @accessor
*/
nodeParam: 'node',
/**
* @cfg {String} defaultRootId
* The default root id. Defaults to 'root'
* @accessor
*/
defaultRootId: 'root',
/**
* @cfg {String} defaultRootProperty
* The root property to specify on the reader if one is not explicitly defined.
* @accessor
*/
defaultRootProperty: 'children',
/**
* @cfg {Boolean} recursive
* @private
* @hide
*/
recursive: true
/**
* @cfg {Object} node
* @private
* @hide
*/
},
applyProxy: function() {
return Ext.data.Store.prototype.applyProxy.apply(this, arguments);
},
applyRoot: function(root) {
var me = this;
root = root || {};
root = Ext.apply({}, root);
if (!root.isModel) {
Ext.applyIf(root, {
id: me.getStoreId() + '-' + me.getDefaultRootId(),
text: 'Root',
allowDrag: false
});
root = Ext.data.ModelManager.create(root, me.getModel());
}
Ext.data.NodeInterface.decorate(root);
root.set(root.raw);
return root;
},
handleTreeInsertionIndex: function(items, item, collection, originalFn) {
if (item.parentNode) {
item.parentNode.sort(collection.getSortFn(), true, true);
}
return this.callParent(arguments);
},
handleTreeSort: function(data, collection) {
if (this._sorting) {
return data;
}
this._sorting = true;
this.getNode().sort(collection.getSortFn(), true, true);
delete this._sorting;
return this.callParent(arguments);
},
updateRoot: function(root, oldRoot) {
if (oldRoot) {
oldRoot.unBefore({
expand: 'onNodeBeforeExpand',
scope: this
});
oldRoot.unjoin(this);
}
root.onBefore({
expand: 'onNodeBeforeExpand',
scope: this
});
this.onNodeAppend(null, root);
this.setNode(root);
if (!root.isLoaded() && !root.isLoading() && root.isExpanded()) {
this.load({
node: root
});
}
/**
* @event rootchange
* Fires whenever the root node changes on this TreeStore.
* @param {Ext.data.TreeStore} store This tree Store
* @param {Ext.data.Model} newRoot The new root node
* @param {Ext.data.Model} oldRoot The old root node
*/
this.fireEvent('rootchange', this, root, oldRoot);
},
/**
* Returns the record node by id
* @return {Ext.data.NodeInterface}
*/
getNodeById: function(id) {
return this.data.getByKey(id);
},
onNodeBeforeExpand: function(node, options, e) {
if (node.isLoading()) {
e.pause();
this.on('load', function() {
e.resume();
}, this, {single: true});
}
else if (!node.isLoaded()) {
e.pause();
this.load({
node: node,
callback: function() {
e.resume();
}
});
}
},
onNodeAppend: function(parent, node) {
var proxy = this.getProxy(),
reader = proxy.getReader(),
Model = this.getModel(),
data = node.raw,
records = [],
rootProperty = reader.getRootProperty(),
dataRoot, processedData, i, ln, processedDataItem;
if (!node.isLeaf()) {
dataRoot = reader.getRoot(data);
if (dataRoot) {
processedData = reader.extractData(dataRoot);
for (i = 0, ln = processedData.length; i < ln; i++) {
processedDataItem = processedData[i];
records.push(new Model(processedDataItem.data, processedDataItem.id, processedDataItem.node));
}
if (records.length) {
this.fillNode(node, records);
} else {
node.set('loaded', true);
}
// If the child record is not a leaf, and it has a data root (e.g. items: [])
// and there are items in this data root, then we call fillNode to automatically
// add these items. fillNode sets the loaded property on the node, meaning that
// the next time you expand that node, it's not going to the server to request the
// children. If however you pass back an empty array as items, we have to set the
// loaded property to true here as well to prevent the items from being be loaded
// from the server the next time you expand it.
// If you want to have the items loaded on the next expand, then the data for the
// node should not contain the items: [] array.
delete data[rootProperty];
}
}
},
updateAutoLoad: function(autoLoad) {
if (autoLoad) {
var root = this.getRoot();
if (!root.isLoaded() && !root.isLoading()) {
this.load({node: root});
}
}
},
/**
* Loads the Store using its configured {@link #proxy}.
* @param {Object} options (Optional) config object. This is passed into the {@link Ext.data.Operation Operation}
* object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function.
* The options can also contain a node, which indicates which node is to be loaded. If not specified, it will
* default to the root node.
* @return {Object}
*/
load: function(options) {
options = options || {};
options.params = options.params || {};
var me = this,
node = options.node = options.node || me.getRoot();
options.params[me.getNodeParam()] = node.getId();
if (me.getClearOnLoad()) {
node.removeAll(true);
}
node.set('loading', true);
return me.callParent([options]);
},
updateProxy: function(proxy) {
this.callParent(arguments);
var reader = proxy.getReader();
if (!reader.getRootProperty()) {
reader.setRootProperty(this.getDefaultRootProperty());
reader.buildExtractors();
}
},
/**
* @inheritdoc
*/
removeAll: function() {
this.getRoot().removeAll(true);
this.callParent(arguments);
},
/**
* @inheritdoc
*/
onProxyLoad: function(operation) {
var me = this,
records = operation.getRecords(),
successful = operation.wasSuccessful(),
node = operation.getNode();
node.beginEdit();
node.set('loading', false);
if (successful) {
records = me.fillNode(node, records);
}
node.endEdit();
me.loading = false;
me.loaded = true;
node.fireEvent('load', node, records, successful);
me.fireEvent('load', this, records, successful, operation);
//this is a callback that would have been passed to the 'read' function and is optional
Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, successful]);
},
/**
* Fills a node with a series of child records.
* @private
* @param {Ext.data.NodeInterface} node The node to fill.
* @param {Ext.data.Model[]} records The records to add.
*/
fillNode: function(node, records) {
var ln = records ? records.length : 0,
i, child;
for (i = 0; i < ln; i++) {
// true/true to suppress any events fired by the node, or the new child node
child = node.appendChild(records[i], true, true);
this.onNodeAppend(node, child);
}
node.set('loaded', true);
return records;
}
});
/**
* @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.
* @return {Object}
*/
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: /^\s*[\w\-\+_]+(\.[\w\-\+_]+)*\@[\w\-\+_]+\.[\w\-\+_]+(\.[\w\-\+_]+)*\s*$/,
/**
* 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 (arguments.length === 1) {
value = config;
}
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) {
if (value === undefined || value === null) {
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 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('');
}
});
/**
* @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](http://www.ietf.org/rfc/rfc4122.txt) for details.
*/
Ext.define('Ext.data.identifier.Uuid', {
extend: 'Ext.data.identifier.Simple',
alias: 'data.identifier.uuid',
isUnique: true,
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
* @aside guide proxies
*
* The JsonP proxy is useful when you need to load data from a domain other than the one your application is running on. If
* your application is running on http://domainA.com it cannot use {@link Ext.data.proxy.Ajax Ajax} to load its data
* from http://domainB.com because cross-domain ajax requests are prohibited by the browser.
*
* We can get around this using a JsonP proxy. JsonP proxy injects a `<script>` tag into the DOM whenever an AJAX request
* would usually be made. Let's say we want to load data from http://domainB.com/users - the script tag that would be
* injected might look like this:
*
* <script src="http://domainB.com/users?callback=someCallback"></script>
*
* When we inject the tag above, the browser makes a request to that url and includes the response as if it was any
* other type of JavaScript include. By passing a callback in the url above, we're telling domainB's server that we want
* to be notified when the result comes in and that it should call our callback function with the data it sends back. So
* long as the server formats the response to look like this, everything will work:
*
* someCallback({
* users: [
* {
* id: 1,
* name: "Ed Spencer",
* email: "ed@sencha.com"
* }
* ]
* });
*
* As soon as the script finishes loading, the 'someCallback' function that we passed in the url is called with the JSON
* object that the server returned.
*
* JsonP proxy takes care of all of this automatically. It formats the url you pass, adding the callback parameter
* automatically. It even creates a temporary callback function, waits for it to be called and then puts the data into
* the Proxy making it look just like you loaded it through a normal {@link Ext.data.proxy.Ajax AjaxProxy}. Here's how
* we might set that up:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['id', 'name', 'email']
* }
* });
*
* var store = Ext.create('Ext.data.Store', {
* model: 'User',
* proxy: {
* type: 'jsonp',
* url : 'http://domainB.com/users'
* }
* });
*
* store.load();
*
* That's all we need to do - JsonP proxy takes care of the rest. In this case the Proxy will have injected a script tag
* like this:
*
* <script src="http://domainB.com/users?callback=callback1"></script>
*
* # Customization
*
* This script tag can be customized using the {@link #callbackKey} configuration. For example:
*
* var store = Ext.create('Ext.data.Store', {
* model: 'User',
* proxy: {
* type: 'jsonp',
* url : 'http://domainB.com/users',
* callbackKey: 'theCallbackFunction'
* }
* });
*
* store.load();
*
* Would inject a script tag like this:
*
* <script src="http://domainB.com/users?theCallbackFunction=callback1"></script>
*
* # Implementing on the server side
*
* The remote server side needs to be configured to return data in this format. Here are suggestions for how you might
* achieve this using Java, PHP and ASP.net:
*
* Java:
*
* boolean jsonP = false;
* String cb = request.getParameter("callback");
* if (cb != null) {
* jsonP = true;
* response.setContentType("text/javascript");
* } else {
* response.setContentType("application/x-json");
* }
* Writer out = response.getWriter();
* if (jsonP) {
* out.write(cb + "(");
* }
* out.print(dataBlock.toJsonString());
* if (jsonP) {
* out.write(");");
* }
*
* PHP:
*
* $callback = $_REQUEST['callback'];
*
* // Create the output object.
* $output = array('a' => 'Apple', 'b' => 'Banana');
*
* //start output
* if ($callback) {
* header('Content-Type: text/javascript');
* echo $callback . '(' . json_encode($output) . ');';
* } else {
* header('Content-Type: application/x-json');
* echo json_encode($output);
* }
*
* ASP.net:
*
* String jsonString = "{success: true}";
* String cb = Request.Params.Get("callback");
* String responseString = "";
* if (!String.IsNullOrEmpty(cb)) {
* responseString = cb + "(" + jsonString + ")";
* } else {
* responseString = jsonString;
* }
* Response.Write(responseString);
*/
Ext.define('Ext.data.proxy.JsonP', {
extend: 'Ext.data.proxy.Server',
alternateClassName: 'Ext.data.ScriptTagProxy',
alias: ['proxy.jsonp', 'proxy.scripttag'],
requires: ['Ext.data.JsonP'],
config: {
defaultWriterType: 'base',
/**
* @cfg {String} callbackKey
* See {@link Ext.data.JsonP#callbackKey}.
* @accessor
*/
callbackKey : 'callback',
/**
* @cfg {String} recordParam
* The param name to use when passing records to the server (e.g. 'records=someEncodedRecordString').
* @accessor
*/
recordParam: 'records',
/**
* @cfg {Boolean} autoAppendParams
* `true` to automatically append the request's params to the generated url.
* @accessor
*/
autoAppendParams: true
},
/**
* Performs the read request to the remote domain. JsonP proxy does not actually create an Ajax request,
* instead we write out a `<script>` tag based on the configuration of the internal Ext.data.Request object
* @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute.
* @param {Function} callback A callback function to execute when the Operation has been completed.
* @param {Object} scope The scope to execute the callback in.
* @return {Object}
* @protected
*/
doRequest: function(operation, callback, scope) {
// <debug>
var action = operation.getAction();
if (action !== 'read') {
Ext.Logger.error('JsonP proxies can only be used to read data.');
}
// </debug>
//generate the unique IDs for this request
var me = this,
request = me.buildRequest(operation),
params = request.getParams();
// apply JsonP proxy-specific attributes to the Request
request.setConfig({
callbackKey: me.getCallbackKey(),
timeout: me.getTimeout(),
scope: me,
callback: me.createRequestCallback(request, operation, callback, scope)
});
// Prevent doubling up because the params are already added to the url in buildUrl
if (me.getAutoAppendParams()) {
request.setParams({});
}
request.setJsonP(Ext.data.JsonP.request(request.getCurrentConfig()));
// Set the params back once we have made the request though
request.setParams(params);
operation.setStarted();
me.lastRequest = request;
return request;
},
/**
* @private
* Creates and returns the function that is called when the request has completed. The returned function
* should accept a Response object, which contains the response to be read by the configured Reader.
* The third argument is the callback that should be called after the request has been completed and the Reader has decoded
* the response. This callback will typically be the callback passed by a store, e.g. in proxy.read(operation, theCallback, scope)
* theCallback refers to the callback argument received by this function.
* See {@link #doRequest} for details.
* @param {Ext.data.Request} request The Request object.
* @param {Ext.data.Operation} operation The Operation being executed.
* @param {Function} callback The callback function to be called when the request completes. This is usually the callback
* passed to doRequest.
* @param {Object} scope The scope in which to execute the callback function.
* @return {Function} The callback function.
*/
createRequestCallback: function(request, operation, callback, scope) {
var me = this;
return function(success, response, errorType) {
delete me.lastRequest;
me.processResponse(success, operation, request, response, callback, scope);
};
},
// @inheritdoc
setException: function(operation, response) {
operation.setException(operation.getRequest().getJsonP().errorType);
},
/**
* Generates a url based on a given Ext.data.Request object. Adds the params and callback function name to the url
* @param {Ext.data.Request} request The request object.
* @return {String} The url.
*/
buildUrl: function(request) {
var me = this,
url = me.callParent(arguments),
params = Ext.apply({}, request.getParams()),
filters = params.filters,
records,
filter, i, value;
delete params.filters;
if (me.getAutoAppendParams()) {
url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
}
if (filters && filters.length) {
for (i = 0; i < filters.length; i++) {
filter = filters[i];
value = filter.getValue();
if (value) {
url = Ext.urlAppend(url, filter.getProperty() + "=" + value);
}
}
}
return url;
},
/**
* @inheritdoc
*/
destroy: function() {
this.abort();
this.callParent(arguments);
},
/**
* Aborts the current server request if one is currently running.
*/
abort: function() {
var lastRequest = this.lastRequest;
if (lastRequest) {
Ext.data.JsonP.abort(lastRequest.getJsonP());
}
}
});
/**
* @author Ed Spencer
*
* WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage} proxy. It uses the
* new HTML5 key/value client-side storage objects to save {@link Ext.data.Model model instances} for offline use.
* @private
*/
Ext.define('Ext.data.proxy.WebStorage', {
extend: 'Ext.data.proxy.Client',
alternateClassName: 'Ext.data.WebStorageProxy',
requires: 'Ext.Date',
config: {
/**
* @cfg {String} id
* The unique ID used as the key in which all record data are stored in the local storage object.
*/
id: undefined,
// WebStorage proxies dont use readers and writers
/**
* @cfg
* @hide
*/
reader: null,
/**
* @cfg
* @hide
*/
writer: null,
/**
* @cfg {Boolean} enablePagingParams This can be set to true if you want the webstorage proxy to comply
* to the paging params set on the store.
*/
enablePagingParams: false
},
/**
* Creates the proxy, throws an error if local storage is not supported in the current browser.
* @param {Object} config (optional) Config object.
*/
constructor: function(config) {
this.callParent(arguments);
/**
* @property {Object} cache
* Cached map of records already retrieved by this Proxy. Ensures that the same instance is always retrieved.
*/
this.cache = {};
//<debug>
if (this.getStorageObject() === undefined) {
Ext.Logger.error("Local Storage is not supported in this browser, please use another type of data proxy");
}
//</debug>
},
updateModel: function(model) {
if (!this.getId()) {
this.setId(model.modelName);
}
},
//inherit docs
create: function(operation, callback, scope) {
var records = operation.getRecords(),
length = records.length,
ids = this.getIds(),
id, record, i;
operation.setStarted();
for (i = 0; i < length; i++) {
record = records[i];
// <debug>
if (!this.getModel().getIdentifier().isUnique) {
Ext.Logger.warn('Your identifier generation strategy for the model does not ensure unique id\'s. Please use the UUID strategy, or implement your own identifier strategy with the flag isUnique.');
}
// </debug>
id = record.getId();
this.setRecord(record);
ids.push(id);
}
this.setIds(ids);
operation.setCompleted();
operation.setSuccessful();
if (typeof callback == 'function') {
callback.call(scope || this, operation);
}
},
//inherit docs
read: function(operation, callback, scope) {
var records = [],
ids = this.getIds(),
model = this.getModel(),
idProperty = model.getIdProperty(),
params = operation.getParams() || {},
sorters = operation.getSorters(),
filters = operation.getFilters(),
start = operation.getStart(),
limit = operation.getLimit(),
length = ids.length,
i, record, collection;
//read a single record
if (params[idProperty] !== undefined) {
record = this.getRecord(params[idProperty]);
if (record) {
records.push(record);
operation.setSuccessful();
}
} else {
for (i = 0; i < length; i++) {
records.push(this.getRecord(ids[i]));
}
collection = Ext.create('Ext.util.Collection');
// First we comply to filters
if (filters && filters.length) {
collection.setFilters(filters);
}
// Then we comply to sorters
if (sorters && sorters.length) {
collection.setSorters(sorters);
}
collection.addAll(records);
if (this.getEnablePagingParams() && start !== undefined && limit !== undefined) {
records = collection.items.slice(start, start + limit);
} else {
records = collection.items.slice();
}
operation.setSuccessful();
}
operation.setCompleted();
operation.setResultSet(Ext.create('Ext.data.ResultSet', {
records: records,
total : records.length,
loaded : true
}));
operation.setRecords(records);
if (typeof callback == 'function') {
callback.call(scope || this, operation);
}
},
//inherit docs
update: function(operation, callback, scope) {
var records = operation.getRecords(),
length = records.length,
ids = this.getIds(),
record, id, i;
operation.setStarted();
for (i = 0; i < length; i++) {
record = records[i];
this.setRecord(record);
//we need to update the set of ids here because it's possible that a non-phantom record was added
//to this proxy - in which case the record's id would never have been added via the normal 'create' call
id = record.getId();
if (id !== undefined && Ext.Array.indexOf(ids, id) == -1) {
ids.push(id);
}
}
this.setIds(ids);
operation.setCompleted();
operation.setSuccessful();
if (typeof callback == 'function') {
callback.call(scope || this, operation);
}
},
//inherit
destroy: function(operation, callback, scope) {
var records = operation.getRecords(),
length = records.length,
ids = this.getIds(),
//newIds is a copy of ids, from which we remove the destroyed records
newIds = [].concat(ids),
i;
operation.setStarted();
for (i = 0; i < length; i++) {
Ext.Array.remove(newIds, records[i].getId());
this.removeRecord(records[i], false);
}
this.setIds(newIds);
operation.setCompleted();
operation.setSuccessful();
if (typeof callback == 'function') {
callback.call(scope || this, operation);
}
},
/**
* @private
* Fetches a model instance from the Proxy by ID. Runs each field's decode function (if present) to decode the data.
* @param {String} id The record's unique ID
* @return {Ext.data.Model} The model instance or undefined if the record did not exist in the storage.
*/
getRecord: function(id) {
if (this.cache[id] === undefined) {
var recordKey = this.getRecordKey(id),
item = this.getStorageObject().getItem(recordKey),
data = {},
Model = this.getModel(),
fields = Model.getFields().items,
length = fields.length,
i, field, name, record, rawData, dateFormat;
if (!item) {
return undefined;
}
rawData = Ext.decode(item);
for (i = 0; i < length; i++) {
field = fields[i];
name = field.getName();
if (typeof field.getDecode() == 'function') {
data[name] = field.getDecode()(rawData[name]);
} else {
if (field.getType().type == 'date') {
dateFormat = field.getDateFormat();
if (dateFormat) {
data[name] = Ext.Date.parse(rawData[name], dateFormat);
} else {
data[name] = new Date(rawData[name]);
}
} else {
data[name] = rawData[name];
}
}
}
record = new Model(data, id);
this.cache[id] = record;
}
return this.cache[id];
},
/**
* Saves the given record in the Proxy. Runs each field's encode function (if present) to encode the data.
* @param {Ext.data.Model} record The model instance
* @param {String} [id] The id to save the record under (defaults to the value of the record's getId() function)
*/
setRecord: function(record, id) {
if (id) {
record.setId(id);
} else {
id = record.getId();
}
var me = this,
rawData = record.getData(),
data = {},
Model = me.getModel(),
fields = Model.getFields().items,
length = fields.length,
i = 0,
field, name, obj, key, dateFormat;
for (; i < length; i++) {
field = fields[i];
name = field.getName();
if (field.getPersist() === false) {
continue;
}
if (typeof field.getEncode() == 'function') {
data[name] = field.getEncode()(rawData[name], record);
} else {
if (field.getType().type == 'date' && Ext.isDate(rawData[name])) {
dateFormat = field.getDateFormat();
if (dateFormat) {
data[name] = Ext.Date.format(rawData[name], dateFormat);
} else {
data[name] = rawData[name].getTime();
}
} else {
data[name] = rawData[name];
}
}
}
obj = me.getStorageObject();
key = me.getRecordKey(id);
//keep the cache up to date
me.cache[id] = record;
//iPad bug requires that we remove the item before setting it
obj.removeItem(key);
try {
obj.setItem(key, Ext.encode(data));
} catch(e){
this.fireEvent('exception', this, e);
}
record.commit();
},
/**
* @private
* Physically removes a given record from the local storage. Used internally by {@link #destroy}, which you should
* use instead because it updates the list of currently-stored record ids
* @param {String/Number/Ext.data.Model} id The id of the record to remove, or an Ext.data.Model instance
*/
removeRecord: function(id, updateIds) {
var me = this,
ids;
if (id.isModel) {
id = id.getId();
}
if (updateIds !== false) {
ids = me.getIds();
Ext.Array.remove(ids, id);
me.setIds(ids);
}
me.getStorageObject().removeItem(me.getRecordKey(id));
},
/**
* @private
* Given the id of a record, returns a unique string based on that id and the id of this proxy. This is used when
* storing data in the local storage object and should prevent naming collisions.
* @param {String/Number/Ext.data.Model} id The record id, or a Model instance
* @return {String} The unique key for this record
*/
getRecordKey: function(id) {
if (id.isModel) {
id = id.getId();
}
return Ext.String.format("{0}-{1}", this.getId(), id);
},
/**
* @private
* Returns the array of record IDs stored in this Proxy
* @return {Number[]} The record IDs. Each is cast as a Number
*/
getIds: function() {
var ids = (this.getStorageObject().getItem(this.getId()) || "").split(","),
length = ids.length,
i;
if (length == 1 && ids[0] === "") {
ids = [];
}
return ids;
},
/**
* @private
* Saves the array of ids representing the set of all records in the Proxy
* @param {Number[]} ids The ids to set
*/
setIds: function(ids) {
var obj = this.getStorageObject(),
str = ids.join(","),
id = this.getId();
obj.removeItem(id);
if (!Ext.isEmpty(str)) {
try {
obj.setItem(id, str);
} catch(e){
this.fireEvent('exception', this, e);
}
}
},
/**
* @private
* Sets up the Proxy by claiming the key in the storage object that corresponds to the unique id of this Proxy. Called
* automatically by the constructor, this should not need to be called again unless {@link #clear} has been called.
*/
initialize: function() {
this.callParent(arguments);
var storageObject = this.getStorageObject();
try {
storageObject.setItem(this.getId(), storageObject.getItem(this.getId()) || "");
} catch(e){
this.fireEvent('exception', this, e);
}
},
/**
* Destroys all records stored in the proxy and removes all keys and values used to support the proxy from the
* storage object.
*/
clear: function() {
var obj = this.getStorageObject(),
ids = this.getIds(),
len = ids.length,
i;
//remove all the records
for (i = 0; i < len; i++) {
this.removeRecord(ids[i], false);
}
//remove the supporting objects
obj.removeItem(this.getId());
},
/**
* @private
* Abstract function which should return the storage object that data will be saved to. This must be implemented
* in each subclass.
* @return {Object} The storage object
*/
getStorageObject: function() {
//<debug>
Ext.Logger.error("The getStorageObject function has not been defined in your Ext.data.proxy.WebStorage subclass");
//</debug>
}
});
/**
* @author Ed Spencer
* @aside guide proxies
*
* The LocalStorageProxy uses the new HTML5 localStorage API to save {@link Ext.data.Model Model} data locally on the
* client browser. HTML5 localStorage is a key-value store (e.g. cannot save complex objects like JSON), so
* LocalStorageProxy automatically serializes and deserializes data when saving and retrieving it.
*
* localStorage is extremely useful for saving user-specific information without needing to build server-side
* infrastructure to support it. Let's imagine we're writing a Twitter search application and want to save the user's
* searches locally so they can easily perform a saved search again later. We'd start by creating a Search model:
*
* Ext.define('Search', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['id', 'query'],
* proxy: {
* type: 'localstorage',
* id : 'twitter-Searches'
* }
* }
* });
*
* Our Search model contains just two fields - id and query - plus a Proxy definition. The only configuration we need to
* pass to the LocalStorage proxy is an {@link #id}. This is important as it separates the Model data in this Proxy from
* all others. The localStorage API puts all data into a single shared namespace, so by setting an id we enable
* LocalStorageProxy to manage the saved Search data.
*
* Saving our data into localStorage is easy and would usually be done with a {@link Ext.data.Store Store}:
*
* //our Store automatically picks up the LocalStorageProxy defined on the Search model
* var store = Ext.create('Ext.data.Store', {
* model: "Search"
* });
*
* //loads any existing Search data from localStorage
* store.load();
*
* //now add some Searches
* store.add({query: 'Sencha Touch'});
* store.add({query: 'Ext JS'});
*
* //finally, save our Search data to localStorage
* store.sync();
*
* The LocalStorageProxy automatically gives our new Searches an id when we call store.sync(). It encodes the Model data
* and places it into localStorage. We can also save directly to localStorage, bypassing the Store altogether:
*
* var search = Ext.create('Search', {query: 'Sencha Animator'});
*
* //uses the configured LocalStorageProxy to save the new Search to localStorage
* search.save();
*
* # Limitations
*
* If this proxy is used in a browser where local storage is not supported, the constructor will throw an error. A local
* storage proxy requires a unique ID which is used as a key in which all record data are stored in the local storage
* object.
*
* It's important to supply this unique ID as it cannot be reliably determined otherwise. If no id is provided but the
* attached store has a storeId, the storeId will be used. If neither option is presented the proxy will throw an error.
*/
Ext.define('Ext.data.proxy.LocalStorage', {
extend: 'Ext.data.proxy.WebStorage',
alias: 'proxy.localstorage',
alternateClassName: 'Ext.data.LocalStorageProxy',
//inherit docs
getStorageObject: function() {
return window.localStorage;
}
});
/**
* @author Ed Spencer
* @aside guide proxies
*
* The Rest proxy is a specialization of the {@link Ext.data.proxy.Ajax AjaxProxy} which simply maps the four actions
* (create, read, update and destroy) to RESTful HTTP verbs. For example, let's set up a {@link Ext.data.Model Model}
* with an inline Rest proxy:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['id', 'name', 'email'],
*
* proxy: {
* type: 'rest',
* url : '/users'
* }
* }
* });
*
* Now we can create a new User instance and save it via the Rest proxy. Doing this will cause the Proxy to send a POST
* request to '/users':
*
* var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
*
* user.save(); //POST /users
*
* Let's expand this a little and provide a callback for the {@link Ext.data.Model#save} call to update the Model once
* it has been created. We'll assume the creation went successfully and that the server gave this user an ID of 123:
*
* user.save({
* success: function(user) {
* user.set('name', 'Khan Noonien Singh');
*
* user.save(); //PUT /users/123
* }
* });
*
* Now that we're no longer creating a new Model instance, the request method is changed to an HTTP PUT, targeting the
* relevant url for that user. Now let's delete this user, which will use the DELETE method:
*
* user.erase(); //DELETE /users/123
*
* Finally, when we perform a load of a Model or Store, Rest proxy will use the GET method:
*
* //1. Load via Store
*
* //the Store automatically picks up the Proxy from the User model
* var store = Ext.create('Ext.data.Store', {
* model: 'User'
* });
*
* store.load(); //GET /users
*
* //2. Load directly from the Model
*
* //GET /users/123
* Ext.ModelManager.getModel('User').load(123, {
* success: function(user) {
* console.log(user.getId()); //outputs 123
* }
* });
*
* # Url generation
*
* The Rest proxy is able to automatically generate the urls above based on two configuration options - {@link #appendId} and
* {@link #format}. If appendId is true (it is by default) then Rest proxy will automatically append the ID of the Model
* instance in question to the configured url, resulting in the '/users/123' that we saw above.
*
* If the request is not for a specific Model instance (e.g. loading a Store), the url is not appended with an id.
* The Rest proxy will automatically insert a '/' before the ID if one is not already present.
*
* new Ext.data.proxy.Rest({
* url: '/users',
* appendId: true //default
* });
*
* // Collection url: /users
* // Instance url : /users/123
*
* The Rest proxy can also optionally append a format string to the end of any generated url:
*
* new Ext.data.proxy.Rest({
* url: '/users',
* format: 'json'
* });
*
* // Collection url: /users.json
* // Instance url : /users/123.json
*
* If further customization is needed, simply implement the {@link #buildUrl} method and add your custom generated url
* onto the {@link Ext.data.Request Request} object that is passed to buildUrl. See [Rest proxy's implementation][1] for
* an example of how to achieve this.
*
* Note that Rest proxy inherits from {@link Ext.data.proxy.Ajax AjaxProxy}, which already injects all of the sorter,
* filter, group and paging options into the generated url. See the {@link Ext.data.proxy.Ajax AjaxProxy docs} for more
* details.
*
* [1]: source/Rest.html#Ext-data-proxy-Rest-method-buildUrl
*/
Ext.define('Ext.data.proxy.Rest', {
extend: 'Ext.data.proxy.Ajax',
alternateClassName: 'Ext.data.RestProxy',
alias : 'proxy.rest',
config: {
/**
* @cfg {Boolean} appendId
* `true` to automatically append the ID of a Model instance when performing a request based on that single instance.
* See Rest proxy intro docs for more details.
*/
appendId: true,
/**
* @cfg {String} format
* Optional data format to send to the server when making any request (e.g. 'json'). See the Rest proxy intro docs
* for full details.
*/
format: null,
/**
* @cfg {Boolean} batchActions
* `true` to batch actions of a particular type when synchronizing the store.
*/
batchActions: false,
actionMethods: {
create : 'POST',
read : 'GET',
update : 'PUT',
destroy: 'DELETE'
}
},
/**
* Specialized version of `buildUrl` that incorporates the {@link #appendId} and {@link #format} options into the
* generated url. Override this to provide further customizations, but remember to call the superclass `buildUrl` so
* that additional parameters like the cache buster string are appended.
* @param {Object} request
* @return {Object}
*/
buildUrl: function(request) {
var me = this,
operation = request.getOperation(),
records = operation.getRecords() || [],
record = records[0],
model = me.getModel(),
idProperty= model.getIdProperty(),
format = me.getFormat(),
url = me.getUrl(request),
params = request.getParams() || {},
id = (record && !record.phantom) ? record.getId() : params[idProperty];
if (me.getAppendId() && id) {
if (!url.match(/\/$/)) {
url += '/';
}
url += id;
delete params[idProperty];
}
if (format) {
if (!url.match(/\.$/)) {
url += '.';
}
url += format;
}
request.setUrl(url);
return me.callParent([request]);
}
});
/**
* SQL proxy.
*/
Ext.define('Ext.data.proxy.SQL', {
alias: 'proxy.sql',
extend: 'Ext.data.proxy.Client',
config: {
/**
* @cfg {Object} reader
* @hide
*/
reader: null,
/**
* @cfg {Object} writer
* @hide
*/
writer: null,
table: null,
database: 'Sencha',
columns: '',
uniqueIdStrategy: false,
tableExists: false,
defaultDateFormat: 'Y-m-d H:i:s.u'
},
updateModel: function(model) {
if (model && !this.getTable()) {
var modelName = model.modelName,
defaultDateFormat = this.getDefaultDateFormat(),
table = modelName.slice(modelName.lastIndexOf('.') + 1);
model.getFields().each(function (field) {
if (field.getType().type === 'date' && !field.getDateFormat()) {
field.setDateFormat(defaultDateFormat);
}
});
this.setUniqueIdStrategy(model.getIdentifier().isUnique);
this.setTable(table);
this.setColumns(this.getPersistedModelColumns(model));
}
this.callParent(arguments);
},
create: function (operation, callback, scope) {
var me = this,
db = me.getDatabaseObject(),
records = operation.getRecords(),
tableExists = me.getTableExists();
operation.setStarted();
db.transaction(function(transaction) {
if (!tableExists) {
me.createTable(transaction);
}
me.insertRecords(records, transaction, function(resultSet, errors) {
if (operation.process(operation.getAction(), resultSet) === false) {
me.fireEvent('exception', this, operation);
}
if (typeof callback == 'function') {
callback.call(scope || this, operation);
}
}, this);
});
},
read: function(operation, callback, scope) {
var me = this,
db = me.getDatabaseObject(),
model = me.getModel(),
idProperty = model.getIdProperty(),
tableExists = me.getTableExists(),
params = operation.getParams() || {},
id = params[idProperty],
sorters = operation.getSorters(),
filters = operation.getFilters(),
page = operation.getPage(),
start = operation.getStart(),
limit = operation.getLimit(),
filtered, i, ln;
params = Ext.apply(params, {
page: page,
start: start,
limit: limit,
sorters: sorters,
filters: filters
});
operation.setStarted();
db.transaction(function(transaction) {
if (!tableExists) {
me.createTable(transaction);
}
me.selectRecords(transaction, id !== undefined ? id : params, function (resultSet, errors) {
if (operation.process(operation.getAction(), resultSet) === false) {
me.fireEvent('exception', me, operation);
}
if (filters.length) {
filtered = Ext.create('Ext.util.Collection', function(record) {
return record.getId();
});
filtered.setFilterRoot('data');
for (i = 0, ln = filters.length; i < ln; i++) {
if (filters[i].getProperty() === null) {
filtered.addFilter(filters[i]);
}
}
filtered.addAll(operation.getRecords());
operation.setRecords(filtered.items.slice());
resultSet.setRecords(operation.getRecords());
resultSet.setCount(filtered.items.length);
resultSet.setTotal(filtered.items.length);
}
if (typeof callback == 'function') {
callback.call(scope || me, operation);
}
});
});
},
update: function(operation, callback, scope) {
var me = this,
records = operation.getRecords(),
db = me.getDatabaseObject(),
tableExists = me.getTableExists();
operation.setStarted();
db.transaction(function (transaction) {
if (!tableExists) {
me.createTable(transaction);
}
me.updateRecords(transaction, records, function(resultSet, errors) {
if (operation.process(operation.getAction(), resultSet) === false) {
me.fireEvent('exception', me, operation);
}
if (typeof callback == 'function') {
callback.call(scope || me, operation);
}
});
});
},
destroy: function(operation, callback, scope) {
var me = this,
records = operation.getRecords(),
db = me.getDatabaseObject(),
tableExists = me.getTableExists();
operation.setStarted();
db.transaction(function(transaction) {
if (!tableExists) {
me.createTable(transaction);
}
me.destroyRecords(transaction, records, function(resultSet, errors) {
if (operation.process(operation.getAction(), resultSet) === false) {
me.fireEvent('exception', me, operation);
}
if (typeof callback == 'function') {
callback.call(scope || me, operation);
}
});
});
},
createTable: function (transaction) {
transaction.executeSql('CREATE TABLE IF NOT EXISTS ' + this.getTable() + ' (' + this.getSchemaString() + ')');
this.setTableExists(true);
},
insertRecords: function(records, transaction, callback, scope) {
var me = this,
table = me.getTable(),
columns = me.getColumns(),
totalRecords = records.length,
executed = 0,
tmp = [],
insertedRecords = [],
errors = [],
uniqueIdStrategy = me.getUniqueIdStrategy(),
i, ln, placeholders, result;
result = new Ext.data.ResultSet({
records: insertedRecords,
success: true
});
for (i = 0, ln = columns.length; i < ln; i++) {
tmp.push('?');
}
placeholders = tmp.join(', ');
Ext.each(records, function (record) {
var id = record.getId(),
data = me.getRecordData(record),
values = me.getColumnValues(columns, data);
transaction.executeSql(
'INSERT INTO ' + table + ' (' + columns.join(', ') + ') VALUES (' + placeholders + ')', values,
function (transaction, resultSet) {
executed++;
insertedRecords.push({
clientId: id,
id: uniqueIdStrategy ? id : resultSet.insertId,
data: data,
node: data
});
if (executed === totalRecords && typeof callback == 'function') {
callback.call(scope || me, result, errors);
}
},
function (transaction, error) {
executed++;
errors.push({
clientId: id,
error: error
});
if (executed === totalRecords && typeof callback == 'function') {
callback.call(scope || me, result, errors);
}
}
);
});
},
selectRecords: function(transaction, params, callback, scope) {
var me = this,
table = me.getTable(),
idProperty = me.getModel().getIdProperty(),
sql = 'SELECT * FROM ' + table,
records = [],
filterStatement = ' WHERE ',
sortStatement = ' ORDER BY ',
i, ln, data, result, count, rows, filter, sorter, property, value;
result = new Ext.data.ResultSet({
records: records,
success: true
});
if (!Ext.isObject(params)) {
sql += filterStatement + idProperty + ' = ' + params;
} else {
ln = params.filters && params.filters.length;
if (ln) {
for (i = 0; i < ln; i++) {
filter = params.filters[i];
property = filter.getProperty();
value = filter.getValue();
if (property !== null) {
sql += filterStatement + property + ' ' + (filter.getAnyMatch() ? ('LIKE \'%' + value + '%\'') : ('= \'' + value + '\''));
filterStatement = ' AND ';
}
}
}
ln = params.sorters.length;
if (ln) {
for (i = 0; i < ln; i++) {
sorter = params.sorters[i];
property = sorter.getProperty();
if (property !== null) {
sql += sortStatement + property + ' ' + sorter.getDirection();
sortStatement = ', ';
}
}
}
// handle start, limit, sort, filter and group params
if (params.page !== undefined) {
sql += ' LIMIT ' + parseInt(params.start, 10) + ', ' + parseInt(params.limit, 10);
}
}
transaction.executeSql(sql, null,
function(transaction, resultSet) {
rows = resultSet.rows;
count = rows.length;
for (i = 0, ln = count; i < ln; i++) {
data = rows.item(i);
records.push({
clientId: null,
id: data[idProperty],
data: data,
node: data
});
}
result.setSuccess(true);
result.setTotal(count);
result.setCount(count);
if (typeof callback == 'function') {
callback.call(scope || me, result)
}
},
function(transaction, errors) {
result.setSuccess(false);
result.setTotal(0);
result.setCount(0);
if (typeof callback == 'function') {
callback.call(scope || me, result)
}
}
);
},
updateRecords: function (transaction, records, callback, scope) {
var me = this,
table = me.getTable(),
columns = me.getColumns(),
totalRecords = records.length,
idProperty = me.getModel().getIdProperty(),
executed = 0,
updatedRecords = [],
errors = [],
i, ln, result;
result = new Ext.data.ResultSet({
records: updatedRecords,
success: true
});
Ext.each(records, function (record) {
var id = record.getId(),
data = me.getRecordData(record),
values = me.getColumnValues(columns, data),
updates = [];
for (i = 0, ln = columns.length; i < ln; i++) {
updates.push(columns[i] + ' = ?');
}
transaction.executeSql(
'UPDATE ' + table + ' SET ' + updates.join(', ') + ' WHERE ' + idProperty + ' = ?', values.concat(id),
function (transaction, resultSet) {
executed++;
updatedRecords.push({
clientId: id,
id: id,
data: data,
node: data
});
if (executed === totalRecords && typeof callback == 'function') {
callback.call(scope || me, result, errors);
}
},
function (transaction, error) {
executed++;
errors.push({
clientId: id,
error: error
});
if (executed === totalRecords && typeof callback == 'function') {
callback.call(scope || me, result, errors);
}
}
);
});
},
destroyRecords: function (transaction, records, callback, scope) {
var me = this,
table = me.getTable(),
idProperty = me.getModel().getIdProperty(),
ids = [],
values = [],
destroyedRecords = [],
i, ln, result, record;
for (i = 0, ln = records.length; i < ln; i++) {
ids.push(idProperty + ' = ?');
values.push(records[i].getId());
}
result = new Ext.data.ResultSet({
records: destroyedRecords,
success: true
});
transaction.executeSql(
'DELETE FROM ' + table + ' WHERE ' + ids.join(' OR '), values,
function (transaction, resultSet) {
for (i = 0, ln = records.length; i < ln; i++) {
record = records[i];
destroyedRecords.push({
id: record.getId()
});
}
if (typeof callback == 'function') {
callback.call(scope || me, result);
}
},
function (transaction, error) {
if (typeof callback == 'function') {
callback.call(scope || me, result);
}
}
);
},
/**
* 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 me = this,
fields = record.getFields(),
idProperty = record.getIdProperty(),
uniqueIdStrategy = me.getUniqueIdStrategy(),
data = {},
name, value;
fields.each(function (field) {
if (field.getPersist()) {
name = field.getName();
if (name === idProperty && !uniqueIdStrategy) {
return;
}
value = record.get(name);
if (field.getType().type == 'date') {
value = me.writeDate(field, value);
}
data[name] = value;
}
}, this);
return data;
},
getColumnValues: function(columns, data) {
var ln = columns.length,
values = [],
i, column, value;
for (i = 0; i < ln; i++) {
column = columns[i];
value = data[column];
if (value !== undefined) {
values.push(value);
}
}
return values;
},
getSchemaString: function() {
var me = this,
schema = [],
model = me.getModel(),
idProperty = model.getIdProperty(),
fields = model.getFields().items,
uniqueIdStrategy = me.getUniqueIdStrategy(),
ln = fields.length,
i, field, type, name;
for (i = 0; i < ln; i++) {
field = fields[i];
type = field.getType().type;
name = field.getName();
if (name === idProperty) {
if (uniqueIdStrategy) {
type = me.convertToSqlType(type);
schema.unshift(idProperty + ' ' + type);
} else {
schema.unshift(idProperty + ' INTEGER PRIMARY KEY AUTOINCREMENT');
}
} else {
type = me.convertToSqlType(type);
schema.push(name + ' ' + type);
}
}
return schema.join(', ');
},
getPersistedModelColumns: function(model) {
var fields = model.getFields().items,
uniqueIdStrategy = this.getUniqueIdStrategy(),
idProperty = model.getIdProperty(),
columns = [],
ln = fields.length,
i, field, name;
for (i = 0; i < ln; i++) {
field = fields[i];
name = field.getName();
if (name === idProperty && !uniqueIdStrategy) {
continue;
}
if (field.getPersist()) {
columns.push(field.getName());
}
}
return columns;
},
convertToSqlType: function(type) {
switch (type.toLowerCase()) {
case 'date':
case 'string':
case 'auto':
return 'TEXT';
case 'int':
return 'INTEGER';
case 'float':
return 'REAL';
case 'bool':
return 'NUMERIC'
}
},
writeDate: function (field, date) {
var dateFormat = field.getDateFormat() || this.getDefaultDateFormat();
switch (dateFormat) {
case 'timestamp':
return date.getTime() / 1000;
case 'time':
return date.getTime();
default:
return Ext.Date.format(date, dateFormat);
}
},
dropTable: function() {
var me = this,
table = me.getTable(),
db = me.getDatabaseObject();
db.transaction(function(transaction) {
transaction.executeSql('DROP TABLE ' + table);
});
me.setTableExists(false);
},
getDatabaseObject: function() {
return openDatabase(this.getDatabase(), '1.0', 'Sencha Database', 5 * 1024 * 1024);
}
});
/**
* @author Ed Spencer
* @aside guide proxies
*
* Proxy which uses HTML5 session storage as its data storage/retrieval mechanism. If this proxy is used in a browser
* where session storage is not supported, the constructor will throw an error. A session storage proxy requires a
* unique ID which is used as a key in which all record data are stored in the session storage object.
*
* It's important to supply this unique ID as it cannot be reliably determined otherwise. If no id is provided but the
* attached store has a storeId, the storeId will be used. If neither option is presented the proxy will throw an error.
*
* Proxies are almost always used with a {@link Ext.data.Store store}:
*
* new Ext.data.Store({
* proxy: {
* type: 'sessionstorage',
* id : 'myProxyKey'
* }
* });
*
* Alternatively you can instantiate the Proxy directly:
*
* new Ext.data.proxy.SessionStorage({
* id : 'myOtherProxyKey'
* });
*
* Note that session storage is different to local storage (see {@link Ext.data.proxy.LocalStorage}) - if a browser
* session is ended (e.g. by closing the browser) then all data in a SessionStorageProxy are lost. Browser restarts
* don't affect the {@link Ext.data.proxy.LocalStorage} - the data are preserved.
*/
Ext.define('Ext.data.proxy.SessionStorage', {
extend: 'Ext.data.proxy.WebStorage',
alias: 'proxy.sessionstorage',
alternateClassName: 'Ext.data.SessionStorageProxy',
//inherit docs
getStorageObject: function() {
return window.sessionStorage;
}
});
/**
* @author Ed Spencer
* @class Ext.data.reader.Xml
* @extends Ext.data.reader.Reader
*
* The XML Reader is used by a Proxy to read a server response that is sent back in XML format. This usually
* happens as a result of loading a Store - for example we might create something like this:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['id', 'name', 'email']
* }
* });
*
* var store = Ext.create('Ext.data.Store', {
* model: 'User',
* proxy: {
* type: 'ajax',
* url : 'users.xml',
* reader: {
* type: 'xml',
* record: 'user'
* }
* }
* });
*
* The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're
* not already familiar with them.
*
* We created the simplest type of XML Reader possible by simply telling our {@link Ext.data.Store Store}'s
* {@link Ext.data.proxy.Proxy Proxy} that we want a XML Reader. The Store automatically passes the configured model to the
* Store, so it is as if we passed this instead:
*
* reader: {
* type : 'xml',
* model: 'User',
* record: 'user'
* }
*
* The reader we set up is ready to read data from our server - at the moment it will accept a response like this:
*
* <?xml version="1.0" encoding="UTF-8"?>
* <user>
* <id>1</id>
* <name>Ed Spencer</name>
* <email>ed@sencha.com</email>
* </user>
* <user>
* <id>2</id>
* <name>Abe Elias</name>
* <email>abe@sencha.com</email>
* </user>
*
* The XML Reader uses the configured {@link #record} option to pull out the data for each record - in this case we
* set record to 'user', so each `<user>` above will be converted into a User model.
*
* ## Reading other XML formats
*
* If you already have your XML format defined and it doesn't look quite like what we have above, you can usually
* pass XmlReader a couple of configuration options to make it parse your format. For example, we can use the
* {@link #rootProperty} configuration to parse data that comes back like this:
*
* <?xml version="1.0" encoding="UTF-8"?>
* <users>
* <user>
* <id>1</id>
* <name>Ed Spencer</name>
* <email>ed@sencha.com</email>
* </user>
* <user>
* <id>2</id>
* <name>Abe Elias</name>
* <email>abe@sencha.com</email>
* </user>
* </users>
*
* To parse this we just pass in a {@link #rootProperty} configuration that matches the 'users' above:
*
* reader: {
* type: 'xml',
* record: 'user',
* rootProperty: 'users'
* }
*
* Note that XmlReader doesn't care whether your {@link #rootProperty} and {@link #record} elements are nested deep
* inside a larger structure, so a response like this will still work:
*
* <?xml version="1.0" encoding="UTF-8"?>
* <deeply>
* <nested>
* <xml>
* <users>
* <user>
* <id>1</id>
* <name>Ed Spencer</name>
* <email>ed@sencha.com</email>
* </user>
* <user>
* <id>2</id>
* <name>Abe Elias</name>
* <email>abe@sencha.com</email>
* </user>
* </users>
* </xml>
* </nested>
* </deeply>
*
* ## Response metadata
*
* The server can return additional data in its response, such as the {@link #totalProperty total number of records}
* and the {@link #successProperty success status of the response}. These are typically included in the XML response
* like this:
*
* <?xml version="1.0" encoding="UTF-8"?>
* <total>100</total>
* <success>true</success>
* <users>
* <user>
* <id>1</id>
* <name>Ed Spencer</name>
* <email>ed@sencha.com</email>
* </user>
* <user>
* <id>2</id>
* <name>Abe Elias</name>
* <email>abe@sencha.com</email>
* </user>
* </users>
*
* If these properties are present in the XML response they can be parsed out by the XmlReader and used by the
* Store that loaded it. We can set up the names of these properties by specifying a final pair of configuration
* options:
*
* reader: {
* type: 'xml',
* rootProperty: 'users',
* totalProperty : 'total',
* successProperty: 'success'
* }
*
* These final options are not necessary to make the Reader work, but can be useful when the server needs to report
* an error or if it needs to indicate that there is a lot of data available of which only a subset is currently being
* returned.
*
* ## Response format
*
* __Note:__ In order for the browser to parse a returned XML document, the Content-Type header in the HTTP
* response must be set to "text/xml" or "application/xml". This is very important - the XmlReader will not
* work correctly otherwise.
*/
Ext.define('Ext.data.reader.Xml', {
extend: 'Ext.data.reader.Reader',
alternateClassName: 'Ext.data.XmlReader',
alias : 'reader.xml',
config: {
/**
* @cfg {String} record The DomQuery path to the repeated element which contains record information.
*/
record: null
},
/**
* @private
* Creates a function to return some particular key of data from a response. The {@link #totalProperty} and
* {@link #successProperty} are treated as special cases for type casting, everything else is just a simple selector.
* @param {String} expr
* @return {Function}
*/
createAccessor: function(expr) {
var me = this;
if (Ext.isEmpty(expr)) {
return Ext.emptyFn;
}
if (Ext.isFunction(expr)) {
return expr;
}
return function(root) {
return me.getNodeValue(Ext.DomQuery.selectNode(expr, root));
};
},
getNodeValue: function(node) {
if (node && node.firstChild) {
return node.firstChild.nodeValue;
}
return undefined;
},
//inherit docs
getResponseData: function(response) {
// Check to see if the response is already an xml node.
if (response.nodeType === 1 || response.nodeType === 9) {
return response;
}
var xml = response.responseXML;
//<debug>
if (!xml) {
/**
* @event exception Fires whenever the reader is unable to parse a response.
* @param {Ext.data.reader.Xml} reader A reference to this reader.
* @param {XMLHttpRequest} response The XMLHttpRequest response object.
* @param {String} error The error message.
*/
this.fireEvent('exception', this, response, 'XML data not found in the response');
Ext.Logger.warn('XML data not found in the response');
}
//</debug>
return xml;
},
/**
* Normalizes the data object.
* @param {Object} data The raw data object.
* @return {Object} Returns the `documentElement` property of the data object if present, or the same object if not.
*/
getData: function(data) {
return data.documentElement || data;
},
/**
* @private
* Given an XML object, returns the Element that represents the root as configured by the Reader's meta data.
* @param {Object} data The XML data object.
* @return {XMLElement} The root node element.
*/
getRoot: function(data) {
var nodeName = data.nodeName,
root = this.getRootProperty();
if (!root || (nodeName && nodeName == root)) {
return data;
} else if (Ext.DomQuery.isXml(data)) {
// This fix ensures we have XML data
// Related to TreeStore calling getRoot with the root node, which isn't XML
// Probably should be resolved in TreeStore at some point
return Ext.DomQuery.selectNode(root, data);
}
},
/**
* @private
* We're just preparing the data for the superclass by pulling out the record nodes we want.
* @param {XMLElement} root The XML root node.
* @return {Ext.data.Model[]} The records.
*/
extractData: function(root) {
var recordName = this.getRecord();
//<debug>
if (!recordName) {
Ext.Logger.error('Record is a required parameter');
}
//</debug>
if (recordName != root.nodeName && recordName !== root.localName) {
root = Ext.DomQuery.select(recordName, root);
} else {
root = [root];
}
return this.callParent([root]);
},
/**
* @private
* See {@link Ext.data.reader.Reader#getAssociatedDataRoot} docs.
* @param {Object} data The raw data object.
* @param {String} associationName The name of the association to get data for (uses {@link Ext.data.association.Association#associationKey} if present).
* @return {XMLElement} The root.
*/
getAssociatedDataRoot: function(data, associationName) {
return Ext.DomQuery.select(associationName, data)[0];
},
/**
* Parses an XML document and returns a ResultSet containing the model instances.
* @param {Object} doc Parsed XML document.
* @return {Ext.data.ResultSet} The parsed result set.
*/
readRecords: function(doc) {
//it's possible that we get passed an array here by associations. Make sure we strip that out (see Ext.data.reader.Reader#readAssociated)
if (Ext.isArray(doc)) {
doc = doc[0];
}
return this.callParent([doc]);
},
/**
* @private
* Returns an accessor expression for the passed Field from an XML element using either the Field's mapping, or
* its ordinal position in the fields collection as the index.
*
* This is used by `buildExtractors` to create optimized on extractor function which converts raw data into model instances.
*/
createFieldAccessExpression: function(field, fieldVarName, dataName) {
var selector = field.getMapping() || field.getName(),
result;
if (typeof selector === 'function') {
result = fieldVarName + '.getMapping()(' + dataName + ', this)';
} else {
selector = selector.split('@');
if (selector.length === 2 && selector[0]) {
result = 'me.getNodeValue(Ext.DomQuery.selectNode("@' + selector[1] + '", Ext.DomQuery.selectNode("' + selector[0] + '", ' + dataName + ')))';
} else if (selector.length === 2) {
result = 'me.getNodeValue(Ext.DomQuery.selectNode("@' + selector[1] + '", ' + dataName + '))';
} else if (selector.length === 1) {
result = 'me.getNodeValue(Ext.DomQuery.selectNode("' + selector[0] + '", ' + dataName + '))';
} else {
throw "Unsupported query - too many queries for attributes in " + selector.join('@');
}
}
return result;
}
});
/**
* @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.
*/
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.
* 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).
*/
header: '',
/**
* @cfg {String} record The name of the node to use for each record.
*/
record: 'record'
},
/**
* @param request
* @param data
* @return {Object}
*/
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;
}
});
/**
* @aside video list
* @aside guide list
*
* IndexBar is a component used to display a list of data (primarily an alphabet) which can then be used to quickly
* navigate through a list (see {@link Ext.List}) of data. When a user taps on an item in the {@link Ext.IndexBar},
* it will fire the {@link #index} event.
*
* Here is an example of the usage in a {@link Ext.List}:
*
* @example phone portrait preview
* Ext.define('Contact', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['firstName', 'lastName']
* }
* });
*
* var store = new Ext.data.JsonStore({
* model: 'Contact',
* sorters: 'lastName',
*
* grouper: {
* groupFn: function(record) {
* return record.get('lastName')[0];
* }
* },
*
* data: [
* {firstName: 'Tommy', lastName: 'Maintz'},
* {firstName: 'Rob', lastName: 'Dougan'},
* {firstName: 'Ed', lastName: 'Spencer'},
* {firstName: 'Jamie', lastName: 'Avins'},
* {firstName: 'Aaron', lastName: 'Conran'},
* {firstName: 'Dave', lastName: 'Kaneda'},
* {firstName: 'Jacky', lastName: 'Nguyen'},
* {firstName: 'Abraham', lastName: 'Elias'},
* {firstName: 'Jay', lastName: 'Robinson'},
* {firstName: 'Nigel', lastName: 'White'},
* {firstName: 'Don', lastName: 'Griffin'},
* {firstName: 'Nico', lastName: 'Ferrero'},
* {firstName: 'Jason', lastName: 'Johnston'}
* ]
* });
*
* var list = new Ext.List({
* fullscreen: true,
* itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
*
* grouped : true,
* indexBar : true,
* store: store,
* hideOnMaskTap: false
* });
*
*/
Ext.define('Ext.dataview.IndexBar', {
extend: 'Ext.Component',
alternateClassName: 'Ext.IndexBar',
/**
* @event index
* Fires when an item in the index bar display has been tapped.
* @param {Ext.dataview.IndexBar} this The IndexBar instance
* @param {String} html The HTML inside the tapped node.
* @param {Ext.dom.Element} target The node on the indexbar that has been tapped.
*/
config: {
baseCls: Ext.baseCSSPrefix + 'indexbar',
/**
* @cfg {String} direction
* Layout direction, can be either 'vertical' or 'horizontal'
* @accessor
*/
direction: 'vertical',
/**
* @cfg {Array} letters
* The letters to show on the index bar.
* @accessor
*/
letters: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
ui: 'alphabet',
/**
* @cfg {String} listPrefix
* The prefix string to be used at the beginning of the list.
* E.g: useful to add a "#" prefix before numbers.
* @accessor
*/
listPrefix: null
},
// @private
itemCls: Ext.baseCSSPrefix + '',
updateDirection: function(newDirection, oldDirection) {
var baseCls = this.getBaseCls();
this.element.replaceCls(baseCls + '-' + oldDirection, baseCls + '-' + newDirection);
},
getElementConfig: function() {
return {
reference: 'wrapper',
classList: ['x-centered', 'x-indexbar-wrapper'],
children: [this.callParent()]
};
},
updateLetters: function(letters) {
this.innerElement.setHtml('');
if (letters) {
var ln = letters.length,
i;
for (i = 0; i < ln; i++) {
this.innerElement.createChild({
html: letters[i]
});
}
}
},
updateListPrefix: function(listPrefix) {
if (listPrefix && listPrefix.length) {
this.innerElement.createChild({
html: listPrefix
}, 0);
}
},
// @private
initialize: function() {
this.callParent();
this.innerElement.on({
touchstart: this.onTouchStart,
touchend: this.onTouchEnd,
touchmove: this.onTouchMove,
scope: this
});
},
// @private
onTouchStart: function(e, t) {
e.stopPropagation();
this.innerElement.addCls(this.getBaseCls() + '-pressed');
this.pageBox = this.innerElement.getPageBox();
this.onTouchMove(e);
},
// @private
onTouchEnd: function(e, t) {
this.innerElement.removeCls(this.getBaseCls() + '-pressed');
},
// @private
onTouchMove: function(e) {
var point = Ext.util.Point.fromEvent(e),
target,
pageBox = this.pageBox;
if (!pageBox) {
pageBox = this.pageBox = this.el.getPageBox();
}
if (this.getDirection() === 'vertical') {
if (point.y > pageBox.bottom || point.y < pageBox.top) {
return;
}
target = Ext.Element.fromPoint(pageBox.left + (pageBox.width / 2), point.y);
}
else {
if (point.x > pageBox.right || point.x < pageBox.left) {
return;
}
target = Ext.Element.fromPoint(point.x, pageBox.top + (pageBox.height / 2));
}
if (target) {
this.fireEvent('index', this, target.dom.innerHTML, target);
}
},
destroy: function() {
var me = this,
elements = Array.prototype.slice.call(me.innerElement.dom.childNodes),
ln = elements.length,
i = 0;
for (; i < ln; i++) {
Ext.removeNode(elements[i]);
}
this.callParent();
}
}, function() {
});
/**
* @private - To be made a sample
*/
Ext.define('Ext.dataview.ListItemHeader', {
extend: 'Ext.Component',
xtype : 'listitemheader',
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'list-header',
docked: 'top'
}
});
/**
* A DataItem is a container for {@link Ext.dataview.DataView} with useComponents: true. It ties together
* {@link Ext.data.Model records} to its contained Components via a {@link #dataMap dataMap} configuration.
*
* For example, lets say you have a `text` configuration which, when applied, gets turned into an instance of an
* Ext.Component. We want to update the {@link #html} of a sub-component when the 'text' field of the record gets
* changed.
*
* As you can see below, it is simply a matter of setting the key of the object to be the getter of the config
* (`getText`), and then give that property a value of an object, which then has 'setHtml' (the `html` setter) as the key,
* and 'text' (the field name) as the value. You can continue this for a as many sub-components as you wish.
*
* dataMap: {
* // When the record is updated, get the text configuration, and
* // call {@link #setHtml} with the 'text' field of the record.
* getText: {
* setHtml: 'text'
* },
*
* // When the record is updated, get the userName configuration, and
* // call {@link #setHtml} with the 'from_user' field of the record.
* getUserName: {
* setHtml: 'from_user'
* },
*
* // When the record is updated, get the avatar configuration, and
* // call `setSrc` with the 'profile_image_url' field of the record.
* getAvatar: {
* setSrc: 'profile_image_url'
* }
* }
*/
Ext.define('Ext.dataview.component.ListItem', {
extend: 'Ext.dataview.component.DataItem',
xtype : 'listitem',
config: {
baseCls: Ext.baseCSSPrefix + 'list-item',
dataMap: null,
body: {
xtype: 'component',
cls: 'x-list-item-body'
},
disclosure: {
xtype: 'component',
cls: 'x-list-disclosure',
hidden: true,
docked: 'right'
},
header: {
xtype: 'component',
cls: 'x-list-header',
html: ' ',
hidden: true
},
tpl: null,
items: null
},
applyBody: function(body) {
if (body && !body.isComponent) {
body = Ext.factory(body, Ext.Component, this.getBody());
}
return body;
},
updateBody: function(body, oldBody) {
if (body) {
this.add(body);
} else if (oldBody) {
oldBody.destroy();
}
},
applyHeader: function(header) {
if (header && !header.isComponent) {
header = Ext.factory(header, Ext.Component, this.getHeader());
}
return header;
},
updateHeader: function(header, oldHeader) {
if (header) {
this.element.getFirstChild().insertFirst(header.element);
} else if (oldHeader) {
oldHeader.destroy();
}
},
applyDisclosure: function(disclosure) {
if (disclosure && !disclosure.isComponent) {
disclosure = Ext.factory(disclosure, Ext.Component, this.getDisclosure());
}
return disclosure;
},
updateDisclosure: function(disclosure, oldDisclosure) {
if (disclosure) {
this.add(disclosure);
} else if (oldDisclosure) {
oldDisclosure.destroy();
}
},
updateTpl: function(tpl) {
this.getBody().setTpl(tpl);
},
updateRecord: function(record) {
var me = this,
dataview = me.dataview || this.getDataview(),
data = record && dataview.prepareData(record.getData(true), dataview.getStore().indexOf(record), record),
dataMap = me.getDataMap(),
body = this.getBody(),
disclosure = this.getDisclosure();
me._record = record;
if (dataMap) {
me.doMapData(dataMap, data, body);
} else if (body) {
body.updateData(data || null);
}
if (disclosure && record && dataview.getOnItemDisclosure()) {
var disclosureProperty = dataview.getDisclosureProperty();
disclosure[(data.hasOwnProperty(disclosureProperty) && data[disclosureProperty] === false) ? 'hide' : 'show']();
}
/**
* @event updatedata
* Fires whenever the data of the DataItem is updated.
* @param {Ext.dataview.component.DataItem} this The DataItem instance.
* @param {Object} newData The new data.
*/
me.fireEvent('updatedata', me, data);
}
});
/**
* @private
*/
Ext.define('Ext.util.TranslatableList', {
extend: 'Ext.util.translatable.Abstract',
config: {
items: []
},
applyItems: function(items) {
return Ext.Array.from(items);
},
doTranslate: function(x, y) {
var items = this.getItems(),
offset = 0,
i, ln, item, translateY;
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
if (item && !item._list_hidden) {
translateY = y + offset;
offset += item.$height;
item.translate(0, translateY);
}
}
}
});
/**
* @private
*/
Ext.define('Ext.util.PositionMap', {
config: {
minimumHeight: 50
},
constructor: function(config) {
this.map = [];
this.adjustments = {};
this.offset = 0;
this.initConfig(config);
},
populate: function(count, offset) {
var map = this.map = this.map || [],
minimumHeight = this.getMinimumHeight(),
i, previousIndex, ln;
// We add 1 item to the count so that we can get the height of the bottom item
count++;
map.length = count;
map[0] = 0;
for (i = offset + 1, ln = count - 1; i <= ln; i++) {
previousIndex = i - 1;
map[i] = map[previousIndex] + minimumHeight;
}
this.adjustments = {
indices: [],
heights: {}
};
this.offset = 0;
for (i = 1, ln = count - 1; i <= ln; i++) {
previousIndex = i - 1;
this.offset += map[i] - map[previousIndex] - minimumHeight;
}
},
setItemHeight: function(index, height) {
height = Math.max(height, this.getMinimumHeight());
if (height !== this.getItemHeight(index)) {
var adjustments = this.adjustments;
adjustments.indices.push(parseInt(index, 10));
adjustments.heights[index] = height;
}
},
update: function() {
var adjustments = this.adjustments,
indices = adjustments.indices,
heights = adjustments.heights,
map = this.map,
ln = indices.length,
minimumHeight = this.getMinimumHeight(),
difference = 0,
i, j, height, index, nextIndex, currentHeight;
if (!adjustments.indices.length) {
return false;
}
Ext.Array.sort(indices, function(a, b) {
return a - b;
});
for (i = 0; i < ln; i++) {
index = indices[i];
nextIndex = indices[i + 1] || map.length - 1;
currentHeight = (map[index + 1] !== undefined) ? (map[index + 1] - map[index] + difference) : minimumHeight;
height = heights[index];
difference += height - currentHeight;
for (j = index + 1; j <= nextIndex; j++) {
map[j] += difference;
}
}
this.offset += difference;
this.adjustments = {
indices: [],
heights: {}
};
return true;
},
getItemHeight: function(index) {
return this.map[index + 1] - this.map[index];
},
getTotalHeight: function() {
return ((this.map.length - 1) * this.getMinimumHeight()) + this.offset;
},
findIndex: function(pos) {
return this.map.length ? this.binarySearch(this.map, pos) : 0;
},
binarySearch: function(sorted, value) {
var start = 0,
end = sorted.length;
if (value < sorted[0]) {
return 0;
}
if (value > sorted[end - 1]) {
return end - 1;
}
while (start + 1 < end) {
var mid = (start + end) >> 1,
val = sorted[mid];
if (val == value) {
return mid;
} else if (val < value) {
start = mid;
} else {
end = mid;
}
}
return start;
}
});
/**
* @aside guide list
* @aside video list
*
* List is a custom styled DataView which allows Grouping, Indexing, Icons, and a Disclosure. See the
* [Guide](#!/guide/list) and [Video](#!/video/list) for more.
*
* @example miniphone preview
* Ext.create('Ext.List', {
* fullscreen: true,
* itemTpl: '{title}',
* data: [
* { title: 'Item 1' },
* { title: 'Item 2' },
* { title: 'Item 3' },
* { title: 'Item 4' }
* ]
* });
*
* A more advanced example showing a list of people groped by last name:
*
* @example miniphone preview
* Ext.define('Contact', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['firstName', 'lastName']
* }
* });
*
* var store = Ext.create('Ext.data.Store', {
* model: 'Contact',
* sorters: 'lastName',
*
* grouper: {
* groupFn: function(record) {
* return record.get('lastName')[0];
* }
* },
*
* data: [
* { firstName: 'Tommy', lastName: 'Maintz' },
* { firstName: 'Rob', lastName: 'Dougan' },
* { firstName: 'Ed', lastName: 'Spencer' },
* { firstName: 'Jamie', lastName: 'Avins' },
* { firstName: 'Aaron', lastName: 'Conran' },
* { firstName: 'Dave', lastName: 'Kaneda' },
* { firstName: 'Jacky', lastName: 'Nguyen' },
* { firstName: 'Abraham', lastName: 'Elias' },
* { firstName: 'Jay', lastName: 'Robinson'},
* { firstName: 'Nigel', lastName: 'White' },
* { firstName: 'Don', lastName: 'Griffin' },
* { firstName: 'Nico', lastName: 'Ferrero' },
* { firstName: 'Jason', lastName: 'Johnston'}
* ]
* });
*
* Ext.create('Ext.List', {
* fullscreen: true,
* itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
* store: store,
* grouped: true
* });
*/
Ext.define('Ext.dataview.List', {
alternateClassName: 'Ext.List',
extend: 'Ext.dataview.DataView',
xtype: 'list',
mixins: ['Ext.mixin.Bindable'],
requires: [
'Ext.dataview.IndexBar',
'Ext.dataview.ListItemHeader',
'Ext.dataview.component.ListItem',
'Ext.util.TranslatableList',
'Ext.util.PositionMap'
],
/**
* @event disclose
* @preventable doDisclose
* Fires whenever a disclosure is handled
* @param {Ext.dataview.List} this The List instance
* @param {Ext.data.Model} record The record associated to the item
* @param {HTMLElement} target The element disclosed
* @param {Number} index The index of the item disclosed
* @param {Ext.EventObject} e The event object
*/
config: {
/**
* @cfg layout
* Hide layout config in DataView. It only causes confusion.
* @accessor
* @private
*/
layout: 'fit',
/**
* @cfg {Boolean/Object} indexBar
* `true` to render an alphabet IndexBar docked on the right.
* This can also be a config object that will be passed to {@link Ext.IndexBar}.
* @accessor
*/
indexBar: false,
icon: null,
/**
* @cfg {Boolean} clearSelectionOnDeactivate
* `true` to clear any selections on the list when the list is deactivated.
* @removed 2.0.0
*/
/**
* @cfg {Boolean} preventSelectionOnDisclose `true` to prevent the item selection when the user
* taps a disclose icon.
* @accessor
*/
preventSelectionOnDisclose: true,
/**
* @cfg baseCls
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'list',
/**
* @cfg {Boolean} pinHeaders
* Whether or not to pin headers on top of item groups while scrolling for an iPhone native list experience.
* @accessor
*/
pinHeaders: true,
/**
* @cfg {Boolean} grouped
* Whether or not to group items in the provided Store with a header for each item.
* @accessor
*/
grouped: false,
/**
* @cfg {Boolean/Function/Object} onItemDisclosure
* `true` to display a disclosure icon on each list item.
* The list will still fire the disclose event, and the event can be stopped before itemtap.
* By setting this config to a function, the function passed will be called when the disclosure
* is tapped.
* Finally you can specify an object with a 'scope' and 'handler'
* property defined. This will also be bound to the tap event listener
* and is useful when you want to change the scope of the handler.
* @accessor
*/
onItemDisclosure: null,
/**
* @cfg {String} disclosureProperty
* A property to check on each record to display the disclosure on a per record basis. This
* property must be false to prevent the disclosure from being displayed on the item.
* @accessor
*/
disclosureProperty: 'disclosure',
/**
* @cfg {String} ui
* The style of this list. Available options are `normal` and `round`.
*/
ui: 'normal',
/**
* @cfg {Boolean} useComponents
* Flag the use a component based DataView implementation. This allows the full use of components in the
* DataView at the cost of some performance.
*
* Checkout the [DataView Guide](#!/guide/dataview) for more information on using this configuration.
* @accessor
* @private
*/
/**
* @cfg {Object} itemConfig
* A configuration object that is passed to every item created by a component based DataView. Because each
* item that a DataView renders is a Component, we can pass configuration options to each component to
* easily customize how each child component behaves.
* Note this is only used when useComponents is true.
* @accessor
* @private
*/
/**
* @cfg {Number} maxItemCache
* Maintains a cache of reusable components when using a component based DataView. Improving performance at
* the cost of memory.
* Note this is currently only used when useComponents is true.
* @accessor
* @private
*/
/**
* @cfg {String} defaultType
* The xtype used for the component based DataView. Defaults to dataitem.
* Note this is only used when useComponents is true.
* @accessor
*/
defaultType: 'listitem',
/**
* @cfg {Object} itemMap
* @private
*/
itemMap: {
minimumHeight: 47
},
/**
* @cfg {Boolean} variableHeights
* Whether or not this list contains items with variable heights. If you want to force the
* items in the list to have a fixed height, set the {@link #itemHeight} configuration.
* If you also variableHeights to false, the scrolling performance of the list will be
* improved.
*/
variableHeights: true,
/**
* @cfg {Number} itemHeight
* This allows you to set the default item height and is used to roughly calculate the amount
* of items needed to fill the list. By default items are around 50px high. If you set this
* configuration in combination with setting the {@link #variableHeights} to false you
* can improve the scrolling speed
*/
itemHeight: 47,
/**
* @cfg {Boolean} refreshHeightOnUpdate
* Set this to false if you make many updates to your list (like in an interval), but updates
* won't affect the item's height. Doing this will increase the performance of these updates.
* Note that if you have {@link #variableHeights} set to false, this configuration option has
* no effect.
*/
refreshHeightOnUpdate: true,
scrollable: false
},
constructor: function(config) {
var me = this,
layout;
me.callParent(arguments);
if (Ext.os.is.Android4 && !Ext.browser.is.ChromeMobile) {
me.headerTranslateFn = Ext.Function.createThrottled(me.headerTranslateFn, 50, me);
}
//<debug>
layout = this.getLayout();
if (layout && !layout.isFit) {
Ext.Logger.error('The base layout for a DataView must always be a Fit Layout');
}
//</debug>
},
topItemIndex: 0,
topItemPosition: 0,
updateItemHeight: function(itemHeight) {
this.getItemMap().setMinimumHeight(itemHeight);
},
applyItemMap: function(itemMap) {
return Ext.factory(itemMap, Ext.util.PositionMap, this.getItemMap());
},
// apply to the selection model to maintain visual UI cues
// onItemTrigger: function(me, index, target, record, e) {
// if (!(this.getPreventSelectionOnDisclose() && Ext.fly(e.target).hasCls(this.getBaseCls() + '-disclosure'))) {
// this.callParent(arguments);
// }
// },
beforeInitialize: function() {
var me = this,
container;
me.listItems = [];
me.scrollDockItems = {
top: [],
bottom: []
};
container = me.container = me.add(new Ext.Container({
scrollable: {
scroller: {
autoRefresh: false,
direction: 'vertical',
translatable: {
xclass: 'Ext.util.TranslatableList'
}
}
}
}));
container.getScrollable().getScroller().getTranslatable().setItems(me.listItems);
// Tie List's scroller to its container's scroller
me.setScrollable(container.getScrollable());
me.scrollableBehavior = container.getScrollableBehavior();
},
initialize: function() {
var me = this,
container = me.container,
i, ln;
me.updatedItems = [];
me.headerMap = [];
me.on(me.getTriggerCtEvent(), me.onContainerTrigger, me);
me.on(me.getTriggerEvent(), me.onItemTrigger, me);
me.header = Ext.factory({
xclass: 'Ext.dataview.ListItemHeader',
html: '&nbsp;',
translatable: true,
role: 'globallistheader',
cls: ['x-list-header', 'x-list-header-swap']
});
me.container.innerElement.insertFirst(me.header.element);
me.headerTranslate = me.header.getTranslatable();
me.headerTranslate.translate(0, -10000);
if (!me.getGrouped()) {
me.updatePinHeaders(null);
}
container.element.on({
delegate: '.' + me.getBaseCls() + '-disclosure',
tap: 'handleItemDisclosure',
scope: me
});
container.element.on({
resize: 'onResize',
scope: me
});
// Android 2.x not a direct child
container.innerElement.on({
touchstart: 'onItemTouchStart',
touchend: 'onItemTouchEnd',
tap: 'onItemTap',
taphold: 'onItemTapHold',
singletap: 'onItemSingleTap',
doubletap: 'onItemDoubleTap',
swipe: 'onItemSwipe',
delegate: '.' + Ext.baseCSSPrefix + 'list-item-body',
scope: me
});
for (i = 0, ln = me.scrollDockItems.top.length; i < ln; i++) {
container.add(me.scrollDockItems.top[i]);
}
for (i = 0, ln = me.scrollDockItems.bottom.length; i < ln; i++) {
container.add(me.scrollDockItems.bottom[i]);
}
if (me.getStore()) {
me.refresh();
}
},
updateInline: function(newInline) {
var me = this;
me.callParent(arguments);
if (newInline) {
me.setOnItemDisclosure(false);
me.setIndexBar(false);
me.setGrouped(false);
}
},
applyIndexBar: function(indexBar) {
return Ext.factory(indexBar, Ext.dataview.IndexBar, this.getIndexBar());
},
updateIndexBar: function(indexBar) {
var me = this;
if (indexBar && me.getScrollable()) {
me.indexBarElement = me.getScrollableBehavior().getScrollView().getElement().appendChild(indexBar.renderElement);
indexBar.on({
index: 'onIndex',
scope: me
});
me.element.addCls(me.getBaseCls() + '-indexed');
}
},
updateGrouped: function(grouped) {
var me = this,
baseCls = this.getBaseCls(),
cls = baseCls + '-grouped',
unCls = baseCls + '-ungrouped';
if (grouped) {
me.addCls(cls);
me.removeCls(unCls);
me.updatePinHeaders(me.getPinHeaders());
}
else {
me.addCls(unCls);
me.removeCls(cls);
me.updatePinHeaders(null);
}
if (me.isPainted() && me.listItems.length) {
me.setItemsCount(me.listItems.length);
}
},
updatePinHeaders: function(pinnedHeaders) {
if (this.headerTranslate) {
this.headerTranslate.translate(0, -10000);
}
},
updateScrollerSize: function() {
var me = this,
totalHeight = me.getItemMap().getTotalHeight(),
scroller = me.container.getScrollable().getScroller();
if (totalHeight > 0) {
scroller.givenSize = totalHeight;
scroller.refresh();
}
},
onResize: function() {
var me = this,
container = me.container,
element = container.element,
minimumHeight = me.getItemMap().getMinimumHeight(),
containerSize;
if (!me.listItems.length) {
me.bind(container.getScrollable().getScroller().getTranslatable(), 'doTranslate', 'onTranslate');
}
me.containerSize = containerSize = element.getHeight();
me.setItemsCount(Math.ceil(containerSize / minimumHeight) + 1);
},
scrollDockHeightRefresh: function() {
var items = this.listItems,
scrollDockItems = this.scrollDockItems,
ln = items.length,
i, item;
for (i = 0; i < ln; i++) {
item = items[i];
if ((item.isFirst && scrollDockItems.top.length) || (item.isLast && scrollDockItems.bottom.length)) {
this.updatedItems.push(item);
}
}
this.refreshScroller();
},
headerTranslateFn: function(record, transY, headerTranslate) {
var headerString = this.getStore().getGroupString(record);
if (this.currentHeader !== headerString) {
this.currentHeader = headerString;
this.header.setHtml(headerString);
}
headerTranslate.translate(0, transY);
},
onTranslate: function(x, y, args) {
var me = this,
listItems = me.listItems,
itemsCount = listItems.length,
currentTopIndex = me.topItemIndex,
itemMap = me.getItemMap(),
store = me.getStore(),
storeCount = store.getCount(),
info = me.getListItemInfo(),
grouped = me.getGrouped(),
storeGroups = me.groups,
headerMap = me.headerMap,
headerTranslate = me.headerTranslate,
pinHeaders = me.getPinHeaders(),
maxIndex = storeCount - itemsCount + 1,
topIndex, changedCount, i, index, item,
closestHeader, record, pushedHeader, transY, element;
if (me.updatedItems.length) {
me.updateItemHeights();
}
me.topItemPosition = itemMap.findIndex(-y) || 0;
me.indexOffset = me.topItemIndex = topIndex = Math.max(0, Math.min(me.topItemPosition, maxIndex));
if (grouped && headerTranslate && storeGroups.length && pinHeaders) {
closestHeader = itemMap.binarySearch(headerMap, -y);
record = storeGroups[closestHeader].children[0];
if (record) {
pushedHeader = y + headerMap[closestHeader + 1] - me.headerHeight;
// Top of the list or above (hide the floating header offscreen)
if (y >= 0) {
transY = -10000;
}
// Scroll the floating header a bit
else if (pushedHeader < 0) {
transY = pushedHeader;
}
// Stick to the top of the screen
else {
transY = Math.max(0, y);
}
this.headerTranslateFn(record, transY, headerTranslate);
}
}
args[1] = (itemMap.map[topIndex] || 0) + y;
if (currentTopIndex !== topIndex && topIndex <= maxIndex) {
// Scroll up
if (currentTopIndex > topIndex) {
changedCount = Math.min(itemsCount, currentTopIndex - topIndex);
for (i = changedCount - 1; i >= 0; i--) {
item = listItems.pop();
listItems.unshift(item);
me.updateListItem(item, i + topIndex, info);
}
}
else {
// Scroll down
changedCount = Math.min(itemsCount, topIndex - currentTopIndex);
for (i = 0; i < changedCount; i++) {
item = listItems.shift();
listItems.push(item);
index = i + topIndex + itemsCount - changedCount;
me.updateListItem(item, index, info);
}
}
}
if (listItems.length && grouped && pinHeaders) {
if (me.headerIndices[topIndex]) {
element = listItems[0].getHeader().element;
if (y < itemMap.map[topIndex]) {
element.setVisibility(false);
}
else {
element.setVisibility(true);
}
}
for (i = 1; i <= changedCount; i++) {
if (listItems[i]) {
listItems[i].getHeader().element.setVisibility(true);
}
}
}
},
setItemsCount: function(itemsCount) {
var me = this,
listItems = me.listItems,
minimumHeight = me.getItemMap().getMinimumHeight(),
config = {
xtype: me.getDefaultType(),
itemConfig: me.getItemConfig(),
tpl: me.getItemTpl(),
minHeight: minimumHeight,
cls: me.getItemCls()
},
info = me.getListItemInfo(),
i, item;
for (i = 0; i < itemsCount; i++) {
// We begin by checking if we already have an item for this length
item = listItems[i];
// If we don't have an item yet at this index then create one
if (!item) {
item = Ext.factory(config);
item.dataview = me;
item.$height = minimumHeight;
me.container.doAdd(item);
listItems.push(item);
}
item.dataIndex = null;
if (info.store) {
me.updateListItem(item, i + me.topItemIndex, info);
}
}
me.updateScrollerSize();
},
getListItemInfo: function() {
var me = this,
baseCls = me.getBaseCls();
return {
store: me.getStore(),
grouped: me.getGrouped(),
baseCls: baseCls,
selectedCls: me.getSelectedCls(),
headerCls: baseCls + '-header-wrap',
footerCls: baseCls + '-footer-wrap',
firstCls: baseCls + '-item-first',
lastCls: baseCls + '-item-last',
itemMap: me.getItemMap(),
variableHeights: me.getVariableHeights(),
defaultItemHeight: me.getItemHeight()
};
},
updateListItem: function(item, index, info) {
var record = info.store.getAt(index);
if (this.isSelected(record)) {
item.addCls(info.selectedCls);
}
else {
item.removeCls(info.selectedCls);
}
item.removeCls([info.headerCls, info.footerCls, info.firstCls, info.lastCls]);
this.replaceItemContent(item, index, info)
},
taskRunner: function() {
delete this.intervalId;
if (this.scheduledTasks && this.scheduledTasks.length > 0) {
var task = this.scheduledTasks.shift();
this.doUpdateListItem(task.item, task.index, task.info);
if (this.scheduledTasks.length === 0 && this.getVariableHeights() && !this.container.getScrollable().getScroller().getTranslatable().isAnimating) {
this.refreshScroller();
} else if (this.scheduledTasks.length > 0) {
this.intervalId = requestAnimationFrame(Ext.Function.bind(this.taskRunner, this));
}
}
},
scheduledTasks: null,
replaceItemContent: function(item, index, info) {
var translatable = this.container.getScrollable().getScroller().getTranslatable();
// This falls apart when scrolling up. Turning off for now.
if (Ext.os.is.Android4
&& !Ext.browser.is.Chrome
&& !info.variableHeights
&& !info.grouped
&& translatable.isAnimating
&& translatable.activeEasingY
&& Math.abs(translatable.activeEasingY._startVelocity) > .75) {
if (!this.scheduledTasks) {
this.scheduledTasks = [];
}
for (var i = 0; i < this.scheduledTasks.length; i++) {
if (this.scheduledTasks[i].item === item) {
Ext.Array.remove(this.scheduledTasks, this.scheduledTasks[i]);
break;
}
}
this.scheduledTasks.push({
item: item,
index: index,
info: info
});
if (!this.intervalId) {
this.intervalId = requestAnimationFrame(Ext.Function.bind(this.taskRunner, this));
}
} else {
this.doUpdateListItem(item, index, info);
}
},
doUpdateListItem: function(item, index, info) {
var record = info.store.getAt(index),
headerIndices = this.headerIndices,
footerIndices = this.footerIndices,
headerItem = item.getHeader(),
scrollDockItems = this.scrollDockItems,
updatedItems = this.updatedItems,
itemHeight = info.itemMap.getItemHeight(index),
ln, i, scrollDockItem;
if (!record) {
item.setRecord(null);
item.translate(0, -10000);
item._list_hidden = true;
return;
}
item._list_hidden = false;
if (item.isFirst && scrollDockItems.top.length) {
for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {
scrollDockItem = scrollDockItems.top[i];
scrollDockItem.addCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
item.remove(scrollDockItem, false);
}
item.isFirst = false;
}
if (item.isLast && scrollDockItems.bottom.length) {
for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {
scrollDockItem = scrollDockItems.bottom[i];
scrollDockItem.addCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
item.remove(scrollDockItem, false);
}
item.isLast = false;
}
if (item.getRecord) {
if (item.dataIndex !== index) {
item.dataIndex = index;
this.fireEvent('itemindexchange', this, record, index, item);
}
if (item.getRecord() === record) {
item.updateRecord(record);
} else {
item.setRecord(record);
}
}
if (this.isSelected(record)) {
item.addCls(info.selectedCls);
}
else {
item.removeCls(info.selectedCls);
}
item.removeCls([info.headerCls, info.footerCls, info.firstCls, info.lastCls]);
if (info.grouped) {
if (headerIndices[index]) {
item.addCls(info.headerCls);
headerItem.setHtml(info.store.getGroupString(record));
headerItem.show();
headerItem.element.setVisibility(true);
// If this record is a group header, and the items height is still the default height
// we need to read the actual size of the item (including the header)
if (!info.variableHeights && itemHeight === info.defaultItemHeight) {
Ext.Array.include(updatedItems, item);
}
}
else {
headerItem.hide();
// If this record is not a header (anymore) and its height is unequal to the default item height
// it means the item must have gotten a different height because being a header before and now needs
// to become the default height again
if (!info.variableHeights && !footerIndices[index] && itemHeight !== info.defaultItemHeight) {
info.itemMap.setItemHeight(index, info.defaultItemHeight);
info.itemMap.update();
}
}
if (footerIndices[index]) {
item.addCls(info.footerCls);
// If this record is a footer and its height is still the same as the default item height, we have
// to make sure to read this items height to see if adding the foot cls effects its height
if (!info.variableHeights && itemHeight === info.defaultItemHeight) {
Ext.Array.include(updatedItems, item);
}
}
} else if (!info.variableHeights && itemHeight !== info.defaultItemHeight) {
// If this list is not grouped, the only thing that can change the height of an item
// can be scroll dock items. If an items height is not equal to the default item height
// it means it must have had scroll dock items. In this case we set the items height
// to become the default height again.
info.itemMap.setItemHeight(index, info.defaultItemHeight);
info.itemMap.update();
}
if (index === 0) {
item.isFirst = true;
item.addCls(info.firstCls);
if (!info.grouped) {
item.addCls(info.headerCls);
}
for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {
scrollDockItem = scrollDockItems.top[i];
item.insert(0, scrollDockItem);
scrollDockItem.removeCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
}
// If an item gets scrolldock items inside of it, we need to always read the height
// in the next frame so we add it to the updatedItems array
if (ln && !info.variableHeights) {
Ext.Array.include(updatedItems, item);
}
}
if (index === info.store.getCount() - 1) {
item.isLast = true;
item.addCls(info.lastCls);
if (!info.grouped) {
item.addCls(info.footerCls);
}
for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {
scrollDockItem = scrollDockItems.bottom[i];
item.insert(0, scrollDockItem);
scrollDockItem.removeCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
}
// If an item gets scrolldock items inside of it, we need to always read the height
// in the next frame so we add it to the updatedItems array
if (ln && !info.variableHeights) {
Ext.Array.include(updatedItems, item);
}
}
item.$height = info.itemMap.getItemHeight(index);
if (info.variableHeights) {
updatedItems.push(item);
}
},
updateItemHeights: function() {
if (!this.isPainted()) {
this.pendingHeightUpdate = true;
if (!this.pendingHeightUpdate) {
this.on('painted', this.updateItemHeights, this, {single: true});
}
return;
}
var updatedItems = this.updatedItems,
ln = updatedItems.length,
itemMap = this.getItemMap(),
scroller = this.container.getScrollable().getScroller(),
minimumHeight = itemMap.getMinimumHeight(),
headerIndices = this.headerIndices,
headerMap = this.headerMap,
translatable = scroller.getTranslatable(),
itemIndex, i, item, height;
this.pendingHeightUpdate = false;
// First we do all the reads
for (i = 0; i < ln; i++) {
item = updatedItems[i];
itemIndex = item.dataIndex;
// itemIndex may not be set yet if the store is still being loaded
if (itemIndex !== null) {
height = item.element.getFirstChild().getHeight();
height = Math.max(height, minimumHeight);
if (headerIndices && !this.headerHeight && headerIndices[itemIndex]) {
this.headerHeight = parseInt(item.getHeader().element.getHeight(), 10);
}
itemMap.setItemHeight(itemIndex, height);
}
}
itemMap.update();
height = itemMap.getTotalHeight();
headerMap.length = 0;
for (i in headerIndices) {
headerMap.push(itemMap.map[i]);
}
// Now do the dom writes
for (i = 0; i < ln; i++) {
item = updatedItems[i];
itemIndex = item.dataIndex;
item.$height = itemMap.getItemHeight(itemIndex);
}
if (height != scroller.givenSize) {
scroller.setSize(height);
scroller.refreshMaxPosition();
if (translatable.isAnimating) {
translatable.activeEasingY.setMinMomentumValue(-scroller.getMaxPosition().y);
}
}
this.updatedItems.length = 0;
},
/**
* Returns an item at the specified index.
* @param {Number} index Index of the item.
* @return {Ext.dom.Element/Ext.dataview.component.DataItem} item Item at the specified index.
*/
getItemAt: function(index) {
var listItems = this.listItems,
ln = listItems.length,
i, listItem;
for (i = 0; i < ln; i++) {
listItem = listItems[i];
if (listItem.dataIndex === index) {
return listItem;
}
}
},
/**
* Returns an index for the specified item.
* @param {Number} item The item to locate.
* @return {Number} Index for the specified item.
*/
getItemIndex: function(item) {
var index = item.dataIndex;
return (index === -1) ? index : this.indexOffset + index;
},
/**
* Returns an array of the current items in the DataView.
* @return {Ext.dom.Element[]/Ext.dataview.component.DataItem[]} Array of Items.
*/
getViewItems: function() {
return this.listItems;
},
doRefresh: function(list) {
if (this.intervalId) {
cancelAnimationFrame(this.intervalId);
delete this.intervalId;
}
if (this.scheduledTasks) {
this.scheduledTasks.length = 0;
}
var me = this,
store = me.getStore(),
scrollable = me.container.getScrollable(),
scroller = scrollable && scrollable.getScroller(),
painted = me.isPainted(),
storeCount = store.getCount();
me.getItemMap().populate(storeCount, this.topItemPosition);
if (me.getGrouped()) {
me.findGroupHeaderIndices();
}
// This will refresh the items on the screen with the new data
if (me.listItems.length) {
me.setItemsCount(me.listItems.length);
if (painted) {
me.refreshScroller(scroller);
}
}
if (painted && this.getScrollToTopOnRefresh() && scroller && list) {
scroller.scrollToTop();
}
// No items, hide all the items from the collection.
if (storeCount < 1) {
me.onStoreClear();
return;
} else {
me.hideEmptyText();
}
},
findGroupHeaderIndices: function() {
var me = this,
store = me.getStore(),
storeLn = store.getCount(),
groups = store.getGroups(),
groupLn = groups.length,
headerIndices = me.headerIndices = {},
footerIndices = me.footerIndices = {},
i, previousIndex, firstGroupedRecord, storeIndex;
me.groups = groups;
for (i = 0; i < groupLn; i++) {
firstGroupedRecord = groups[i].children[0];
storeIndex = store.indexOf(firstGroupedRecord);
headerIndices[storeIndex] = true;
previousIndex = storeIndex - 1;
if (previousIndex) {
footerIndices[previousIndex] = true;
}
}
footerIndices[storeLn - 1] = true;
return headerIndices;
},
// Handling adds and removes like this is fine for now. It should not perform much slower then a dedicated solution
onStoreAdd: function() {
this.doRefresh();
},
onStoreRemove: function() {
this.doRefresh();
},
onStoreUpdate: function(store, record, newIndex, oldIndex) {
var me = this,
scroller = me.container.getScrollable().getScroller(),
item;
oldIndex = (typeof oldIndex === 'undefined') ? newIndex : oldIndex;
if (oldIndex !== newIndex) {
// Just refreshing the list here saves a lot of code and shouldnt be much slower
me.doRefresh();
}
else {
if (newIndex >= me.topItemIndex && newIndex < me.topItemIndex + me.listItems.length) {
item = me.getItemAt(newIndex);
me.doUpdateListItem(item, newIndex, me.getListItemInfo());
// Bypassing setter because sometimes we pass the same record (different data)
//me.updateListItem(me.getItemAt(newIndex), newIndex, me.getListItemInfo());
if (me.getVariableHeights() && me.getRefreshHeightOnUpdate()) {
me.updatedItems.push(item);
me.updateItemHeights();
me.refreshScroller(scroller);
}
}
}
},
/*
* @private
* This is to fix the variable heights again since the item height might have changed after the update
*/
refreshScroller: function(scroller) {
if (!scroller) {
scroller = this.container.getScrollable().getScroller()
}
scroller.scrollTo(0, scroller.position.y + 1);
scroller.scrollTo(0, scroller.position.y - 1);
},
onStoreClear: function() {
if (this.headerTranslate) {
this.headerTranslate.translate(0, -10000);
}
this.showEmptyText();
},
onIndex: function(indexBar, index) {
var me = this,
key = index.toLowerCase(),
store = me.getStore(),
groups = store.getGroups(),
ln = groups.length,
scrollable = me.container.getScrollable(),
scroller, group, i, closest, id;
if (scrollable) {
scroller = scrollable.getScroller();
}
else {
return;
}
for (i = 0; i < ln; i++) {
group = groups[i];
id = group.name.toLowerCase();
if (id == key || id > key) {
closest = group;
break;
}
else {
closest = group;
}
}
if (scrollable && closest) {
index = store.indexOf(closest.children[0]);
//stop the scroller from scrolling
scroller.stopAnimation();
//make sure the new offsetTop is not out of bounds for the scroller
var containerSize = scroller.getContainerSize().y,
size = scroller.getSize().y,
maxOffset = size - containerSize,
offsetTop = me.getItemMap().map[index],
offset = (offsetTop > maxOffset) ? maxOffset : offsetTop;
// This is kind of hacky, but since there might be variable heights we have to render the frame
// twice. First to update all the content, then to read the heights and translate items accordingly
scroller.scrollTo(0, offset);
if (this.updatedItems.length > 0 && (!this.scheduledTasks || this.scheduledTasks.length === 0)) {
this.refreshScroller();
}
//scroller.scrollTo(0, offset);
}
},
applyOnItemDisclosure: function(config) {
if (Ext.isFunction(config)) {
return {
scope: this,
handler: config
};
}
return config;
},
handleItemDisclosure: function(e) {
var me = this,
item = Ext.getCmp(Ext.get(e.getTarget()).up('.x-list-item').id),
index = item.dataIndex,
record = me.getStore().getAt(index);
me.fireAction('disclose', [me, record, item, index, e], 'doDisclose');
},
doDisclose: function(me, record, item, index, e) {
var onItemDisclosure = me.getOnItemDisclosure();
if (onItemDisclosure && onItemDisclosure.handler) {
onItemDisclosure.handler.call(onItemDisclosure.scope || me, record, item, index, e);
}
},
updateItemCls: function(newCls, oldCls) {
var items = this.listItems,
ln = items.length,
i, item;
for (i = 0; i < ln; i++) {
item = items[i];
item.removeCls(oldCls);
item.addCls(newCls);
}
},
onItemTouchStart: function(e) {
this.container.innerElement.on({
touchmove: 'onItemTouchMove',
delegate: '.' + Ext.baseCSSPrefix + 'list-item-body',
single: true,
scope: this
});
this.callParent(this.parseEvent(e));
},
onItemTouchMove: function(e) {
this.callParent(this.parseEvent(e));
},
onItemTouchEnd: function(e) {
this.container.innerElement.un({
touchmove: 'onItemTouchMove',
delegate: '.' + Ext.baseCSSPrefix + 'list-item-body',
scope: this
});
this.callParent(this.parseEvent(e));
},
onItemTap: function(e) {
this.callParent(this.parseEvent(e));
},
onItemTapHold: function(e) {
this.callParent(this.parseEvent(e));
},
onItemSingleTap: function(e) {
this.callParent(this.parseEvent(e));
},
onItemDoubleTap: function(e) {
this.callParent(this.parseEvent(e));
},
onItemSwipe: function(e) {
this.callParent(this.parseEvent(e));
},
parseEvent: function(e) {
var me = this,
target = Ext.fly(e.getTarget()).findParent('.' + Ext.baseCSSPrefix + 'list-item', 8),
item = Ext.getCmp(target.id);
return [me, item, item.dataIndex, e];
},
onItemAdd: function(item) {
var me = this,
config = item.config;
if (config.scrollDock) {
if (config.scrollDock == 'bottom') {
me.scrollDockItems.bottom.push(item);
} else {
me.scrollDockItems.top.push(item);
}
item.addCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
if (me.container) {
me.container.add(item);
}
} else {
me.callParent(arguments);
}
},
destroy: function() {
Ext.destroy(this.getIndexBar(), this.indexBarElement, this.header);
if (this.intervalId) {
cancelAnimationFrame(this.intervalId);
delete this.intervalId;
}
this.callParent();
}
});
/**
* NestedList provides a miller column interface to navigate between nested sets
* and provide a clean interface with limited screen real-estate.
*
* @example miniphone preview
* var data = {
* text: 'Groceries',
* items: [{
* text: 'Drinks',
* items: [{
* text: 'Water',
* items: [{
* text: 'Sparkling',
* leaf: true
* }, {
* text: 'Still',
* leaf: true
* }]
* }, {
* text: 'Coffee',
* leaf: true
* }, {
* text: 'Espresso',
* leaf: true
* }, {
* text: 'Redbull',
* leaf: true
* }, {
* text: 'Coke',
* leaf: true
* }, {
* text: 'Diet Coke',
* leaf: true
* }]
* }, {
* text: 'Fruit',
* items: [{
* text: 'Bananas',
* leaf: true
* }, {
* text: 'Lemon',
* leaf: true
* }]
* }, {
* text: 'Snacks',
* items: [{
* text: 'Nuts',
* leaf: true
* }, {
* text: 'Pretzels',
* leaf: true
* }, {
* text: 'Wasabi Peas',
* leaf: true
* }]
* }]
* };
*
* Ext.define('ListItem', {
* extend: 'Ext.data.Model',
* config: {
* fields: [{
* name: 'text',
* type: 'string'
* }]
* }
* });
*
* var store = Ext.create('Ext.data.TreeStore', {
* model: 'ListItem',
* defaultRootProperty: 'items',
* root: data
* });
*
* var nestedList = Ext.create('Ext.NestedList', {
* fullscreen: true,
* title: 'Groceries',
* displayField: 'text',
* store: store
* });
*
* @aside guide nested_list
* @aside example nested-list
* @aside example navigation-view
*/
Ext.define('Ext.dataview.NestedList', {
alternateClassName: 'Ext.NestedList',
extend: 'Ext.Container',
xtype: 'nestedlist',
requires: [
'Ext.List',
'Ext.TitleBar',
'Ext.Button',
'Ext.XTemplate',
'Ext.data.StoreManager',
'Ext.data.NodeStore',
'Ext.data.TreeStore'
],
config: {
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'nested-list',
/**
* @cfg {String/Object/Boolean} cardSwitchAnimation
* Animation to be used during transitions of cards.
* @removed 2.0.0 please use {@link Ext.layout.Card#animation}
*/
/**
* @cfg {String} backText
* The label to display for the back button.
* @accessor
*/
backText: 'Back',
/**
* @cfg {Boolean} useTitleAsBackText
* `true` to use title as a label for back button.
* @accessor
*/
useTitleAsBackText: true,
/**
* @cfg {Boolean} updateTitleText
* Update the title with the currently selected category.
* @accessor
*/
updateTitleText: true,
/**
* @cfg {String} displayField
* Display field to use when setting item text and title.
* This configuration is ignored when overriding {@link #getItemTextTpl} or
* {@link #getTitleTextTpl} for the item text or title.
* @accessor
*/
displayField: 'text',
/**
* @cfg {String} loadingText
* Loading text to display when a subtree is loading.
* @accessor
*/
loadingText: 'Loading...',
/**
* @cfg {String} emptyText
* Empty text to display when a subtree is empty.
* @accessor
*/
emptyText: 'No items available.',
/**
* @cfg {Boolean/Function} onItemDisclosure
* Maps to the {@link Ext.List#onItemDisclosure} configuration for individual lists.
* @accessor
*/
onItemDisclosure: false,
/**
* @cfg {Boolean} allowDeselect
* Set to `true` to allow the user to deselect leaf items via interaction.
* @accessor
*/
allowDeselect: false,
/**
* @deprecated 2.0.0 Please set the {@link #toolbar} configuration to `false` instead
* @cfg {Boolean} useToolbar `true` to show the header toolbar.
* @accessor
*/
useToolbar: null,
/**
* @cfg {Ext.Toolbar/Object/Boolean} toolbar
* The configuration to be used for the toolbar displayed in this nested list.
* @accessor
*/
toolbar: {
docked: 'top',
xtype: 'titlebar',
ui: 'light',
inline: true
},
/**
* @cfg {String} title The title of the toolbar
* @accessor
*/
title: '',
/**
* @cfg {String} layout
* @hide
* @accessor
*/
layout: {
type: 'card',
animation: {
type: 'slide',
duration: 250,
direction: 'left'
}
},
/**
* @cfg {Ext.data.TreeStore/String} store The tree store to be used for this nested list.
*/
store: null,
/**
* @cfg {Ext.Container} detailContainer The container of the `detailCard`.
* @accessor
*/
detailContainer: undefined,
/**
* @cfg {Ext.Component} detailCard to provide a final card for leaf nodes.
* @accessor
*/
detailCard: null,
/**
* @cfg {Object} backButton The configuration for the back button used in the nested list.
*/
backButton: {
ui: 'back',
hidden: true
},
/**
* @cfg {Object} listConfig An optional config object which is merged with the default
* configuration used to create each nested list.
*/
listConfig: null,
// @private
lastNode: null,
// @private
lastActiveList: null
},
/**
* @event itemtap
* Fires when a node is tapped on.
* @param {Ext.dataview.NestedList} this
* @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
* @param {Number} index The index of the item tapped.
* @param {Ext.dom.Element} target The element tapped.
* @param {Ext.data.Record} record The record tapped.
* @param {Ext.event.Event} e The event object.
*/
/**
* @event itemdoubletap
* Fires when a node is double tapped on.
* @param {Ext.dataview.NestedList} this
* @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
* @param {Number} index The index of the item that was tapped.
* @param {Ext.dom.Element} target The element tapped.
* @param {Ext.data.Record} record The record tapped.
* @param {Ext.event.Event} e The event object.
*/
/**
* @event containertap
* Fires when a tap occurs and it is not on a template node.
* @param {Ext.dataview.NestedList} this
* @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
* @param {Ext.event.Event} e The raw event object.
*/
/**
* @event selectionchange
* Fires when the selected nodes change.
* @param {Ext.dataview.NestedList} this
* @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
* @param {Array} selections Array of the selected nodes.
*/
/**
* @event beforeselectionchange
* Fires before a selection is made.
* @param {Ext.dataview.NestedList} this
* @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
* @param {HTMLElement} node The node to be selected.
* @param {Array} selections Array of currently selected nodes.
* @deprecated 2.0.0 Please listen to the {@link #selectionchange} event with an order of `before` instead.
*/
/**
* @event listchange
* Fires when the user taps a list item.
* @param {Ext.dataview.NestedList} this
* @param {Object} listitem The new active list.
*/
/**
* @event leafitemtap
* Fires when the user taps a leaf list item.
* @param {Ext.dataview.NestedList} this
* @param {Ext.List} list The subList the item is on.
* @param {Number} index The index of the item tapped.
* @param {Ext.dom.Element} target The element tapped.
* @param {Ext.data.Record} record The record tapped.
* @param {Ext.event.Event} e The event.
*/
/**
* @event back
* @preventable doBack
* Fires when the user taps Back.
* @param {Ext.dataview.NestedList} this
* @param {HTMLElement} node The node to be selected.
* @param {Ext.dataview.List} lastActiveList The Ext.dataview.List that was last active.
* @param {Boolean} detailCardActive Flag set if the detail card is currently active.
*/
/**
* @event beforeload
* Fires before a request is made for a new data object.
* @param {Ext.dataview.NestedList} this
* @param {Ext.data.Store} store The store instance.
* @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
* load the Store.
*/
/**
* @event load
* Fires whenever records have been loaded into the store.
* @param {Ext.dataview.NestedList} this
* @param {Ext.data.Store} store The store instance.
* @param {Ext.util.Grouper[]} records An array of records.
* @param {Boolean} successful `true` if the operation was successful.
* @param {Ext.data.Operation} operation The associated operation.
*/
constructor: function (config) {
if (Ext.isObject(config)) {
if (config.getTitleTextTpl) {
this.getTitleTextTpl = config.getTitleTextTpl;
}
if (config.getItemTextTpl) {
this.getItemTextTpl = config.getItemTextTpl;
}
}
this.callParent(arguments);
},
onItemInteraction: function () {
if (this.isGoingTo) {
return false;
}
},
applyDetailContainer: function (config) {
if (!config) {
config = this;
}
return config;
},
updateDetailContainer: function (newContainer, oldContainer) {
if (newContainer) {
newContainer.onBefore('activeitemchange', 'onBeforeDetailContainerChange', this);
newContainer.onAfter('activeitemchange', 'onDetailContainerChange', this);
}
},
onBeforeDetailContainerChange: function () {
this.isGoingTo = true;
},
onDetailContainerChange: function () {
this.isGoingTo = false;
},
/**
* Called when an list item has been tapped.
* @param {Ext.List} list The subList the item is on.
* @param {Number} index The id of the item tapped.
* @param {Ext.Element} target The list item tapped.
* @param {Ext.data.Record} record The record which as tapped.
* @param {Ext.event.Event} e The event.
*/
onItemTap: function (list, index, target, record, e) {
var me = this,
store = list.getStore(),
node = store.getAt(index);
me.fireEvent('itemtap', this, list, index, target, record, e);
if (node.isLeaf()) {
me.fireEvent('leafitemtap', this, list, index, target, record, e);
me.goToLeaf(node);
}
else {
this.goToNode(node);
}
},
onBeforeSelect: function () {
this.fireEvent.apply(this, [].concat('beforeselect', this, Array.prototype.slice.call(arguments)));
},
onContainerTap: function () {
this.fireEvent.apply(this, [].concat('containertap', this, Array.prototype.slice.call(arguments)));
},
onSelectionChange: function () {
this.fireEvent.apply(this, [].concat('selectionchange', this, Array.prototype.slice.call(arguments)));
},
onItemDoubleTap: function () {
this.fireEvent.apply(this, [].concat('itemdoubletap', this, Array.prototype.slice.call(arguments)));
},
onStoreBeforeLoad: function () {
var loadingText = this.getLoadingText(),
scrollable = this.getScrollable();
if (loadingText) {
this.setMasked({
xtype: 'loadmask',
message: loadingText
});
//disable scrolling while it is masked
if (scrollable) {
scrollable.getScroller().setDisabled(true);
}
}
this.fireEvent.apply(this, [].concat('beforeload', this, Array.prototype.slice.call(arguments)));
},
onStoreLoad: function (store, records, successful, operation) {
this.setMasked(false);
this.fireEvent.apply(this, [].concat('load', this, Array.prototype.slice.call(arguments)));
if (store.indexOf(this.getLastNode()) === -1) {
this.goToNode(store.getRoot());
}
},
/**
* Called when the backButton has been tapped.
*/
onBackTap: function () {
var me = this,
node = me.getLastNode(),
detailCard = me.getDetailCard(),
detailCardActive = detailCard && me.getActiveItem() == detailCard,
lastActiveList = me.getLastActiveList();
this.fireAction('back', [this, node, lastActiveList, detailCardActive], 'doBack');
},
doBack: function (me, node, lastActiveList, detailCardActive) {
var layout = me.getLayout(),
animation = (layout) ? layout.getAnimation() : null;
if (detailCardActive && lastActiveList) {
if (animation) {
animation.setReverse(true);
}
me.setActiveItem(lastActiveList);
me.setLastNode(node.parentNode);
me.syncToolbar();
}
else {
this.goToNode(node.parentNode);
}
},
updateData: function (data) {
if (!this.getStore()) {
this.setStore(new Ext.data.TreeStore({
root: data
}));
}
},
applyStore: function (store) {
if (store) {
if (Ext.isString(store)) {
// store id
store = Ext.data.StoreManager.get(store);
} else {
// store instance or store config
if (!(store instanceof Ext.data.TreeStore)) {
store = Ext.factory(store, Ext.data.TreeStore, null);
}
}
// <debug>
if (!store) {
Ext.Logger.warn("The specified Store cannot be found", this);
}
//</debug>
}
return store;
},
storeListeners: {
rootchange: 'onStoreRootChange',
load: 'onStoreLoad',
beforeload: 'onStoreBeforeLoad'
},
updateStore: function (newStore, oldStore) {
var me = this,
listeners = this.storeListeners;
listeners.scope = me;
if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
if (oldStore.autoDestroy) {
oldStore.destroy();
}
oldStore.un(listeners);
}
if (newStore) {
me.goToNode(newStore.getRoot());
newStore.on(listeners);
}
},
onStoreRootChange: function (store, node) {
this.goToNode(node);
},
applyBackButton: function (config) {
return Ext.factory(config, Ext.Button, this.getBackButton());
},
applyDetailCard: function (config, oldDetailCard) {
if (config === null) {
return Ext.factory(config, Ext.Component, oldDetailCard);
} else {
return Ext.factory(config, Ext.Component);
}
},
updateBackButton: function (newButton, oldButton) {
if (newButton) {
var me = this;
newButton.on('tap', me.onBackTap, me);
newButton.setText(me.getBackText());
me.getToolbar().insert(0, newButton);
}
else if (oldButton) {
oldButton.destroy();
}
},
applyToolbar: function (config) {
return Ext.factory(config, Ext.TitleBar, this.getToolbar());
},
updateToolbar: function (newToolbar, oldToolbar) {
var me = this;
if (newToolbar) {
newToolbar.setTitle(me.getTitle());
if (!newToolbar.getParent()) {
me.add(newToolbar);
}
}
else if (oldToolbar) {
oldToolbar.destroy();
}
},
updateUseToolbar: function (newUseToolbar, oldUseToolbar) {
if (!newUseToolbar) {
this.setToolbar(false);
}
},
updateTitle: function (newTitle) {
var me = this,
toolbar = me.getToolbar();
if (toolbar) {
if (me.getUpdateTitleText()) {
toolbar.setTitle(newTitle);
}
}
},
/**
* Override this method to provide custom template rendering of individual
* nodes. The template will receive all data within the Record and will also
* receive whether or not it is a leaf node.
* @param {Ext.data.Record} node
* @return {String}
*/
getItemTextTpl: function (node) {
return '{' + this.getDisplayField() + '}';
},
/**
* Override this method to provide custom template rendering of titles/back
* buttons when {@link #useTitleAsBackText} is enabled.
* @param {Ext.data.Record} node
* @return {String}
*/
getTitleTextTpl: function (node) {
return '{' + this.getDisplayField() + '}';
},
/**
* @private
*/
renderTitleText: function (node, forBackButton) {
if (!node.titleTpl) {
node.titleTpl = Ext.create('Ext.XTemplate', this.getTitleTextTpl(node));
}
if (node.isRoot()) {
var initialTitle = this.getInitialConfig('title');
return (forBackButton && initialTitle === '') ? this.getInitialConfig('backText') : initialTitle;
}
return node.titleTpl.applyTemplate(node.data);
},
/**
* Method to handle going to a specific node within this nested list. Node must be part of the
* internal {@link #store}.
* @param {Ext.data.NodeInterface} node The specified node to navigate to.
*/
goToNode: function (node) {
if (!node) {
return;
}
var me = this,
activeItem = me.getActiveItem(),
detailCard = me.getDetailCard(),
detailCardActive = detailCard && me.getActiveItem() == detailCard,
reverse = me.goToNodeReverseAnimation(node),
firstList = me.firstList,
secondList = me.secondList,
layout = me.getLayout(),
animation = (layout) ? layout.getAnimation() : null,
list;
//if the node is a leaf, throw an error
if (node.isLeaf()) {
throw new Error('goToNode: passed a node which is a leaf.');
}
//if we are currently at the passed node, do nothing.
if (node == me.getLastNode() && !detailCardActive) {
return;
}
if (detailCardActive) {
if (animation) {
animation.setReverse(true);
}
list = me.getLastActiveList();
list.getStore().setNode(node);
node.expand();
me.setActiveItem(list);
}
else {
if (firstList && secondList) {
//firstList and secondList have both been created
activeItem = me.getActiveItem();
me.setLastActiveList(activeItem);
list = (activeItem == firstList) ? secondList : firstList;
list.getStore().setNode(node);
node.expand();
if (animation) {
animation.setReverse(reverse);
}
me.setActiveItem(list);
list.deselectAll();
}
else if (firstList) {
//only firstList has been created
me.setLastActiveList(me.getActiveItem());
me.setActiveItem(me.getList(node));
me.secondList = me.getActiveItem();
}
else {
//no lists have been created
me.setActiveItem(me.getList(node));
me.firstList = me.getActiveItem();
}
}
me.fireEvent('listchange', this, me.getActiveItem());
me.setLastNode(node);
me.syncToolbar();
},
/**
* The leaf you want to navigate to. You should pass a node instance.
* @param {Ext.data.NodeInterface} node The specified node to navigate to.
*/
goToLeaf: function (node) {
if (!node.isLeaf()) {
throw new Error('goToLeaf: passed a node which is not a leaf.');
}
var me = this,
card = me.getDetailCard(node),
container = me.getDetailContainer(),
sharedContainer = container == this,
layout = me.getLayout(),
animation = (layout) ? layout.getAnimation() : false;
if (card) {
if (container.getItems().indexOf(card) === -1) {
container.add(card);
}
if (sharedContainer) {
if (me.getActiveItem() instanceof Ext.dataview.List) {
me.setLastActiveList(me.getActiveItem());
}
me.setLastNode(node);
}
if (animation) {
animation.setReverse(false);
}
container.setActiveItem(card);
me.syncToolbar();
}
},
/**
* @private
* Method which updates the {@link #backButton} and {@link #toolbar} with the latest information from
* the current node.
*/
syncToolbar: function (forceDetail) {
var me = this,
detailCard = me.getDetailCard(),
node = me.getLastNode(),
detailActive = forceDetail || (detailCard && (me.getActiveItem() == detailCard)),
parentNode = (detailActive) ? node : node.parentNode,
backButton = me.getBackButton();
//show/hide the backButton, and update the backButton text, if one exists
if (backButton) {
backButton[parentNode ? 'show' : 'hide']();
if (parentNode && me.getUseTitleAsBackText()) {
backButton.setText(me.renderTitleText(node.parentNode, true));
}
}
if (node) {
me.setTitle(me.renderTitleText(node));
}
},
updateBackText: function (newText) {
this.getBackButton().setText(newText);
},
/**
* @private
* Returns `true` if the passed node should have a reverse animation from the previous current node.
* @param {Ext.data.NodeInterface} node
*/
goToNodeReverseAnimation: function (node) {
var me = this,
lastNode = me.getLastNode();
if (!lastNode) {
return false;
}
return (!lastNode.contains(node) && lastNode.isAncestor(node)) ? true : false;
},
/**
* @private
* Returns the list config for a specified node.
* @param {HTMLElement} node The node for the list config.
*/
getList: function (node) {
var me = this,
nodeStore = Ext.create('Ext.data.NodeStore', {
recursive: false,
node: node,
rootVisible: false,
model: me.getStore().getModel()
});
node.expand();
return Ext.Object.merge({
xtype: 'list',
pressedDelay: 250,
autoDestroy: true,
store: nodeStore,
onItemDisclosure: me.getOnItemDisclosure(),
allowDeselect: me.getAllowDeselect(),
variableHeights: false,
listeners: [
{ event: 'itemdoubletap', fn: 'onItemDoubleTap', scope: me },
{ event: 'itemtap', fn: 'onItemInteraction', scope: me, order: 'before'},
{ event: 'itemtouchstart', fn: 'onItemInteraction', scope: me, order: 'before'},
{ event: 'itemtap', fn: 'onItemTap', scope: me },
{ event: 'beforeselectionchange', fn: 'onBeforeSelect', scope: me },
{ event: 'containertap', fn: 'onContainerTap', scope: me },
{ event: 'selectionchange', fn: 'onSelectionChange', order: 'before', scope: me }
],
itemTpl: '<span<tpl if="leaf == true"> class="x-list-item-leaf"</tpl>>' + me.getItemTextTpl(node) + '</span>'
}, this.getListConfig());
}
}, function () {
});
/**
* @private
*/
Ext.define('Ext.dataview.element.List', {
extend: 'Ext.dataview.element.Container',
updateBaseCls: function(newBaseCls) {
var me = this;
me.itemClsShortCache = newBaseCls + '-item';
me.headerClsShortCache = newBaseCls + '-header';
me.headerClsCache = '.' + me.headerClsShortCache;
me.headerItemClsShortCache = newBaseCls + '-header-item';
me.footerClsShortCache = newBaseCls + '-footer-item';
me.footerClsCache = '.' + me.footerClsShortCache;
me.labelClsShortCache = newBaseCls + '-item-label';
me.labelClsCache = '.' + me.labelClsShortCache;
me.disclosureClsShortCache = newBaseCls + '-disclosure';
me.disclosureClsCache = '.' + me.disclosureClsShortCache;
me.iconClsShortCache = newBaseCls + '-icon';
me.iconClsCache = '.' + me.iconClsShortCache;
this.callParent(arguments);
},
hiddenDisplayCache: Ext.baseCSSPrefix + 'hidden-display',
getItemElementConfig: function(index, data) {
var me = this,
dataview = me.dataview,
itemCls = dataview.getItemCls(),
cls = me.itemClsShortCache,
config, iconSrc;
if (itemCls) {
cls += ' ' + itemCls;
}
config = {
cls: cls,
children: [{
cls: me.labelClsShortCache,
html: dataview.getItemTpl().apply(data)
}]
};
if (dataview.getIcon()) {
iconSrc = data.iconSrc;
config.children.push({
cls: me.iconClsShortCache,
style: 'background-image: ' + iconSrc ? 'url("' + newSrc + '")' : ''
});
}
if (dataview.getOnItemDisclosure()) {
config.children.push({
cls: me.disclosureClsShortCache + ' ' + ((data[dataview.getDisclosureProperty()] === false) ? me.hiddenDisplayCache : '')
});
}
return config;
},
updateListItem: function(record, item) {
var me = this,
dataview = me.dataview,
extItem = Ext.fly(item),
innerItem = extItem.down(me.labelClsCache, true),
data = dataview.prepareData(record.getData(true), dataview.getStore().indexOf(record), record),
disclosureProperty = dataview.getDisclosureProperty(),
hasDisclosureProperty = data && data.hasOwnProperty(disclosureProperty),
iconSrc = data && data.hasOwnProperty('iconSrc'),
disclosureEl, iconEl;
innerItem.innerHTML = dataview.getItemTpl().apply(data);
if (hasDisclosureProperty) {
disclosureEl = extItem.down(me.disclosureClsCache);
disclosureEl[data[disclosureProperty] === false ? 'addCls' : 'removeCls'](me.hiddenDisplayCache);
}
if (dataview.getIcon()) {
iconEl = extItem.down(me.iconClsCache, true);
iconEl.style.backgroundImage = iconSrc ? 'url("' + iconSrc + '")' : '';
}
},
doRemoveHeaders: function() {
var me = this,
headerClsShortCache = me.headerItemClsShortCache,
existingHeaders = me.element.query(me.headerClsCache),
existingHeadersLn = existingHeaders.length,
i = 0,
item;
for (; i < existingHeadersLn; i++) {
item = existingHeaders[i];
Ext.fly(item.parentNode).removeCls(headerClsShortCache);
Ext.get(item).destroy();
}
},
doRemoveFooterCls: function() {
var me = this,
footerClsShortCache = me.footerClsShortCache,
existingFooters = me.element.query(me.footerClsCache),
existingFootersLn = existingFooters.length,
i = 0;
for (; i < existingFootersLn; i++) {
Ext.fly(existingFooters[i]).removeCls(footerClsShortCache);
}
},
doAddHeader: function(item, html) {
item = Ext.fly(item);
if (html) {
item.insertFirst(Ext.Element.create({
cls: this.headerClsShortCache,
html: html
}));
}
item.addCls(this.headerItemClsShortCache);
},
destroy: function() {
this.doRemoveHeaders();
this.callParent();
}
});
/**
* @private
*
* This object handles communication between the WebView and Sencha's native shell.
* Currently it has two primary responsibilities:
*
* 1. Maintaining unique string ids for callback functions, together with their scope objects
* 2. Serializing given object data into HTTP GET request parameters
*
* As an example, to capture a photo from the device's camera, we use `Ext.device.Camera.capture()` like:
*
* Ext.device.Camera.capture(
* function(dataUri){
* // Do something with the base64-encoded `dataUri` string
* },
* function(errorMessage) {
*
* },
* callbackScope,
* {
* quality: 75,
* width: 500,
* height: 500
* }
* );
*
* Internally, `Ext.device.Communicator.send()` will then be invoked with the following argument:
*
* Ext.device.Communicator.send({
* command: 'Camera#capture',
* callbacks: {
* onSuccess: function() {
* // ...
* },
* onError: function() {
* // ...
* }
* },
* scope: callbackScope,
* quality: 75,
* width: 500,
* height: 500
* });
*
* Which will then be transformed into a HTTP GET request, sent to native shell's local
* HTTP server with the following parameters:
*
* ?quality=75&width=500&height=500&command=Camera%23capture&onSuccess=3&onError=5
*
* Notice that `onSuccess` and `onError` have been converted into string ids (`3` and `5`
* respectively) and maintained by `Ext.device.Communicator`.
*
* Whenever the requested operation finishes, `Ext.device.Communicator.invoke()` simply needs
* to be executed from the native shell with the corresponding ids given before. For example:
*
* Ext.device.Communicator.invoke('3', ['DATA_URI_OF_THE_CAPTURED_IMAGE_HERE']);
*
* will invoke the original `onSuccess` callback under the given scope. (`callbackScope`), with
* the first argument of 'DATA_URI_OF_THE_CAPTURED_IMAGE_HERE'
*
* Note that `Ext.device.Communicator` maintains the uniqueness of each function callback and
* its scope object. If subsequent calls to `Ext.device.Communicator.send()` have the same
* callback references, the same old ids will simply be reused, which guarantee the best possible
* performance for a large amount of repetitive calls.
*/
Ext.define('Ext.device.communicator.Default', {
SERVER_URL: 'http://localhost:3000', // Change this to the correct server URL
callbackDataMap: {},
callbackIdMap: {},
idSeed: 0,
globalScopeId: '0',
generateId: function() {
return String(++this.idSeed);
},
getId: function(object) {
var id = object.$callbackId;
if (!id) {
object.$callbackId = id = this.generateId();
}
return id;
},
getCallbackId: function(callback, scope) {
var idMap = this.callbackIdMap,
dataMap = this.callbackDataMap,
id, scopeId, callbackId, data;
if (!scope) {
scopeId = this.globalScopeId;
}
else if (scope.isIdentifiable) {
scopeId = scope.getId();
}
else {
scopeId = this.getId(scope);
}
callbackId = this.getId(callback);
if (!idMap[scopeId]) {
idMap[scopeId] = {};
}
if (!idMap[scopeId][callbackId]) {
id = this.generateId();
data = {
callback: callback,
scope: scope
};
idMap[scopeId][callbackId] = id;
dataMap[id] = data;
}
return idMap[scopeId][callbackId];
},
getCallbackData: function(id) {
return this.callbackDataMap[id];
},
invoke: function(id, args) {
var data = this.getCallbackData(id);
data.callback.apply(data.scope, args);
},
send: function(args) {
var callbacks, scope, name, callback;
if (!args) {
args = {};
}
else if (args.callbacks) {
callbacks = args.callbacks;
scope = args.scope;
delete args.callbacks;
delete args.scope;
for (name in callbacks) {
if (callbacks.hasOwnProperty(name)) {
callback = callbacks[name];
if (typeof callback == 'function') {
args[name] = this.getCallbackId(callback, scope);
}
}
}
}
this.doSend(args);
},
doSend: function(args) {
var xhr = new XMLHttpRequest();
xhr.open('GET', this.SERVER_URL + '?' + Ext.Object.toQueryString(args) + '&_dc=' + new Date().getTime(), false);
// wrap the request in a try/catch block so we can check if any errors are thrown and attempt to call any
// failure/callback functions if defined
try {
xhr.send(null);
} catch(e) {
if (args.failure) {
this.invoke(args.failure);
} else if (args.callback) {
this.invoke(args.callback);
}
}
}
});
/**
* @private
*/
Ext.define('Ext.device.communicator.Android', {
extend: 'Ext.device.communicator.Default',
doSend: function(args) {
window.Sencha.action(JSON.stringify(args));
}
});
/**
* @private
*/
Ext.define('Ext.device.Communicator', {
requires: [
'Ext.device.communicator.Default',
'Ext.device.communicator.Android'
],
singleton: true,
constructor: function() {
if (Ext.os.is.Android) {
return new Ext.device.communicator.Android();
}
return new Ext.device.communicator.Default();
}
});
/**
* @private
*/
Ext.define('Ext.device.camera.Abstract', {
source: {
library: 0,
camera: 1,
album: 2
},
destination: {
data: 0, // Returns base64-encoded string
file: 1 // Returns file's URI
},
encoding: {
jpeg: 0,
jpg: 0,
png: 1
},
/**
* Allows you to capture a photo.
*
* @param {Object} options
* The options to use when taking a photo.
*
* @param {Function} options.success
* The success callback which is called when the photo has been taken.
*
* @param {String} options.success.image
* The image which was just taken, either a base64 encoded string or a URI depending on which
* option you chose (destination).
*
* @param {Function} options.failure
* The function which is called when something goes wrong.
*
* @param {Object} scope
* The scope in which to call the `success` and `failure` functions, if specified.
*
* @param {Number} options.quality
* The quality of the image which is returned in the callback. This should be a percentage.
*
* @param {String} options.source
* The source of where the image should be taken. Available options are:
*
* - **album** - prompts the user to choose an image from an album
* - **camera** - prompts the user to take a new photo
* - **library** - prompts the user to choose an image from the library
*
* @param {String} destination
* The destination of the image which is returned. Available options are:
*
* - **data** - returns a base64 encoded string
* - **file** - returns the file's URI
*
* @param {String} encoding
* The encoding of the returned image. Available options are:
*
* - **jpg**
* - **png**
*
* @param {Number} width
* The width of the image to return
*
* @param {Number} height
* The height of the image to return
*/
capture: Ext.emptyFn
});
/**
* @private
*/
Ext.define('Ext.device.camera.PhoneGap', {
extend: 'Ext.device.camera.Abstract',
capture: function(args) {
var onSuccess = args.success,
onError = args.failure,
scope = args.scope,
sources = this.source,
destinations = this.destination,
encodings = this.encoding,
source = args.source,
destination = args.destination,
encoding = args.encoding,
options = {};
if (scope) {
onSuccess = Ext.Function.bind(onSuccess, scope);
onError = Ext.Function.bind(onError, scope);
}
if (source !== undefined) {
options.sourceType = sources.hasOwnProperty(source) ? sources[source] : source;
}
if (destination !== undefined) {
options.destinationType = destinations.hasOwnProperty(destination) ? destinations[destination] : destination;
}
if (encoding !== undefined) {
options.encodingType = encodings.hasOwnProperty(encoding) ? encodings[encoding] : encoding;
}
if ('quality' in args) {
options.quality = args.quality;
}
if ('width' in args) {
options.targetWidth = args.width;
}
if ('height' in args) {
options.targetHeight = args.height;
}
try {
navigator.camera.getPicture(onSuccess, onError, options);
}
catch (e) {
alert(e);
}
}
});
/**
* @private
*/
Ext.define('Ext.device.camera.Sencha', {
extend: 'Ext.device.camera.Abstract',
requires: [
'Ext.device.Communicator'
],
capture: function(options) {
var sources = this.source,
destinations = this.destination,
encodings = this.encoding,
source = options.source,
destination = options.destination,
encoding = options.encoding;
if (sources.hasOwnProperty(source)) {
source = sources[source];
}
if (destinations.hasOwnProperty(destination)) {
destination = destinations[destination];
}
if (encodings.hasOwnProperty(encoding)) {
encoding = encodings[encoding];
}
Ext.device.Communicator.send({
command: 'Camera#capture',
callbacks: {
success: options.success,
failure: options.failure
},
scope: options.scope,
quality: options.quality,
width: options.width,
height: options.height,
source: source,
destination: destination,
encoding: encoding
});
}
});
/**
* @private
*/
Ext.define('Ext.device.camera.Simulator', {
extend: 'Ext.device.camera.Abstract',
config: {
samples: [
{
success: 'http://www.sencha.com/img/sencha-large.png'
}
]
},
constructor: function(config) {
this.initConfig(config);
},
updateSamples: function(samples) {
this.sampleIndex = 0;
},
capture: function(options) {
var index = this.sampleIndex,
samples = this.getSamples(),
samplesCount = samples.length,
sample = samples[index],
scope = options.scope,
success = options.success,
failure = options.failure;
if ('success' in sample) {
if (success) {
success.call(scope, sample.success);
}
}
else {
if (failure) {
failure.call(scope, sample.failure);
}
}
if (++index > samplesCount - 1) {
index = 0;
}
this.sampleIndex = index;
}
});
/**
* This class allows you to use native APIs to take photos using the device camera.
*
* When this singleton is instantiated, it will automatically select the correct implementation depending on the
* current device:
*
* - Sencha Packager
* - PhoneGap
* - Simulator
*
* Both the Sencha Packager and PhoneGap implementations will use the native camera functionality to take or select
* a photo. The Simulator implementation will simply return fake images.
*
* ## Example
*
* You can use the {@link Ext.device.Camera#capture} function to take a photo:
*
* Ext.device.Camera.capture({
* success: function(image) {
* imageView.setSrc(image);
* },
* quality: 75,
* width: 200,
* height: 200,
* destination: 'data'
* });
*
* See the documentation for {@link Ext.device.Camera#capture} all available configurations.
*
* @mixins Ext.device.camera.Abstract
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Camera', {
singleton: true,
requires: [
'Ext.device.Communicator',
'Ext.device.camera.PhoneGap',
'Ext.device.camera.Sencha',
'Ext.device.camera.Simulator'
],
constructor: function() {
var browserEnv = Ext.browser.is;
if (browserEnv.WebView) {
if (browserEnv.PhoneGap) {
return Ext.create('Ext.device.camera.PhoneGap');
}
else {
return Ext.create('Ext.device.camera.Sencha');
}
}
else {
return Ext.create('Ext.device.camera.Simulator');
}
}
});
/**
* @private
*/
Ext.define('Ext.device.connection.Abstract', {
extend: 'Ext.Evented',
config: {
online: false,
type: null
},
/**
* @property {String} UNKNOWN
* Text label for a connection type.
*/
UNKNOWN: 'Unknown connection',
/**
* @property {String} ETHERNET
* Text label for a connection type.
*/
ETHERNET: 'Ethernet connection',
/**
* @property {String} WIFI
* Text label for a connection type.
*/
WIFI: 'WiFi connection',
/**
* @property {String} CELL_2G
* Text label for a connection type.
*/
CELL_2G: 'Cell 2G connection',
/**
* @property {String} CELL_3G
* Text label for a connection type.
*/
CELL_3G: 'Cell 3G connection',
/**
* @property {String} CELL_4G
* Text label for a connection type.
*/
CELL_4G: 'Cell 4G connection',
/**
* @property {String} NONE
* Text label for a connection type.
*/
NONE: 'No network connection',
/**
* True if the device is currently online
* @return {Boolean} online
*/
isOnline: function() {
return this.getOnline();
}
/**
* @method getType
* Returns the current connection type.
* @return {String} type
*/
});
/**
* @private
*/
Ext.define('Ext.device.connection.Sencha', {
extend: 'Ext.device.connection.Abstract',
/**
* @event onlinechange
* Fires when the connection status changes.
* @param {Boolean} online True if you are {@link Ext.device.Connection#isOnline online}
* @param {String} type The new online {@link Ext.device.Connection#getType type}
*/
initialize: function() {
Ext.device.Communicator.send({
command: 'Connection#watch',
callbacks: {
callback: this.onConnectionChange
},
scope: this
});
},
onConnectionChange: function(e) {
this.setOnline(Boolean(e.online));
this.setType(this[e.type]);
this.fireEvent('onlinechange', this.getOnline(), this.getType());
}
});
/**
* @private
*/
Ext.define('Ext.device.connection.PhoneGap', {
extend: 'Ext.device.connection.Abstract',
syncOnline: function() {
var type = navigator.network.connection.type;
this._type = type;
this._online = type != Connection.NONE;
},
getOnline: function() {
this.syncOnline();
return this._online;
},
getType: function() {
this.syncOnline();
return this._type;
}
});
/**
* @private
*/
Ext.define('Ext.device.connection.Simulator', {
extend: 'Ext.device.connection.Abstract',
getOnline: function() {
this._online = navigator.onLine;
this._type = Ext.device.Connection.UNKNOWN;
return this._online;
}
});
/**
* This class is used to check if the current device is currently online or not. It has three different implementations:
*
* - Sencha Packager
* - PhoneGap
* - Simulator
*
* Both the Sencha Packager and PhoneGap implementations will use the native functionality to determine if the current
* device is online. The Simulator version will simply use `navigator.onLine`.
*
* When this singleton ({@link Ext.device.Connection}) is instantiated, it will automatically decide which version to
* use based on the current platform.
*
* ## Examples
*
* Determining if the current device is online:
*
* alert(Ext.device.Connection.isOnline());
*
* Checking the type of connection the device has:
*
* alert('Your connection type is: ' + Ext.device.Connection.getType());
*
* The available connection types are:
*
* - {@link Ext.device.Connection#UNKNOWN UNKNOWN} - Unknown connection
* - {@link Ext.device.Connection#ETHERNET ETHERNET} - Ethernet connection
* - {@link Ext.device.Connection#WIFI WIFI} - WiFi connection
* - {@link Ext.device.Connection#CELL_2G CELL_2G} - Cell 2G connection
* - {@link Ext.device.Connection#CELL_3G CELL_3G} - Cell 3G connection
* - {@link Ext.device.Connection#CELL_4G CELL_4G} - Cell 4G connection
* - {@link Ext.device.Connection#NONE NONE} - No network connection
*
* @mixins Ext.device.connection.Abstract
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Connection', {
singleton: true,
requires: [
'Ext.device.Communicator',
'Ext.device.connection.Sencha',
'Ext.device.connection.PhoneGap',
'Ext.device.connection.Simulator'
],
/**
* @event onlinechange
* @inheritdoc Ext.device.connection.Sencha#onlinechange
*/
constructor: function() {
var browserEnv = Ext.browser.is;
if (browserEnv.WebView) {
if (browserEnv.PhoneGap) {
return Ext.create('Ext.device.connection.PhoneGap');
}
else {
return Ext.create('Ext.device.connection.Sencha');
}
}
else {
return Ext.create('Ext.device.connection.Simulator');
}
}
});
/**
* @private
*/
Ext.define('Ext.device.contacts.Abstract', {
extend: 'Ext.Evented',
config: {
/**
* @cfg {Boolean} includeImages
* True to include images when you get the contacts store. Please beware that this can be very slow.
*/
includeImages: false
},
/**
* Returns an Array of contact objects.
* @return {Object[]} An array of contact objects.
*/
getContacts: function(config) {
if (!this._store) {
this._store = [
{
first: 'Robert',
last: 'Dougan',
emails: {
work: 'rob@sencha.com'
}
},
{
first: 'Jamie',
last: 'Avins',
emails: {
work: 'jamie@sencha.com'
}
}
];
}
config.success.call(config.scope || this, this._store);
},
/**
* Returns base64 encoded image thumbnail for a contact specified in config.id
* @return {String} base64 string
*/
getThumbnail: function(config) {
config.callback.call(config.scope || this, "");
},
/**
* Returns localized, user readable label for a contact field (i.e. "Mobile", "Home")
* @return {String} user readable string
*/
getLocalizedLabel: function(config) {
config.callback.call(config.scope || this, config.label.toUpperCase(), config.label);
}
});
/**
* @private
*/
Ext.define('Ext.device.contacts.Sencha', {
extend: 'Ext.device.contacts.Abstract',
getContacts: function(config) {
var includeImages = this.getIncludeImages();
if (typeof config.includeImages != "undefined") {
includeImages = config.includeImages;
}
if (!config) {
Ext.Logger.warn('Ext.device.Contacts#getContacts: You must specify a `config` object.');
return false;
}
if (!config.success) {
Ext.Logger.warn('Ext.device.Contacts#getContacts: You must specify a `success` method.');
return false;
}
Ext.device.Communicator.send({
command: 'Contacts#all',
callbacks: {
success: function(contacts) {
config.success.call(config.scope || this, contacts);
},
failure: function() {
if (config.failure) {
config.failure.call(config.scope || this);
}
}
},
includeImages: includeImages,
scope: this
});
},
getThumbnail: function(config) {
if (!config || typeof config.id == "undefined") {
Ext.Logger.warn('Ext.device.Contacts#getThumbnail: You must specify an `id` of the contact.');
return false;
}
if (!config || !config.callback) {
Ext.Logger.warn('Ext.device.Contacts#getThumbnail: You must specify a `callback`.');
return false;
}
Ext.device.Communicator.send({
command: 'Contacts#getThumbnail',
callbacks: {
success: function(src) {
this.set('thumbnail', src);
if (config.callback) {
config.callback.call(config.scope || this, this);
}
}
},
id: id,
scope: this
});
},
getLocalizedLabel: function(config) {
if (!config || typeof config.label == "undefined") {
Ext.Logger.warn('Ext.device.Contacts#getLocalizedLabel: You must specify an `label` to be localized.');
return false;
}
if (!config || !config.callback) {
Ext.Logger.warn('Ext.device.Contacts#getLocalizedLabel: You must specify a `callback`.');
return false;
}
Ext.device.Communicator.send({
command: 'Contacts#getLocalizedLabel',
callbacks: {
callback: function(label) {
config.callback.call(config.scope || this, label, config.label);
}
},
label: config.label,
scope: this
});
}
});
/**
* This device API allows you to access a users contacts using a {@link Ext.data.Store}. This allows you to search, filter
* and sort through all the contacts using its methods.
*
* To use this API, all you need to do is require this class (`Ext.device.Contacts`) and then use `Ext.device.Contacts.getContacts()`
* to retrieve an array of contacts.
*
* **Please note that this will *only* work using the Sencha Native Packager.**
*
* # Example
*
* Ext.application({
* name: 'Sencha',
* requires: 'Ext.device.Contacts',
*
* launch: function() {
* Ext.Viewport.add({
* xtype: 'list',
* itemTpl: '{First} {Last}',
* store: {
* fields: ['First', 'Last'],
* data: Ext.device.Contacts.getContacts()
* }
* });
* }
* });
*
* @mixins Ext.device.contacts.Abstract
* @mixins Ext.device.contacts.Sencha
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Contacts', {
singleton: true,
requires: [
'Ext.device.Communicator',
'Ext.device.contacts.Sencha'
],
constructor: function() {
var browserEnv = Ext.browser.is;
if (browserEnv.WebView && !browserEnv.PhoneGap) {
return Ext.create('Ext.device.contacts.Sencha');
} else {
return Ext.create('Ext.device.contacts.Abstract');
}
}
});
/**
* @private
*/
Ext.define('Ext.device.device.Abstract', {
extend: 'Ext.EventedBase',
/**
* @event schemeupdate
* Event which is fired when your Sencha Native packaged application is opened from another application using a custom URL scheme.
*
* This event will only fire if the application was already open (in other words; `onReady` was already fired). This means you should check
* if {@link Ext.device.Device#scheme} is set in your Application `launch`/`onReady` method, and perform any needed changes for that URL (if defined).
* Then listen to this event for future changed.
*
* ## Example
*
* Ext.application({
* name: 'Sencha',
* requires: ['Ext.device.Device'],
* launch: function() {
* if (Ext.device.Device.scheme) {
* // the application was opened via another application. Do something:
* console.log('Applicaton opened via another application: ' + Ext.device.Device.scheme.url);
* }
*
* // Listen for future changes
* Ext.device.Device.on('schemeupdate', function(device, scheme) {
* // the application was launched, closed, and then launched another from another application
* // this means onReady wont be called again ('cause the application is already running in the
* // background) - but this event will be fired
* console.log('Applicated reopened via another application: ' + scheme.url);
* }, this);
* }
* });
*
* __Note:__ This currently only works with the Sencha Native Packager. If you attempt to listen to this event when packaged with
* PhoneGap or simply in the browser, it will never fire.**
*
* @param {Ext.device.Device} this The instance of Ext.device.Device
* @param {Object/Boolean} scheme The scheme information, if opened via another application
* @param {String} scheme.url The URL that was opened, if this application was opened via another application. Example: `sencha:`
* @param {String} scheme.sourceApplication The source application that opened this application. Example: `com.apple.safari`.
*/
/**
* @property {String} name
* Returns the name of the current device. If the current device does not have a name (for example, in a browser), it will
* default to `not available`.
*
* alert('Device name: ' + Ext.device.Device.name);
*/
name: 'not available',
/**
* @property {String} uuid
* Returns a unique identifier for the current device. If the current device does not have a unique identifier (for example,
* in a browser), it will default to `anonymous`.
*
* alert('Device UUID: ' + Ext.device.Device.uuid);
*/
uuid: 'anonymous',
/**
* @property {String} platform
* The current platform the device is running on.
*
* alert('Device platform: ' + Ext.device.Device.platform);
*/
platform: Ext.os.name,
/**
* @property {Object/Boolean} scheme
*
*/
scheme: false,
/**
* Opens a specified URL. The URL can contain a custom URL Scheme for another app or service:
*
* // Safari
* Ext.device.Device.openURL('http://sencha.com');
*
* // Telephone
* Ext.device.Device.openURL('tel:6501231234');
*
* // SMS with a default number
* Ext.device.Device.openURL('sms:+12345678901');
*
* // Email client
* Ext.device.Device.openURL('mailto:rob@sencha.com');
*
* You can find a full list of available URL schemes here: [http://wiki.akosma.com/IPhone_URL_Schemes](http://wiki.akosma.com/IPhone_URL_Schemes).
*
* __Note:__ This currently only works on iOS using the Sencha Native Packager. Attempting to use this on PhoneGap, iOS Simulator
* or the browser will simply result in the current window location changing.**
*
* If successful, this will close the application (as another one opens).
*
* @param {String} url The URL to open
*/
openURL: function(url) {
window.location = url;
}
});
/**
* @private
*/
Ext.define('Ext.device.device.PhoneGap', {
extend: 'Ext.device.device.Abstract',
constructor: function() {
// We can't get the device details until the device is ready, so lets wait.
if (Ext.Viewport.isReady) {
this.onReady();
} else {
Ext.Viewport.on('ready', this.onReady, this, {single: true});
}
},
onReady: function() {
this.name = device.name;
this.uuid = device.uuid;
this.platform = device.platformName || Ext.os.name;
}
});
/**
* @private
*/
Ext.define('Ext.device.device.Sencha', {
extend: 'Ext.device.device.Abstract',
constructor: function() {
this.name = device.name;
this.uuid = device.uuid;
this.platform = device.platformName || Ext.os.name;
this.initURL();
},
openURL: function(url) {
Ext.device.Communicator.send({
command: 'OpenURL#open',
url: url
});
},
/**
* @private
*/
initURL: function() {
Ext.device.Communicator.send({
command: "OpenURL#watch",
callbacks: {
callback: this.updateURL
},
scope: this
});
},
/**
* @private
*/
updateURL: function() {
this.scheme = device.scheme || false;
this.fireEvent('schemeupdate', this, this.scheme);
}
});
/**
* @private
*/
Ext.define('Ext.device.device.Simulator', {
extend: 'Ext.device.device.Abstract'
});
/**
* Provides a cross device way to get information about the device your application is running on. There are 3 different implementations:
*
* - Sencha Packager
* - [PhoneGap](http://docs.phonegap.com/en/1.4.1/phonegap_device_device.md.html)
* - Simulator
*
* ## Examples
*
* #### Device Information
*
* Getting the device information:
*
* Ext.application({
* name: 'Sencha',
*
* // Remember that the Ext.device.Device class *must* be required
* requires: ['Ext.device.Device'],
*
* launch: function() {
* alert([
* 'Device name: ' + Ext.device.Device.name,
* 'Device platform: ' + Ext.device.Device.platform,
* 'Device UUID: ' + Ext.device.Device.uuid
* ].join('\n'));
* }
* });
*
* ### Custom Scheme URLs
*
* Using custom scheme URLs to application your application from other applications:
*
* Ext.application({
* name: 'Sencha',
* requires: ['Ext.device.Device'],
* launch: function() {
* if (Ext.device.Device.scheme) {
* // the application was opened via another application. Do something:
* alert('Applicaton pened via another application: ' + Ext.device.Device.scheme.url);
* }
*
* // Listen for future changes
* Ext.device.Device.on('schemeupdate', function(device, scheme) {
* // the application was launched, closed, and then launched another from another application
* // this means onReady wont be called again ('cause the application is already running in the
* // background) - but this event will be fired
* alert('Applicated reopened via another application: ' + scheme.url);
* }, this);
* }
* });
*
* Of course, you must add add the custom URLs you would like to use when packaging your application. You can do this by adding
* the following code into the `rawConfig` property inside your `package.json` file (Sencha Native Packager configuration file):
*
* {
* ...
* "rawConfig": "<key>CFBundleURLTypes</key><array><dict><key>CFBundleURLSchemes</key><array><string>sencha</string></array><key>CFBundleURLName</key><string>com.sencha.example</string></dict></array>"
* ...
* }
*
* You can change the available URL schemes and the application identifier above.
*
* You can then test it by packaging and installing the application onto a device/iOS Simulator, opening Safari and typing: `sencha:testing`.
* The application will launch and it will `alert` the URL you specified.
*
* **PLEASE NOTE: This currently only works with the Sencha Native Packager. If you attempt to listen to this event when packaged with
* PhoneGap or simply in the browser, it will not function.**
*
* @mixins Ext.device.device.Abstract
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Device', {
singleton: true,
requires: [
'Ext.device.Communicator',
'Ext.device.device.PhoneGap',
'Ext.device.device.Sencha',
'Ext.device.device.Simulator'
],
constructor: function() {
var browserEnv = Ext.browser.is;
if (browserEnv.WebView) {
if (browserEnv.PhoneGap) {
return Ext.create('Ext.device.device.PhoneGap');
}
else {
return Ext.create('Ext.device.device.Sencha');
}
}
else {
return Ext.create('Ext.device.device.Simulator');
}
}
});
/**
* @private
*/
Ext.define('Ext.device.geolocation.Abstract', {
config: {
/**
* @cfg {Number} maximumAge
* This option indicates that the application is willing to accept cached location information whose age
* is no greater than the specified time in milliseconds. If maximumAge is set to 0, an attempt to retrieve
* new location information is made immediately.
*/
maximumAge: 0,
/**
* @cfg {Number} frequency The default frequency to get the current position when using {@link Ext.device.Geolocation#watchPosition}.
*/
frequency: 10000,
/**
* @cfg {Boolean} allowHighAccuracy True to allow high accuracy when getting the current position.
*/
allowHighAccuracy: false,
/**
* @cfg {Number} timeout
* The maximum number of milliseconds allowed to elapse between a location update operation.
*/
timeout: Infinity
},
/**
* Attempts to get the current position of this device.
*
* Ext.device.Geolocation.getCurrentPosition({
* success: function(position) {
* console.log(position);
* },
* failure: function() {
* Ext.Msg.alert('Geolocation', 'Something went wrong!');
* }
* });
*
* *Note:* If you want to watch the current position, you could use {@link Ext.device.Geolocation#watchPosition} instead.
*
* @param {Object} config An object which contains the following config options:
*
* @param {Function} config.success
* The function to call when the location of the current device has been received.
*
* @param {Object} config.success.position
*
* @param {Function} config.failure
* The function that is called when something goes wrong.
*
* @param {Object} config.scope
* The scope of the `success` and `failure` functions.
*
* @param {Number} config.maximumAge
* The maximum age of a cached location. If you do not enter a value for this, the value of {@link #maximumAge}
* will be used.
*
* @param {Number} config.timeout
* The timeout for this request. If you do not specify a value, it will default to {@link #timeout}.
*
* @param {Boolean} config.allowHighAccuracy
* True to enable allow accuracy detection of the location of the current device. If you do not specify a value, it will
* default to {@link #allowHighAccuracy}.
*/
getCurrentPosition: function(config) {
var defaultConfig = Ext.device.geolocation.Abstract.prototype.config;
config = Ext.applyIf(config, {
maximumAge: defaultConfig.maximumAge,
frequency: defaultConfig.frequency,
allowHighAccuracy: defaultConfig.allowHighAccuracy,
timeout: defaultConfig.timeout
});
// <debug>
if (!config.success) {
Ext.Logger.warn('You need to specify a `success` function for #getCurrentPosition');
}
// </debug>
return config;
},
/**
* Watches for the current position and calls the callback when successful depending on the specified {@link #frequency}.
*
* Ext.device.Geolocation.watchPosition({
* callback: function(position) {
* console.log(position);
* },
* failure: function() {
* Ext.Msg.alert('Geolocation', 'Something went wrong!');
* }
* });
*
* @param {Object} config An object which contains the following config options:
*
* @param {Function} config.callback
* The function to be called when the position has been updated.
*
* @param {Function} config.failure
* The function that is called when something goes wrong.
*
* @param {Object} config.scope
* The scope of the `success` and `failure` functions.
*
* @param {Boolean} config.frequency
* The frequency in which to call the supplied callback. Defaults to {@link #frequency} if you do not specify a value.
*
* @param {Boolean} config.allowHighAccuracy
* True to enable allow accuracy detection of the location of the current device. If you do not specify a value, it will
* default to {@link #allowHighAccuracy}.
*/
watchPosition: function(config) {
var defaultConfig = Ext.device.geolocation.Abstract.prototype.config;
config = Ext.applyIf(config, {
maximumAge: defaultConfig.maximumAge,
frequency: defaultConfig.frequency,
allowHighAccuracy: defaultConfig.allowHighAccuracy,
timeout: defaultConfig.timeout
});
// <debug>
if (!config.callback) {
Ext.Logger.warn('You need to specify a `callback` function for #watchPosition');
}
// </debug>
return config;
},
/**
* If you are currently watching for the current position, this will stop that task.
*/
clearWatch: function() {}
});
/**
* @private
*/
Ext.define('Ext.device.geolocation.Sencha', {
extend: 'Ext.device.geolocation.Abstract',
getCurrentPosition: function(config) {
config = this.callParent([config]);
Ext.apply(config, {
command: 'Geolocation#getCurrentPosition',
callbacks: {
success: config.success,
failure: config.failure
}
});
Ext.applyIf(config, {
scope: this
});
delete config.success;
delete config.failure;
Ext.device.Communicator.send(config);
return config;
},
watchPosition: function(config) {
config = this.callParent([config]);
Ext.apply(config, {
command: 'Geolocation#watchPosition',
callbacks: {
success: config.callback,
failure: config.failure
}
});
Ext.applyIf(config, {
scope: this
});
delete config.callback;
delete config.failure;
Ext.device.Communicator.send(config);
return config;
},
clearWatch: function() {
Ext.device.Communicator.send({
command: 'Geolocation#clearWatch'
});
}
});
/**
* @private
*/
Ext.define('Ext.device.geolocation.Simulator', {
extend: 'Ext.device.geolocation.Abstract',
requires: ['Ext.util.Geolocation'],
getCurrentPosition: function(config) {
config = this.callParent([config]);
Ext.apply(config, {
autoUpdate: false,
listeners: {
scope: this,
locationupdate: function(geolocation) {
if (config.success) {
config.success.call(config.scope || this, geolocation.position);
}
},
locationerror: function() {
if (config.failure) {
config.failure.call(config.scope || this);
}
}
}
});
this.geolocation = Ext.create('Ext.util.Geolocation', config);
this.geolocation.updateLocation();
return config;
},
watchPosition: function(config) {
config = this.callParent([config]);
Ext.apply(config, {
listeners: {
scope: this,
locationupdate: function(geolocation) {
if (config.callback) {
config.callback.call(config.scope || this, geolocation.position);
}
},
locationerror: function() {
if (config.failure) {
config.failure.call(config.scope || this);
}
}
}
});
this.geolocation = Ext.create('Ext.util.Geolocation', config);
return config;
},
clearWatch: function() {
if (this.geolocation) {
this.geolocation.destroy();
}
this.geolocation = null;
}
});
/**
* Provides access to the native Geolocation API when running on a device. There are three implementations of this API:
*
* - Sencha Packager
* - [PhoneGap](http://docs.phonegap.com/en/1.4.1/phonegap_device_device.md.html)
* - Browser
*
* This class will automatically select the correct implementation depending on the device your application is running on.
*
* ## Examples
*
* Getting the current location:
*
* Ext.device.Geolocation.getCurrentPosition({
* success: function(position) {
* console.log(position.coords);
* },
* failure: function() {
* console.log('something went wrong!');
* }
* });
*
* Watching the current location:
*
* Ext.device.Geolocation.watchPosition({
* frequency: 3000, // Update every 3 seconds
* callback: function(position) {
* console.log('Position updated!', position.coords);
* },
* failure: function() {
* console.log('something went wrong!');
* }
* });
*
* @mixins Ext.device.geolocation.Abstract
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Geolocation', {
singleton: true,
requires: [
'Ext.device.Communicator',
'Ext.device.geolocation.Sencha',
'Ext.device.geolocation.Simulator'
],
constructor: function() {
var browserEnv = Ext.browser.is;
if (browserEnv.WebView && browserEnv.Sencha) {
return Ext.create('Ext.device.geolocation.Sencha');
}
else {
return Ext.create('Ext.device.geolocation.Simulator');
}
}
});
/**
* @private
*/
Ext.define('Ext.device.notification.Abstract', {
/**
* A simple way to show a notification.
*
* Ext.device.Notification.show({
* title: 'Verification',
* message: 'Is your email address is: test@sencha.com',
* buttons: Ext.MessageBox.OKCANCEL,
* callback: function(button) {
* if (button == "ok") {
* console.log('Verified');
* } else {
* console.log('Nope.');
* }
* }
* });
*
* @param {Object} config An object which contains the following config options:
*
* @param {String} config.title The title of the notification
*
* @param {String} config.message The message to be displayed on the notification
*
* @param {String/String[]} [config.buttons="OK"]
* The buttons to be displayed on the notification. It can be a string, which is the title of the button, or an array of multiple strings.
* Please not that you should not use more than 2 buttons, as they may not be displayed correct on all devices.
*
* @param {Function} config.callback
* A callback function which is called when the notification is dismissed by clicking on the configured buttons.
* @param {String} config.callback.buttonId The id of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
*
* @param {Object} config.scope The scope of the callback function
*/
show: function(config) {
if (!config.message) {
throw('[Ext.device.Notification#show] You passed no message');
}
if (!config.buttons) {
config.buttons = "OK";
}
if (!Ext.isArray(config.buttons)) {
config.buttons = [config.buttons];
}
if (!config.scope) {
config.scope = this;
}
return config;
},
/**
* Vibrates the device.
*/
vibrate: Ext.emptyFn
});
/**
* @private
*/
Ext.define('Ext.device.notification.PhoneGap', {
extend: 'Ext.device.notification.Abstract',
requires: ['Ext.device.Communicator'],
show: function() {
var config = this.callParent(arguments),
buttons = (config.buttons) ? config.buttons.join(',') : null,
onShowCallback = function(index) {
if (config.callback) {
config.callback.apply(config.scope, (config.buttons) ? [config.buttons[index - 1]].toLowerCase() : []);
}
};
// change Ext.MessageBox buttons into normal arrays
var ln = butons.length;
if (ln && typeof buttons[0] != "string") {
var newButtons = [],
i;
for (i = 0; i < ln; i++) {
newButtons.push(buttons[i].text);
}
buttons = newButtons;
}
navigator.notification.confirm(
config.message, // message
onShowCallback, // callback
config.title, // title
buttons // array of button names
);
},
vibrate: function() {
navigator.notification.vibrate(2000);
}
});
/**
* @private
*/
Ext.define('Ext.device.notification.Sencha', {
extend: 'Ext.device.notification.Abstract',
requires: ['Ext.device.Communicator'],
show: function() {
var config = this.callParent(arguments);
Ext.device.Communicator.send({
command: 'Notification#show',
callbacks: {
callback: config.callback
},
scope : config.scope,
title : config.title,
message: config.message,
buttons: config.buttons.join(',') //@todo fix this
});
},
vibrate: function() {
Ext.device.Communicator.send({
command: 'Notification#vibrate'
});
}
});
/**
* @private
*/
Ext.define('Ext.device.notification.Simulator', {
extend: 'Ext.device.notification.Abstract',
requires: ['Ext.MessageBox'],
// @private
msg: null,
show: function() {
var config = this.callParent(arguments),
buttons = [],
ln = config.buttons.length,
button, i, callback, msg;
//buttons
for (i = 0; i < ln; i++) {
button = config.buttons[i];
if (Ext.isString(button)) {
button = {
text: config.buttons[i],
itemId: config.buttons[i].toLowerCase()
};
}
buttons.push(button);
}
this.msg = Ext.create('Ext.MessageBox');
msg = this.msg;
callback = function(itemId) {
if (config.callback) {
config.callback.apply(config.scope, [itemId]);
}
};
this.msg.show({
title : config.title,
message: config.message,
scope : this.msg,
buttons: buttons,
fn : callback
});
},
vibrate: function() {
//nice animation to fake vibration
var animation = [
"@-webkit-keyframes vibrate{",
" from {",
" -webkit-transform: rotate(-2deg);",
" }",
" to{",
" -webkit-transform: rotate(2deg);",
" }",
"}",
"body {",
" -webkit-animation: vibrate 50ms linear 10 alternate;",
"}"
];
var head = document.getElementsByTagName("head")[0];
var cssNode = document.createElement('style');
cssNode.innerHTML = animation.join('\n');
head.appendChild(cssNode);
setTimeout(function() {
head.removeChild(cssNode);
}, 400);
}
});
/**
* Provides a cross device way to show notifications. There are three different implementations:
*
* - Sencha Packager
* - PhoneGap
* - Simulator
*
* When this singleton is instantiated, it will automatically use the correct implementation depending on the current device.
*
* Both the Sencha Packager and PhoneGap versions will use the native implementations to display the notification. The
* Simulator implementation will use {@link Ext.MessageBox} for {@link #show} and a simply animation when you call {@link #vibrate}.
*
* ## Examples
*
* To show a simple notification:
*
* Ext.device.Notification.show({
* title: 'Verification',
* message: 'Is your email address: test@sencha.com',
* buttons: Ext.MessageBox.OKCANCEL,
* callback: function(button) {
* if (button === "ok") {
* console.log('Verified');
* } else {
* console.log('Nope');
* }
* }
* });
*
* To make the device vibrate:
*
* Ext.device.Notification.vibrate();
*
* @mixins Ext.device.notification.Abstract
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Notification', {
singleton: true,
requires: [
'Ext.device.Communicator',
'Ext.device.notification.PhoneGap',
'Ext.device.notification.Sencha',
'Ext.device.notification.Simulator'
],
constructor: function() {
var browserEnv = Ext.browser.is;
if (browserEnv.WebView) {
if (browserEnv.PhoneGap) {
return Ext.create('Ext.device.notification.PhoneGap');
}
else {
return Ext.create('Ext.device.notification.Sencha');
}
}
else {
return Ext.create('Ext.device.notification.Simulator');
}
}
});
/**
* @private
*/
Ext.define('Ext.device.orientation.Abstract', {
extend: 'Ext.EventedBase',
/**
* @event orientationchange
* Fires when the orientation has been changed on this device.
*
* Ext.device.Orientation.on({
* scope: this,
* orientationchange: function(e) {
* console.log('Alpha: ', e.alpha);
* console.log('Beta: ', e.beta);
* console.log('Gamma: ', e.gamma);
* }
* });
*
* @param {Object} event The event object
* @param {Object} event.alpha The alpha value of the orientation event
* @param {Object} event.beta The beta value of the orientation event
* @param {Object} event.gamma The gamma value of the orientation event
*/
onDeviceOrientation: function(e) {
this.doFireEvent('orientationchange', [e]);
}
});
/**
* Provides the HTML5 implementation for the orientation API.
* @private
*/
Ext.define('Ext.device.orientation.HTML5', {
extend: 'Ext.device.orientation.Abstract',
initialize: function() {
this.onDeviceOrientation = Ext.Function.bind(this.onDeviceOrientation, this);
window.addEventListener('deviceorientation', this.onDeviceOrientation, true);
}
});
/**
* @private
*/
Ext.define('Ext.device.orientation.Sencha', {
extend: 'Ext.device.orientation.Abstract',
requires: [
'Ext.device.Communicator'
],
/**
* From the native shell, the callback needs to be invoked infinitely using a timer, ideally 50 times per second.
* The callback expects one event object argument, the format of which should looks like this:
*
* {
* alpha: 0,
* beta: 0,
* gamma: 0
* }
*
* Refer to [Safari DeviceOrientationEvent Class Reference][1] for more details.
*
* [1]: http://developer.apple.com/library/safari/#documentation/SafariDOMAdditions/Reference/DeviceOrientationEventClassRef/DeviceOrientationEvent/DeviceOrientationEvent.html
*/
initialize: function() {
Ext.device.Communicator.send({
command: 'Orientation#watch',
callbacks: {
callback: this.onDeviceOrientation
},
scope: this
});
}
});
/**
* This class provides you with a cross platform way of listening to when the the orientation changes on the
* device your application is running on.
*
* The {@link Ext.device.Orientation#orientationchange orientationchange} event gets passes the `alpha`, `beta` and
* `gamma` values.
*
* You can find more information about these values and how to use them on the [W3C device orientation specification](http://dev.w3.org/geo/api/spec-source-orientation.html#deviceorientation).
*
* ## Example
*
* To listen to the device orientation, you can do the following:
*
* Ext.device.Orientation.on({
* scope: this,
* orientationchange: function(e) {
* console.log('Alpha: ', e.alpha);
* console.log('Beta: ', e.beta);
* console.log('Gamma: ', e.gamma);
* }
* });
*
* @mixins Ext.device.orientation.Abstract
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Orientation', {
singleton: true,
requires: [
'Ext.device.Communicator',
'Ext.device.orientation.HTML5',
'Ext.device.orientation.Sencha'
],
constructor: function() {
var browserEnv = Ext.browser.is;
if (browserEnv.Sencha) {
return Ext.create('Ext.device.orientation.Sencha');
}
else {
return Ext.create('Ext.device.orientation.HTML5');
}
}
});
/**
* @private
*/
Ext.define('Ext.device.purchases.Sencha', {
/**
* Checks if the current user is able to make payments.
*
* ## Example
*
* Ext.device.Purchases.canMakePayments({
* success: function() {
* console.log('Yup! :)');
* },
* failure: function() {
* console.log('Nope! :(');
* }
* });
*
* @param {Object} config
* @param {Function} config.success
* @param {Function} config.failure
* @param {Object} config.scope
*/
canMakePayments: function(config) {
if (!config.success) {
Ext.Logger.error('You must specify a `success` callback for `#canMakePayments` to work.');
return false;
}
if (!config.failure) {
Ext.Logger.error('You must specify a `failure` callback for `#canMakePayments` to work.');
return false;
}
Ext.device.Communicator.send({
command: 'Purchase#canMakePayments',
callbacks: {
success: config.success,
failure: config.failure
},
scope: config.scope || this
});
},
/**
* Returns a {@link Ext.data.Store} instance of all the available products.
*
* ## Example
*
* Ext.device.Purchases.getProducts({
* success: function(store) {
* console.log('Got the store! You have ' + store.getCount() + ' products.');
* },
* failure: function() {
* console.log('Oops. Looks like something went wrong.');
* }
* });
*
* @param {Object} config
* @param {Function} config.success
* @param {Ext.data.Store} config.success.store A store of products available to purchase.
* @param {Function} config.failure
* @param {Object} config.scope
*/
getProducts: function(config) {
if (!config.success) {
Ext.Logger.error('You must specify a `success` callback for `#getProducts` to work.');
return false;
}
if (!config.failure) {
Ext.Logger.error('You must specify a `failure` callback for `#getProducts` to work.');
return false;
}
Ext.device.Communicator.send({
command: 'Purchase#getProducts',
callbacks: {
success: function(products) {
var store = Ext.create('Ext.data.Store', {
model: 'Ext.device.Purchases.Product',
data: products
});
config.success.call(config.scope || this, store);
},
failure: config.failure
},
scope: config.scope || this
});
},
/**
* Returns all purchases ever made by this user.
* @param {Object} config
* @param {Function} config.success
* @param {Array[]} config.success.purchases
* @param {Function} config.failure
* @param {Object} config.scope
*/
getPurchases: function(config) {
if (!config.success) {
Ext.Logger.error('You must specify a `success` callback for `#getPurchases` to work.');
return false;
}
if (!config.failure) {
Ext.Logger.error('You must specify a `failure` callback for `#getPurchases` to work.');
return false;
}
Ext.device.Communicator.send({
command: 'Purchase#getPurchases',
callbacks: {
success: function(purchases) {
var array = [],
ln = purchases.length,
i;
for (i = 0; i < ln; i++) {
array.push({
productIdentifier: purchases[i]
});
}
var store = Ext.create('Ext.data.Store', {
model: 'Ext.device.Purchases.Purchase',
data: array
});
config.success.call(config.scope || this, store);
},
failure: function() {
config.failure.call(config.scope || this);
}
},
scope: config.scope || this
});
},
/**
* Returns all purchases that are currently pending.
* @param {Object} config
* @param {Function} config.success
* @param {Ext.data.Store} config.success.purchases
* @param {Function} config.failure
* @param {Object} config.scope
*/
getPendingPurchases: function(config) {
if (!config.success) {
Ext.Logger.error('You must specify a `success` callback for `#getPendingPurchases` to work.');
return false;
}
if (!config.failure) {
Ext.Logger.error('You must specify a `failure` callback for `#getPendingPurchases` to work.');
return false;
}
Ext.device.Communicator.send({
command: 'Purchase#getPendingPurchases',
callbacks: {
success: function(purchases) {
var array = [],
ln = purchases.length,
i;
for (i = 0; i < ln; i++) {
array.push({
productIdentifier: purchases[i],
state: 'pending'
});
}
var store = Ext.create('Ext.data.Store', {
model: 'Ext.device.Purchases.Purchase',
data: array
});
config.success.call(config.scope || this, store);
},
failure: function() {
config.failure.call(config.scope || this);
}
},
scope: config.scope || this
});
}
}, function() {
/**
* The product model class which is uses when fetching available products using {@link Ext.device.Purchases#getProducts}.
*/
Ext.define('Ext.device.Purchases.Product', {
extend: 'Ext.data.Model',
config: {
fields: [
'localizeTitle',
'price',
'priceLocale',
'localizedDescription',
'productIdentifier'
]
},
/**
* Will attempt to purchase this product.
*
* ## Example
*
* product.purchase({
* success: function() {
* console.log(product.get('title') + ' purchased!');
* },
* failure: function() {
* console.log('Something went wrong while trying to purchase ' + product.get('title'));
* }
* });
*
* @param {Object} config
* @param {Ext.data.Model/String} config.product
* @param {Function} config.success
* @param {Function} config.failure
*/
purchase: function(config) {
if (!config.success) {
Ext.Logger.error('You must specify a `success` callback for `#product` to work.');
return false;
}
if (!config.failure) {
Ext.Logger.error('You must specify a `failure` callback for `#product` to work.');
return false;
}
Ext.device.Communicator.send({
command: 'Purchase#purchase',
callbacks: {
success: config.success,
failure: config.failure
},
identifier: this.get('productIdentifier'),
scope: config.scope || this
});
}
});
/**
*
*/
Ext.define('Ext.device.Purchases.Purchase', {
extend: 'Ext.data.Model',
config: {
fields: [
'productIdentifier',
'state'
]
},
/**
* Attempts to mark this purchase as complete
* @param {Object} config
* @param {Function} config.success
* @param {Function} config.failure
* @param {Object} config.scope
*/
complete: function(config) {
var me = this;
if (!config.success) {
Ext.Logger.error('You must specify a `success` callback for `#complete` to work.');
return false;
}
if (!config.failure) {
Ext.Logger.error('You must specify a `failure` callback for `#complete` to work.');
return false;
}
if (this.get('state') != "pending") {
config.failure.call(config.scope || this, "purchase is not pending");
}
Ext.device.Communicator.send({
command: 'Purchase#completePurchase',
identifier: me.get('productIdentifier'),
callbacks: {
success: function() {
me.set('state', 'complete');
config.success.call(config.scope || this);
},
failure: function() {
me.set('state', 'pending');
config.failure.call(config.scope || this);
}
},
scope: config.scope || this
});
}
});
});
/**
*
*
* @mixins Ext.device.purchases.Sencha
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Purchases', {
singleton: true,
requires: [
'Ext.device.purchases.Sencha'
],
constructor: function() {
return Ext.create('Ext.device.purchases.Sencha');
}
});
/**
* @private
*/
Ext.define('Ext.device.push.Abstract', {
/**
* @property
* Notification type: alert.
*/
ALERT: 1,
/**
* @property
* Notification type: badge.
*/
BADGE: 2,
/**
* @property
* Notification type: sound.
*/
SOUND: 4,
/**
* @method getInitialConfig
* @hide
*/
/**
* Registers a push notification.
*
* Ext.device.Push.register({
* type: Ext.device.Push.ALERT|Ext.device.Push.BADGE|Ext.device.Push.SOUND,
* success: function(token) {
* console.log('# Push notification registration successful:');
* console.log(' token: ' + token);
* },
* failure: function(error) {
* console.log('# Push notification registration unsuccessful:');
* console.log(' error: ' + error);
* },
* received: function(notifications) {
* console.log('# Push notification received:');
* console.log(' ' + JSON.stringify(notifications));
* }
* });
*
* @param {Object} config
* The configuration for to pass when registering this push notification service.
*
* @param {Number} config.type
* The type(s) of notifications to enable. Available options are:
*
* - {@link Ext.device.Push#ALERT}
* - {@link Ext.device.Push#BADGE}
* - {@link Ext.device.Push#SOUND}
*
* **Usage**
*
* Enable alerts and badges:
*
* Ext.device.Push.register({
* type: Ext.device.Push.ALERT|Ext.device.Push.BADGE
* // ...
* });
*
* Enable alerts, badges and sounds:
*
* Ext.device.Push.register({
* type: Ext.device.Push.ALERT|Ext.device.Push.BADGE|Ext.device.Push.SOUND
* // ...
* });
*
* Enable only sounds:
*
* Ext.device.Push.register({
* type: Ext.device.Push.SOUND
* // ...
* });
*
* @param {Function} config.success
* The callback to be called when registration is complete.
*
* @param {String} config.success.token
* A unique token for this push notification service.
*
* @param {Function} config.failure
* The callback to be called when registration fails.
*
* @param {String} config.failure.error
* The error message.
*
* @param {Function} config.received
* The callback to be called when a push notification is received on this device.
*
* @param {Object} config.received.notifications
* The notifications that have been received.
*/
register: function(config) {
var me = this;
if (!config.received) {
Ext.Logger.error('Failed to pass a received callback. This is required.');
}
if (!config.type) {
Ext.Logger.error('Failed to pass a type. This is required.');
}
return {
success: function(token) {
me.onSuccess(token, config.success, config.scope || me);
},
failure: function(error) {
me.onFailure(error, config.failure, config.scope || me);
},
received: function(notifications) {
me.onReceived(notifications, config.received, config.scope || me);
},
type: config.type
};
},
onSuccess: function(token, callback, scope) {
if (callback) {
callback.call(scope, token);
}
},
onFailure: function(error, callback, scope) {
if (callback) {
callback.call(scope, error);
}
},
onReceived: function(notifications, callback, scope) {
if (callback) {
callback.call(scope, notifications);
}
}
});
/**
* @private
*/
Ext.define('Ext.device.push.Sencha', {
extend: 'Ext.device.push.Abstract',
register: function() {
var config = this.callParent(arguments);
Ext.apply(config, {
command: 'PushNotification#Register',
callbacks: {
success: config.success,
failure: config.failure,
received: config.received
},
type: config.type
});
Ext.device.Communicator.send(config);
}
});
/**
* Provides a way to send push notifications to a device. Currently only available on iOS.
*
* # Example
*
* Ext.device.Push.register({
* type: Ext.device.Push.ALERT|Ext.device.Push.BADGE|Ext.device.Push.SOUND,
* success: function(token) {
* console.log('# Push notification registration successful:');
* console.log(' token: ' + token);
* },
* failure: function(error) {
* console.log('# Push notification registration unsuccessful:');
* console.log(' error: ' + error);
* },
* received: function(notifications) {
* console.log('# Push notification received:');
* console.log(' ' + JSON.stringify(notifications));
* }
* });
*
* @mixins Ext.device.push.Abstract
*
* @aside guide native_apis
*/
Ext.define('Ext.device.Push', {
singleton: true,
requires: [
'Ext.device.Communicator',
'Ext.device.push.Sencha'
],
constructor: function() {
var browserEnv = Ext.browser.is;
if (browserEnv.WebView) {
if (!browserEnv.PhoneGap) {
return Ext.create('Ext.device.push.Sencha');
}
else {
return Ext.create('Ext.device.push.Abstract');
}
}
else {
return Ext.create('Ext.device.push.Abstract');
}
}
});
/**
* @class Ext.direct.Event
* A base class for all Ext.direct events. An event is
* created after some kind of interaction with the server.
* The event class is essentially just a data structure
* to hold a Direct response.
*/
Ext.define('Ext.direct.Event', {
alias: 'direct.event',
requires: ['Ext.direct.Manager'],
config: {
status: true,
/**
* @cfg {Object} data The raw data for this event.
* @accessor
*/
data: null,
/**
* @cfg {String} name The name of this Event.
* @accessor
*/
name: 'event',
xhr: null,
code: null,
message: '',
result: null,
transaction: null
},
constructor: function(config) {
this.initConfig(config)
}
});
/**
* @class Ext.direct.RemotingEvent
* An event that is fired when data is received from a
* {@link Ext.direct.RemotingProvider}. Contains a method to the
* related transaction for the direct request, see {@link #getTransaction}
*/
Ext.define('Ext.direct.RemotingEvent', {
extend: 'Ext.direct.Event',
alias: 'direct.rpc',
config: {
name: 'remoting',
tid: null,
transaction: null
},
/**
* Get the transaction associated with this event.
* @return {Ext.direct.Transaction} The transaction
*/
getTransaction: function() {
return this._transaction || Ext.direct.Manager.getTransaction(this.getTid());
}
});
/**
* @class Ext.direct.ExceptionEvent
* An event that is fired when an exception is received from a {@link Ext.direct.RemotingProvider}
*/
Ext.define('Ext.direct.ExceptionEvent', {
extend: 'Ext.direct.RemotingEvent',
alias: 'direct.exception',
config: {
status: false,
name: 'exception',
error: null
}
});
/**
* Ext.direct.Provider is an abstract class meant to be extended.
*
* For example Ext JS implements the following subclasses:
*
* Provider
* |
* +---{@link Ext.direct.JsonProvider JsonProvider}
* |
* +---{@link Ext.direct.PollingProvider PollingProvider}
* |
* +---{@link Ext.direct.RemotingProvider RemotingProvider}
*
* @abstract
*/
Ext.define('Ext.direct.Provider', {
alias: 'direct.provider',
mixins: {
observable: 'Ext.mixin.Observable'
},
config: {
/**
* @cfg {String} id
* The unique id of the provider (defaults to an auto-assigned id).
* You should assign an id if you need to be able to access the provider later and you do
* not have an object reference available, for example:
*
* Ext.direct.Manager.addProvider({
* type: 'polling',
* url: 'php/poll.php',
* id: 'poll-provider'
* });
* var p = {@link Ext.direct.Manager}.{@link Ext.direct.Manager#getProvider getProvider}('poll-provider');
* p.disconnect();
*
*/
id: undefined
},
/**
* @event connect
* Fires when the Provider connects to the server-side
* @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
*/
/**
* @event disconnect
* Fires when the Provider disconnects from the server-side
* @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
*/
/**
* @event data
* Fires when the Provider receives data from the server-side
* @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
* @param {Ext.direct.Event} e The Ext.direct.Event type that occurred.
*/
/**
* @event exception
* Fires when the Provider receives an exception from the server-side
*/
constructor : function(config){
this.initConfig(config);
},
applyId: function(id) {
if (id === undefined) {
id = this.getUniqueId();
}
return id;
},
/**
* Returns whether or not the server-side is currently connected.
* Abstract method for subclasses to implement.
* @return {Boolean}
*/
isConnected: function() {
return false;
},
/**
* Abstract methods for subclasses to implement.
* @method
*/
connect: Ext.emptyFn,
/**
* Abstract methods for subclasses to implement.
* @method
*/
disconnect: Ext.emptyFn
});
/**
* @class Ext.direct.JsonProvider
*
* A base provider for communicating using JSON. This is an abstract class
* and should not be instanced directly.
* @abstract
*/
Ext.define('Ext.direct.JsonProvider', {
extend: 'Ext.direct.Provider',
alias: 'direct.jsonprovider',
uses: ['Ext.direct.ExceptionEvent'],
/**
* Parse the JSON response.
* @private
* @param {Object} response The XHR response object.
* @return {Object} The data in the response.
*/
parseResponse: function(response) {
if (!Ext.isEmpty(response.responseText)) {
if (Ext.isObject(response.responseText)) {
return response.responseText;
}
return Ext.decode(response.responseText);
}
return null;
},
/**
* Creates a set of events based on the XHR response.
* @private
* @param {Object} response The XHR response.
* @return {Ext.direct.Event[]} An array of {@link Ext.direct.Event} objects.
*/
createEvents: function(response) {
var data = null,
events = [],
i = 0,
ln, event;
try {
data = this.parseResponse(response);
} catch(e) {
event = Ext.create('Ext.direct.ExceptionEvent', {
data: e,
xhr: response,
code: Ext.direct.Manager.exceptions.PARSE,
message: 'Error parsing json response: \n\n ' + data
});
return [event];
}
if (Ext.isArray(data)) {
for (ln = data.length; i < ln; ++i) {
events.push(this.createEvent(data[i]));
}
} else {
events.push(this.createEvent(data));
}
return events;
},
/**
* Create an event from a response object.
* @param {Object} response The XHR response object.
* @return {Ext.direct.Event} The event.
*/
createEvent: function(response) {
return Ext.create('direct.' + response.type, response);
}
});
/**
* 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 during that time period,
* the task is called again, the original call will be canceled. 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 canceled 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;
}
});
/**
* @class Ext.direct.PollingProvider
*
* Provides for repetitive polling of the server at distinct {@link #interval intervals}.
* The initial request for data originates from the client, and then is responded to by the
* server.
*
* All configurations for the PollingProvider should be generated by the server-side
* API portion of the Ext.Direct stack.
*
* An instance of PollingProvider may be created directly via the new keyword or by simply
* specifying `type = 'polling'`. For example:
*
* var pollA = Ext.create('Ext.direct.PollingProvider', {
* type:'polling',
* url: 'php/pollA.php'
* });
*
* Ext.direct.Manager.addProvider(pollA);
* pollA.disconnect();
*
* Ext.direct.Manager.addProvider({
* type:'polling',
* url: 'php/pollB.php',
* id: 'pollB-provider'
* });
*
* var pollB = Ext.direct.Manager.getProvider('pollB-provider');
*/
Ext.define('Ext.direct.PollingProvider', {
extend: 'Ext.direct.JsonProvider',
alias: 'direct.pollingprovider',
uses: ['Ext.direct.ExceptionEvent'],
requires: ['Ext.Ajax', 'Ext.util.DelayedTask'],
config: {
/**
* @cfg {Number} interval
* How often to poll the server-side, in milliseconds.
*/
interval: 3000,
/**
* @cfg {Object} baseParams
* An object containing properties which are to be sent as parameters on every polling request.
*/
baseParams: null,
/**
* @cfg {String/Function} url
* The url which the PollingProvider should contact with each request. This can also be
* an imported {@link Ext.Direct} method which will accept the `{@link #baseParams}` as its only argument.
*/
url: null
},
/**
* @event beforepoll
* Fired immediately before a poll takes place, an event handler can return `false`
* in order to cancel the poll.
* @param {Ext.direct.PollingProvider} this
*/
/**
* @event poll
* This event has not yet been implemented.
* @param {Ext.direct.PollingProvider} this
*/
/**
* @inheritdoc
*/
isConnected: function() {
return !!this.pollTask;
},
/**
* Connect to the server-side and begin the polling process. To handle each
* response subscribe to the `data` event.
*/
connect: function() {
var me = this,
url = me.getUrl(),
baseParams = me.getBaseParams();
if (url && !me.pollTask) {
me.pollTask = setInterval(function() {
if (me.fireEvent('beforepoll', me) !== false) {
if (Ext.isFunction(url)) {
url(baseParams);
} else {
Ext.Ajax.request({
url: url,
callback: me.onData,
scope: me,
params: baseParams
});
}
}
}, me.getInterval());
me.fireEvent('connect', me);
} else if (!url) {
//<debug>
Ext.Error.raise('Error initializing PollingProvider, no url configured.');
//</debug>
}
},
/**
* Disconnect from the server-side and stop the polling process. The `disconnect`
* event will be fired on a successful disconnect.
*/
disconnect: function() {
var me = this;
if (me.pollTask) {
clearInterval(me.pollTask);
delete me.pollTask;
me.fireEvent('disconnect', me);
}
},
// @private
onData: function(opt, success, response) {
var me = this,
i = 0,
len,
events;
if (success) {
events = me.createEvents(response);
for (len = events.length; i < len; ++i) {
me.fireEvent('data', me, events[i]);
}
} else {
me.fireEvent('data', me, Ext.create('Ext.direct.ExceptionEvent', {
data: null,
code: Ext.direct.Manager.exceptions.TRANSPORT,
message: 'Unable to connect to the server.',
xhr: response
}));
}
}
});
/**
* Small utility class used internally to represent a Direct method.
* @class Ext.direct.RemotingMethod
* @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();
}
});
/**
* @class Ext.direct.RemotingProvider
*
* The {@link Ext.direct.RemotingProvider RemotingProvider} exposes access to
* server side methods on the client (a remote procedure call (RPC) type of
* connection where the client can initiate a procedure on the server).
*
* This allows for code to be organized in a fashion that is maintainable,
* while providing a clear path between client and server, something that is
* not always apparent when using URLs.
*
* To accomplish this the server-side needs to describe what classes and methods
* are available on the client-side. This configuration will typically be
* outputted by the server-side Ext.Direct stack when the API description is built.
*/
Ext.define('Ext.direct.RemotingProvider', {
alias: 'direct.remotingprovider',
extend: 'Ext.direct.JsonProvider',
requires: [
'Ext.util.MixedCollection',
'Ext.util.DelayedTask',
'Ext.direct.Transaction',
'Ext.direct.RemotingMethod'
],
config: {
/**
* @cfg {String/Object} namespace
* Namespace for the Remoting Provider (defaults to the browser global scope of _window_).
* Explicitly specify the namespace Object, or specify a String to have a
* {@link Ext#namespace namespace created} implicitly.
*/
namespace: undefined,
/**
* @cfg {String} url (required) The url to connect to the {@link Ext.direct.Manager} server-side router.
*/
url: null,
/**
* @cfg {String} enableUrlEncode
* Specify which param will hold the arguments for the method.
*/
enableUrlEncode: null,
/**
* @cfg {Number/Boolean} enableBuffer
*
* `true` or `false` to enable or disable combining of method
* calls. If a number is specified this is the amount of time in milliseconds
* to wait before sending a batched request.
*
* Calls which are received within the specified timeframe will be
* concatenated together and sent in a single request, optimizing the
* application by reducing the amount of round trips that have to be made
* to the server.
*/
enableBuffer: 10,
/**
* @cfg {Number} maxRetries
* Number of times to re-attempt delivery on failure of a call.
*/
maxRetries: 1,
/**
* @cfg {Number} timeout
* The timeout to use for each request.
*/
timeout: undefined,
/**
* @cfg {Object} actions
* Object literal defining the server side actions and methods. For example, if
* the Provider is configured with:
*
* actions: { // each property within the 'actions' object represents a server side Class
* // array of methods within each server side Class to be stubbed out on client
* TestAction: [{
* name: "doEcho",
* len: 1
* }, {
* "name": "multiply", // name of method
* "len": 2 // The number of parameters that will be used to create an
* // array of data to send to the server side function.
* // Ensure the server sends back a Number, not a String.
* }, {
* name: "doForm",
* formHandler: true, // direct the client to use specialized form handling method
* len: 1
* }]
* }
*
* __Note:__ A Store is not required, a server method can be called at any time.
* In the following example a **client side** handler is used to call the
* server side method "multiply" in the server-side "TestAction" Class:
*
* TestAction.multiply(
* 2, 4, // pass two arguments to server, so specify len=2
* // callback function after the server is called
* // result: the result returned by the server
* // e: Ext.direct.RemotingEvent object
* function(result, e) {
* var t = e.getTransaction();
* var action = t.action; // server side Class called
* var method = t.method; // server side method called
* if (e.getStatus()) {
* var answer = Ext.encode(result); // 8
* } else {
* var msg = e.getMessage(); // failure message
* }
* }
* );
*
* In the example above, the server side "multiply" function will be passed two
* arguments (2 and 4). The "multiply" method should return the value 8 which will be
* available as the `result` in the example above.
*/
actions: {}
},
/**
* @event beforecall
* Fires immediately before the client-side sends off the RPC call.
* By returning `false` from an event handler you can prevent the call from
* executing.
* @param {Ext.direct.RemotingProvider} provider
* @param {Ext.direct.Transaction} transaction
* @param {Object} meta The meta data.
*/
/**
* @event call
* Fires immediately after the request to the server-side is sent. This does
* NOT fire after the response has come back from the call.
* @param {Ext.direct.RemotingProvider} provider
* @param {Ext.direct.Transaction} transaction
* @param {Object} meta The meta data.
*/
constructor : function(config) {
var me = this;
me.callParent(arguments);
me.transactions = Ext.create('Ext.util.Collection', function(item) {
return item.getId();
});
me.callBuffer = [];
},
applyNamespace: function(namespace) {
if (Ext.isString(namespace)) {
return Ext.ns(namespace);
}
return namespace || window;
},
/**
* Initialize the API
* @private
*/
initAPI : function() {
var actions = this.getActions(),
namespace = this.getNamespace(),
action, cls, methods,
i, ln, method;
for (action in actions) {
if (actions.hasOwnProperty(action)) {
cls = namespace[action];
if (!cls) {
cls = namespace[action] = {};
}
methods = actions[action];
for (i = 0, ln = methods.length; i < ln; ++i) {
method = Ext.create('Ext.direct.RemotingMethod', methods[i]);
cls[method.getName()] = this.createHandler(action, method);
}
}
}
},
/**
* Create a handler function for a direct call.
* @private
* @param {String} action The action the call is for.
* @param {Object} method The details of the method.
* @return {Function} A JavaScript function that will kick off the call.
*/
createHandler : function(action, method) {
var me = this,
handler;
if (!method.getFormHandler()) {
handler = function() {
me.configureRequest(action, method, Array.prototype.slice.call(arguments, 0));
};
} else {
handler = function(form, callback, scope) {
me.configureFormRequest(action, method, form, callback, scope);
};
}
handler.directCfg = {
action: action,
method: method
};
return handler;
},
// @inheritdoc
isConnected: function() {
return !!this.connected;
},
// @inheritdoc
connect: function() {
var me = this;
if (me.getUrl()) {
me.initAPI();
me.connected = true;
me.fireEvent('connect', me);
} else {
//<debug>
Ext.Error.raise('Error initializing RemotingProvider, no url configured.');
//</debug>
}
},
// @inheritdoc
disconnect: function() {
var me = this;
if (me.connected) {
me.connected = false;
me.fireEvent('disconnect', me);
}
},
/**
* Run any callbacks related to the transaction.
* @private
* @param {Ext.direct.Transaction} transaction The transaction
* @param {Ext.direct.Event} event The event
*/
runCallback: function(transaction, event) {
var success = !!event.getStatus(),
functionName = success ? 'success' : 'failure',
callback = transaction && transaction.getCallback(),
result;
if (callback) {
// this doesnt make any sense. why do we have both result and data?
// result = Ext.isDefined(event.getResult()) ? event.result : event.data;
result = event.getResult();
if (Ext.isFunction(callback)) {
callback(result, event, success);
} else {
Ext.callback(callback[functionName], callback.scope, [result, event, success]);
Ext.callback(callback.callback, callback.scope, [result, event, success]);
}
}
},
/**
* React to the AJAX request being completed.
* @private
*/
onData: function(options, success, response) {
var me = this,
i = 0,
ln, events, event,
transaction, transactions;
if (success) {
events = me.createEvents(response);
for (ln = events.length; i < ln; ++i) {
event = events[i];
transaction = me.getTransaction(event);
me.fireEvent('data', me, event);
if (transaction) {
me.runCallback(transaction, event, true);
Ext.direct.Manager.removeTransaction(transaction);
}
}
} else {
transactions = [].concat(options.transaction);
for (ln = transactions.length; i < ln; ++i) {
transaction = me.getTransaction(transactions[i]);
if (transaction && transaction.getRetryCount() < me.getMaxRetries()) {
transaction.retry();
} else {
event = Ext.create('Ext.direct.ExceptionEvent', {
data: null,
transaction: transaction,
code: Ext.direct.Manager.exceptions.TRANSPORT,
message: 'Unable to connect to the server.',
xhr: response
});
me.fireEvent('data', me, event);
if (transaction) {
me.runCallback(transaction, event, false);
Ext.direct.Manager.removeTransaction(transaction);
}
}
}
}
},
/**
* Get transaction from XHR options.
* @private
* @param {Object} options The options sent to the AJAX request.
* @return {Ext.direct.Transaction/null} The transaction, `null` if not found.
*/
getTransaction: function(options) {
return options && options.getTid ? Ext.direct.Manager.getTransaction(options.getTid()) : null;
},
/**
* Configure a direct request.
* @private
* @param {String} action The action being executed.
* @param {Object} method The being executed.
*/
configureRequest: function(action, method, args) {
var me = this,
callData = method.getCallData(args),
data = callData.data,
callback = callData.callback,
scope = callData.scope,
transaction;
transaction = Ext.create('Ext.direct.Transaction', {
provider: me,
args: args,
action: action,
method: method.getName(),
data: data,
callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback
});
if (me.fireEvent('beforecall', me, transaction, method) !== false) {
Ext.direct.Manager.addTransaction(transaction);
me.queueTransaction(transaction);
me.fireEvent('call', me, transaction, method);
}
},
/**
* Gets the AJAX call info for a transaction.
* @private
* @param {Ext.direct.Transaction} transaction The transaction.
* @return {Object} The call params.
*/
getCallData: function(transaction) {
return {
action: transaction.getAction(),
method: transaction.getMethod(),
data: transaction.getData(),
type: 'rpc',
tid: transaction.getId()
};
},
/**
* Sends a request to the server.
* @private
* @param {Object/Array} data The data to send.
*/
sendRequest : function(data) {
var me = this,
request = {
url: me.getUrl(),
callback: me.onData,
scope: me,
transaction: data,
timeout: me.getTimeout()
}, callData,
enableUrlEncode = me.getEnableUrlEncode(),
i = 0,
ln, params;
if (Ext.isArray(data)) {
callData = [];
for (ln = data.length; i < ln; ++i) {
callData.push(me.getCallData(data[i]));
}
} else {
callData = me.getCallData(data);
}
if (enableUrlEncode) {
params = {};
params[Ext.isString(enableUrlEncode) ? enableUrlEncode : 'data'] = Ext.encode(callData);
request.params = params;
} else {
request.jsonData = callData;
}
Ext.Ajax.request(request);
},
/**
* Add a new transaction to the queue.
* @private
* @param {Ext.direct.Transaction} transaction The transaction.
*/
queueTransaction: function(transaction) {
var me = this,
enableBuffer = me.getEnableBuffer();
if (transaction.getForm()) {
me.sendFormRequest(transaction);
return;
}
me.callBuffer.push(transaction);
if (enableBuffer) {
if (!me.callTask) {
me.callTask = Ext.create('Ext.util.DelayedTask', me.combineAndSend, me);
}
me.callTask.delay(Ext.isNumber(enableBuffer) ? enableBuffer : 10);
} else {
me.combineAndSend();
}
},
/**
* Combine any buffered requests and send them off.
* @private
*/
combineAndSend : function() {
var buffer = this.callBuffer,
ln = buffer.length;
if (ln > 0) {
this.sendRequest(ln == 1 ? buffer[0] : buffer);
this.callBuffer = [];
}
}
// /**
// * Configure a form submission request.
// * @private
// * @param {String} action The action being executed.
// * @param {Object} method The method being executed.
// * @param {HTMLElement} form The form being submitted.
// * @param {Function} callback (optional) A callback to run after the form submits.
// * @param {Object} scope (optional) A scope to execute the callback in.
// */
// configureFormRequest : function(action, method, form, callback, scope){
// var me = this,
// transaction = new Ext.direct.Transaction({
// provider: me,
// action: action,
// method: method.name,
// args: [form, callback, scope],
// callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback,
// isForm: true
// }),
// isUpload,
// params;
//
// if (me.fireEvent('beforecall', me, transaction, method) !== false) {
// Ext.direct.Manager.addTransaction(transaction);
// isUpload = String(form.getAttribute("enctype")).toLowerCase() == 'multipart/form-data';
//
// params = {
// extTID: transaction.id,
// extAction: action,
// extMethod: method.name,
// extType: 'rpc',
// extUpload: String(isUpload)
// };
//
// // change made from typeof callback check to callback.params
// // to support addl param passing in DirectSubmit EAC 6/2
// Ext.apply(transaction, {
// form: Ext.getDom(form),
// isUpload: isUpload,
// params: callback && Ext.isObject(callback.params) ? Ext.apply(params, callback.params) : params
// });
// me.fireEvent('call', me, transaction, method);
// me.sendFormRequest(transaction);
// }
// },
//
// /**
// * Sends a form request
// * @private
// * @param {Ext.direct.Transaction} transaction The transaction to send
// */
// sendFormRequest: function(transaction){
// Ext.Ajax.request({
// url: this.url,
// params: transaction.params,
// callback: this.onData,
// scope: this,
// form: transaction.form,
// isUpload: transaction.isUpload,
// transaction: transaction
// });
// }
});
/**
* 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 {
//<debug>
throw new Error("[Ext.select] Invalid selector specified: " + selector);
//</debug>
}
return (unique === true) ? new Ext.CompositeElement(elements) : new Ext.CompositeElementLite(elements);
};
});
// 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);
* }
* });
*
* ## Recognizers
*
* Sencha Touch includes a bunch of default event recognizers to know when a user taps, swipes, etc.
*
* For a full list of default recognizers, 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 `{@link #stopPropagation}`).
* @chainable
*/
stopEvent: function() {
return this.stopPropagation();
},
/**
* Cancels bubbling of the event.
* @chainable
*/
stopPropagation: function() {
this.isStopped = true;
return this;
}
});
/**
* @private
* @extends Object
* DOM event. This class really extends {@link Ext.event.Event}, but for documentation
* purposes it's members are listed inside {@link 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.
* @return {Number}
*/
getPageX: function() {
return this.browserEvent.pageX;
},
/**
* Gets the y coordinate of the event.
* @deprecated 2.0 Please use {@link #pageX} property directly.
* @return {Number}
*/
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.
* @return {Array}
*/
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=10||document.body] (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.ComponentDelegation', {
extend: 'Ext.event.publisher.Publisher',
requires: [
'Ext.Component',
'Ext.ComponentQuery'
],
targetType: 'component',
optimizedSelectorRegex: /^#([\w\-]+)((?:[\s]*)>(?:[\s]*)|(?:\s*))([\w\-]+)$/i,
handledEvents: ['*'],
getSubscribers: function(eventName, createIfNotExist) {
var subscribers = this.subscribers,
eventSubscribers = subscribers[eventName];
if (!eventSubscribers && createIfNotExist) {
eventSubscribers = subscribers[eventName] = {
type: {
$length: 0
},
selector: [],
$length: 0
}
}
return eventSubscribers;
},
subscribe: function(target, eventName) {
// Ignore id-only selectors since they are already handled
if (this.idSelectorRegex.test(target)) {
return false;
}
var optimizedSelector = target.match(this.optimizedSelectorRegex),
subscribers = this.getSubscribers(eventName, true),
typeSubscribers = subscribers.type,
selectorSubscribers = subscribers.selector,
id, isDescendant, type, map, subMap;
if (optimizedSelector !== null) {
id = optimizedSelector[1];
isDescendant = optimizedSelector[2].indexOf('>') === -1;
type = optimizedSelector[3];
map = typeSubscribers[type];
if (!map) {
typeSubscribers[type] = map = {
descendents: {
$length: 0
},
children: {
$length: 0
},
$length: 0
}
}
subMap = isDescendant ? map.descendents : map.children;
if (subMap.hasOwnProperty(id)) {
subMap[id]++;
return true;
}
subMap[id] = 1;
subMap.$length++;
map.$length++;
typeSubscribers.$length++;
}
else {
if (selectorSubscribers.hasOwnProperty(target)) {
selectorSubscribers[target]++;
return true;
}
selectorSubscribers[target] = 1;
selectorSubscribers.push(target);
}
subscribers.$length++;
return true;
},
unsubscribe: function(target, eventName, all) {
var subscribers = this.getSubscribers(eventName);
if (!subscribers) {
return false;
}
var match = target.match(this.optimizedSelectorRegex),
typeSubscribers = subscribers.type,
selectorSubscribers = subscribers.selector,
id, isDescendant, type, map, subMap;
all = Boolean(all);
if (match !== null) {
id = match[1];
isDescendant = match[2].indexOf('>') === -1;
type = match[3];
map = typeSubscribers[type];
if (!map) {
return true;
}
subMap = isDescendant ? map.descendents : map.children;
if (!subMap.hasOwnProperty(id) || (!all && --subMap[id] > 0)) {
return true;
}
delete subMap[id];
subMap.$length--;
map.$length--;
typeSubscribers.$length--;
}
else {
if (!selectorSubscribers.hasOwnProperty(target) || (!all && --selectorSubscribers[target] > 0)) {
return true;
}
delete selectorSubscribers[target];
Ext.Array.remove(selectorSubscribers, target);
}
if (--subscribers.$length === 0) {
delete this.subscribers[eventName];
}
return true;
},
notify: function(target, eventName) {
var subscribers = this.getSubscribers(eventName),
id, component;
if (!subscribers || subscribers.$length === 0) {
return false;
}
id = target.substr(1);
component = Ext.ComponentManager.get(id);
if (component) {
this.dispatcher.doAddListener(this.targetType, target, eventName, 'publish', this, {
args: [eventName, component]
}, 'before');
}
},
matchesSelector: function(component, selector) {
return Ext.ComponentQuery.is(component, selector);
},
dispatch: function(target, eventName, args, connectedController) {
this.dispatcher.doDispatchEvent(this.targetType, target, eventName, args, null, connectedController);
},
publish: function(eventName, component) {
var subscribers = this.getSubscribers(eventName);
if (!subscribers) {
return;
}
var eventController = arguments[arguments.length - 1],
typeSubscribers = subscribers.type,
selectorSubscribers = subscribers.selector,
args = Array.prototype.slice.call(arguments, 2, -2),
types = component.xtypesChain,
descendentsSubscribers, childrenSubscribers,
parentId, ancestorIds, ancestorId, parentComponent,
selector,
i, ln, type, j, subLn;
for (i = 0, ln = types.length; i < ln; i++) {
type = types[i];
subscribers = typeSubscribers[type];
if (subscribers && subscribers.$length > 0) {
descendentsSubscribers = subscribers.descendents;
if (descendentsSubscribers.$length > 0) {
if (!ancestorIds) {
ancestorIds = component.getAncestorIds();
}
for (j = 0, subLn = ancestorIds.length; j < subLn; j++) {
ancestorId = ancestorIds[j];
if (descendentsSubscribers.hasOwnProperty(ancestorId)) {
this.dispatch('#' + ancestorId + ' ' + type, eventName, args, eventController);
}
}
}
childrenSubscribers = subscribers.children;
if (childrenSubscribers.$length > 0) {
if (!parentId) {
if (ancestorIds) {
parentId = ancestorIds[0];
}
else {
parentComponent = component.getParent();
if (parentComponent) {
parentId = parentComponent.getId();
}
}
}
if (parentId) {
if (childrenSubscribers.hasOwnProperty(parentId)) {
this.dispatch('#' + parentId + ' > ' + type, eventName, args, eventController);
}
}
}
}
}
ln = selectorSubscribers.length;
if (ln > 0) {
for (i = 0; i < ln; i++) {
selector = selectorSubscribers[i];
if (this.matchesSelector(component, selector)) {
this.dispatch(selector, eventName, args, eventController);
}
}
}
}
});
/**
* @private
*/
Ext.define('Ext.event.publisher.ComponentPaint', {
extend: 'Ext.event.publisher.Publisher',
targetType: 'component',
handledEvents: ['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.ComponentSize', {
extend: 'Ext.event.publisher.Publisher',
requires: [
'Ext.ComponentManager'
],
targetType: 'component',
handledEvents: ['resize', 'innerresize'],
constructor: function() {
this.callParent(arguments);
this.sizeMonitors = {};
},
getSubscribers: function(target, createIfNotExist) {
var subscribers = this.subscribers;
if (!subscribers.hasOwnProperty(target)) {
if (!createIfNotExist) {
return null;
}
subscribers[target] = {
$length: 0
};
}
return subscribers[target];
},
subscribe: function(target, eventName) {
var match = target.match(this.idSelectorRegex),
sizeMonitors = this.sizeMonitors,
dispatcher = this.dispatcher,
targetType = this.targetType,
subscribers, component, id;
if (!match) {
return false;
}
id = match[1];
subscribers = this.getSubscribers(target, true);
subscribers.$length++;
if (subscribers.hasOwnProperty(eventName)) {
subscribers[eventName]++;
return true;
}
if (subscribers.$length === 1) {
dispatcher.addListener(targetType, target, 'painted', 'onComponentPainted', this, null, 'before');
}
component = Ext.ComponentManager.get(id);
//<debug error>
if (!component) {
Ext.Logger.error("Adding a listener to the 'resize' event of a non-existing component");
}
//</debug>
if (!sizeMonitors[target]) {
sizeMonitors[target] = {};
}
sizeMonitors[target][eventName] = new Ext.util.SizeMonitor({
element: eventName === 'resize' ? component.element : component.innerElement,
callback: this.onComponentSizeChange,
scope: this,
args: [this, target, eventName]
});
subscribers[eventName] = 1;
return true;
},
unsubscribe: function(target, eventName, all) {
var match = target.match(this.idSelectorRegex),
dispatcher = this.dispatcher,
targetType = this.targetType,
sizeMonitors = this.sizeMonitors,
subscribers,
id;
if (!match || !(subscribers = this.getSubscribers(target))) {
return false;
}
id = match[1];
if (!subscribers.hasOwnProperty(eventName) || (!all && --subscribers[eventName] > 0)) {
return true;
}
delete subscribers[eventName];
sizeMonitors[target][eventName].destroy();
delete sizeMonitors[target][eventName];
if (--subscribers.$length === 0) {
delete sizeMonitors[target];
delete this.subscribers[target];
dispatcher.removeListener(targetType, target, 'painted', 'onComponentPainted', this, 'before');
}
return true;
},
onComponentPainted: function(component) {
var target = component.getObservableId(),
sizeMonitors = this.sizeMonitors[target];
if (sizeMonitors.resize) {
sizeMonitors.resize.refresh();
}
if (sizeMonitors.innerresize) {
sizeMonitors.innerresize.refresh();
}
},
onComponentSizeChange: function(component, observableId, eventName) {
this.dispatcher.doDispatchEvent(this.targetType, observableId, eventName, [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', 'paste', 'input',
'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];
// Set the current frame start time to be the timestamp of the event.
Ext.frameStartTime = e.timeStamp;
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));
},
//<debug>
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;
},
//</debug>
getSubscribersCount: function(eventName) {
if (!this.handles(eventName)) {
return 0;
}
return this.getSubscribers(eventName).$length + this.getSubscribers('*').$length;
}
});
/**
* @private
*/
Ext.define('Ext.util.paintmonitor.Abstract', {
config: {
element: null,
callback: Ext.emptyFn,
scope: null,
args: []
},
eventName: '',
monitorClass: '',
constructor: function(config) {
this.onElementPainted = Ext.Function.bind(this.onElementPainted, this);
this.initConfig(config);
},
bindListeners: function(bind) {
this.monitorElement[bind ? 'addEventListener' : 'removeEventListener'](this.eventName, this.onElementPainted, true);
},
applyElement: function(element) {
if (element) {
return Ext.get(element);
}
},
updateElement: function(element) {
this.monitorElement = Ext.Element.create({
classList: ['x-paint-monitor', this.monitorClass]
}, true);
element.appendChild(this.monitorElement);
element.addCls('x-paint-monitored');
this.bindListeners(true);
},
onElementPainted: function() {},
destroy: function() {
var monitorElement = this.monitorElement,
parentNode = monitorElement.parentNode,
element = this.getElement();
this.bindListeners(false);
delete this.monitorElement;
if (element && !element.isDestroyed) {
element.removeCls('x-paint-monitored');
delete this._element;
}
if (parentNode) {
parentNode.removeChild(monitorElement);
}
this.callSuper();
}
});
/**
* @private
*/
Ext.define('Ext.util.paintmonitor.CssAnimation', {
extend: 'Ext.util.paintmonitor.Abstract',
eventName: 'webkitAnimationEnd',
monitorClass: 'cssanimation',
onElementPainted: function(e) {
if (e.animationName === 'x-paint-monitor-helper') {
this.getCallback().apply(this.getScope(), this.getArgs());
}
}
});
/**
* @private
*/
Ext.define('Ext.util.paintmonitor.OverflowChange', {
extend: 'Ext.util.paintmonitor.Abstract',
eventName: 'overflowchanged',
monitorClass: 'overflowchange',
onElementPainted: function(e) {
this.getCallback().apply(this.getScope(), this.getArgs());
}
});
/**
*
*/
Ext.define('Ext.util.PaintMonitor', {
requires: [
'Ext.util.paintmonitor.CssAnimation',
'Ext.util.paintmonitor.OverflowChange'
],
constructor: function(config) {
if (Ext.browser.engineVersion.gtEq('536')) {
return new Ext.util.paintmonitor.OverflowChange(config);
}
else {
return new Ext.util.paintmonitor.CssAnimation(config);
}
}
});
/**
* @private
*/
Ext.define('Ext.event.publisher.ElementPaint', {
extend: 'Ext.event.publisher.Publisher',
requires: [
'Ext.util.PaintMonitor',
'Ext.TaskQueue'
],
targetType: 'element',
handledEvents: ['painted'],
constructor: function() {
this.monitors = {};
this.callSuper(arguments);
},
subscribe: function(target) {
var match = target.match(this.idSelectorRegex),
subscribers = this.subscribers,
id, element;
if (!match) {
return false;
}
id = match[1];
if (subscribers.hasOwnProperty(id)) {
subscribers[id]++;
return true;
}
subscribers[id] = 1;
element = Ext.get(id);
this.monitors[id] = new Ext.util.PaintMonitor({
element: element,
callback: this.onElementPainted,
scope: this,
args: [target, element]
});
return true;
},
unsubscribe: function(target, eventName, all) {
var match = target.match(this.idSelectorRegex),
subscribers = this.subscribers,
id;
if (!match) {
return false;
}
id = match[1];
if (!subscribers.hasOwnProperty(id) || (!all && --subscribers[id] > 0)) {
return true;
}
delete subscribers[id];
this.monitors[id].destroy();
delete this.monitors[id];
return true;
},
onElementPainted: function(target, element) {
Ext.TaskQueue.requestRead('dispatch', this, [target, 'painted', [element]]);
}
});
/**
*
*/
Ext.define('Ext.mixin.Templatable', {
extend: 'Ext.mixin.Mixin',
mixinConfig: {
id: 'templatable'
},
referenceAttributeName: 'reference',
referenceSelector: '[reference]',
getElementConfig: function() {
return {
reference: 'element'
};
},
getElementTemplate: function() {
var elementTemplate = document.createDocumentFragment();
elementTemplate.appendChild(Ext.Element.create(this.getElementConfig(), true));
return elementTemplate;
},
initElement: function() {
var prototype = this.self.prototype;
prototype.elementTemplate = this.getElementTemplate();
prototype.initElement = prototype.doInitElement;
this.initElement.apply(this, arguments);
},
linkElement: function(reference, node) {
this.link(reference, node);
},
doInitElement: function() {
var referenceAttributeName = this.referenceAttributeName,
renderElement, referenceNodes, i, ln, referenceNode, reference;
renderElement = this.elementTemplate.cloneNode(true);
referenceNodes = renderElement.querySelectorAll(this.referenceSelector);
for (i = 0,ln = referenceNodes.length; i < ln; i++) {
referenceNode = referenceNodes[i];
reference = referenceNode.getAttribute(referenceAttributeName);
referenceNode.removeAttribute(referenceAttributeName);
this.linkElement(reference, referenceNode);
}
}
});
/**
* @private
*/
Ext.define('Ext.util.sizemonitor.Abstract', {
mixins: ['Ext.mixin.Templatable'],
requires: [
'Ext.TaskQueue'
],
config: {
element: null,
callback: Ext.emptyFn,
scope: null,
args: []
},
width: 0,
height: 0,
contentWidth: 0,
contentHeight: 0,
constructor: function(config) {
this.refresh = Ext.Function.bind(this.refresh, this);
this.info = {
width: 0,
height: 0,
contentWidth: 0,
contentHeight: 0,
flag: 0
};
this.initElement();
this.initConfig(config);
this.bindListeners(true);
},
bindListeners: Ext.emptyFn,
applyElement: function(element) {
if (element) {
return Ext.get(element);
}
},
updateElement: function(element) {
element.append(this.detectorsContainer);
element.addCls('x-size-monitored');
},
applyArgs: function(args) {
return args.concat([this.info]);
},
refreshMonitors: Ext.emptyFn,
forceRefresh: function() {
Ext.TaskQueue.requestRead('refresh', this);
},
refreshSize: function() {
var element = this.getElement();
if (!element || element.isDestroyed) {
return false;
}
var width = element.getWidth(),
height = element.getHeight(),
contentElement = this.detectorsContainer,
contentWidth = contentElement.offsetWidth,
contentHeight = contentElement.offsetHeight,
currentContentWidth = this.contentWidth,
currentContentHeight = this.contentHeight,
info = this.info,
resized = false,
flag;
this.width = width;
this.height = height;
this.contentWidth = contentWidth;
this.contentHeight = contentHeight;
flag = ((currentContentWidth !== contentWidth ? 1 : 0) + (currentContentHeight !== contentHeight ? 2 : 0));
if (flag > 0) {
info.width = width;
info.height = height;
info.contentWidth = contentWidth;
info.contentHeight = contentHeight;
info.flag = flag;
resized = true;
this.getCallback().apply(this.getScope(), this.getArgs());
}
return resized;
},
refresh: function(force) {
if (this.refreshSize() || force) {
Ext.TaskQueue.requestWrite('refreshMonitors', this);
}
},
destroy: function() {
var element = this.getElement();
this.bindListeners(false);
if (element && !element.isDestroyed) {
element.removeCls('x-size-monitored');
}
delete this._element;
this.callSuper();
}
});
/**
* @private
*/
Ext.define('Ext.util.sizemonitor.Scroll', {
extend: 'Ext.util.sizemonitor.Abstract',
getElementConfig: function() {
return {
reference: 'detectorsContainer',
classList: ['x-size-monitors', 'scroll'],
children: [
{
reference: 'expandMonitor',
className: 'expand'
},
{
reference: 'shrinkMonitor',
className: 'shrink'
}
]
}
},
constructor: function(config) {
this.onScroll = Ext.Function.bind(this.onScroll, this);
this.callSuper(arguments);
},
bindListeners: function(bind) {
var method = bind ? 'addEventListener' : 'removeEventListener';
this.expandMonitor[method]('scroll', this.onScroll, true);
this.shrinkMonitor[method]('scroll', this.onScroll, true);
},
forceRefresh: function() {
Ext.TaskQueue.requestRead('refresh', this, [true]);
},
onScroll: function() {
Ext.TaskQueue.requestRead('refresh', this);
},
refreshMonitors: function() {
var expandMonitor = this.expandMonitor,
shrinkMonitor = this.shrinkMonitor,
end = 1000000;
if (expandMonitor && !expandMonitor.isDestroyed) {
expandMonitor.scrollLeft = end;
expandMonitor.scrollTop = end;
}
if (shrinkMonitor && !shrinkMonitor.isDestroyed) {
shrinkMonitor.scrollLeft = end;
shrinkMonitor.scrollTop = end;
}
}
});
/**
* @private
*/
Ext.define('Ext.util.sizemonitor.OverflowChange', {
extend: 'Ext.util.sizemonitor.Abstract',
constructor: function(config) {
this.onExpand = Ext.Function.bind(this.onExpand, this);
this.onShrink = Ext.Function.bind(this.onShrink, this);
this.callSuper(arguments);
},
getElementConfig: function() {
return {
reference: 'detectorsContainer',
classList: ['x-size-monitors', 'overflowchanged'],
children: [
{
reference: 'expandMonitor',
className: 'expand',
children: [{
reference: 'expandHelper'
}]
},
{
reference: 'shrinkMonitor',
className: 'shrink',
children: [{
reference: 'shrinkHelper'
}]
}
]
}
},
bindListeners: function(bind) {
var method = bind ? 'addEventListener' : 'removeEventListener';
this.expandMonitor[method]('overflowchanged', this.onExpand, true);
this.shrinkMonitor[method]('overflowchanged', this.onShrink, true);
},
onExpand: function(e) {
if (e.horizontalOverflow && e.verticalOverflow) {
return;
}
Ext.TaskQueue.requestRead('refresh', this);
},
onShrink: function(e) {
if (!e.horizontalOverflow && !e.verticalOverflow) {
return;
}
Ext.TaskQueue.requestRead('refresh', this);
},
refreshMonitors: function() {
var expandHelper = this.expandHelper,
shrinkHelper = this.shrinkHelper,
width = this.contentWidth,
height = this.contentHeight;
if (expandHelper && !expandHelper.isDestroyed) {
expandHelper.style.width = (width + 1) + 'px';
expandHelper.style.height = (height + 1) + 'px';
}
if (shrinkHelper && !shrinkHelper.isDestroyed) {
shrinkHelper.style.width = width + 'px';
shrinkHelper.style.height = height + 'px';
}
Ext.TaskQueue.requestRead('refresh', this);
}
});
/**
*
*/
Ext.define('Ext.util.SizeMonitor', {
requires: [
'Ext.util.sizemonitor.Scroll',
'Ext.util.sizemonitor.OverflowChange'
],
constructor: function(config) {
if (Ext.browser.engineVersion.gtEq('535')) {
return new Ext.util.sizemonitor.OverflowChange(config);
}
else {
return new Ext.util.sizemonitor.Scroll(config);
}
}
});
/**
* @private
*/
Ext.define('Ext.event.publisher.ElementSize', {
extend: 'Ext.event.publisher.Publisher',
requires: [
'Ext.util.SizeMonitor'
],
targetType: 'element',
handledEvents: ['resize'],
constructor: function() {
this.monitors = {};
this.callSuper(arguments);
},
subscribe: function(target) {
var match = target.match(this.idSelectorRegex),
subscribers = this.subscribers,
id, element, sizeMonitor;
if (!match) {
return false;
}
id = match[1];
if (subscribers.hasOwnProperty(id)) {
subscribers[id]++;
return true;
}
subscribers[id] = 1;
element = Ext.get(id);
this.monitors[id] = sizeMonitor = new Ext.util.SizeMonitor({
element: element,
callback: this.onElementResize,
scope: this,
args: [target, element]
});
this.dispatcher.addListener('element', target, 'painted', 'forceRefresh', sizeMonitor);
return true;
},
unsubscribe: function(target, eventName, all) {
var match = target.match(this.idSelectorRegex),
subscribers = this.subscribers,
monitors = this.monitors,
id, sizeMonitor;
if (!match) {
return false;
}
id = match[1];
if (!subscribers.hasOwnProperty(id) || (!all && --subscribers[id] > 0)) {
return true;
}
delete subscribers[id];
sizeMonitor = monitors[id];
this.dispatcher.removeListener('element', target, 'painted', 'forceRefresh', sizeMonitor);
sizeMonitor.destroy();
delete monitors[id];
return true;
},
onElementResize: function(target, element, info) {
Ext.TaskQueue.requestRead('dispatch', this, [target, 'resize', [element, info]]);
}
});
/**
* @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,
currentIdentifiers = {},
changedTouches = e.changedTouches,
changedTouchedLn = changedTouches.length,
touches = e.touches,
touchesLn = touches.length,
i, touch, identifier, fakeEndEvent;
currentTouchesCount += changedTouchedLn;
if (currentTouchesCount > touchesLn) {
for (i = 0; i < touchesLn; i++) {
touch = touches[i];
identifier = touch.identifier;
currentIdentifiers[identifier] = true;
}
if (!Ext.os.is.Android3 && !Ext.os.is.Android4) {
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 (Ext.os.is.Android2 && currentTouchesCount > touchesLn) {
return;
}
}
for (i = 0; i < changedTouchedLn; 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 < changedTouchedLn; 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);
}
});
}
});
/**
* A base class for all event recognizers in Sencha Touch.
*
* Sencha Touch, by default, includes various different {@link Ext.event.recognizer.Recognizer} subclasses to recognize
* events happening in your application.
*
* ## Default recognizers
*
* * {@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 recognizers
*
* * {@link Ext.event.recognizer.VerticalSwipe}
*
* If you want to create custom recognizers, or disable recognizers 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.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 recognizer which knows when you double tap.
*
* @private
*/
Ext.define('Ext.event.recognizer.DoubleTap', {
extend: 'Ext.event.recognizer.SingleTouch',
inheritableStatics: {
DIFFERENT_TARGET: 0x03
},
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,
startTime: 0,
lastTapTime: 0,
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,
target = e.target,
lastTapTime = this.lastTapTime,
lastTarget = this.lastTarget,
duration;
this.lastTapTime = time;
this.lastTarget = target;
if (lastTapTime) {
duration = time - lastTapTime;
if (duration <= maxDuration) {
if (target !== lastTarget) {
return this.fail(this.self.DIFFERENT_TARGET);
}
this.lastTarget = null;
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 recognizer 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 base class used for both {@link Ext.event.recognizer.VerticalSwipe} and {@link Ext.event.recognizer.HorizontalSwipe}
* event recognizers.
*
* @private
*/
Ext.define('Ext.event.recognizer.Swipe', {
extend: 'Ext.event.recognizer.SingleTouch',
handledEvents: ['swipe'],
/**
* @member Ext.dom.Element
* @event swipe
* Fires when there is a swipe
* When listening to this, ensure you know about the {@link Ext.event.Event#direction} property 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.
*/
/**
* @property {Number} direction
* The direction of the swipe. Available options are:
*
* - up
* - down
* - left
* - right
*
* __Note:__ In order to recognize swiping up and down, you must enable the vertical swipe recognizer.
*
* **This is only available when the event type is `swipe`**
* @member Ext.event.Event
*/
/**
* @property {Number} duration
* The duration of the swipe.
*
* **This is only available when the event type is `swipe`**
* @member Ext.event.Event
*/
inheritableStatics: {
MAX_OFFSET_EXCEEDED: 0x10,
MAX_DURATION_EXCEEDED: 0x11,
DISTANCE_NOT_ENOUGH: 0x12
},
config: {
minDistance: 80,
maxOffset: 35,
maxDuration: 1000
},
onTouchStart: function(e) {
if (this.callParent(arguments) === false) {
return false;
}
var touch = e.changedTouches[0];
this.startTime = e.time;
this.isHorizontal = true;
this.isVertical = true;
this.startX = touch.pageX;
this.startY = touch.pageY;
},
onTouchMove: function(e) {
var touch = e.changedTouches[0],
x = touch.pageX,
y = touch.pageY,
absDeltaX = Math.abs(x - this.startX),
absDeltaY = Math.abs(y - this.startY),
time = e.time;
if (time - this.startTime > this.getMaxDuration()) {
return this.fail(this.self.MAX_DURATION_EXCEEDED);
}
if (this.isVertical && absDeltaX > this.getMaxOffset()) {
this.isVertical = false;
}
if (this.isHorizontal && absDeltaY > this.getMaxOffset()) {
this.isHorizontal = false;
}
if (!this.isHorizontal && !this.isVertical) {
return this.fail(this.self.MAX_OFFSET_EXCEEDED);
}
},
onTouchEnd: function(e) {
if (this.onTouchMove(e) === false) {
return false;
}
var touch = e.changedTouches[0],
x = touch.pageX,
y = touch.pageY,
deltaX = x - this.startX,
deltaY = y - this.startY,
absDeltaX = Math.abs(deltaX),
absDeltaY = Math.abs(deltaY),
minDistance = this.getMinDistance(),
duration = e.time - this.startTime,
direction, distance;
if (this.isVertical && absDeltaY < minDistance) {
this.isVertical = false;
}
if (this.isHorizontal && absDeltaX < minDistance) {
this.isHorizontal = false;
}
if (this.isHorizontal) {
direction = (deltaX < 0) ? 'left' : 'right';
distance = absDeltaX;
}
else if (this.isVertical) {
direction = (deltaY < 0) ? 'up' : 'down';
distance = absDeltaY;
}
else {
return this.fail(this.self.DISTANCE_NOT_ENOUGH);
}
this.fire('swipe', e, [touch], {
touch: touch,
direction: direction,
distance: distance,
duration: duration
});
}
});
/**
* A event recognizer created to recognize horizontal swipe movements.
*
* @private
*/
Ext.define('Ext.event.recognizer.HorizontalSwipe', {
extend: 'Ext.event.recognizer.Swipe',
handledEvents: ['swipe'],
onTouchStart: function(e) {
if (this.callParent(arguments) === false) {
return false;
}
var touch = e.changedTouches[0];
this.startTime = e.time;
this.startX = touch.pageX;
this.startY = touch.pageY;
},
onTouchMove: function(e) {
var touch = e.changedTouches[0],
y = touch.pageY,
absDeltaY = Math.abs(y - this.startY),
time = e.time,
maxDuration = this.getMaxDuration(),
maxOffset = this.getMaxOffset();
if (time - this.startTime > maxDuration) {
return this.fail(this.self.MAX_DURATION_EXCEEDED);
}
if (absDeltaY > maxOffset) {
return this.fail(this.self.MAX_OFFSET_EXCEEDED);
}
},
onTouchEnd: function(e) {
if (this.onTouchMove(e) !== false) {
var touch = e.changedTouches[0],
x = touch.pageX,
deltaX = x - this.startX,
distance = Math.abs(deltaX),
duration = e.time - this.startTime,
minDistance = this.getMinDistance(),
direction;
if (distance < minDistance) {
return this.fail(this.self.DISTANCE_NOT_ENOUGH);
}
direction = (deltaX < 0) ? 'left' : 'right';
this.fire('swipe', e, [touch], {
touch: touch,
direction: direction,
distance: distance,
duration: duration
});
}
}
});
/**
* A event recognizer 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
*/
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() {
this.override({
handledEvents: ['longpress', 'taphold'],
fire: function(eventName) {
if (eventName === 'longpress') {
var args = Array.prototype.slice.call(arguments);
args[0] = 'taphold';
this.fire.apply(this, args);
}
return this.callOverridden(arguments);
}
});
});
/**
* @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 recognizer 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 recognizer 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);
}
});
/**
* A simple event recognizer 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() {
});
/**
* A event recognizer created to recognize vertical swipe movements.
*
* This is disabled by default in Sencha Touch as it has a performance impact when your application
* has vertical scrollers, plus, in most cases it is not very useful.
*
* If you wish to recognize vertical swipe movements in your application, please refer to the documentation of
* {@link Ext.event.recognizer.Recognizer} and {@link Ext#setup}.
*
* @private
*/
Ext.define('Ext.event.recognizer.VerticalSwipe', {
extend: 'Ext.event.recognizer.Swipe',
onTouchStart: function(e) {
if (this.callParent(arguments) === false) {
return false;
}
var touch = e.changedTouches[0];
this.startTime = e.time;
this.startX = touch.pageX;
this.startY = touch.pageY;
},
onTouchMove: function(e) {
var touch = e.changedTouches[0],
x = touch.pageX,
absDeltaX = Math.abs(x - this.startX),
maxDuration = this.getMaxDuration(),
maxOffset = this.getMaxOffset(),
time = e.time;
if (time - this.startTime > maxDuration) {
return this.fail(this.self.MAX_DURATION_EXCEEDED);
}
if (absDeltaX > maxOffset) {
return this.fail(this.self.MAX_OFFSET_EXCEEDED);
}
},
onTouchEnd: function(e) {
if (this.onTouchMove(e) !== false) {
var touch = e.changedTouches[0],
y = touch.pageY,
deltaY = y - this.startY,
distance = Math.abs(deltaY),
duration = e.time - this.startTime,
minDistance = this.getMinDistance(),
direction;
if (distance < minDistance) {
return this.fail(this.self.DISTANCE_NOT_ENOUGH);
}
direction = (deltaY < 0) ? 'up' : 'down';
this.fire('swipe', e, [touch], {
touch: touch,
distance: distance,
duration: duration,
duration: duration
});
}
}
});
/**
* @aside guide forms
*
* The checkbox field is an enhanced version of the native browser checkbox and is great for enabling your user to
* choose one or more items from a set (for example choosing toppings for a pizza order). It works like any other
* {@link Ext.field.Field field} and is usually found in the context of a form:
*
* ## Example
*
* @example miniphone preview
* var form = Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'checkboxfield',
* name : 'tomato',
* label: 'Tomato',
* value: 'tomato',
* checked: true
* },
* {
* xtype: 'checkboxfield',
* name : 'salami',
* label: 'Salami'
* },
* {
* xtype: 'toolbar',
* docked: 'bottom',
* items: [
* { xtype: 'spacer' },
* {
* text: 'getValues',
* handler: function() {
* var form = Ext.ComponentQuery.query('formpanel')[0],
* values = form.getValues();
*
* Ext.Msg.alert(null,
* "Tomato: " + ((values.tomato) ? "yes" : "no") +
* "<br />Salami: " + ((values.salami) ? "yes" : "no")
* );
* }
* },
* { xtype: 'spacer' }
* ]
* }
* ]
* });
*
*
* The form above contains two check boxes - one for Tomato, one for Salami. We configured the Tomato checkbox to be
* checked immediately on load, and the Salami checkbox to be unchecked. We also specified an optional text
* {@link #value} that will be sent when we submit the form. We can get this value using the Form's
* {@link Ext.form.Panel#getValues getValues} function, or have it sent as part of the data that is sent when the
* form is submitted:
*
* form.getValues(); //contains a key called 'tomato' if the Tomato field is still checked
* form.submit(); //will send 'tomato' in the form submission data
*
*/
Ext.define('Ext.field.Checkbox', {
extend: 'Ext.field.Field',
alternateClassName: 'Ext.form.Checkbox',
xtype: 'checkboxfield',
qsaLeftRe: /[\[]/g,
qsaRightRe: /[\]]/g,
isCheckbox: true,
/**
* @event change
* Fires just before the field blurs if the field value has changed.
* @param {Ext.field.Checkbox} this This field.
* @param {Boolean} newValue The new value.
* @param {Boolean} oldValue The original value.
*/
/**
* @event check
* Fires when the checkbox is checked.
* @param {Ext.field.Checkbox} this This checkbox.
* @param {Ext.EventObject} e This event object.
*/
/**
* @event uncheck
* Fires when the checkbox is unchecked.
* @param {Ext.field.Checkbox} this This checkbox.
* @param {Ext.EventObject} e This event object.
*/
config: {
/**
* @cfg
* @inheritdoc
*/
ui: 'checkbox',
/**
* @cfg {String} value The string value to submit if the item is in a checked state.
* @accessor
*/
value: '',
/**
* @cfg {Boolean} checked `true` if the checkbox should render initially checked.
* @accessor
*/
checked: false,
/**
* @cfg {Number} tabIndex
* @hide
*/
tabIndex: -1,
/**
* @cfg
* @inheritdoc
*/
component: {
xtype : 'input',
type : 'checkbox',
useMask : true,
cls : Ext.baseCSSPrefix + 'input-checkbox'
}
},
// @private
initialize: function() {
var me = this;
me.callParent();
me.getComponent().on({
scope: me,
order: 'before',
masktap: 'onMaskTap'
});
},
// @private
doInitValue: function() {
var me = this,
initialConfig = me.getInitialConfig();
// you can have a value or checked config, but checked get priority
if (initialConfig.hasOwnProperty('value')) {
me.originalState = initialConfig.value;
}
if (initialConfig.hasOwnProperty('checked')) {
me.originalState = initialConfig.checked;
}
me.callParent(arguments);
},
// @private
updateInputType: function(newInputType) {
var component = this.getComponent();
if (component) {
component.setType(newInputType);
}
},
// @private
updateName: function(newName) {
var component = this.getComponent();
if (component) {
component.setName(newName);
}
},
/**
* Returns the field checked value.
* @return {Mixed} The field value.
*/
getChecked: function() {
// we need to get the latest value from the {@link #input} and then update the value
this._checked = this.getComponent().getChecked();
return this._checked;
},
/**
* Returns the submit value for the checkbox which can be used when submitting forms.
* @return {Boolean/String} value The value of {@link #value} or `true`, if {@link #checked}.
*/
getSubmitValue: function() {
return (this.getChecked()) ? this._value || true : null;
},
setChecked: function(newChecked) {
this.updateChecked(newChecked);
this._checked = newChecked;
},
updateChecked: function(newChecked) {
this.getComponent().setChecked(newChecked);
// only call onChange (which fires events) if the component has been initialized
if (this.initialized) {
this.onChange();
}
},
// @private
onMaskTap: function(component, e) {
var me = this,
dom = component.input.dom;
if (me.getDisabled()) {
return false;
}
//we must manually update the input dom with the new checked value
dom.checked = !dom.checked;
me.onChange(e);
//return false so the mask does not disappear
return false;
},
/**
* Fires the `check` or `uncheck` event when the checked value of this component changes.
* @private
*/
onChange: function(e) {
var me = this,
oldChecked = me._checked,
newChecked = me.getChecked();
// only fire the event when the value changes
if (oldChecked != newChecked) {
if (newChecked) {
me.fireEvent('check', me, e);
} else {
me.fireEvent('uncheck', me, e);
}
me.fireEvent('change', me, newChecked, oldChecked);
}
},
/**
* @method
* Method called when this {@link Ext.field.Checkbox} has been checked.
*/
doChecked: Ext.emptyFn,
/**
* @method
* Method called when this {@link Ext.field.Checkbox} has been unchecked.
*/
doUnChecked: Ext.emptyFn,
/**
* Returns the checked state of the checkbox.
* @return {Boolean} `true` if checked, `false` otherwise.
*/
isChecked: function() {
return this.getChecked();
},
/**
* Set the checked state of the checkbox to `true`.
* @return {Ext.field.Checkbox} This checkbox.
*/
check: function() {
return this.setChecked(true);
},
/**
* Set the checked state of the checkbox to `false`.
* @return {Ext.field.Checkbox} This checkbox.
*/
uncheck: function() {
return this.setChecked(false);
},
getSameGroupFields: function() {
var component = this.up('formpanel') || this.up('fieldset'),
name = this.getName(),
replaceLeft = this.qsaLeftRe,
replaceRight = this.qsaRightRe,
components = [],
elements, element, i, ln;
if (!component) {
// <debug>
Ext.Logger.warn('Ext.field.Radio components must always be descendants of an Ext.form.Panel or Ext.form.FieldSet.');
// </debug>
component = Ext.Viewport;
}
// This is to handle ComponentQuery's lack of handling [name=foo[bar]] properly
name = name.replace(replaceLeft, '\\[');
name = name.replace(replaceRight, '\\]');
elements = Ext.query('[name=' + name + ']', component.element.dom);
ln = elements.length;
for (i = 0; i < ln; i++) {
element = elements[i];
element = Ext.fly(element).up('.x-field-' + element.getAttribute('type'));
if (element && element.id) {
components.push(Ext.getCmp(element.id));
}
}
return components;
},
/**
* Returns an array of values from the checkboxes in the group that are checked.
* @return {Array}
*/
getGroupValues: function() {
var values = [];
this.getSameGroupFields().forEach(function(field) {
if (field.getChecked()) {
values.push(field.getValue());
}
});
return values;
},
/**
* Set the status of all matched checkboxes in the same group to checked.
* @param {Array} values An array of values.
* @return {Ext.field.Checkbox} This checkbox.
*/
setGroupValues: function(values) {
this.getSameGroupFields().forEach(function(field) {
field.setChecked((values.indexOf(field.getValue()) !== -1));
});
return this;
},
/**
* Resets the status of all matched checkboxes in the same group to checked.
* @return {Ext.field.Checkbox} This checkbox.
*/
resetGroupValues: function() {
this.getSameGroupFields().forEach(function(field) {
field.setChecked(field.originalState);
});
return this;
},
reset: function() {
this.setChecked(this.originalState);
return this;
}
});
/**
* @private
*
* A general {@link Ext.picker.Picker} slot class. Slots are used to organize multiple scrollable slots into
* a single {@link Ext.picker.Picker}.
*
* {
* name : 'limit_speed',
* title: 'Speed Limit',
* data : [
* {text: '50 KB/s', value: 50},
* {text: '100 KB/s', value: 100},
* {text: '200 KB/s', value: 200},
* {text: '300 KB/s', value: 300}
* ]
* }
*
* See the {@link Ext.picker.Picker} documentation on how to use slots.
*/
Ext.define('Ext.picker.Slot', {
extend: 'Ext.dataview.DataView',
xtype : 'pickerslot',
alternateClassName: 'Ext.Picker.Slot',
requires: [
'Ext.XTemplate',
'Ext.data.Store',
'Ext.Component',
'Ext.data.StoreManager'
],
/**
* @event slotpick
* Fires whenever an slot is picked
* @param {Ext.picker.Slot} this
* @param {Mixed} value The value of the pick
* @param {HTMLElement} node The node element of the pick
*/
isSlot: true,
config: {
/**
* @cfg {String} title The title to use for this slot, or `null` for no title.
* @accessor
*/
title: null,
/**
* @private
* @cfg {Boolean} showTitle
* @accessor
*/
showTitle: true,
/**
* @private
* @cfg {String} cls The main component class
* @accessor
*/
cls: Ext.baseCSSPrefix + 'picker-slot',
/**
* @cfg {String} name (required) The name of this slot.
* @accessor
*/
name: null,
/**
* @cfg {Number} value The value of this slot
* @accessor
*/
value: null,
/**
* @cfg {Number} flex
* @accessor
* @private
*/
flex: 1,
/**
* @cfg {String} align The horizontal alignment of the slot's contents.
*
* Valid values are: "left", "center", and "right".
* @accessor
*/
align: 'left',
/**
* @cfg {String} displayField The display field in the store.
* @accessor
*/
displayField: 'text',
/**
* @cfg {String} valueField The value field in the store.
* @accessor
*/
valueField: 'value',
/**
* @cfg {Object} scrollable
* @accessor
* @hide
*/
scrollable: {
direction: 'vertical',
indicators: false,
momentumEasing: {
minVelocity: 2
},
slotSnapEasing: {
duration: 100
}
}
},
constructor: function() {
/**
* @property selectedIndex
* @type Number
* The current `selectedIndex` of the picker slot.
* @private
*/
this.selectedIndex = 0;
/**
* @property picker
* @type Ext.picker.Picker
* A reference to the owner Picker.
* @private
*/
this.callParent(arguments);
},
/**
* Sets the title for this dataview by creating element.
* @param {String} title
* @return {String}
*/
applyTitle: function(title) {
//check if the title isnt defined
if (title) {
//create a new title element
title = Ext.create('Ext.Component', {
cls: Ext.baseCSSPrefix + 'picker-slot-title',
docked : 'top',
html : title
});
}
return title;
},
updateTitle: function(newTitle, oldTitle) {
if (newTitle) {
this.add(newTitle);
this.setupBar();
}
if (oldTitle) {
this.remove(oldTitle);
}
},
updateShowTitle: function(showTitle) {
var title = this.getTitle();
if (title) {
title[showTitle ? 'show' : 'hide']();
this.setupBar();
}
},
updateDisplayField: function(newDisplayField) {
this.setItemTpl('<div class="' + Ext.baseCSSPrefix + 'picker-item {cls} <tpl if="extra">' + Ext.baseCSSPrefix + 'picker-invalid</tpl>">{' + newDisplayField + '}</div>');
},
/**
* Updates the {@link #align} configuration
*/
updateAlign: function(newAlign, oldAlign) {
var element = this.element;
element.addCls(Ext.baseCSSPrefix + 'picker-' + newAlign);
element.removeCls(Ext.baseCSSPrefix + 'picker-' + oldAlign);
},
/**
* Looks at the {@link #data} configuration and turns it into {@link #store}.
* @param {Object} data
* @return {Object}
*/
applyData: function(data) {
var parsedData = [],
ln = data && data.length,
i, item, obj;
if (data && Ext.isArray(data) && ln) {
for (i = 0; i < ln; i++) {
item = data[i];
obj = {};
if (Ext.isArray(item)) {
obj[this.valueField] = item[0];
obj[this.displayField] = item[1];
}
else if (Ext.isString(item)) {
obj[this.valueField] = item;
obj[this.displayField] = item;
}
else if (Ext.isObject(item)) {
obj = item;
}
parsedData.push(obj);
}
}
return data;
},
updateData: function(data) {
this.setStore(Ext.create('Ext.data.Store', {
fields: ['text', 'value'],
data : data
}));
},
// @private
initialize: function() {
this.callParent();
var scroller = this.getScrollable().getScroller();
this.on({
scope: this,
painted: 'onPainted',
itemtap: 'doItemTap'
});
scroller.on({
scope: this,
scrollend: 'onScrollEnd'
});
},
// @private
onPainted: function() {
this.setupBar();
},
/**
* Returns an instance of the owner picker.
* @return {Object}
* @private
*/
getPicker: function() {
if (!this.picker) {
this.picker = this.getParent();
}
return this.picker;
},
// @private
setupBar: function() {
if (!this.rendered) {
//if the component isnt rendered yet, there is no point in calculating the padding just eyt
return;
}
var element = this.element,
innerElement = this.innerElement,
picker = this.getPicker(),
bar = picker.bar,
value = this.getValue(),
showTitle = this.getShowTitle(),
title = this.getTitle(),
scrollable = this.getScrollable(),
scroller = scrollable.getScroller(),
titleHeight = 0,
barHeight, padding;
barHeight = bar.getHeight();
if (showTitle && title) {
titleHeight = title.element.getHeight();
}
padding = Math.ceil((element.getHeight() - titleHeight - barHeight) / 2);
innerElement.setStyle({
padding: padding + 'px 0 ' + (padding) + 'px'
});
scroller.refresh();
scroller.setSlotSnapSize(barHeight);
this.setValue(value);
},
// @private
doItemTap: function(list, index, item, e) {
var me = this;
me.selectedIndex = index;
me.selectedNode = item;
me.scrollToItem(item, true);
},
// @private
scrollToItem: function(item, animated) {
var y = item.getY(),
parentEl = item.parent(),
parentY = parentEl.getY(),
scrollView = this.getScrollable(),
scroller = scrollView.getScroller(),
difference;
difference = y - parentY;
scroller.scrollTo(0, difference, animated);
},
// @private
onScrollEnd: function(scroller, x, y) {
var me = this,
index = Math.round(y / me.picker.bar.getHeight()),
viewItems = me.getViewItems(),
item = viewItems[index];
if (item) {
me.selectedIndex = index;
me.selectedNode = item;
me.fireEvent('slotpick', me, me.getValue(), me.selectedNode);
}
},
/**
* Returns the value of this slot
* @private
*/
getValue: function(useDom) {
var store = this.getStore(),
record, value;
if (!store) {
return;
}
if (!this.rendered || !useDom) {
return this._value;
}
//if the value is ever false, that means we do not want to return anything
if (this._value === false) {
return null;
}
record = store.getAt(this.selectedIndex);
value = record ? record.get(this.getValueField()) : null;
// this._value = value;
return value;
},
/**
* Sets the value of this slot
* @private
*/
setValue: function(value) {
if (!Ext.isDefined(value)) {
return;
}
if (!this.rendered) {
//we don't want to call this until the slot has been rendered
this._value = value;
return;
}
var store = this.getStore(),
viewItems = this.getViewItems(),
valueField = this.getValueField(),
index, item;
index = store.findExact(valueField, value);
if (index != -1) {
item = Ext.get(viewItems[index]);
this.selectedIndex = index;
if (item) {
this.scrollToItem(item);
}
this._value = value;
}
},
/**
* Sets the value of this slot
* @private
*/
setValueAnimated: function(value) {
if (!this.rendered) {
//we don't want to call this until the slot has been rendered
this._value = value;
return;
}
var store = this.getStore(),
viewItems = this.getViewItems(),
valueField = this.getValueField(),
index, item;
index = store.find(valueField, value);
if (index != -1) {
item = Ext.get(viewItems[index]);
this.selectedIndex = index;
if (item) {
this.scrollToItem(item, {
duration: 100
});
}
this._value = value;
}
}
});
/**
* @aside example pickers
* A general picker class. {@link Ext.picker.Slot}s are used to organize multiple scrollable slots into a single picker. {@link #slots} is
* the only necessary configuration.
*
* The {@link #slots} configuration with a few key values:
*
* - `name`: The name of the slot (will be the key when using {@link #getValues} in this {@link Ext.picker.Picker}).
* - `title`: The title of this slot (if {@link #useTitles} is set to `true`).
* - `data`/`store`: The data or store to use for this slot.
*
* Remember, {@link Ext.picker.Slot} class extends from {@link Ext.dataview.DataView}.
*
* ## Examples
*
* @example miniphone preview
* var picker = Ext.create('Ext.Picker', {
* slots: [
* {
* name : 'limit_speed',
* title: 'Speed',
* data : [
* {text: '50 KB/s', value: 50},
* {text: '100 KB/s', value: 100},
* {text: '200 KB/s', value: 200},
* {text: '300 KB/s', value: 300}
* ]
* }
* ]
* });
* Ext.Viewport.add(picker);
* picker.show();
*
* You can also customize the top toolbar on the {@link Ext.picker.Picker} by changing the {@link #doneButton} and {@link #cancelButton} configurations:
*
* @example miniphone preview
* var picker = Ext.create('Ext.Picker', {
* doneButton: 'I\'m done!',
* cancelButton: false,
* slots: [
* {
* name : 'limit_speed',
* title: 'Speed',
* data : [
* {text: '50 KB/s', value: 50},
* {text: '100 KB/s', value: 100},
* {text: '200 KB/s', value: 200},
* {text: '300 KB/s', value: 300}
* ]
* }
* ]
* });
* Ext.Viewport.add(picker);
* picker.show();
*
* Or by passing a custom {@link #toolbar} configuration:
*
* @example miniphone preview
* var picker = Ext.create('Ext.Picker', {
* doneButton: false,
* cancelButton: false,
* toolbar: {
* ui: 'light',
* title: 'My Picker!'
* },
* slots: [
* {
* name : 'limit_speed',
* title: 'Speed',
* data : [
* {text: '50 KB/s', value: 50},
* {text: '100 KB/s', value: 100},
* {text: '200 KB/s', value: 200},
* {text: '300 KB/s', value: 300}
* ]
* }
* ]
* });
* Ext.Viewport.add(picker);
* picker.show();
*/
Ext.define('Ext.picker.Picker', {
extend: 'Ext.Sheet',
alias : 'widget.picker',
alternateClassName: 'Ext.Picker',
requires: ['Ext.picker.Slot', 'Ext.TitleBar', 'Ext.data.Model'],
isPicker: true,
/**
* @event pick
* Fired when a slot has been picked
* @param {Ext.Picker} this This Picker.
* @param {Object} The values of this picker's slots, in `{name:'value'}` format.
* @param {Ext.Picker.Slot} slot An instance of Ext.Picker.Slot that has been picked.
*/
/**
* @event change
* Fired when the value of this picker has changed the Done button has been pressed.
* @param {Ext.picker.Picker} this This Picker.
* @param {Object} value The values of this picker's slots, in `{name:'value'}` format.
*/
/**
* @event cancel
* Fired when the cancel button is tapped and the values are reverted back to
* what they were.
* @param {Ext.Picker} this This Picker.
*/
config: {
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'picker',
/**
* @cfg {String/Mixed} doneButton
* Can be either:
*
* - A {String} text to be used on the Done button.
* - An {Object} as config for {@link Ext.Button}.
* - `false` or `null` to hide it.
* @accessor
*/
doneButton: true,
/**
* @cfg {String/Mixed} cancelButton
* Can be either:
*
* - A {String} text to be used on the Cancel button.
* - An {Object} as config for {@link Ext.Button}.
* - `false` or `null` to hide it.
* @accessor
*/
cancelButton: true,
/**
* @cfg {Boolean} useTitles
* Generate a title header for each individual slot and use
* the title configuration of the slot.
* @accessor
*/
useTitles: false,
/**
* @cfg {Array} slots
* An array of slot configurations.
*
* - `name` {String} - Name of the slot
* - `data` {Array} - An array of text/value pairs in the format `{text: 'myKey', value: 'myValue'}`
* - `title` {String} - Title of the slot. This is used in conjunction with `useTitles: true`.
*
* @accessor
*/
slots: null,
/**
* @cfg {String/Number} value The value to initialize the picker with.
* @accessor
*/
value: null,
/**
* @cfg {Number} height
* The height of the picker.
* @accessor
*/
height: 220,
/**
* @cfg
* @inheritdoc
*/
layout: {
type : 'hbox',
align: 'stretch'
},
/**
* @cfg
* @hide
*/
centered: false,
/**
* @cfg
* @inheritdoc
*/
left : 0,
/**
* @cfg
* @inheritdoc
*/
right: 0,
/**
* @cfg
* @inheritdoc
*/
bottom: 0,
// @private
defaultType: 'pickerslot',
/**
* @cfg {Ext.TitleBar/Ext.Toolbar/Object} toolbar
* The toolbar which contains the {@link #doneButton} and {@link #cancelButton} buttons.
* You can override this if you wish, and add your own configurations. Just ensure that you take into account
* the {@link #doneButton} and {@link #cancelButton} configurations.
*
* The default xtype is a {@link Ext.TitleBar}:
*
* toolbar: {
* items: [
* {
* xtype: 'button',
* text: 'Left',
* align: 'left'
* },
* {
* xtype: 'button',
* text: 'Right',
* align: 'left'
* }
* ]
* }
*
* Or to use a {@link Ext.Toolbar instead}:
*
* toolbar: {
* xtype: 'toolbar',
* items: [
* {
* xtype: 'button',
* text: 'Left'
* },
* {
* xtype: 'button',
* text: 'Left Two'
* }
* ]
* }
*
* @accessor
*/
toolbar: true
},
initElement: function() {
this.callParent(arguments);
var me = this,
clsPrefix = Ext.baseCSSPrefix,
innerElement = this.innerElement;
//insert the mask, and the picker bar
this.mask = innerElement.createChild({
cls: clsPrefix + 'picker-mask'
});
this.bar = this.mask.createChild({
cls: clsPrefix + 'picker-bar'
});
me.on({
scope : this,
delegate: 'pickerslot',
slotpick: 'onSlotPick'
});
me.on({
scope: this,
show: 'onShow'
});
},
/**
* @private
*/
applyToolbar: function(config) {
if (config === true) {
config = {};
}
Ext.applyIf(config, {
docked: 'top'
});
return Ext.factory(config, 'Ext.TitleBar', this.getToolbar());
},
/**
* @private
*/
updateToolbar: function(newToolbar, oldToolbar) {
if (newToolbar) {
this.add(newToolbar);
}
if (oldToolbar) {
this.remove(oldToolbar);
}
},
/**
* Updates the {@link #doneButton} configuration. Will change it into a button when appropriate, or just update the text if needed.
* @param {Object} config
* @return {Object}
*/
applyDoneButton: function(config) {
if (config) {
if (Ext.isBoolean(config)) {
config = {};
}
if (typeof config == "string") {
config = {
text: config
};
}
Ext.applyIf(config, {
ui: 'action',
align: 'right',
text: 'Done'
});
}
return Ext.factory(config, 'Ext.Button', this.getDoneButton());
},
updateDoneButton: function(newDoneButton, oldDoneButton) {
var toolbar = this.getToolbar();
if (newDoneButton) {
toolbar.add(newDoneButton);
newDoneButton.on('tap', this.onDoneButtonTap, this);
} else if (oldDoneButton) {
toolbar.remove(oldDoneButton);
}
},
/**
* Updates the {@link #cancelButton} configuration. Will change it into a button when appropriate, or just update the text if needed.
* @param {Object} config
* @return {Object}
*/
applyCancelButton: function(config) {
if (config) {
if (Ext.isBoolean(config)) {
config = {};
}
if (typeof config == "string") {
config = {
text: config
};
}
Ext.applyIf(config, {
align: 'left',
text: 'Cancel'
});
}
return Ext.factory(config, 'Ext.Button', this.getCancelButton());
},
updateCancelButton: function(newCancelButton, oldCancelButton) {
var toolbar = this.getToolbar();
if (newCancelButton) {
toolbar.add(newCancelButton);
newCancelButton.on('tap', this.onCancelButtonTap, this);
} else if (oldCancelButton) {
toolbar.remove(oldCancelButton);
}
},
/**
* @private
*/
updateUseTitles: function(useTitles) {
var innerItems = this.getInnerItems(),
ln = innerItems.length,
cls = Ext.baseCSSPrefix + 'use-titles',
i, innerItem;
//add a cls onto the picker
if (useTitles) {
this.addCls(cls);
} else {
this.removeCls(cls);
}
//show the time on each of the slots
for (i = 0; i < ln; i++) {
innerItem = innerItems[i];
if (innerItem.isSlot) {
innerItem.setShowTitle(useTitles);
}
}
},
applySlots: function(slots) {
//loop through each of the slots and add a reference to this picker
if (slots) {
var ln = slots.length,
i;
for (i = 0; i < ln; i++) {
slots[i].picker = this;
}
}
return slots;
},
/**
* Adds any new {@link #slots} to this picker, and removes existing {@link #slots}
* @private
*/
updateSlots: function(newSlots) {
var bcss = Ext.baseCSSPrefix,
innerItems;
this.removeAll();
if (newSlots) {
this.add(newSlots);
}
innerItems = this.getInnerItems();
if (innerItems.length > 0) {
innerItems[0].addCls(bcss + 'first');
innerItems[innerItems.length - 1].addCls(bcss + 'last');
}
this.updateUseTitles(this.getUseTitles());
},
/**
* @private
* Called when the done button has been tapped.
*/
onDoneButtonTap: function() {
var oldValue = this._value,
newValue = this.getValue(true);
if (newValue != oldValue) {
this.fireEvent('change', this, newValue);
}
this.hide();
},
/**
* @private
* Called when the cancel button has been tapped.
*/
onCancelButtonTap: function() {
this.fireEvent('cancel', this);
this.hide();
},
/**
* @private
* Called when a slot has been picked.
*/
onSlotPick: function(slot) {
this.fireEvent('pick', this, this.getValue(true), slot);
},
onShow: function() {
if (!this.isHidden()) {
this.setValue(this._value);
}
},
/**
* Sets the values of the pickers slots.
* @param {Object} values The values in a {name:'value'} format.
* @param {Boolean} animated `true` to animate setting the values.
* @return {Ext.Picker} this This picker.
*/
setValue: function(values, animated) {
var me = this,
slots = me.getInnerItems(),
ln = slots.length,
key, slot, loopSlot, i, value;
if (!values) {
values = {};
for (i = 0; i < ln; i++) {
//set the value to false so the slot will return null when getValue is called
values[slots[i].config.name] = null;
}
}
for (key in values) {
slot = null;
value = values[key];
for (i = 0; i < slots.length; i++) {
loopSlot = slots[i];
if (loopSlot.config.name == key) {
slot = loopSlot;
break;
}
}
if (slot) {
if (animated) {
slot.setValueAnimated(value);
} else {
slot.setValue(value);
}
}
}
me._values = me._value = values;
return me;
},
setValueAnimated: function(values) {
this.setValue(values, true);
},
/**
* Returns the values of each of the pickers slots
* @return {Object} The values of the pickers slots
*/
getValue: function(useDom) {
var values = {},
items = this.getItems().items,
ln = items.length,
item, i;
if (useDom) {
for (i = 0; i < ln; i++) {
item = items[i];
if (item && item.isSlot) {
values[item.getName()] = item.getValue(useDom);
}
}
this._values = values;
}
return this._values;
},
/**
* Returns the values of each of the pickers slots.
* @return {Object} The values of the pickers slots.
*/
getValues: function() {
return this.getValue();
},
destroy: function() {
this.callParent();
Ext.destroy(this.mask, this.bar);
}
}, function() {
});
/**
* A date picker component which shows a Date Picker on the screen. This class extends from {@link Ext.picker.Picker}
* and {@link Ext.Sheet} so it is a popup.
*
* This component has no required configurations.
*
* ## Examples
*
* @example miniphone preview
* var datePicker = Ext.create('Ext.picker.Date');
* Ext.Viewport.add(datePicker);
* datePicker.show();
*
* You may want to adjust the {@link #yearFrom} and {@link #yearTo} properties:
*
* @example miniphone preview
* var datePicker = Ext.create('Ext.picker.Date', {
* yearFrom: 2000,
* yearTo : 2015
* });
* Ext.Viewport.add(datePicker);
* datePicker.show();
*
* You can set the value of the {@link Ext.picker.Date} to the current date using `new Date()`:
*
* @example miniphone preview
* var datePicker = Ext.create('Ext.picker.Date', {
* value: new Date()
* });
* Ext.Viewport.add(datePicker);
* datePicker.show();
*
* And you can hide the titles from each of the slots by using the {@link #useTitles} configuration:
*
* @example miniphone preview
* var datePicker = Ext.create('Ext.picker.Date', {
* useTitles: false
* });
* Ext.Viewport.add(datePicker);
* datePicker.show();
*/
Ext.define('Ext.picker.Date', {
extend: 'Ext.picker.Picker',
xtype: 'datepicker',
alternateClassName: 'Ext.DatePicker',
requires: ['Ext.DateExtras'],
/**
* @event change
* Fired when the value of this picker has changed and the done button is pressed.
* @param {Ext.picker.Date} this This Picker
* @param {Date} value The date value
*/
config: {
/**
* @cfg {Number} yearFrom
* The start year for the date picker. If {@link #yearFrom} is greater than
* {@link #yearTo} then the order of years will be reversed.
* @accessor
*/
yearFrom: 1980,
/**
* @cfg {Number} [yearTo=new Date().getFullYear()]
* The last year for the date picker. If {@link #yearFrom} is greater than
* {@link #yearTo} then the order of years will be reversed.
* @accessor
*/
yearTo: new Date().getFullYear(),
/**
* @cfg {String} monthText
* The label to show for the month column.
* @accessor
*/
monthText: 'Month',
/**
* @cfg {String} dayText
* The label to show for the day column.
* @accessor
*/
dayText: 'Day',
/**
* @cfg {String} yearText
* The label to show for the year column.
* @accessor
*/
yearText: 'Year',
/**
* @cfg {Array} slotOrder
* An array of strings that specifies the order of the slots.
* @accessor
*/
slotOrder: ['month', 'day', 'year']
/**
* @cfg {Object/Date} value
* Default value for the field and the internal {@link Ext.picker.Date} component. Accepts an object of 'year',
* 'month' and 'day' values, all of which should be numbers, or a {@link Date}.
*
* Examples:
*
* - `{year: 1989, day: 1, month: 5}` = 1st May 1989
* - `new Date()` = current date
* @accessor
*/
/**
* @cfg {Array} slots
* @hide
* @accessor
*/
},
initialize: function() {
this.callParent();
this.on({
scope: this,
delegate: '> slot',
slotpick: this.onSlotPick
});
this.on({
scope: this,
show: this.onSlotPick
});
},
setValue: function(value, animated) {
if (Ext.isDate(value)) {
value = {
day : value.getDate(),
month: value.getMonth() + 1,
year : value.getFullYear()
};
}
this.callParent([value, animated]);
},
getValue: function(useDom) {
var values = {},
items = this.getItems().items,
ln = items.length,
daysInMonth, day, month, year, item, i;
for (i = 0; i < ln; i++) {
item = items[i];
if (item instanceof Ext.picker.Slot) {
values[item.getName()] = item.getValue(useDom);
}
}
//if all the slots return null, we should not return a date
if (values.year === null && values.month === null && values.day === null) {
return null;
}
year = Ext.isNumber(values.year) ? values.year : 1;
month = Ext.isNumber(values.month) ? values.month : 1;
day = Ext.isNumber(values.day) ? values.day : 1;
if (month && year && month && day) {
daysInMonth = this.getDaysInMonth(month, year);
}
day = (daysInMonth) ? Math.min(day, daysInMonth): day;
return new Date(year, month - 1, day);
},
/**
* Updates the yearFrom configuration
*/
updateYearFrom: function() {
if (this.initialized) {
this.createSlots();
}
},
/**
* Updates the yearTo configuration
*/
updateYearTo: function() {
if (this.initialized) {
this.createSlots();
}
},
/**
* Updates the monthText configuration
*/
updateMonthText: function(newMonthText, oldMonthText) {
var innerItems = this.getInnerItems,
ln = innerItems.length,
item, i;
//loop through each of the current items and set the title on the correct slice
if (this.initialized) {
for (i = 0; i < ln; i++) {
item = innerItems[i];
if ((typeof item.title == "string" && item.title == oldMonthText) || (item.title.html == oldMonthText)) {
item.setTitle(newMonthText);
}
}
}
},
/**
* Updates the {@link #dayText} configuration.
*/
updateDayText: function(newDayText, oldDayText) {
var innerItems = this.getInnerItems,
ln = innerItems.length,
item, i;
//loop through each of the current items and set the title on the correct slice
if (this.initialized) {
for (i = 0; i < ln; i++) {
item = innerItems[i];
if ((typeof item.title == "string" && item.title == oldDayText) || (item.title.html == oldDayText)) {
item.setTitle(newDayText);
}
}
}
},
/**
* Updates the yearText configuration
*/
updateYearText: function(yearText) {
var innerItems = this.getInnerItems,
ln = innerItems.length,
item, i;
//loop through each of the current items and set the title on the correct slice
if (this.initialized) {
for (i = 0; i < ln; i++) {
item = innerItems[i];
if (item.title == this.yearText) {
item.setTitle(yearText);
}
}
}
},
// @private
constructor: function() {
this.callParent(arguments);
this.createSlots();
},
/**
* Generates all slots for all years specified by this component, and then sets them on the component
* @private
*/
createSlots: function() {
var me = this,
slotOrder = me.getSlotOrder(),
yearsFrom = me.getYearFrom(),
yearsTo = me.getYearTo(),
years = [],
days = [],
months = [],
reverse = yearsFrom > yearsTo,
ln, i, daysInMonth;
while (yearsFrom) {
years.push({
text : yearsFrom,
value : yearsFrom
});
if (yearsFrom === yearsTo) {
break;
}
if (reverse) {
yearsFrom--;
} else {
yearsFrom++;
}
}
daysInMonth = me.getDaysInMonth(1, new Date().getFullYear());
for (i = 0; i < daysInMonth; i++) {
days.push({
text : i + 1,
value : i + 1
});
}
for (i = 0, ln = Ext.Date.monthNames.length; i < ln; i++) {
months.push({
text : Ext.Date.monthNames[i],
value : i + 1
});
}
var slots = [];
slotOrder.forEach(function (item) {
slots.push(me.createSlot(item, days, months, years));
});
me.setSlots(slots);
},
/**
* Returns a slot config for a specified date.
* @private
*/
createSlot: function(name, days, months, years) {
switch (name) {
case 'year':
return {
name: 'year',
align: 'center',
data: years,
title: this.getYearText(),
flex: 3
};
case 'month':
return {
name: name,
align: 'right',
data: months,
title: this.getMonthText(),
flex: 4
};
case 'day':
return {
name: 'day',
align: 'center',
data: days,
title: this.getDayText(),
flex: 2
};
}
},
onSlotPick: function() {
var value = this.getValue(true),
slot = this.getDaySlot(),
year = value.getFullYear(),
month = value.getMonth(),
days = [],
daysInMonth, i;
if (!value || !Ext.isDate(value) || !slot) {
return;
}
this.callParent();
//get the new days of the month for this new date
daysInMonth = this.getDaysInMonth(month + 1, year);
for (i = 0; i < daysInMonth; i++) {
days.push({
text: i + 1,
value: i + 1
});
}
// We don't need to update the slot days unless it has changed
if (slot.getData().length == days.length) {
return;
}
slot.setData(days);
// Now we have the correct amount of days for the day slot, lets update it
var store = slot.getStore(),
viewItems = slot.getViewItems(),
valueField = slot.getValueField(),
index, item;
index = store.find(valueField, value.getDate());
if (index == -1) {
return;
}
item = Ext.get(viewItems[index]);
slot.selectedIndex = index;
slot.scrollToItem(item);
// slot._value = value;
},
getDaySlot: function() {
var innerItems = this.getInnerItems(),
ln = innerItems.length,
i, slot;
if (this.daySlot) {
return this.daySlot;
}
for (i = 0; i < ln; i++) {
slot = innerItems[i];
if (slot.isSlot && slot.getName() == "day") {
this.daySlot = slot;
return slot;
}
}
return null;
},
// @private
getDaysInMonth: function(month, year) {
var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return month == 2 && this.isLeapYear(year) ? 29 : daysInMonth[month-1];
},
// @private
isLeapYear: function(year) {
return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)));
},
onDoneButtonTap: function() {
var oldValue = this._value,
newValue = this.getValue(true),
testValue = newValue;
if (Ext.isDate(newValue)) {
testValue = newValue.toDateString();
}
if (Ext.isDate(oldValue)) {
oldValue = oldValue.toDateString();
}
if (testValue != oldValue) {
this.fireEvent('change', this, newValue);
}
this.hide();
}
});
/**
* @aside guide forms
*
* This is a specialized field which shows a {@link Ext.picker.Date} when tapped. If it has a predefined value,
* or a value is selected in the {@link Ext.picker.Date}, it will be displayed like a normal {@link Ext.field.Text}
* (but not selectable/changable).
*
* Ext.create('Ext.field.DatePicker', {
* label: 'Birthday',
* value: new Date()
* });
*
* {@link Ext.field.DatePicker} fields are very simple to implement, and have no required configurations.
*
* ## Examples
*
* It can be very useful to set a default {@link #value} configuration on {@link Ext.field.DatePicker} fields. In
* this example, we set the {@link #value} to be the current date. You can also use the {@link #setValue} method to
* update the value at any time.
*
* @example miniphone preview
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* items: [
* {
* xtype: 'datepickerfield',
* label: 'Birthday',
* name: 'birthday',
* value: new Date()
* }
* ]
* },
* {
* xtype: 'toolbar',
* docked: 'bottom',
* items: [
* { xtype: 'spacer' },
* {
* text: 'setValue',
* handler: function() {
* var datePickerField = Ext.ComponentQuery.query('datepickerfield')[0];
*
* var randomNumber = function(from, to) {
* return Math.floor(Math.random() * (to - from + 1) + from);
* };
*
* datePickerField.setValue({
* month: randomNumber(0, 11),
* day : randomNumber(0, 28),
* year : randomNumber(1980, 2011)
* });
* }
* },
* { xtype: 'spacer' }
* ]
* }
* ]
* });
*
* When you need to retrieve the date from the {@link Ext.field.DatePicker}, you can either use the {@link #getValue} or
* {@link #getFormattedValue} methods:
*
* @example preview
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* items: [
* {
* xtype: 'datepickerfield',
* label: 'Birthday',
* name: 'birthday',
* value: new Date()
* }
* ]
* },
* {
* xtype: 'toolbar',
* docked: 'bottom',
* items: [
* {
* text: 'getValue',
* handler: function() {
* var datePickerField = Ext.ComponentQuery.query('datepickerfield')[0];
* Ext.Msg.alert(null, datePickerField.getValue());
* }
* },
* { xtype: 'spacer' },
* {
* text: 'getFormattedValue',
* handler: function() {
* var datePickerField = Ext.ComponentQuery.query('datepickerfield')[0];
* Ext.Msg.alert(null, datePickerField.getFormattedValue());
* }
* }
* ]
* }
* ]
* });
*
*
*/
Ext.define('Ext.field.DatePicker', {
extend: 'Ext.field.Text',
alternateClassName: 'Ext.form.DatePicker',
xtype: 'datepickerfield',
requires: [
'Ext.picker.Date',
'Ext.DateExtras'
],
/**
* @event change
* Fires when a date is selected
* @param {Ext.field.DatePicker} this
* @param {Date} newDate The new date
* @param {Date} oldDate The old date
*/
config: {
ui: 'select',
/**
* @cfg {Object/Ext.picker.Date} picker
* An object that is used when creating the internal {@link Ext.picker.Date} component or a direct instance of {@link Ext.picker.Date}.
* @accessor
*/
picker: true,
/**
* @cfg {Boolean}
* @hide
* @accessor
*/
clearIcon: false,
/**
* @cfg {Object/Date} value
* Default value for the field and the internal {@link Ext.picker.Date} component. Accepts an object of 'year',
* 'month' and 'day' values, all of which should be numbers, or a {@link Date}.
*
* Example: {year: 1989, day: 1, month: 5} = 1st May 1989 or new Date()
* @accessor
*/
/**
* @cfg {Boolean} destroyPickerOnHide
* Whether or not to destroy the picker widget on hide. This save memory if it's not used frequently,
* but increase delay time on the next show due to re-instantiation.
* @accessor
*/
destroyPickerOnHide: false,
/**
* @cfg {String} [dateFormat=Ext.util.Format.defaultDateFormat] The format to be used when displaying the date in this field.
* Accepts any valid date format. You can view formats over in the {@link Ext.Date} documentation.
*/
dateFormat: null,
/**
* @cfg {Object}
* @hide
*/
component: {
useMask: true
}
},
initialize: function() {
var me = this,
component = me.getComponent();
me.callParent();
component.on({
scope: me,
masktap: 'onMaskTap'
});
if (Ext.os.is.Android2) {
component.input.dom.disabled = true;
}
},
syncEmptyCls: Ext.emptyFn,
applyValue: function(value) {
if (!Ext.isDate(value) && !Ext.isObject(value)) {
return null;
}
if (Ext.isObject(value)) {
return new Date(value.year, value.month - 1, value.day);
}
return value;
},
updateValue: function(newValue, oldValue) {
var me = this,
picker = me._picker;
if (picker && picker.isPicker) {
picker.setValue(newValue);
}
// Ext.Date.format expects a Date
if (newValue !== null) {
me.getComponent().setValue(Ext.Date.format(newValue, me.getDateFormat() || Ext.util.Format.defaultDateFormat));
} else {
me.getComponent().setValue('');
}
if (newValue !== oldValue) {
me.fireEvent('change', me, newValue, oldValue);
}
},
/**
* Updates the date format in the field.
* @private
*/
updateDateFormat: function(newDateFormat, oldDateFormat) {
var value = this.getValue();
if (newDateFormat != oldDateFormat && Ext.isDate(value)) {
this.getComponent().setValue(Ext.Date.format(value, newDateFormat || Ext.util.Format.defaultDateFormat));
}
},
/**
* Returns the {@link Date} value of this field.
* If you wanted a formated date
* @return {Date} The date selected
*/
getValue: function() {
if (this._picker && this._picker instanceof Ext.picker.Date) {
return this._picker.getValue();
}
return this._value;
},
/**
* Returns the value of the field formatted using the specified format. If it is not specified, it will default to
* {@link #dateFormat} and then {@link Ext.util.Format#defaultDateFormat}.
* @param {String} format The format to be returned.
* @return {String} The formatted date.
*/
getFormattedValue: function(format) {
var value = this.getValue();
return (Ext.isDate(value)) ? Ext.Date.format(value, format || this.getDateFormat() || Ext.util.Format.defaultDateFormat) : value;
},
applyPicker: function(picker, pickerInstance) {
if (pickerInstance && pickerInstance.isPicker) {
picker = pickerInstance.setConfig(picker);
}
return picker;
},
getPicker: function() {
var picker = this._picker,
value = this.getValue();
if (picker && !picker.isPicker) {
picker = Ext.factory(picker, Ext.picker.Date);
if (value != null) {
picker.setValue(value);
}
}
picker.on({
scope: this,
change: 'onPickerChange',
hide : 'onPickerHide'
});
Ext.Viewport.add(picker);
this._picker = picker;
return picker;
},
/**
* @private
* Listener to the tap event of the mask element. Shows the internal DatePicker component when the button has been tapped.
*/
onMaskTap: function() {
if (this.getDisabled()) {
return false;
}
this.onFocus();
return false;
},
/**
* Called when the picker changes its value.
* @param {Ext.picker.Date} picker The date picker.
* @param {Object} value The new value from the date picker.
* @private
*/
onPickerChange: function(picker, value) {
var me = this,
oldValue = me.getValue();
me.setValue(value);
me.fireEvent('select', me, value);
me.onChange(me, value, oldValue);
},
/**
* Override this or change event will be fired twice. change event is fired in updateValue
* for this field. TOUCH-2861
*/
onChange: Ext.emptyFn,
/**
* Destroys the picker when it is hidden, if
* {@link Ext.field.DatePicker#destroyPickerOnHide destroyPickerOnHide} is set to `true`.
* @private
*/
onPickerHide: function() {
var me = this,
picker = me.getPicker();
if (me.getDestroyPickerOnHide() && picker) {
picker.destroy();
me._picker = me.getInitialConfig().picker || true;
}
},
reset: function() {
this.setValue(this.originalValue);
},
onFocus: function(e) {
var component = this.getComponent();
this.fireEvent('focus', this, e);
if (Ext.os.is.Android4) {
component.input.dom.focus();
}
component.input.dom.blur();
if (this.getReadOnly()) {
return false;
}
this.isFocused = true;
this.getPicker().show();
},
// @private
destroy: function() {
var picker = this._picker;
if (picker && picker.isPicker) {
picker.destroy();
}
this.callParent(arguments);
}
});
/**
* @aside guide forms
*
* The Email field creates an HTML5 email input and is usually created inside a form. Because it creates an HTML email
* input field, most browsers will show a specialized virtual keyboard for email address input. Aside from that, the
* email field is just a normal text field. Here's an example of how to use it in a form:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'Register',
* items: [
* {
* xtype: 'emailfield',
* label: 'Email',
* name: 'email'
* },
* {
* xtype: 'passwordfield',
* label: 'Password',
* name: 'password'
* }
* ]
* }
* ]
* });
*
* Or on its own, outside of a form:
*
* Ext.create('Ext.field.Email', {
* label: 'Email address',
* value: 'prefilled@email.com'
* });
*
* Because email field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text fields
* provide, including getting and setting the value at runtime, validations and various events that are fired as the
* user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional functionality
* available.
*/
Ext.define('Ext.field.Email', {
extend: 'Ext.field.Text',
alternateClassName: 'Ext.form.Email',
xtype: 'emailfield',
config: {
/**
* @cfg
* @inheritdoc
*/
component: {
type: 'email'
},
/**
* @cfg
* @inheritdoc
*/
autoCapitalize: false
}
});
/**
* @aside guide forms
*
* Hidden fields allow you to easily inject additional data into a {@link Ext.form.Panel form} without displaying
* additional fields on the screen. This is often useful for sending dynamic or previously collected data back to the
* server in the same request as the normal form submission. For example, here is how we might set up a form to send
* back a hidden userId field:
*
* @example
* var form = Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'Enter your name',
* items: [
* {
* xtype: 'hiddenfield',
* name: 'userId',
* value: 123
* },
* {
* xtype: 'checkboxfield',
* label: 'Enable notifications',
* name: 'notifications'
* }
* ]
* }
* ]
* });
*
* In the form above we created two fields - a hidden field and a {@link Ext.field.Checkbox check box field}. Only the
* check box will be visible, but both fields will be submitted. Hidden fields cannot be tabbed to - they are removed
* from the tab index so when your user taps the next/previous field buttons the hidden field is skipped over.
*
* It's easy to read and update the value of a hidden field within a form. Using the example above, we can get a
* reference to the hidden field and then set it to a new value in 2 lines of code:
*
* var userId = form.down('hiddenfield')[0];
* userId.setValue(1234);
*/
Ext.define('Ext.field.Hidden', {
extend: 'Ext.field.Text',
alternateClassName: 'Ext.form.Hidden',
xtype: 'hiddenfield',
config: {
/**
* @cfg
* @inheritdoc
*/
component: {
xtype: 'input',
type : 'hidden'
},
/**
* @cfg
* @inheritdoc
*/
ui: 'hidden',
/**
* @cfg hidden
* @hide
*/
hidden: true,
/**
* @cfg {Number} tabIndex
* @hide
*/
tabIndex: -1
}
});
/**
* @aside guide forms
*
* The Number field creates an HTML5 number input and is usually created inside a form. Because it creates an HTML
* number input field, most browsers will show a specialized virtual keyboard for entering numbers. The Number field
* only accepts numerical input and also provides additional spinner UI that increases or decreases the current value
* by a configured {@link #stepValue step value}. Here's how we might use one in a form:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'How old are you?',
* items: [
* {
* xtype: 'numberfield',
* label: 'Age',
* minValue: 18,
* maxValue: 150,
* name: 'age'
* }
* ]
* }
* ]
* });
*
* Or on its own, outside of a form:
*
* Ext.create('Ext.field.Number', {
* label: 'Age',
* value: '26'
* });
*
* ## minValue, maxValue and stepValue
*
* The {@link #minValue} and {@link #maxValue} configurations are self-explanatory and simply constrain the value
* entered to the range specified by the configured min and max values. The other option exposed by this component
* is {@link #stepValue}, which enables you to set how much the value changes every time the up and down spinners
* are tapped on. For example, to create a salary field that ticks up and down by $1,000 each tap we can do this:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'Are you rich yet?',
* items: [
* {
* xtype: 'numberfield',
* label: 'Salary',
* value: 30000,
* minValue: 25000,
* maxValue: 50000,
* stepValue: 1000
* }
* ]
* }
* ]
* });
*
* This creates a field that starts with a value of $30,000, steps up and down in $1,000 increments and will not go
* beneath $25,000 or above $50,000.
*
* Because number field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text
* fields provide, including getting and setting the value at runtime, validations and various events that are fired as
* the user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional functionality
* available.
*/
Ext.define('Ext.field.Number', {
extend: 'Ext.field.Text',
xtype: 'numberfield',
alternateClassName: 'Ext.form.Number',
config: {
/**
* @cfg
* @inheritdoc
*/
component: {
type: 'number'
},
/**
* @cfg
* @inheritdoc
*/
ui: 'number'
},
proxyConfig: {
/**
* @cfg {Number} minValue The minimum value that this Number field can accept
* @accessor
*/
minValue: null,
/**
* @cfg {Number} maxValue The maximum value that this Number field can accept
* @accessor
*/
maxValue: null,
/**
* @cfg {Number} stepValue The amount by which the field is incremented or decremented each time the spinner is tapped.
* Defaults to undefined, which means that the field goes up or down by 1 each time the spinner is tapped
* @accessor
*/
stepValue: null
},
applyValue: function(value) {
var minValue = this.getMinValue(),
maxValue = this.getMaxValue();
if (Ext.isNumber(minValue)) {
value = Math.max(value, minValue);
}
if (Ext.isNumber(maxValue)) {
value = Math.min(value, maxValue);
}
value = parseFloat(value);
return (isNaN(value)) ? '' : value;
},
getValue: function() {
var value = parseFloat(this.callParent(), 10);
return (isNaN(value)) ? null : value;
},
doClearIconTap: function(me, e) {
me.getComponent().setValue('');
me.getValue();
me.hideClearIcon();
}
});
/**
* @aside guide forms
*
* The Password field creates a password input and is usually created inside a form. Because it creates a password
* field, when the user enters text it will show up as stars. Aside from that, the password field is just a normal text
* field. Here's an example of how to use it in a form:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'Register',
* items: [
* {
* xtype: 'emailfield',
* label: 'Email',
* name: 'email'
* },
* {
* xtype: 'passwordfield',
* label: 'Password',
* name: 'password'
* }
* ]
* }
* ]
* });
*
* Or on its own, outside of a form:
*
* Ext.create('Ext.field.Password', {
* label: 'Password',
* value: 'existingPassword'
* });
*
* Because the password field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text
* fields provide, including getting and setting the value at runtime, validations and various events that are fired as
* the user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional functionality
* available.
*/
Ext.define('Ext.field.Password', {
extend: 'Ext.field.Text',
xtype: 'passwordfield',
alternateClassName: 'Ext.form.Password',
config: {
/**
* @cfg
* @inheritdoc
*/
autoCapitalize: false,
/**
* @cfg
* @inheritdoc
*/
component: {
type: 'password'
}
}
});
/**
* @aside guide forms
*
* The radio field is an enhanced version of the native browser radio controls and is a good way of allowing your user
* to choose one option out of a selection of several (for example, choosing a favorite color):
*
* @example
* var form = Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'radiofield',
* name : 'color',
* value: 'red',
* label: 'Red',
* checked: true
* },
* {
* xtype: 'radiofield',
* name : 'color',
* value: 'green',
* label: 'Green'
* },
* {
* xtype: 'radiofield',
* name : 'color',
* value: 'blue',
* label: 'Blue'
* }
* ]
* });
*
* Above we created a simple form which allows the user to pick a color from the options red, green and blue. Because
* we gave each of the fields above the same {@link #name}, the radio field ensures that only one of them can be
* checked at a time. When we come to get the values out of the form again or submit it to the server, only 1 value
* will be sent for each group of radio fields with the same name:
*
* form.getValues(); //looks like {color: 'red'}
* form.submit(); //sends a single field back to the server (in this case color: red)
*
*/
Ext.define('Ext.field.Radio', {
extend: 'Ext.field.Checkbox',
xtype: 'radiofield',
alternateClassName: 'Ext.form.Radio',
isRadio: true,
config: {
/**
* @cfg
* @inheritdoc
*/
ui: 'radio',
/**
* @cfg
* @inheritdoc
*/
component: {
type: 'radio',
cls: Ext.baseCSSPrefix + 'input-radio'
}
},
getValue: function() {
return (this._value) ? this._value : null;
},
setValue: function(value) {
this._value = value;
return this;
},
getSubmitValue: function() {
var value = this._value;
if (typeof value == "undefined" || value == null) {
value = true;
}
return (this.getChecked()) ? value : null;
},
updateChecked: function(newChecked) {
this.getComponent().setChecked(newChecked);
if (this.initialized) {
this.refreshGroupValues();
}
},
// @private
onMaskTap: function(component, e) {
var me = this,
dom = component.input.dom;
if (me.getDisabled()) {
return false;
}
if (!me.getChecked()) {
dom.checked = true;
}
me.refreshGroupValues();
//return false so the mask does not disappear
return false;
},
/**
* Returns the selected value if this radio is part of a group (other radio fields with the same name, in the same FormPanel),
* @return {String}
*/
getGroupValue: function() {
var fields = this.getSameGroupFields(),
ln = fields.length,
i = 0,
field;
for (; i < ln; i++) {
field = fields[i];
if (field.getChecked()) {
return field.getValue();
}
}
return null;
},
/**
* Set the matched radio field's status (that has the same value as the given string) to checked.
* @param {String} value The value of the radio field to check.
* @return {Ext.field.Radio} The field that is checked.
*/
setGroupValue: function(value) {
var fields = this.getSameGroupFields(),
ln = fields.length,
i = 0,
field;
for (; i < ln; i++) {
field = fields[i];
if (field.getValue() === value) {
field.setChecked(true);
return field;
}
}
},
/**
* Loops through each of the fields this radiofield is linked to (has the same name) and
* calls `onChange` on those fields so the appropriate event is fired.
* @private
*/
refreshGroupValues: function() {
var fields = this.getSameGroupFields(),
ln = fields.length,
i = 0,
field;
for (; i < ln; i++) {
field = fields[i];
field.onChange();
}
}
});
/**
* @aside guide forms
*
* The Search field creates an HTML5 search input and is usually created inside a form. Because it creates an HTML
* search input type, the visual styling of this input is slightly different to normal text input controls (the corners
* are rounded), though the virtual keyboard displayed by the operating system is the standard keyboard control.
*
* As with all other form fields in Sencha Touch, the search field gains a "clear" button that appears whenever there
* is text entered into the form, and which removes that text when tapped.
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'Search',
* items: [
* {
* xtype: 'searchfield',
* label: 'Query',
* name: 'query'
* }
* ]
* }
* ]
* });
*
* Or on its own, outside of a form:
*
* Ext.create('Ext.field.Search', {
* label: 'Search:',
* value: 'query'
* });
*
* Because search field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text
* fields provide, including getting and setting the value at runtime, validations and various events that are fired
* as the user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional
* functionality available.
*/
Ext.define('Ext.field.Search', {
extend: 'Ext.field.Text',
xtype: 'searchfield',
alternateClassName: 'Ext.form.Search',
config: {
/**
* @cfg
* @inheritdoc
*/
component: {
type: 'search'
},
/**
* @cfg
* @inheritdoc
*/
ui: 'search'
}
});
/**
* @aside guide forms
*
* Simple Select field wrapper. Example usage:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'Select',
* items: [
* {
* xtype: 'selectfield',
* label: 'Choose one',
* options: [
* {text: 'First Option', value: 'first'},
* {text: 'Second Option', value: 'second'},
* {text: 'Third Option', value: 'third'}
* ]
* }
* ]
* }
* ]
* });
*/
Ext.define('Ext.field.Select', {
extend: 'Ext.field.Text',
xtype: 'selectfield',
alternateClassName: 'Ext.form.Select',
requires: [
'Ext.Panel',
'Ext.picker.Picker',
'Ext.data.Store',
'Ext.data.StoreManager',
'Ext.dataview.List'
],
/**
* @event change
* Fires when an option selection has changed
* @param {Ext.field.Select} this
* @param {Mixed} newValue The new value
* @param {Mixed} oldValue The old value
*/
/**
* @event focus
* Fires when this field receives input focus. This happens both when you tap on the field and when you focus on the field by using
* 'next' or 'tab' on a keyboard.
*
* Please note that this event is not very reliable on Android. For example, if your Select field is second in your form panel,
* you cannot use the Next button to get to this select field. This functionality works as expected on iOS.
* @param {Ext.field.Select} this This field
* @param {Ext.event.Event} e
*/
config: {
/**
* @cfg
* @inheritdoc
*/
ui: 'select',
/**
* @cfg {Boolean} useClearIcon
* @hide
*/
/**
* @cfg {String/Number} valueField The underlying {@link Ext.data.Field#name data value name} (or numeric Array index) to bind to this
* Select control.
* @accessor
*/
valueField: 'value',
/**
* @cfg {String/Number} displayField The underlying {@link Ext.data.Field#name data value name} (or numeric Array index) to bind to this
* Select control. This resolved value is the visibly rendered value of the available selection options.
* @accessor
*/
displayField: 'text',
/**
* @cfg {Ext.data.Store/Object/String} store The store to provide selection options data.
* Either a Store instance, configuration object or store ID.
* @accessor
*/
store: null,
/**
* @cfg {Array} options An array of select options.
*
* [
* {text: 'First Option', value: 'first'},
* {text: 'Second Option', value: 'second'},
* {text: 'Third Option', value: 'third'}
* ]
*
* __Note:__ Option object member names should correspond with defined {@link #valueField valueField} and {@link #displayField displayField} values.
* This config will be ignored if a {@link #store store} instance is provided.
* @accessor
*/
options: null,
/**
* @cfg {String} hiddenName Specify a `hiddenName` if you're using the {@link Ext.form.Panel#standardSubmit standardSubmit} option.
* This name will be used to post the underlying value of the select to the server.
* @accessor
*/
hiddenName: null,
/**
* @cfg {Object} component
* @accessor
* @hide
*/
component: {
useMask: true
},
/**
* @cfg {Boolean} clearIcon
* @hide
* @accessor
*/
clearIcon: false,
/**
* @cfg {String/Boolean} usePicker
* `true` if you want this component to always use a {@link Ext.picker.Picker}.
* `false` if you want it to use a popup overlay {@link Ext.List}.
* `auto` if you want to show a {@link Ext.picker.Picker} only on phones.
*/
usePicker: 'auto',
/**
* @cfg {Boolean} autoSelect
* `true` to auto select the first value in the {@link #store} or {@link #options} when they are changed. Only happens when
* the {@link #value} is set to `null`.
*/
autoSelect: true,
/**
* @cfg {Object} defaultPhonePickerConfig
* The default configuration for the picker component when you are on a phone.
*/
defaultPhonePickerConfig: null,
/**
* @cfg {Object} defaultTabletPickerConfig
* The default configuration for the picker component when you are on a tablet.
*/
defaultTabletPickerConfig: null,
/**
* @cfg
* @inheritdoc
*/
name: 'picker'
},
// @private
initialize: function() {
var me = this,
component = me.getComponent();
me.callParent();
component.on({
scope: me,
masktap: 'onMaskTap'
});
if (Ext.os.is.Android2) {
component.input.dom.disabled = true;
}
},
/**
* @private
*/
updateDefaultPhonePickerConfig: function(newConfig) {
var picker = this.picker;
if (picker) {
picker.setConfig(newConfig);
}
},
/**
* @private
*/
updateDefaultTabletPickerConfig: function(newConfig) {
var listPanel = this.listPanel;
if (listPanel) {
listPanel.setConfig(newConfig);
}
},
/**
* @private
* Checks if the value is `auto`. If it is, it only uses the picker if the current device type
* is a phone.
*/
applyUsePicker: function(usePicker) {
if (usePicker == "auto") {
usePicker = (Ext.os.deviceType == 'Phone');
}
return Boolean(usePicker);
},
syncEmptyCls: Ext.emptyFn,
/**
* @private
*/
applyValue: function(value) {
var record = value,
index, store;
//we call this so that the options configruation gets intiailized, so that a store exists, and we can
//find the correct value
this.getOptions();
store = this.getStore();
if ((value != undefined && !value.isModel) && store) {
index = store.find(this.getValueField(), value, null, null, null, true);
if (index == -1) {
index = store.find(this.getDisplayField(), value, null, null, null, true);
}
record = store.getAt(index);
}
return record;
},
updateValue: function(newValue, oldValue) {
this.record = newValue;
this.callParent([(newValue && newValue.isModel) ? newValue.get(this.getDisplayField()) : '']);
},
getValue: function() {
var record = this.record;
return (record && record.isModel) ? record.get(this.getValueField()) : null;
},
/**
* Returns the current selected {@link Ext.data.Model record} instance selected in this field.
* @return {Ext.data.Model} the record.
*/
getRecord: function() {
return this.record;
},
// @private
getPhonePicker: function() {
var config = this.getDefaultPhonePickerConfig();
if (!this.picker) {
this.picker = Ext.create('Ext.picker.Picker', Ext.apply({
slots: [{
align : 'center',
name : this.getName(),
valueField : this.getValueField(),
displayField: this.getDisplayField(),
value : this.getValue(),
store : this.getStore()
}],
listeners: {
change: this.onPickerChange,
scope: this
}
}, config));
}
return this.picker;
},
// @private
getTabletPicker: function() {
var config = this.getDefaultTabletPickerConfig();
if (!this.listPanel) {
this.listPanel = Ext.create('Ext.Panel', Ext.apply({
left: 0,
top: 0,
modal: true,
cls: Ext.baseCSSPrefix + 'select-overlay',
layout: 'fit',
hideOnMaskTap: true,
width: Ext.os.is.Phone ? '14em' : '18em',
height: Ext.os.is.Phone ? '12.5em' : '22em',
items: {
xtype: 'list',
store: this.getStore(),
itemTpl: '<span class="x-list-label">{' + this.getDisplayField() + ':htmlEncode}</span>',
listeners: {
select : this.onListSelect,
itemtap: this.onListTap,
scope : this
}
}
}, config));
}
return this.listPanel;
},
// @private
onMaskTap: function() {
if (this.getDisabled()) {
return false;
}
this.onFocus();
return false;
},
/**
* Shows the picker for the select field, whether that is a {@link Ext.picker.Picker} or a simple
* {@link Ext.List list}.
*/
showPicker: function() {
var store = this.getStore();
//check if the store is empty, if it is, return
if (!store || store.getCount() === 0) {
return;
}
if (this.getReadOnly()) {
return;
}
this.isFocused = true;
if (this.getUsePicker()) {
var picker = this.getPhonePicker(),
name = this.getName(),
value = {};
value[name] = this.getValue();
picker.setValue(value);
if (!picker.getParent()) {
Ext.Viewport.add(picker);
}
picker.show();
} else {
var listPanel = this.getTabletPicker(),
list = listPanel.down('list'),
index, record;
store = list.getStore();
index = store.find(this.getValueField(), this.getValue(), null, null, null, true);
record = store.getAt((index == -1) ? 0 : index);
if (!listPanel.getParent()) {
Ext.Viewport.add(listPanel);
}
listPanel.showBy(this.getComponent());
list.select(record, null, true);
}
},
// @private
onListSelect: function(item, record) {
var me = this;
if (record) {
me.setValue(record);
}
},
onListTap: function() {
this.listPanel.hide({
type : 'fade',
out : true,
scope: this
});
},
// @private
onPickerChange: function(picker, value) {
var me = this,
newValue = value[me.getName()],
store = me.getStore(),
index = store.find(me.getValueField(), newValue, null, null, null, true),
record = store.getAt(index);
me.setValue(record);
},
onChange: function(component, newValue, oldValue) {
var me = this,
store = me.getStore(),
index = (store) ? store.find(me.getDisplayField(), oldValue) : -1,
valueField = me.getValueField(),
record = (store) ? store.getAt(index) : null;
oldValue = (record) ? record.get(valueField) : null;
me.fireEvent('change', me, me.getValue(), oldValue);
},
/**
* Updates the underlying `<options>` list with new values.
* @param {Array} options An array of options configurations to insert or append.
*
* selectBox.setOptions([
* {text: 'First Option', value: 'first'},
* {text: 'Second Option', value: 'second'},
* {text: 'Third Option', value: 'third'}
* ]).setValue('third');
*
* __Note:__ option object member names should correspond with defined {@link #valueField valueField} and
* {@link #displayField displayField} values.
* @return {Ext.field.Select} this
*/
updateOptions: function(newOptions) {
var store = this.getStore();
if (!store) {
this.setStore(true);
store = this._store;
}
if (!newOptions) {
store.clearData();
}
else {
store.setData(newOptions);
this.onStoreDataChanged(store);
}
return this;
},
applyStore: function(store) {
if (store === true) {
store = Ext.create('Ext.data.Store', {
fields: [this.getValueField(), this.getDisplayField()],
autoDestroy: true
});
}
if (store) {
store = Ext.data.StoreManager.lookup(store);
store.on({
scope: this,
addrecords: 'onStoreDataChanged',
removerecords: 'onStoreDataChanged',
updaterecord: 'onStoreDataChanged',
refresh: 'onStoreDataChanged'
});
}
return store;
},
updateStore: function(newStore) {
if (newStore) {
this.onStoreDataChanged(newStore);
}
if (this.getUsePicker() && this.picker) {
this.picker.down('pickerslot').setStore(newStore);
} else if (this.listPanel) {
this.listPanel.down('dataview').setStore(newStore);
}
},
/**
* Called when the internal {@link #store}'s data has changed.
*/
onStoreDataChanged: function(store) {
var initialConfig = this.getInitialConfig(),
value = this.getValue();
if (value || value == 0) {
this.updateValue(this.applyValue(value));
}
if (this.getValue() === null) {
if (initialConfig.hasOwnProperty('value')) {
this.setValue(initialConfig.value);
}
if (this.getValue() === null && this.getAutoSelect()) {
if (store.getCount() > 0) {
this.setValue(store.getAt(0));
}
}
}
},
/**
* @private
*/
doSetDisabled: function(disabled) {
Ext.Component.prototype.doSetDisabled.apply(this, arguments);
},
/**
* @private
*/
setDisabled: function() {
Ext.Component.prototype.setDisabled.apply(this, arguments);
},
/**
* Resets the Select field to the value of the first record in the store.
* @return {Ext.field.Select} this
* @chainable
*/
reset: function() {
var store = this.getStore(),
record = (this.originalValue) ? this.originalValue : store.getAt(0);
if (store && record) {
this.setValue(record);
}
return this;
},
onFocus: function(e) {
var component = this.getComponent();
this.fireEvent('focus', this, e);
if (Ext.os.is.Android4) {
component.input.dom.focus();
}
component.input.dom.blur();
this.isFocused = true;
this.showPicker();
},
destroy: function () {
this.callParent(arguments);
var store = this.getStore();
if (store && store.getAutoDestroy()) {
Ext.destroy(store);
}
}
});
/**
* @private
* Utility class used by Ext.slider.Slider - should never need to be used directly.
*/
Ext.define('Ext.slider.Thumb', {
extend: 'Ext.Component',
xtype : 'thumb',
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'thumb',
/**
* @cfg
* @inheritdoc
*/
draggable: {
direction: 'horizontal'
}
},
elementWidth: 0,
initialize: function() {
this.callParent();
this.getDraggable().onBefore({
dragstart: 'onDragStart',
drag: 'onDrag',
dragend: 'onDragEnd',
scope: this
});
this.element.on('resize', 'onElementResize', this);
},
onDragStart: function() {
if (this.isDisabled()) {
return false;
}
this.relayEvent(arguments);
},
onDrag: function() {
if (this.isDisabled()) {
return false;
}
this.relayEvent(arguments);
},
onDragEnd: function() {
if (this.isDisabled()) {
return false;
}
this.relayEvent(arguments);
},
onElementResize: function(element, info) {
this.elementWidth = info.width;
},
getElementWidth: function() {
return this.elementWidth;
}
});
/**
* Utility class used by Ext.field.Slider.
* @private
*/
Ext.define('Ext.slider.Slider', {
extend: 'Ext.Container',
xtype: 'slider',
requires: [
'Ext.slider.Thumb',
'Ext.fx.easing.EaseOut'
],
/**
* @event change
* Fires when the value changes
* @param {Ext.slider.Slider} this
* @param {Ext.slider.Thumb} thumb The thumb being changed
* @param {Number} newValue The new value
* @param {Number} oldValue The old value
*/
/**
* @event dragstart
* Fires when the slider thumb starts a drag
* @param {Ext.slider.Slider} this
* @param {Ext.slider.Thumb} thumb The thumb being dragged
* @param {Array} value The start value
* @param {Ext.EventObject} e
*/
/**
* @event drag
* Fires when the slider thumb starts a drag
* @param {Ext.slider.Slider} this
* @param {Ext.slider.Thumb} thumb The thumb being dragged
* @param {Ext.EventObject} e
*/
/**
* @event dragend
* Fires when the slider thumb starts a drag
* @param {Ext.slider.Slider} this
* @param {Ext.slider.Thumb} thumb The thumb being dragged
* @param {Array} value The end value
* @param {Ext.EventObject} e
*/
config: {
baseCls: 'x-slider',
/**
* @cfg {Object} thumbConfig The config object to factory {@link Ext.slider.Thumb} instances
* @accessor
*/
thumbConfig: {
draggable: {
translatable: {
easingX: {
duration: 300,
type: 'ease-out'
}
}
}
},
/**
* @cfg {Number} increment The increment by which to snap each thumb when its value changes. Any thumb movement
* will be snapped to the nearest value that is a multiple of the increment (e.g. if increment is 10 and the user
* tries to move the thumb to 67, it will be snapped to 70 instead)
* @accessor
*/
increment : 1,
/**
* @cfg {Number/Number[]} value The value(s) of this slider's thumbs. If you pass
* a number, it will assume you have just 1 thumb.
* @accessor
*/
value: 0,
/**
* @cfg {Number} minValue The lowest value any thumb on this slider can be set to.
* @accessor
*/
minValue: 0,
/**
* @cfg {Number} maxValue The highest value any thumb on this slider can be set to.
* @accessor
*/
maxValue: 100,
/**
* @cfg {Boolean} allowThumbsOverlapping Whether or not to allow multiple thumbs to overlap each other.
* Setting this to true guarantees the ability to select every possible value in between {@link #minValue}
* and {@link #maxValue} that satisfies {@link #increment}
* @accessor
*/
allowThumbsOverlapping: false,
/**
* @cfg {Boolean/Object} animation
* The animation to use when moving the slider. Possible properties are:
*
* - duration
* - easingX
* - easingY
*
* @accessor
*/
animation: true,
/**
* Will make this field read only, meaning it cannot be changed with used interaction.
* @cfg {Boolean} readOnly
* @accessor
*/
readOnly: false
},
/**
* @cfg {Number/Number[]} values Alias to {@link #value}
*/
elementWidth: 0,
offsetValueRatio: 0,
activeThumb: null,
constructor: function(config) {
config = config || {};
if (config.hasOwnProperty('values')) {
config.value = config.values;
}
this.callParent([config]);
},
// @private
initialize: function() {
var element = this.element;
this.callParent();
element.on({
scope: this,
tap: 'onTap',
resize: 'onResize'
});
this.on({
scope: this,
delegate: '> thumb',
dragstart: 'onThumbDragStart',
drag: 'onThumbDrag',
dragend: 'onThumbDragEnd'
});
},
/**
* @private
*/
factoryThumb: function() {
return Ext.factory(this.getThumbConfig(), Ext.slider.Thumb);
},
/**
* Returns the Thumb instances bound to this Slider
* @return {Ext.slider.Thumb[]} The thumb instances
*/
getThumbs: function() {
return this.innerItems;
},
/**
* Returns the Thumb instance bound to this Slider
* @param {Number} [index=0] The index of Thumb to return.
* @return {Ext.slider.Thumb} The thumb instance
*/
getThumb: function(index) {
if (typeof index != 'number') {
index = 0;
}
return this.innerItems[index];
},
refreshOffsetValueRatio: function() {
var valueRange = this.getMaxValue() - this.getMinValue(),
trackWidth = this.elementWidth - this.thumbWidth;
this.offsetValueRatio = trackWidth / valueRange;
},
onResize: function(element, info) {
var thumb = this.getThumb(0);
if (thumb) {
this.thumbWidth = thumb.getElementWidth();
}
this.elementWidth = info.width;
this.refresh();
},
refresh: function() {
this.refreshValue();
},
setActiveThumb: function(thumb) {
var oldActiveThumb = this.activeThumb;
if (oldActiveThumb && oldActiveThumb !== thumb) {
oldActiveThumb.setZIndex(null);
}
this.activeThumb = thumb;
thumb.setZIndex(2);
return this;
},
onThumbDragStart: function(thumb, e) {
if (e.absDeltaX <= e.absDeltaY || this.getReadOnly()) {
return false;
}
else {
e.stopPropagation();
}
if (this.getAllowThumbsOverlapping()) {
this.setActiveThumb(thumb);
}
this.dragStartValue = this.getValue()[this.getThumbIndex(thumb)];
this.fireEvent('dragstart', this, thumb, this.dragStartValue, e);
},
onThumbDrag: function(thumb, e, offsetX) {
var index = this.getThumbIndex(thumb),
offsetValueRatio = this.offsetValueRatio,
constrainedValue = this.constrainValue(this.getMinValue() + offsetX / offsetValueRatio);
e.stopPropagation();
this.setIndexValue(index, constrainedValue);
this.fireEvent('drag', this, thumb, this.getValue(), e);
return false;
},
setIndexValue: function(index, value, animation) {
var thumb = this.getThumb(index),
values = this.getValue(),
offsetValueRatio = this.offsetValueRatio,
draggable = thumb.getDraggable();
draggable.setOffset((value - this.getMinValue()) * offsetValueRatio, null, animation);
values[index] = value;
},
onThumbDragEnd: function(thumb, e) {
this.refreshThumbConstraints(thumb);
var index = this.getThumbIndex(thumb),
newValue = this.getValue()[index],
oldValue = this.dragStartValue;
this.fireEvent('dragend', this, thumb, this.getValue(), e);
if (oldValue !== newValue) {
this.fireEvent('change', this, thumb, newValue, oldValue);
}
},
getThumbIndex: function(thumb) {
return this.getThumbs().indexOf(thumb);
},
refreshThumbConstraints: function(thumb) {
var allowThumbsOverlapping = this.getAllowThumbsOverlapping(),
offsetX = thumb.getDraggable().getOffset().x,
thumbs = this.getThumbs(),
index = this.getThumbIndex(thumb),
previousThumb = thumbs[index - 1],
nextThumb = thumbs[index + 1],
thumbWidth = this.thumbWidth;
if (previousThumb) {
previousThumb.getDraggable().addExtraConstraint({
max: {
x: offsetX - ((allowThumbsOverlapping) ? 0 : thumbWidth)
}
});
}
if (nextThumb) {
nextThumb.getDraggable().addExtraConstraint({
min: {
x: offsetX + ((allowThumbsOverlapping) ? 0 : thumbWidth)
}
});
}
},
// @private
onTap: function(e) {
if (this.isDisabled()) {
return;
}
var targetElement = Ext.get(e.target);
if (!targetElement || targetElement.hasCls('x-thumb')) {
return;
}
var touchPointX = e.touch.point.x,
element = this.element,
elementX = element.getX(),
offset = touchPointX - elementX - (this.thumbWidth / 2),
value = this.constrainValue(this.getMinValue() + offset / this.offsetValueRatio),
values = this.getValue(),
minDistance = Infinity,
ln = values.length,
i, absDistance, testValue, closestIndex, oldValue, thumb;
if (ln === 1) {
closestIndex = 0;
}
else {
for (i = 0; i < ln; i++) {
testValue = values[i];
absDistance = Math.abs(testValue - value);
if (absDistance < minDistance) {
minDistance = absDistance;
closestIndex = i;
}
}
}
oldValue = values[closestIndex];
thumb = this.getThumb(closestIndex);
this.setIndexValue(closestIndex, value, this.getAnimation());
this.refreshThumbConstraints(thumb);
if (oldValue !== value) {
this.fireEvent('change', this, thumb, value, oldValue);
}
},
// @private
updateThumbs: function(newThumbs) {
this.add(newThumbs);
},
applyValue: function(value) {
var values = Ext.Array.from(value || 0),
filteredValues = [],
previousFilteredValue = this.getMinValue(),
filteredValue, i, ln;
for (i = 0,ln = values.length; i < ln; i++) {
filteredValue = this.constrainValue(values[i]);
if (filteredValue < previousFilteredValue) {
//<debug warn>
Ext.Logger.warn("Invalid values of '"+Ext.encode(values)+"', values at smaller indexes must " +
"be smaller than or equal to values at greater indexes");
//</debug>
filteredValue = previousFilteredValue;
}
filteredValues.push(filteredValue);
previousFilteredValue = filteredValue;
}
return filteredValues;
},
/**
* Updates the sliders thumbs with their new value(s)
*/
updateValue: function(newValue, oldValue) {
var thumbs = this.getThumbs(),
ln = newValue.length,
minValue = this.getMinValue(),
offset = this.offsetValueRatio,
i;
this.setThumbsCount(ln);
for (i = 0; i < ln; i++) {
thumbs[i].getDraggable().setExtraConstraint(null).setOffset((newValue[i] - minValue) * offset);
}
for (i = 0; i < ln; i++) {
this.refreshThumbConstraints(thumbs[i]);
}
},
/**
* @private
*/
refreshValue: function() {
this.refreshOffsetValueRatio();
this.setValue(this.getValue());
},
/**
* @private
* Takes a desired value of a thumb and returns the nearest snap value. e.g if minValue = 0, maxValue = 100, increment = 10 and we
* pass a value of 67 here, the returned value will be 70. The returned number is constrained within {@link #minValue} and {@link #maxValue},
* so in the above example 68 would be returned if {@link #maxValue} was set to 68.
* @param {Number} value The value to snap
* @return {Number} The snapped value
*/
constrainValue: function(value) {
var me = this,
minValue = me.getMinValue(),
maxValue = me.getMaxValue(),
increment = me.getIncrement(),
remainder;
value = parseFloat(value);
if (isNaN(value)) {
value = minValue;
}
remainder = (value - minValue) % increment;
value -= remainder;
if (Math.abs(remainder) >= (increment / 2)) {
value += (remainder > 0) ? increment : -increment;
}
value = Math.max(minValue, value);
value = Math.min(maxValue, value);
return value;
},
setThumbsCount: function(count) {
var thumbs = this.getThumbs(),
thumbsCount = thumbs.length,
i, ln, thumb;
if (thumbsCount > count) {
for (i = 0,ln = thumbsCount - count; i < ln; i++) {
thumb = thumbs[thumbs.length - 1];
thumb.destroy();
}
}
else if (thumbsCount < count) {
for (i = 0,ln = count - thumbsCount; i < ln; i++) {
this.add(this.factoryThumb());
}
}
return this;
},
/**
* Convenience method. Calls {@link #setValue}.
*/
setValues: function(value) {
this.setValue(value);
},
/**
* Convenience method. Calls {@link #getValue}.
* @return {Object}
*/
getValues: function() {
return this.getValue();
},
/**
* Sets the {@link #increment} configuration.
* @param {Number} increment
* @return {Number}
*/
applyIncrement: function(increment) {
if (increment === 0) {
increment = 1;
}
return Math.abs(increment);
},
// @private
updateAllowThumbsOverlapping: function(newValue, oldValue) {
if (typeof oldValue != 'undefined') {
this.refreshValue();
}
},
// @private
updateMinValue: function(newValue, oldValue) {
if (typeof oldValue != 'undefined') {
this.refreshValue();
}
},
// @private
updateMaxValue: function(newValue, oldValue) {
if (typeof oldValue != 'undefined') {
this.refreshValue();
}
},
// @private
updateIncrement: function(newValue, oldValue) {
if (typeof oldValue != 'undefined') {
this.refreshValue();
}
},
doSetDisabled: function(disabled) {
this.callParent(arguments);
var items = this.getItems().items,
ln = items.length,
i;
for (i = 0; i < ln; i++) {
items[i].setDisabled(disabled);
}
}
}, function() {
});
/**
* @aside guide forms
*
* The slider is a way to allow the user to select a value from a given numerical range. You might use it for choosing
* a percentage, combine two of them to get min and max values, or use three of them to specify the hex values for a
* color. Each slider contains a single 'thumb' that can be dragged along the slider's length to change the value.
* Sliders are equally useful inside {@link Ext.form.Panel forms} and standalone. Here's how to quickly create a
* slider in form, in this case enabling a user to choose a percentage:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'sliderfield',
* label: 'Percentage',
* value: 50,
* minValue: 0,
* maxValue: 100
* }
* ]
* });
*
* In this case we set a starting value of 50%, and defined the min and max values to be 0 and 100 respectively, giving
* us a percentage slider. Because this is such a common use case, the defaults for {@link #minValue} and
* {@link #maxValue} are already set to 0 and 100 so in the example above they could be removed.
*
* It's often useful to render sliders outside the context of a form panel too. In this example we create a slider that
* allows a user to choose the waist measurement of a pair of jeans. Let's say the online store we're making this for
* sells jeans with waist sizes from 24 inches to 60 inches in 2 inch increments - here's how we might achieve that:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'sliderfield',
* label: 'Waist Measurement',
* minValue: 24,
* maxValue: 60,
* increment: 2,
* value: 32
* }
* ]
* });
*
* Now that we've got our slider, we can ask it what value it currently has and listen to events that it fires. For
* example, if we wanted our app to show different images for different sizes, we can listen to the {@link #change}
* event to be informed whenever the slider is moved:
*
* slider.on('change', function(field, newValue) {
* if (newValue[0] > 40) {
* imgComponent.setSrc('large.png');
* } else {
* imgComponent.setSrc('small.png');
* }
* }, this);
*
* Here we listened to the {@link #change} event on the slider and updated the background image of an
* {@link Ext.Img image component} based on what size the user selected. Of course, you can use any logic inside your
* event listener.
*/
Ext.define('Ext.field.Slider', {
extend : 'Ext.field.Field',
xtype : 'sliderfield',
requires: ['Ext.slider.Slider'],
alternateClassName: 'Ext.form.Slider',
/**
* @event change
* Fires when an option selection has changed.
* @param {Ext.field.Slider} me
* @param {Ext.slider.Slider} sl Slider Component.
* @param {Ext.slider.Thumb} thumb
* @param {Number} newValue The new value of this thumb.
* @param {Number} oldValue The old value of this thumb.
*/
/**
* @event dragstart
* Fires when the slider thumb starts a drag operation.
* @param {Ext.field.Slider} this
* @param {Ext.slider.Slider} sl Slider Component.
* @param {Ext.slider.Thumb} thumb The thumb being dragged.
* @param {Array} value The start value.
* @param {Ext.EventObject} e
*/
/**
* @event drag
* Fires when the slider thumb starts a drag operation.
* @param {Ext.field.Slider} this
* @param {Ext.slider.Slider} sl Slider Component.
* @param {Ext.slider.Thumb} thumb The thumb being dragged.
* @param {Ext.EventObject} e
*/
/**
* @event dragend
* Fires when the slider thumb ends a drag operation.
* @param {Ext.field.Slider} this
* @param {Ext.slider.Slider} sl Slider Component.
* @param {Ext.slider.Thumb} thumb The thumb being dragged.
* @param {Array} value The end value.
* @param {Ext.EventObject} e
*/
config: {
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'slider-field',
/**
* @cfg
* @inheritdoc
*/
tabIndex: -1,
/**
* Will make this field read only, meaning it cannot be changed with used interaction.
* @cfg {Boolean} readOnly
* @accessor
*/
readOnly: false
},
proxyConfig: {
/**
* @inheritdoc Ext.slider.Slider#increment
* @cfg {Number} increment
* @accessor
*/
increment : 1,
/**
* @inheritdoc Ext.slider.Slider#value
* @cfg {Number/Number[]} value
* @accessor
*/
value: 0,
/**
* @inheritdoc Ext.slider.Slider#minValue
* @cfg {Number} minValue
* @accessor
*/
minValue: 0,
/**
* @inheritdoc Ext.slider.Slider#maxValue
* @cfg {Number} maxValue
* @accessor
*/
maxValue: 100
},
/**
* @inheritdoc Ext.slider.Slider#values
* @cfg {Number/Number[]} values
*/
constructor: function(config) {
config = config || {};
if (config.hasOwnProperty('values')) {
config.value = config.values;
}
this.callParent([config]);
},
// @private
initialize: function() {
this.callParent();
this.getComponent().on({
scope: this,
change: 'onSliderChange',
dragstart: 'onSliderDragStart',
drag: 'onSliderDrag',
dragend: 'onSliderDragEnd'
});
},
// @private
applyComponent: function(config) {
return Ext.factory(config, Ext.slider.Slider);
},
onSliderChange: function() {
this.fireEvent.apply(this, [].concat('change', this, Array.prototype.slice.call(arguments)));
},
onSliderDragStart: function() {
this.fireEvent.apply(this, [].concat('dragstart', this, Array.prototype.slice.call(arguments)));
},
onSliderDrag: function() {
this.fireEvent.apply(this, [].concat('drag', this, Array.prototype.slice.call(arguments)));
},
onSliderDragEnd: function() {
this.fireEvent.apply(this, [].concat('dragend', this, Array.prototype.slice.call(arguments)));
},
/**
* Convenience method. Calls {@link #setValue}.
* @param {Object} value
*/
setValues: function(value) {
this.setValue(value);
},
/**
* Convenience method. Calls {@link #getValue}
* @return {Object}
*/
getValues: function() {
return this.getValue();
},
reset: function() {
var config = this.config,
initialValue = (this.config.hasOwnProperty('values')) ? config.values : config.value;
this.setValue(initialValue);
},
doSetDisabled: function(disabled) {
this.callParent(arguments);
this.getComponent().setDisabled(disabled);
},
updateReadOnly: function(newValue) {
this.getComponent().setReadOnly(newValue);
},
isDirty : function () {
if (this.getDisabled()) {
return false;
}
return this.getValue() !== this.originalValue;
}
});
/**
* A wrapper class which can be applied to any element. Fires a "tap" event while
* touching the device. The interval between firings may be specified in the config but
* defaults to 20 milliseconds.
*/
Ext.define('Ext.util.TapRepeater', {
requires: ['Ext.DateExtras'],
mixins: {
observable: 'Ext.mixin.Observable'
},
/**
* @event touchstart
* Fires when the touch is started.
* @param {Ext.util.TapRepeater} this
* @param {Ext.event.Event} e
*/
/**
* @event tap
* Fires on a specified interval during the time the element is pressed.
* @param {Ext.util.TapRepeater} this
* @param {Ext.event.Event} e
*/
/**
* @event touchend
* Fires when the touch is ended.
* @param {Ext.util.TapRepeater} this
* @param {Ext.event.Event} e
*/
config: {
el: null,
accelerate: true,
interval: 10,
delay: 250,
preventDefault: true,
stopDefault: false,
timer: 0,
pressCls: null
},
/**
* Creates new TapRepeater.
* @param {Mixed} el The element to listen on
* @param {Object} config
*/
constructor: function(config) {
var me = this;
//<debug warn>
for (var configName in config) {
if (me.self.prototype.config && !(configName in me.self.prototype.config)) {
me[configName] = config[configName];
Ext.Logger.warn('Applied config as instance property: "' + configName + '"', me);
}
}
//</debug>
me.initConfig(config);
},
updateEl: function(newEl, oldEl) {
var eventCfg = {
touchstart: 'onTouchStart',
touchend: 'onTouchEnd',
tap: 'eventOptions',
scope: this
};
if (oldEl) {
oldEl.un(eventCfg)
}
newEl.on(eventCfg);
},
// @private
eventOptions: function(e) {
if (this.getPreventDefault()) {
e.preventDefault();
}
if (this.getStopDefault()) {
e.stopEvent();
}
},
// @private
destroy: function() {
this.clearListeners();
Ext.destroy(this.el);
},
// @private
onTouchStart: function(e) {
var me = this,
pressCls = me.getPressCls();
clearTimeout(me.getTimer());
if (pressCls) {
me.getEl().addCls(pressCls);
}
me.tapStartTime = new Date();
me.fireEvent('touchstart', me, e);
me.fireEvent('tap', me, e);
// Do not honor delay or interval if acceleration wanted.
if (me.getAccelerate()) {
me.delay = 400;
}
me.setTimer(Ext.defer(me.tap, me.getDelay() || me.getInterval(), me, [e]));
},
// @private
tap: function(e) {
var me = this;
me.fireEvent('tap', me, e);
me.setTimer(Ext.defer(me.tap, me.getAccelerate() ? me.easeOutExpo(Ext.Date.getElapsed(me.tapStartTime),
400,
-390,
12000) : me.getInterval(), me, [e]));
},
// Easing calculation
// @private
easeOutExpo: function(t, b, c, d) {
return (t == d) ? b + c : c * ( - Math.pow(2, -10 * t / d) + 1) + b;
},
// @private
onTouchEnd: function(e) {
var me = this;
clearTimeout(me.getTimer());
me.getEl().removeCls(me.getPressCls());
me.fireEvent('touchend', me, e);
}
});
/**
* @aside guide forms
*
* Wraps an HTML5 number field. Example usage:
*
* @example miniphone
* var spinner = Ext.create('Ext.field.Spinner', {
* label: 'Spinner Field',
* minValue: 0,
* maxValue: 100,
* increment: 2,
* cycle: true
* });
* Ext.Viewport.add({ xtype: 'container', items: [spinner] });
*
*/
Ext.define('Ext.field.Spinner', {
extend: 'Ext.field.Number',
xtype: 'spinnerfield',
alternateClassName: 'Ext.form.Spinner',
requires: ['Ext.util.TapRepeater'],
/**
* @event spin
* Fires when the value is changed via either spinner buttons.
* @param {Ext.field.Spinner} this
* @param {Number} value
* @param {String} direction 'up' or 'down'.
*/
/**
* @event spindown
* Fires when the value is changed via the spinner down button.
* @param {Ext.field.Spinner} this
* @param {Number} value
*/
/**
* @event spinup
* Fires when the value is changed via the spinner up button.
* @param {Ext.field.Spinner} this
* @param {Number} value
*/
/**
* @event change
* Fires just before the field blurs if the field value has changed.
* @param {Ext.field.Text} this This field.
* @param {Number} newValue The new value.
* @param {Number} oldValue The original value.
*/
/**
* @event updatedata
* @hide
*/
/**
* @event action
* @hide
*/
config: {
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'spinner',
/**
* @cfg {Number} [minValue=-infinity] The minimum allowed value.
* @accessor
*/
minValue: Number.NEGATIVE_INFINITY,
/**
* @cfg {Number} [maxValue=infinity] The maximum allowed value.
* @accessor
*/
maxValue: Number.MAX_VALUE,
/**
* @cfg {Number} stepValue Value that is added or subtracted from the current value when a spinner is used.
* @accessor
*/
stepValue: 0.1,
/**
* @cfg {Boolean} accelerateOnTapHold True if autorepeating should start slowly and accelerate.
* @accessor
*/
accelerateOnTapHold: true,
/**
* @cfg {Boolean} cycle When set to `true`, it will loop the values of a minimum or maximum is reached.
* If the maximum value is reached, the value will be set to the minimum.
* @accessor
*/
cycle: false,
/**
* @cfg {Boolean} clearIcon
* @hide
* @accessor
*/
clearIcon: false,
/**
* @cfg {Number} defaultValue The default value for this field when no value has been set.
* It is also used when the value is set to `NaN`.
*/
defaultValue: 0,
/**
* @cfg {Number} tabIndex
* @hide
*/
tabIndex: -1,
/**
* @cfg {Boolean} groupButtons
* `true` if you want to group the buttons to the right of the fields. `false` if you want the buttons
* to be at either side of the field.
*/
groupButtons: true,
/**
* @cfg
* @inheritdoc
*/
component: {
disabled: true
}
},
constructor: function() {
var me = this;
me.callParent(arguments);
if (!me.getValue()) {
me.suspendEvents();
me.setValue(me.getDefaultValue());
me.resumeEvents();
}
},
syncEmptyCls: Ext.emptyFn,
/**
* Updates the {@link #component} configuration
*/
updateComponent: function(newComponent) {
this.callParent(arguments);
var innerElement = this.innerElement,
cls = this.getCls();
if (newComponent) {
this.spinDownButton = Ext.Element.create({
cls : cls + '-button ' + cls + '-button-down',
html: '-'
});
this.spinUpButton = Ext.Element.create({
cls : cls + '-button ' + cls + '-button-up',
html: '+'
});
this.downRepeater = this.createRepeater(this.spinDownButton, this.onSpinDown);
this.upRepeater = this.createRepeater(this.spinUpButton, this.onSpinUp);
}
},
updateGroupButtons: function(newGroupButtons, oldGroupButtons) {
var me = this,
innerElement = me.innerElement,
cls = me.getBaseCls() + '-grouped-buttons';
me.getComponent();
if (newGroupButtons != oldGroupButtons) {
if (newGroupButtons) {
this.addCls(cls);
innerElement.appendChild(me.spinDownButton);
innerElement.appendChild(me.spinUpButton);
} else {
this.removeCls(cls);
innerElement.insertFirst(me.spinDownButton);
innerElement.appendChild(me.spinUpButton);
}
}
},
applyValue: function(value) {
value = parseFloat(value);
if (isNaN(value) || value === null) {
value = this.getDefaultValue();
}
//round the value to 1 decimal
value = Math.round(value * 10) / 10;
return this.callParent([value]);
},
// @private
createRepeater: function(el, fn) {
var me = this,
repeater = Ext.create('Ext.util.TapRepeater', {
el: el,
accelerate: me.getAccelerateOnTapHold()
});
repeater.on({
tap: fn,
touchstart: 'onTouchStart',
touchend: 'onTouchEnd',
scope: me
});
return repeater;
},
// @private
onSpinDown: function() {
if (!this.getDisabled() && !this.getReadOnly()) {
this.spin(true);
}
},
// @private
onSpinUp: function() {
if (!this.getDisabled() && !this.getReadOnly()) {
this.spin(false);
}
},
// @private
onTouchStart: function(repeater) {
if (!this.getDisabled() && !this.getReadOnly()) {
repeater.getEl().addCls(Ext.baseCSSPrefix + 'button-pressed');
}
},
// @private
onTouchEnd: function(repeater) {
repeater.getEl().removeCls(Ext.baseCSSPrefix + 'button-pressed');
},
// @private
spin: function(down) {
var me = this,
originalValue = me.getValue(),
stepValue = me.getStepValue(),
direction = down ? 'down' : 'up',
minValue = me.getMinValue(),
maxValue = me.getMaxValue(),
value;
if (down) {
value = originalValue - stepValue;
}
else {
value = originalValue + stepValue;
}
//if cycle is true, then we need to check fi the value hasn't changed and we cycle the value
if (me.getCycle()) {
if (originalValue == minValue && value < minValue) {
value = maxValue;
}
if (originalValue == maxValue && value > maxValue) {
value = minValue;
}
}
me.setValue(value);
value = me.getValue();
me.fireEvent('spin', me, value, direction);
me.fireEvent('spin' + direction, me, value);
},
/**
* @private
*/
doSetDisabled: function(disabled) {
Ext.Component.prototype.doSetDisabled.apply(this, arguments);
},
/**
* @private
*/
setDisabled: function() {
Ext.Component.prototype.setDisabled.apply(this, arguments);
},
reset: function() {
this.setValue(this.getDefaultValue());
},
// @private
destroy: function() {
var me = this;
Ext.destroy(me.downRepeater, me.upRepeater, me.spinDownButton, me.spinUpButton);
me.callParent(arguments);
}
}, function() {
});
/**
* @private
*/
Ext.define('Ext.slider.Toggle', {
extend: 'Ext.slider.Slider',
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: 'x-toggle',
/**
* @cfg {String} minValueCls CSS class added to the field when toggled to its minValue
* @accessor
*/
minValueCls: 'x-toggle-off',
/**
* @cfg {String} maxValueCls CSS class added to the field when toggled to its maxValue
* @accessor
*/
maxValueCls: 'x-toggle-on'
},
initialize: function() {
this.callParent();
this.on({
change: 'onChange'
});
},
applyMinValue: function() {
return 0;
},
applyMaxValue: function() {
return 1;
},
applyIncrement: function() {
return 1;
},
updateMinValueCls: function(newCls, oldCls) {
var element = this.element;
if (oldCls && element.hasCls(oldCls)) {
element.replaceCls(oldCls, newCls);
}
},
updateMaxValueCls: function(newCls, oldCls) {
var element = this.element;
if (oldCls && element.hasCls(oldCls)) {
element.replaceCls(oldCls, newCls);
}
},
setValue: function(newValue, oldValue) {
this.callParent(arguments);
this.onChange(this, this.getThumbs()[0], newValue, oldValue);
},
onChange: function(me, thumb, newValue, oldValue) {
var isOn = newValue > 0,
onCls = me.getMaxValueCls(),
offCls = me.getMinValueCls();
this.element.addCls(isOn ? onCls : offCls);
this.element.removeCls(isOn ? offCls : onCls);
},
toggle: function() {
var value = this.getValue();
this.setValue((value == 1) ? 0 : 1);
return this;
},
onTap: function() {
if (this.isDisabled() || this.getReadOnly()) {
return;
}
var oldValue = this.getValue(),
newValue = (oldValue == 1) ? 0 : 1,
thumb = this.getThumb(0);
this.setIndexValue(0, newValue, this.getAnimation());
this.refreshThumbConstraints(thumb);
this.fireEvent('change', this, thumb, newValue, oldValue);
}
});
/**
* @aside guide forms
*
* Specialized {@link Ext.field.Slider} with a single thumb which only supports two {@link #value values}.
*
* ## Examples
*
* @example miniphone preview
* Ext.Viewport.add({
* xtype: 'togglefield',
* name: 'awesome',
* label: 'Are you awesome?',
* labelWidth: '40%'
* });
*
* Having a default value of 'toggled':
*
* @example miniphone preview
* Ext.Viewport.add({
* xtype: 'togglefield',
* name: 'awesome',
* value: 1,
* label: 'Are you awesome?',
* labelWidth: '40%'
* });
*
* And using the {@link #value} {@link #toggle} method:
*
* @example miniphone preview
* Ext.Viewport.add([
* {
* xtype: 'togglefield',
* name: 'awesome',
* value: 1,
* label: 'Are you awesome?',
* labelWidth: '40%'
* },
* {
* xtype: 'toolbar',
* docked: 'top',
* items: [
* {
* xtype: 'button',
* text: 'Toggle',
* flex: 1,
* handler: function() {
* Ext.ComponentQuery.query('togglefield')[0].toggle();
* }
* }
* ]
* }
* ]);
*/
Ext.define('Ext.field.Toggle', {
extend: 'Ext.field.Slider',
xtype : 'togglefield',
alternateClassName: 'Ext.form.Toggle',
requires: ['Ext.slider.Toggle'],
config: {
/**
* @cfg
* @inheritdoc
*/
cls: 'x-toggle-field'
},
/**
* @event change
* Fires when an option selection has changed.
*
* Ext.Viewport.add({
* xtype: 'togglefield',
* label: 'Event Example',
* listeners: {
* change: function(field, newValue) {
* console.log('Value of this toggle has changed:', (newValue) ? 'ON' : 'OFF');
* }
* }
* });
*
* @param {Ext.field.Toggle} me
* @param {Number} newValue the new value of this thumb
* @param {Number} oldValue the old value of this thumb
*/
/**
* @event dragstart
* @hide
*/
/**
* @event drag
* @hide
*/
/**
* @event dragend
* @hide
*/
proxyConfig: {
/**
* @cfg {String} minValueCls See {@link Ext.slider.Toggle#minValueCls}
* @accessor
*/
minValueCls: 'x-toggle-off',
/**
* @cfg {String} maxValueCls See {@link Ext.slider.Toggle#maxValueCls}
* @accessor
*/
maxValueCls: 'x-toggle-on'
},
// @private
applyComponent: function(config) {
return Ext.factory(config, Ext.slider.Toggle);
},
/**
* Sets the value of the toggle.
* @param {Number} value **1** for toggled, **0** for untoggled.
* @return {Object} this
*/
setValue: function(newValue) {
if (newValue === true) {
newValue = 1;
}
var oldValue = this.getValue();
if (oldValue != newValue) {
this.getComponent().setValue(newValue);
this.fireEvent('change', this, newValue, oldValue);
}
return this;
},
getValue: function() {
return (this.getComponent().getValue() == 1) ? 1 : 0;
},
/**
* Toggles the value of this toggle field.
* @return {Object} this
*/
toggle: function() {
// We call setValue directly so the change event can be fired
var value = this.getValue();
this.setValue((value == 1) ? 0 : 1);
return this;
}
});
/**
* @aside guide forms
*
* The Url field creates an HTML5 url input and is usually created inside a form. Because it creates an HTML url input
* field, most browsers will show a specialized virtual keyboard for web address input. Aside from that, the url field
* is just a normal text field. Here's an example of how to use it in a form:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'Add Bookmark',
* items: [
* {
* xtype: 'urlfield',
* label: 'Url',
* name: 'url'
* }
* ]
* }
* ]
* });
*
* Or on its own, outside of a form:
*
* Ext.create('Ext.field.Url', {
* label: 'Web address',
* value: 'http://sencha.com'
* });
*
* Because url field inherits from {@link Ext.field.Text textfield} it gains all of the functionality that text fields
* provide, including getting and setting the value at runtime, validations and various events that are fired as the
* user interacts with the component. Check out the {@link Ext.field.Text} docs to see the additional functionality
* available.
*/
Ext.define('Ext.field.Url', {
extend: 'Ext.field.Text',
xtype: 'urlfield',
alternateClassName: 'Ext.form.Url',
config: {
/**
* @cfg
* @inheritdoc
*/
autoCapitalize: false,
/**
* @cfg
* @inheritdoc
*/
component: {
type: 'url'
}
}
});
/**
* @aside guide forms
* @aside example forms
* @aside example forms-toolbars
*
* A FieldSet is a great way to visually separate elements of a form. It's normally used when you have a form with
* fields that can be divided into groups - for example a customer's billing details in one fieldset and their shipping
* address in another. A fieldset can be used inside a form or on its own elsewhere in your app. Fieldsets can
* optionally have a title at the top and instructions at the bottom. Here's how we might create a FieldSet inside a
* form:
*
* @example
* Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'fieldset',
* title: 'About You',
* instructions: 'Tell us all about yourself',
* items: [
* {
* xtype: 'textfield',
* name : 'firstName',
* label: 'First Name'
* },
* {
* xtype: 'textfield',
* name : 'lastName',
* label: 'Last Name'
* }
* ]
* }
* ]
* });
*
* Above we created a {@link Ext.form.Panel form} with a fieldset that contains two text fields. In this case, all
* of the form fields are in the same fieldset, but for longer forms we may choose to use multiple fieldsets. We also
* configured a {@link #title} and {@link #instructions} to give the user more information on filling out the form if
* required.
*/
Ext.define('Ext.form.FieldSet', {
extend : 'Ext.Container',
alias : 'widget.fieldset',
requires: ['Ext.Title'],
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'form-fieldset',
/**
* @cfg {String} title
* Optional fieldset title, rendered just above the grouped fields.
*
* ## Example
*
* Ext.create('Ext.form.Fieldset', {
* fullscreen: true,
*
* title: 'Login',
*
* items: [{
* xtype: 'textfield',
* label: 'Email'
* }]
* });
*
* @accessor
*/
title: null,
/**
* @cfg {String} instructions
* Optional fieldset instructions, rendered just below the grouped fields.
*
* ## Example
*
* Ext.create('Ext.form.Fieldset', {
* fullscreen: true,
*
* instructions: 'Please enter your email address.',
*
* items: [{
* xtype: 'textfield',
* label: 'Email'
* }]
* });
*
* @accessor
*/
instructions: null
},
// @private
applyTitle: function(title) {
if (typeof title == 'string') {
title = {title: title};
}
Ext.applyIf(title, {
docked : 'top',
baseCls: this.getBaseCls() + '-title'
});
return Ext.factory(title, Ext.Title, this._title);
},
// @private
updateTitle: function(newTitle, oldTitle) {
if (newTitle) {
this.add(newTitle);
}
if (oldTitle) {
this.remove(oldTitle);
}
},
// @private
getTitle: function() {
var title = this._title;
if (title && title instanceof Ext.Title) {
return title.getTitle();
}
return title;
},
// @private
applyInstructions: function(instructions) {
if (typeof instructions == 'string') {
instructions = {title: instructions};
}
Ext.applyIf(instructions, {
docked : 'bottom',
baseCls: this.getBaseCls() + '-instructions'
});
return Ext.factory(instructions, Ext.Title, this._instructions);
},
// @private
updateInstructions: function(newInstructions, oldInstructions) {
if (newInstructions) {
this.add(newInstructions);
}
if (oldInstructions) {
this.remove(oldInstructions);
}
},
// @private
getInstructions: function() {
var instructions = this._instructions;
if (instructions && instructions instanceof Ext.Title) {
return instructions.getTitle();
}
return instructions;
},
/**
* A convenient method to disable all fields in this FieldSet
* @return {Ext.form.FieldSet} This FieldSet
*/
doSetDisabled: function(newDisabled) {
this.getFieldsAsArray().forEach(function(field) {
field.setDisabled(newDisabled);
});
return this;
},
/**
* @private
*/
getFieldsAsArray: function() {
var fields = [],
getFieldsFrom = function(item) {
if (item.isField) {
fields.push(item);
}
if (item.isContainer) {
item.getItems().each(getFieldsFrom);
}
};
this.getItems().each(getFieldsFrom);
return fields;
}
});
/**
* The Form panel presents a set of form fields and provides convenient ways to load and save data. Usually a form
* panel just contains the set of fields you want to display, ordered inside the items configuration like this:
*
* @example
* var form = Ext.create('Ext.form.Panel', {
* fullscreen: true,
* items: [
* {
* xtype: 'textfield',
* name: 'name',
* label: 'Name'
* },
* {
* xtype: 'emailfield',
* name: 'email',
* label: 'Email'
* },
* {
* xtype: 'passwordfield',
* name: 'password',
* label: 'Password'
* }
* ]
* });
*
* Here we just created a simple form panel which could be used as a registration form to sign up to your service. We
* added a plain {@link Ext.field.Text text field} for the user's Name, an {@link Ext.field.Email email field} and
* finally a {@link Ext.field.Password password field}. In each case we provided a {@link Ext.field.Field#name name}
* config on the field so that we can identify it later on when we load and save data on the form.
*
* ##Loading data
*
* Using the form we created above, we can load data into it in a few different ways, the easiest is to use
* {@link #setValues}:
*
* form.setValues({
* name: 'Ed',
* email: 'ed@sencha.com',
* password: 'secret'
* });
*
* It's also easy to load {@link Ext.data.Model Model} instances into a form - let's say we have a User model and want
* to load a particular instance into our form:
*
* Ext.define('MyApp.model.User', {
* extend: 'Ext.data.Model',
* config: {
* fields: ['name', 'email', 'password']
* }
* });
*
* var ed = Ext.create('MyApp.model.User', {
* name: 'Ed',
* email: 'ed@sencha.com',
* password: 'secret'
* });
*
* form.setRecord(ed);
*
* ##Retrieving form data
*
* Getting data out of the form panel is simple and is usually achieve via the {@link #getValues} method:
*
* var values = form.getValues();
*
* //values now looks like this:
* {
* name: 'Ed',
* email: 'ed@sencha.com',
* password: 'secret'
* }
*
* It's also possible to listen to the change events on individual fields to get more timely notification of changes
* that the user is making. Here we expand on the example above with the User model, updating the model as soon as
* any of the fields are changed:
*
* var form = Ext.create('Ext.form.Panel', {
* listeners: {
* '> field': {
* change: function(field, newValue, oldValue) {
* ed.set(field.getName(), newValue);
* }
* }
* },
* items: [
* {
* xtype: 'textfield',
* name: 'name',
* label: 'Name'
* },
* {
* xtype: 'emailfield',
* name: 'email',
* label: 'Email'
* },
* {
* xtype: 'passwordfield',
* name: 'password',
* label: 'Password'
* }
* ]
* });
*
* The above used a new capability of Sencha Touch 2.0, which enables you to specify listeners on child components of any
* container. In this case, we attached a listener to the {@link Ext.field.Text#change change} event of each form
* field that is a direct child of the form panel. Our listener gets the name of the field that fired the change event,
* and updates our {@link Ext.data.Model Model} instance with the new value. For example, changing the email field
* in the form will update the Model's email field.
*
* ##Submitting forms
*
* There are a few ways to submit form data. In our example above we have a Model instance that we have updated, giving
* us the option to use the Model's {@link Ext.data.Model#save save} method to persist the changes back to our server,
* without using a traditional form submission. Alternatively, we can send a normal browser form submit using the
* {@link #method} method:
*
* form.submit({
* url: 'url/to/submit/to',
* method: 'POST',
* success: function() {
* alert('form submitted successfully!');
* }
* });
*
* In this case we provided the `url` to submit the form to inside the submit call - alternatively you can just set the
* {@link #url} configuration when you create the form. We can specify other parameters (see {@link #method} for a
* full list), including callback functions for success and failure, which are called depending on whether or not the
* form submission was successful. These functions are usually used to take some action in your app after your data
* has been saved to the server side.
*
* @aside guide forms
* @aside example forms
* @aside example forms-toolbars
*/
Ext.define('Ext.form.Panel', {
alternateClassName: 'Ext.form.FormPanel',
extend : 'Ext.Panel',
xtype : 'formpanel',
requires: ['Ext.XTemplate', 'Ext.field.Checkbox', 'Ext.Ajax'],
/**
* @event submit
* @preventable doSubmit
* Fires upon successful (Ajax-based) form submission.
* @param {Ext.form.Panel} this This FormPanel.
* @param {Object} result The result object as returned by the server.
* @param {Ext.EventObject} e The event object.
*/
/**
* @event beforesubmit
* @preventable doBeforeSubmit
* Fires immediately preceding any Form submit action.
* Implementations may adjust submitted form values or options prior to execution.
* A return value of `false` from this listener will abort the submission
* attempt (regardless of `standardSubmit` configuration).
* @param {Ext.form.Panel} this This FormPanel.
* @param {Object} values A hash collection of the qualified form values about to be submitted.
* @param {Object} options Submission options hash (only available when `standardSubmit` is `false`).
*/
/**
* @event exception
* Fires when either the Ajax HTTP request reports a failure OR the server returns a `success:false`
* response in the result payload.
* @param {Ext.form.Panel} this This FormPanel.
* @param {Object} result Either a failed Ext.data.Connection request object or a failed (logical) server.
* response payload.
*/
config: {
/**
* @cfg {String} baseCls
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'form',
/**
* @cfg {Boolean} standardSubmit
* Whether or not we want to perform a standard form submit.
* @accessor
*/
standardSubmit: false,
/**
* @cfg {String} url
* The default url for submit actions.
* @accessor
*/
url: null,
/**
* @cfg {Object} baseParams
* Optional hash of params to be sent (when `standardSubmit` configuration is `false`) on every submit.
* @accessor
*/
baseParams : null,
/**
* @cfg {Object} submitOnAction
* When this is set to `true`, the form will automatically submit itself whenever the `action`
* event fires on a field in this form. The action event usually fires whenever you press
* go or enter inside a textfield.
* @accessor
*/
submitOnAction: false,
/**
* @cfg {Ext.data.Model} record The model instance of this form. Can by dynamically set at any time.
* @accessor
*/
record: null,
/**
* @cfg {String} method
* The method which this form will be submitted. `post` or `get`.
*/
method: 'post',
/**
* @cfg {Object} scrollable
* @inheritdoc
*/
scrollable: {
translatable: {
translationMethod: 'scrollposition'
}
}
},
getElementConfig: function() {
var config = this.callParent();
config.tag = "form";
return config;
},
// @private
initialize: function() {
var me = this;
me.callParent();
me.element.on({
submit: 'onSubmit',
scope : me
});
},
updateRecord: function(newRecord) {
var fields, values, name;
if (newRecord && (fields = newRecord.fields)) {
values = this.getValues();
for (name in values) {
if (values.hasOwnProperty(name) && fields.containsKey(name)) {
newRecord.set(name, values[name]);
}
}
}
return this;
},
/**
* Loads matching fields from a model instance into this form.
* @param {Ext.data.Model} instance The model instance.
* @return {Ext.form.Panel} This form.
*/
setRecord: function(record) {
var me = this;
if (record && record.data) {
me.setValues(record.data);
}
me._record = record;
return this;
},
// @private
onSubmit: function(e) {
var me = this;
if (e && !me.getStandardSubmit()) {
e.stopEvent();
} else {
this.submit();
}
},
updateSubmitOnAction: function(newSubmitOnAction) {
if (newSubmitOnAction) {
this.on({
action: 'onFieldAction',
scope: this
});
} else {
this.un({
action: 'onFieldAction',
scope: this
});
}
},
// @private
onFieldAction: function(field) {
if (this.getSubmitOnAction()) {
field.blur();
this.submit();
}
},
/**
* Performs a Ajax-based submission of form values (if `standardSubmit` is `false`) or otherwise
* executes a standard HTML Form submit action.
*
* @param {Object} options
* The configuration when submitting this form.
*
* @param {String} options.url
* The url for the action (defaults to the form's {@link #url}).
*
* @param {String} options.method
* The form method to use (defaults to the form's {@link #method}, or POST if not defined).
*
* @param {String/Object} params
* The params to pass when submitting this form (defaults to this forms {@link #baseParams}).
* Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.
*
* @param {Object} headers
* Request headers to set for the action.
*
* @param {Boolean} [autoAbort=false]
* `true` to abort any pending Ajax request prior to submission.
* __Note:__ Has no effect when `{@link #standardSubmit}` is enabled.
*
* @param {Boolean} [options.submitDisabled=false]
* `true` to submit all fields regardless of disabled state.
* __Note:__ Has no effect when `{@link #standardSubmit}` is enabled.
*
* @param {String/Object} [waitMsg]
* If specified, the value which is passed to the loading {@link #masked mask}. See {@link #masked} for
* more information.
*
* @param {Function} options.success
* The callback that will be invoked after a successful response. A response is successful if
* a response is received from the server and is a JSON object where the `success` property is set
* to `true`, `{"success": true}`.
*
* The function is passed the following parameters:
*
* @param {Ext.form.Panel} options.success.form
* The form that requested the action.
*
* @param {Ext.form.Panel} options.success.result
* The result object returned by the server as a result of the submit request.
*
* @param {Function} options.failure
* The callback that will be invoked after a failed transaction attempt.
*
* The function is passed the following parameters:
*
* @param {Ext.form.Panel} options.failure.form
* The {@link Ext.form.Panel} that requested the submit.
*
* @param {Ext.form.Panel} options.failure.result
* The failed response or result object returned by the server which performed the operation.
*
* @param {Object} options.scope
* The scope in which to call the callback functions (The `this` reference for the callback functions).
*
* @return {Ext.data.Connection} The request object.
*/
submit: function(options) {
var me = this,
form = me.element.dom || {},
formValues;
options = Ext.apply({
url : me.getUrl() || form.action,
submit: false,
method : me.getMethod() || form.method || 'post',
autoAbort : false,
params : null,
waitMsg : null,
headers : null,
success : null,
failure : null
}, options || {});
formValues = me.getValues(me.getStandardSubmit() || !options.submitDisabled);
return me.fireAction('beforesubmit', [me, formValues, options], 'doBeforeSubmit');
},
doBeforeSubmit: function(me, formValues, options) {
var form = me.element.dom || {};
if (me.getStandardSubmit()) {
if (options.url && Ext.isEmpty(form.action)) {
form.action = options.url;
}
// Spinner fields must have their components enabled *before* submitting or else the value
// will not be posted.
var fields = this.query('spinnerfield'),
ln = fields.length,
i, field;
for (i = 0; i < ln; i++) {
field = fields[i];
if (!field.getDisabled()) {
field.getComponent().setDisabled(false);
}
}
form.method = (options.method || form.method).toLowerCase();
form.submit();
}
else {
if (options.waitMsg) {
me.setMasked(options.waitMsg);
}
return Ext.Ajax.request({
url: options.url,
method: options.method,
rawData: Ext.urlEncode(Ext.apply(
Ext.apply({}, me.getBaseParams() || {}),
options.params || {},
formValues
)),
autoAbort: options.autoAbort,
headers: Ext.apply(
{'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
options.headers || {}
),
scope: me,
callback: function(callbackOptions, success, response) {
var me = this,
responseText = response.responseText,
failureFn;
me.setMasked(false);
failureFn = function() {
if (Ext.isFunction(options.failure)) {
options.failure.call(options.scope || me, me, response, responseText);
}
me.fireEvent('exception', me, response);
};
if (success) {
response = Ext.decode(responseText);
success = !!response.success;
if (success) {
if (Ext.isFunction(options.success)) {
options.success.call(options.scope || me, me, response, responseText);
}
me.fireEvent('submit', me, response);
} else {
failureFn();
}
}
else {
failureFn();
}
}
});
}
},
/**
* Sets the values of form fields in bulk. Example usage:
*
* myForm.setValues({
* name: 'Ed',
* crazy: true,
* username: 'edspencer'
* });
*
* If there groups of checkbox fields with the same name, pass their values in an array. For example:
*
* myForm.setValues({
* name: 'Jacky',
* crazy: false,
* hobbies: [
* 'reading',
* 'cooking',
* 'gaming'
* ]
* });
*
* @param {Object} values field name => value mapping object.
* @return {Ext.form.Panel} This form.
*/
setValues: function(values) {
var fields = this.getFields(),
name, field, value, ln, i, f;
values = values || {};
for (name in values) {
if (values.hasOwnProperty(name)) {
field = fields[name];
value = values[name];
if (field) {
// If there are multiple fields with the same name. Checkboxes, radio fields and maybe event just normal fields..
if (Ext.isArray(field)) {
ln = field.length;
// Loop through each of the fields
for (i = 0; i < ln; i++) {
f = field[i];
if (f.isRadio) {
// If it is a radio field just use setGroupValue which will handle all of the radio fields
f.setGroupValue(value);
break;
} else if (f.isCheckbox) {
if (Ext.isArray(value)) {
f.setChecked((value.indexOf(f._value) != -1));
} else {
f.setChecked((value == f._value));
}
} else {
// If it is a bunch of fields with the same name, check if the value is also an array, so we can map it
// to each field
if (Ext.isArray(value)) {
f.setValue(value[i]);
}
}
}
} else {
if (field.isRadio || field.isCheckbox) {
// If the field is a radio or a checkbox
field.setChecked(value);
} else {
// If just a normal field
field.setValue(value);
}
}
}
}
}
return this;
},
/**
* Returns an object containing the value of each field in the form, keyed to the field's name.
* For groups of checkbox fields with the same name, it will be arrays of values. For example:
*
* {
* name: "Jacky Nguyen", // From a TextField
* favorites: [
* 'pizza',
* 'noodle',
* 'cake'
* ]
* }
*
* @param {Boolean} enabled `true` to return only enabled fields.
* @param {Boolean} all `true` to return all fields even if they don't have a
* {@link Ext.field.Field#name name} configured.
* @return {Object} Object mapping field name to its value.
*/
getValues: function(enabled, all) {
var fields = this.getFields(),
values = {},
isArray = Ext.isArray,
field, value, addValue, bucket, name, ln, i;
// Function which you give a field and a name, and it will add it into the values
// object accordingly
addValue = function(field, name) {
if (!all && (!name || name === 'null')) {
return;
}
if (field.isCheckbox) {
value = field.getSubmitValue();
} else {
value = field.getValue();
}
if (!(enabled && field.getDisabled())) {
// RadioField is a special case where the value returned is the fields valUE
// ONLY if it is checked
if (field.isRadio) {
if (field.isChecked()) {
values[name] = value;
}
} else {
// Check if the value already exists
bucket = values[name];
if (bucket) {
// if it does and it isn't an array, we need to make it into an array
// so we can push more
if (!isArray(bucket)) {
bucket = values[name] = [bucket];
}
// Check if it is an array
if (isArray(value)) {
// Concat it into the other values
bucket = values[name] = bucket.concat(value);
} else {
// If it isn't an array, just pushed more values
bucket.push(value);
}
} else {
values[name] = value;
}
}
}
};
// Loop through each of the fields, and add the values for those fields.
for (name in fields) {
if (fields.hasOwnProperty(name)) {
field = fields[name];
if (isArray(field)) {
ln = field.length;
for (i = 0; i < ln; i++) {
addValue(field[i], name);
}
} else {
addValue(field, name);
}
}
}
return values;
},
/**
* Resets all fields in the form back to their original values.
* @return {Ext.form.Panel} This form.
*/
reset: function() {
this.getFieldsAsArray().forEach(function(field) {
field.reset();
});
return this;
},
/**
* A convenient method to disable all fields in this form.
* @return {Ext.form.Panel} This form.
*/
doSetDisabled: function(newDisabled) {
this.getFieldsAsArray().forEach(function(field) {
field.setDisabled(newDisabled);
});
return this;
},
/**
* @private
*/
getFieldsAsArray: function() {
var fields = [],
getFieldsFrom = function(item) {
if (item.isField) {
fields.push(item);
}
if (item.isContainer) {
item.getItems().each(getFieldsFrom);
}
};
this.getItems().each(getFieldsFrom);
return fields;
},
/**
* @private
* Returns all {@link Ext.field.Field field} instances inside this form.
* @param byName return only fields that match the given name, otherwise return all fields.
* @return {Object/Array} All field instances, mapped by field name; or an array if `byName` is passed.
*/
getFields: function(byName) {
var fields = {},
itemName;
var getFieldsFrom = function(item) {
if (item.isField) {
itemName = item.getName();
if ((byName && itemName == byName) || typeof byName == 'undefined') {
if (fields.hasOwnProperty(itemName)) {
if (!Ext.isArray(fields[itemName])) {
fields[itemName] = [fields[itemName]];
}
fields[itemName].push(item);
} else {
fields[itemName] = item;
}
}
}
if (item.isContainer) {
item.items.each(getFieldsFrom);
}
};
this.getItems().each(getFieldsFrom);
return (byName) ? (fields[byName] || []) : fields;
},
/**
* Returns an array of fields in this formpanel.
* @return {Ext.field.Field[]} An array of fields in this form panel.
* @private
*/
getFieldsArray: function() {
var fields = [];
var getFieldsFrom = function(item) {
if (item.isField) {
fields.push(item);
}
if (item.isContainer) {
item.items.each(getFieldsFrom);
}
};
this.items.each(getFieldsFrom);
return fields;
},
getFieldsFromItem: Ext.emptyFn,
/**
* Shows a generic/custom mask over a designated Element.
* @param {String/Object} cfg Either a string message or a configuration object supporting
* the following options:
*
* {
* message : 'Please Wait',
* cls : 'form-mask'
* }
*
* @param {Object} target
* @return {Ext.form.Panel} This form
* @deprecated 2.0.0 Please use {@link #setMasked} instead.
*/
showMask: function(cfg, target) {
//<debug>
Ext.Logger.warn('showMask is now deprecated. Please use Ext.form.Panel#setMasked instead');
//</debug>
cfg = Ext.isObject(cfg) ? cfg.message : cfg;
if (cfg) {
this.setMasked({
xtype: 'loadmask',
message: cfg
});
} else {
this.setMasked(true);
}
return this;
},
/**
* Hides a previously shown wait mask (See {@link #showMask}).
* @return {Ext.form.Panel} this
* @deprecated 2.0.0 Please use {@link #unmask} or {@link #setMasked} instead.
*/
hideMask: function() {
this.setMasked(false);
return this;
},
/**
* Returns the currently focused field
* @return {Ext.field.Field} The currently focused field, if one is focused or `null`.
* @private
*/
getFocusedField: function() {
var fields = this.getFieldsArray(),
ln = fields.length,
field, i;
for (i = 0; i < ln; i++) {
field = fields[i];
if (field.isFocused) {
return field;
}
}
return null;
},
/**
* @private
* @return {Boolean/Ext.field.Field} The next field if one exists, or `false`.
* @private
*/
getNextField: function() {
var fields = this.getFieldsArray(),
focusedField = this.getFocusedField(),
index;
if (focusedField) {
index = fields.indexOf(focusedField);
if (index !== fields.length - 1) {
index++;
return fields[index];
}
}
return false;
},
/**
* Tries to focus the next field in the form, if there is currently a focused field.
* @return {Boolean/Ext.field.Field} The next field that was focused, or `false`.
* @private
*/
focusNextField: function() {
var field = this.getNextField();
if (field) {
field.focus();
return field;
}
return false;
},
/**
* @private
* @return {Boolean/Ext.field.Field} The next field if one exists, or `false`.
*/
getPreviousField: function() {
var fields = this.getFieldsArray(),
focusedField = this.getFocusedField(),
index;
if (focusedField) {
index = fields.indexOf(focusedField);
if (index !== 0) {
index--;
return fields[index];
}
}
return false;
},
/**
* Tries to focus the previous field in the form, if there is currently a focused field.
* @return {Boolean/Ext.field.Field} The previous field that was focused, or `false`.
* @private
*/
focusPreviousField: function() {
var field = this.getPreviousField();
if (field) {
field.focus();
return field;
}
return false;
}
}, function() {
});
/**
* @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
*/
Ext.define('Ext.fx.runner.Css', {
extend: 'Ext.Evented',
requires: [
'Ext.fx.Animation'
],
prefixedProperties: {
'transform' : true,
'transform-origin' : true,
'perspective' : true,
'transform-style' : true,
'transition' : true,
'transition-property' : true,
'transition-duration' : true,
'transition-timing-function': true,
'transition-delay' : true,
'animation' : true,
'animation-name' : true,
'animation-duration' : true,
'animation-iteration-count' : true,
'animation-direction' : true,
'animation-timing-function' : true,
'animation-delay' : true
},
lengthProperties: {
'top' : true,
'right' : true,
'bottom' : true,
'left' : true,
'width' : true,
'height' : true,
'max-height' : true,
'max-width' : true,
'min-height' : true,
'min-width' : true,
'margin-bottom' : true,
'margin-left' : true,
'margin-right' : true,
'margin-top' : true,
'padding-bottom' : true,
'padding-left' : true,
'padding-right' : true,
'padding-top' : true,
'border-bottom-width': true,
'border-left-width' : true,
'border-right-width' : true,
'border-spacing' : true,
'border-top-width' : true,
'border-width' : true,
'outline-width' : true,
'letter-spacing' : true,
'line-height' : true,
'text-indent' : true,
'word-spacing' : true,
'font-size' : true,
'translate' : true,
'translateX' : true,
'translateY' : true,
'translateZ' : true,
'translate3d' : true
},
durationProperties: {
'transition-duration' : true,
'transition-delay' : true,
'animation-duration' : true,
'animation-delay' : true
},
angleProperties: {
rotate : true,
rotateX : true,
rotateY : true,
rotateZ : true,
skew : true,
skewX : true,
skewY : true
},
lengthUnitRegex: /([a-z%]*)$/,
DEFAULT_UNIT_LENGTH: 'px',
DEFAULT_UNIT_ANGLE: 'deg',
DEFAULT_UNIT_DURATION: 'ms',
formattedNameCache: {},
constructor: function() {
var supports3dTransform = Ext.feature.has.Css3dTransforms;
if (supports3dTransform) {
this.transformMethods = ['translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'skewX', 'skewY', 'scaleX', 'scaleY', 'scaleZ'];
}
else {
this.transformMethods = ['translateX', 'translateY', 'rotate', 'skewX', 'skewY', 'scaleX', 'scaleY'];
}
this.vendorPrefix = Ext.browser.getStyleDashPrefix();
this.ruleStylesCache = {};
return this;
},
getStyleSheet: function() {
var styleSheet = this.styleSheet,
styleElement, styleSheets;
if (!styleSheet) {
styleElement = document.createElement('style');
styleElement.type = 'text/css';
(document.head || document.getElementsByTagName('head')[0]).appendChild(styleElement);
styleSheets = document.styleSheets;
this.styleSheet = styleSheet = styleSheets[styleSheets.length - 1];
}
return styleSheet;
},
applyRules: function(selectors) {
var styleSheet = this.getStyleSheet(),
ruleStylesCache = this.ruleStylesCache,
rules = styleSheet.cssRules,
selector, properties, ruleStyle,
ruleStyleCache, rulesLength, name, value;
for (selector in selectors) {
properties = selectors[selector];
ruleStyle = ruleStylesCache[selector];
if (ruleStyle === undefined) {
rulesLength = rules.length;
styleSheet.insertRule(selector + '{}', rulesLength);
ruleStyle = ruleStylesCache[selector] = rules.item(rulesLength).style;
}
ruleStyleCache = ruleStyle.$cache;
if (!ruleStyleCache) {
ruleStyleCache = ruleStyle.$cache = {};
}
for (name in properties) {
value = this.formatValue(properties[name], name);
name = this.formatName(name);
if (ruleStyleCache[name] !== value) {
ruleStyleCache[name] = value;
// console.log(name + " " + value);
if (value === null) {
ruleStyle.removeProperty(name);
}
else {
ruleStyle.setProperty(name, value, 'important');
}
}
}
}
return this;
},
applyStyles: function(styles) {
var id, element, elementStyle, properties, name, value;
for (id in styles) {
// console.log("-> ["+id+"]", "APPLY======================");
element = document.getElementById(id);
if (!element) {
return this;
}
elementStyle = element.style;
properties = styles[id];
for (name in properties) {
value = this.formatValue(properties[name], name);
name = this.formatName(name);
// console.log("->-> ["+id+"]", name, value);
if (value === null) {
elementStyle.removeProperty(name);
}
else {
elementStyle.setProperty(name, value, 'important');
}
}
}
return this;
},
formatName: function(name) {
var cache = this.formattedNameCache,
formattedName = cache[name];
if (!formattedName) {
if (this.prefixedProperties[name]) {
formattedName = this.vendorPrefix + name;
}
else {
formattedName = name;
}
cache[name] = formattedName;
}
return formattedName;
},
formatValue: function(value, name) {
var type = typeof value,
lengthUnit = this.DEFAULT_UNIT_LENGTH,
transformMethods,
method, i, ln,
transformValues, values, unit;
if (type == 'string') {
if (this.lengthProperties[name]) {
unit = value.match(this.lengthUnitRegex)[1];
if (unit.length > 0) {
//<debug error>
if (unit !== lengthUnit) {
Ext.Logger.error("Length unit: '" + unit + "' in value: '" + value + "' of property: '" + name + "' is not " +
"valid for animation. Only 'px' is allowed");
}
//</debug>
}
else {
return value + lengthUnit;
}
}
return value;
}
else if (type == 'number') {
if (value == 0) {
return '0';
}
if (this.lengthProperties[name]) {
return value + lengthUnit;
}
if (this.angleProperties[name]) {
return value + this.DEFAULT_UNIT_ANGLE;
}
if (this.durationProperties[name]) {
return value + this.DEFAULT_UNIT_DURATION;
}
}
else if (name === 'transform') {
transformMethods = this.transformMethods;
transformValues = [];
for (i = 0,ln = transformMethods.length; i < ln; i++) {
method = transformMethods[i];
transformValues.push(method + '(' + this.formatValue(value[method], method) + ')');
}
return transformValues.join(' ');
}
else if (Ext.isArray(value)) {
values = [];
for (i = 0,ln = value.length; i < ln; i++) {
values.push(this.formatValue(value[i], name));
}
return (values.length > 0) ? values.join(', ') : 'none';
}
return value;
}
});
/**
* @author Jacky Nguyen <jacky@sencha.com>
* @private
*/
Ext.define('Ext.fx.runner.CssTransition', {
extend: 'Ext.fx.runner.Css',
listenersAttached: false,
constructor: function() {
this.runningAnimationsData = {};
return this.callParent(arguments);
},
attachListeners: function() {
this.listenersAttached = true;
this.getEventDispatcher().addListener('element', '*', 'transitionend', 'onTransitionEnd', this);
},
onTransitionEnd: function(e) {
var target = e.target,
id = target.id;
if (id && this.runningAnimationsData.hasOwnProperty(id)) {
this.refreshRunningAnimationsData(Ext.get(target), [e.browserEvent.propertyName]);
}
},
onAnimationEnd: function(element, data, animation, isInterrupted, isReplaced) {
var id = element.getId(),
runningData = this.runningAnimationsData[id],
endRules = {},
endData = {},
runningNameMap, toPropertyNames, i, ln, name;
animation.un('stop', 'onAnimationStop', this);
if (runningData) {
runningNameMap = runningData.nameMap;
}
endRules[id] = endData;
if (data.onBeforeEnd) {
data.onBeforeEnd.call(data.scope || this, element, isInterrupted);
}
animation.fireEvent('animationbeforeend', animation, element, isInterrupted);
this.fireEvent('animationbeforeend', this, animation, element, isInterrupted);
if (isReplaced || (!isInterrupted && !data.preserveEndState)) {
toPropertyNames = data.toPropertyNames;
for (i = 0,ln = toPropertyNames.length; i < ln; i++) {
name = toPropertyNames[i];
if (runningNameMap && !runningNameMap.hasOwnProperty(name)) {
endData[name] = null;
}
}
}
if (data.after) {
Ext.merge(endData, data.after);
}
this.applyStyles(endRules);
if (data.onEnd) {
data.onEnd.call(data.scope || this, element, isInterrupted);
}
animation.fireEvent('animationend', animation, element, isInterrupted);
this.fireEvent('animationend', this, animation, element, isInterrupted);
},
onAllAnimationsEnd: function(element) {
var id = element.getId(),
endRules = {};
delete this.runningAnimationsData[id];
endRules[id] = {
'transition-property': null,
'transition-duration': null,
'transition-timing-function': null,
'transition-delay': null
};
this.applyStyles(endRules);
this.fireEvent('animationallend', this, element);
},
hasRunningAnimations: function(element) {
var id = element.getId(),
runningAnimationsData = this.runningAnimationsData;
return runningAnimationsData.hasOwnProperty(id) && runningAnimationsData[id].sessions.length > 0;
},
refreshRunningAnimationsData: function(element, propertyNames, interrupt, replace) {
var id = element.getId(),
runningAnimationsData = this.runningAnimationsData,
runningData = runningAnimationsData[id];
if (!runningData) {
return;
}
var nameMap = runningData.nameMap,
nameList = runningData.nameList,
sessions = runningData.sessions,
ln, j, subLn, name,
i, session, map, list,
hasCompletedSession = false;
interrupt = Boolean(interrupt);
replace = Boolean(replace);
if (!sessions) {
return this;
}
ln = sessions.length;
if (ln === 0) {
return this;
}
if (replace) {
runningData.nameMap = {};
nameList.length = 0;
for (i = 0; i < ln; i++) {
session = sessions[i];
this.onAnimationEnd(element, session.data, session.animation, interrupt, replace);
}
sessions.length = 0;
}
else {
for (i = 0; i < ln; i++) {
session = sessions[i];
map = session.map;
list = session.list;
for (j = 0,subLn = propertyNames.length; j < subLn; j++) {
name = propertyNames[j];
if (map[name]) {
delete map[name];
Ext.Array.remove(list, name);
session.length--;
if (--nameMap[name] == 0) {
delete nameMap[name];
Ext.Array.remove(nameList, name);
}
}
}
if (session.length == 0) {
sessions.splice(i, 1);
i--;
ln--;
hasCompletedSession = true;
this.onAnimationEnd(element, session.data, session.animation, interrupt);
}
}
}
if (!replace && !interrupt && sessions.length == 0 && hasCompletedSession) {
this.onAllAnimationsEnd(element);
}
},
getRunningData: function(id) {
var runningAnimationsData = this.runningAnimationsData;
if (!runningAnimationsData.hasOwnProperty(id)) {
runningAnimationsData[id] = {
nameMap: {},
nameList: [],
sessions: []
};
}
return runningAnimationsData[id];
},
getTestElement: function() {
var testElement = this.testElement,
iframe, iframeDocument, iframeStyle;
if (!testElement) {
iframe = document.createElement('iframe');
iframeStyle = iframe.style;
iframeStyle.setProperty('visibility', 'hidden', 'important');
iframeStyle.setProperty('width', '0px', 'important');
iframeStyle.setProperty('height', '0px', 'important');
iframeStyle.setProperty('position', 'absolute', 'important');
iframeStyle.setProperty('border', '0px', 'important');
iframeStyle.setProperty('zIndex', '-1000', 'important');
document.body.appendChild(iframe);
iframeDocument = iframe.contentDocument;
iframeDocument.open();
iframeDocument.writeln('</body>');
iframeDocument.close();
this.testElement = testElement = iframeDocument.createElement('div');
testElement.style.setProperty('position', 'absolute', '!important');
iframeDocument.body.appendChild(testElement);
this.testElementComputedStyle = window.getComputedStyle(testElement);
}
return testElement;
},
getCssStyleValue: function(name, value) {
var testElement = this.getTestElement(),
computedStyle = this.testElementComputedStyle,
style = testElement.style;
style.setProperty(name, value);
value = computedStyle.getPropertyValue(name);
style.removeProperty(name);
return value;
},
run: function(animations) {
var me = this,
isLengthPropertyMap = this.lengthProperties,
fromData = {},
toData = {},
data = {},
element, elementId, from, to, before,
fromPropertyNames, toPropertyNames,
doApplyTo, message,
runningData,
i, j, ln, animation, propertiesLength, sessionNameMap,
computedStyle, formattedName, name, toFormattedValue,
computedValue, fromFormattedValue, isLengthProperty,
runningNameMap, runningNameList, runningSessions, runningSession;
if (!this.listenersAttached) {
this.attachListeners();
}
animations = Ext.Array.from(animations);
for (i = 0,ln = animations.length; i < ln; i++) {
animation = animations[i];
animation = Ext.factory(animation, Ext.fx.Animation);
element = animation.getElement();
computedStyle = window.getComputedStyle(element.dom);
elementId = element.getId();
data = Ext.merge({}, animation.getData());
if (animation.onBeforeStart) {
animation.onBeforeStart.call(animation.scope || this, element);
}
animation.fireEvent('animationstart', animation);
this.fireEvent('animationstart', this, animation);
data[elementId] = data;
before = data.before;
from = data.from;
to = data.to;
data.fromPropertyNames = fromPropertyNames = [];
data.toPropertyNames = toPropertyNames = [];
for (name in to) {
if (to.hasOwnProperty(name)) {
to[name] = toFormattedValue = this.formatValue(to[name], name);
formattedName = this.formatName(name);
isLengthProperty = isLengthPropertyMap.hasOwnProperty(name);
if (!isLengthProperty) {
toFormattedValue = this.getCssStyleValue(formattedName, toFormattedValue);
}
if (from.hasOwnProperty(name)) {
from[name] = fromFormattedValue = this.formatValue(from[name], name);
if (!isLengthProperty) {
fromFormattedValue = this.getCssStyleValue(formattedName, fromFormattedValue);
}
if (toFormattedValue !== fromFormattedValue) {
fromPropertyNames.push(formattedName);
toPropertyNames.push(formattedName);
}
}
else {
computedValue = computedStyle.getPropertyValue(formattedName);
if (toFormattedValue !== computedValue) {
toPropertyNames.push(formattedName);
}
}
}
}
propertiesLength = toPropertyNames.length;
if (propertiesLength === 0) {
this.onAnimationEnd(element, data, animation);
continue;
}
runningData = this.getRunningData(elementId);
runningSessions = runningData.sessions;
if (runningSessions.length > 0) {
this.refreshRunningAnimationsData(
element, Ext.Array.merge(fromPropertyNames, toPropertyNames), true, data.replacePrevious
);
}
runningNameMap = runningData.nameMap;
runningNameList = runningData.nameList;
sessionNameMap = {};
for (j = 0; j < propertiesLength; j++) {
name = toPropertyNames[j];
sessionNameMap[name] = true;
if (!runningNameMap.hasOwnProperty(name)) {
runningNameMap[name] = 1;
runningNameList.push(name);
}
else {
runningNameMap[name]++;
}
}
runningSession = {
element: element,
map: sessionNameMap,
list: toPropertyNames.slice(),
length: propertiesLength,
data: data,
animation: animation
};
runningSessions.push(runningSession);
animation.on('stop', 'onAnimationStop', this);
fromData[elementId] = from = Ext.apply(Ext.Object.chain(before), from);
if (runningNameList.length > 0) {
fromPropertyNames = Ext.Array.difference(runningNameList, fromPropertyNames);
toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames);
from['transition-property'] = fromPropertyNames;
}
toData[elementId] = to = Ext.Object.chain(to);
to['transition-property'] = toPropertyNames;
to['transition-duration'] = data.duration;
to['transition-timing-function'] = data.easing;
to['transition-delay'] = data.delay;
animation.startTime = Date.now();
}
message = this.$className;
this.applyStyles(fromData);
doApplyTo = function(e) {
if (e.data === message && e.source === window) {
window.removeEventListener('message', doApplyTo, false);
me.applyStyles(toData);
}
};
window.addEventListener('message', doApplyTo, false);
window.postMessage(message, '*');
},
onAnimationStop: function(animation) {
var runningAnimationsData = this.runningAnimationsData,
id, runningData, sessions, i, ln, session;
for (id in runningAnimationsData) {
if (runningAnimationsData.hasOwnProperty(id)) {
runningData = runningAnimationsData[id];
sessions = runningData.sessions;
for (i = 0,ln = sessions.length; i < ln; i++) {
session = sessions[i];
if (session.animation === animation) {
this.refreshRunningAnimationsData(session.element, session.list.slice(), false);
}
}
}
}
}
});
/**
* @class Ext.fx.Runner
* @private
*/
Ext.define('Ext.fx.Runner', {
requires: [
'Ext.fx.runner.CssTransition'
// 'Ext.fx.runner.CssAnimation'
],
constructor: function() {
return new Ext.fx.runner.CssTransition();
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.Cube', {
extend: 'Ext.fx.animation.Abstract',
alias: 'animation.cube',
config: {
/**
* @cfg
* @inheritdoc
*/
before: {
// 'transform-style': 'preserve-3d'
},
after: {},
/**
* @cfg {String} direction The direction of which the slide animates
* @accessor
*/
direction: 'right',
out: false
},
// getData: function() {
// var to = this.getTo(),
// from = this.getFrom(),
// out = this.getOut(),
// direction = this.getDirection(),
// el = this.getElement(),
// elW = el.getWidth(),
// elH = el.getHeight(),
// halfWidth = (elW / 2),
// halfHeight = (elH / 2),
// fromTransform = {},
// toTransform = {},
// originalFromTransform = {
// rotateY: 0,
// translateX: 0,
// translateZ: 0
// },
// originalToTransform = {
// rotateY: 90,
// translateX: halfWidth,
// translateZ: halfWidth
// },
// originalVerticalFromTransform = {
// rotateX: 0,
// translateY: 0,
// translateZ: 0
// },
// originalVerticalToTransform = {
// rotateX: 90,
// translateY: halfHeight,
// translateZ: halfHeight
// },
// tempTransform;
//
// if (direction == "left" || direction == "right") {
// if (out) {
// toTransform = originalToTransform;
// fromTransform = originalFromTransform;
// } else {
// toTransform = originalFromTransform;
// fromTransform = originalToTransform;
// fromTransform.rotateY *= -1;
// fromTransform.translateX *= -1;
// }
//
// if (direction === 'right') {
// tempTransform = fromTransform;
// fromTransform = toTransform;
// toTransform = tempTransform;
// }
// }
//
// if (direction == "up" || direction == "down") {
// if (out) {
// toTransform = originalVerticalFromTransform;
// fromTransform = {
// rotateX: -90,
// translateY: halfHeight,
// translateZ: halfHeight
// };
// } else {
// fromTransform = originalVerticalFromTransform;
// toTransform = {
// rotateX: 90,
// translateY: -halfHeight,
// translateZ: halfHeight
// };
// }
//
// if (direction == "up") {
// tempTransform = fromTransform;
// fromTransform = toTransform;
// toTransform = tempTransform;
// }
// }
//
// from.set('transform', fromTransform);
// to.set('transform', toTransform);
//
// return this.callParent(arguments);
// },
getData: function() {
var to = this.getTo(),
from = this.getFrom(),
before = this.getBefore(),
after = this.getAfter(),
out = this.getOut(),
direction = this.getDirection(),
el = this.getElement(),
elW = el.getWidth(),
elH = el.getHeight(),
origin = out ? '100% 100%' : '0% 0%',
fromOpacity = 1,
toOpacity = 1,
transformFrom = {
rotateY: 0,
translateZ: 0
},
transformTo = {
rotateY: 0,
translateZ: 0
};
if (direction == "left" || direction == "right") {
if (out) {
toOpacity = 0.5;
transformTo.translateZ = elW;
transformTo.rotateY = -90;
} else {
fromOpacity = 0.5;
transformFrom.translateZ = elW;
transformFrom.rotateY = 90;
}
}
before['transform-origin'] = origin;
after['transform-origin'] = null;
to.set('transform', transformTo);
from.set('transform', transformFrom);
from.set('opacity', fromOpacity);
to.set('opacity', toOpacity);
return this.callParent(arguments);
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.Wipe', {
extend: 'Ext.fx.Animation',
alternateClassName: 'Ext.fx.animation.WipeIn',
config: {
/**
* @cfg
* @inheritdoc
*/
easing: 'ease-out',
/**
* @cfg {String} direction The direction of which the slide animates
* @accessor
*/
direction: 'right',
/**
* @cfg {Boolean} out True if you want to make this animation wipe out, instead of slide in.
* @accessor
*/
out: false
},
refresh: function() {
var me = this,
el = me.getElement(),
elBox = el.dom.getBoundingClientRect(),
elWidth = elBox.width,
elHeight = elBox.height,
from = me.getFrom(),
to = me.getTo(),
out = me.getOut(),
direction = me.getDirection(),
maskFromX = 0,
maskFromY = 0,
maskToX = 0,
maskToY = 0,
mask, tmp;
switch (direction) {
case 'up':
if (out) {
mask = '-webkit-gradient(linear, left top, left bottom, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
maskFromY = elHeight * 3 + 'px';
maskToY = elHeight + 'px';
} else {
mask = '-webkit-gradient(linear, left top, left bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
maskFromY = -elHeight * 2 + 'px';
maskToY = 0;
}
break;
case 'down':
if (out) {
mask = '-webkit-gradient(linear, left top, left bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
maskFromY = -elHeight * 2 + 'px';
maskToY = 0;
} else {
mask = '-webkit-gradient(linear, left top, left bottom, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
maskFromY = elHeight * 3 + 'px';
maskToY = elHeight + 'px';
}
break;
case 'right':
if (out) {
mask = '-webkit-gradient(linear, right top, left top, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
maskFromX = -elWidth * 2 + 'px';
maskToX = 0;
} else {
mask = '-webkit-gradient(linear, right top, left top, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
maskToX = -elWidth * 2 + 'px';
}
break;
case 'left':
if (out) {
mask = '-webkit-gradient(linear, right top, left top, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
maskToX = -elWidth * 2 + 'px';
} else {
mask = '-webkit-gradient(linear, right top, left top, from(#000), to(transparent), color-stop(33%, #000), color-stop(66%, transparent))';
maskFromX = -elWidth * 2 + 'px';
maskToX = 0;
}
break;
}
if (!out) {
tmp = maskFromY;
maskFromY = maskToY;
maskToY = tmp;
tmp = maskFromX;
maskFromX = maskToX;
maskToX = tmp;
}
from.set('mask-image', mask);
from.set('mask-size', elWidth * 3 + 'px ' + elHeight * 3 + 'px');
from.set('mask-position-x', maskFromX);
from.set('mask-position-y', maskFromY);
to.set('mask-position-x', maskToX);
to.set('mask-position-y', maskToY);
// me.setEasing(out ? 'ease-in' : 'ease-out');
},
getData: function() {
this.refresh();
return this.callParent(arguments);
}
});
/**
* @private
*/
Ext.define('Ext.fx.animation.WipeOut', {
extend: 'Ext.fx.animation.Wipe',
config: {
// @hide
out: true
}
});
/**
* @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.layout.card.Cube', {
extend: 'Ext.fx.layout.card.Style',
alias: 'fx.layout.card.cube',
config: {
reverse: null,
inAnimation: {
type: 'cube'
},
outAnimation: {
type: 'cube',
out: true
}
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.ScrollCover', {
extend: 'Ext.fx.layout.card.Scroll',
alias: 'fx.layout.card.scrollcover',
onActiveItemChange: function(cardLayout, inItem, outItem, options, controller) {
var containerElement, containerSize, xy, animConfig,
inTranslate, outTranslate;
this.lastController = controller;
this.inItem = inItem;
if (inItem && outItem) {
containerElement = this.getLayout().container.innerElement;
containerSize = containerElement.getSize();
xy = this.calculateXY(containerSize);
animConfig = {
easing: this.getEasing(),
duration: this.getDuration()
};
inItem.renderElement.dom.style.setProperty('visibility', 'hidden', '!important');
inTranslate = inItem.setTranslatable(true).getTranslatable();
outTranslate = outItem.setTranslatable(true).getTranslatable();
outTranslate.translate({ x: 0, y: 0});
// outItem.setTranslate(null);
inTranslate.translate({ x: xy.left, y: xy.top});
inTranslate.getWrapper().dom.style.setProperty('z-index', '100', '!important');
inItem.show();
inTranslate.on({
animationstart: 'onInAnimationStart',
animationend: 'onInAnimationEnd',
scope: this
});
inTranslate.translateAnimated({ x: 0, y: 0}, animConfig);
controller.pause();
}
},
onInAnimationStart: function() {
this.inItem.renderElement.dom.style.removeProperty('visibility');
},
onInAnimationEnd: function() {
this.inItem.getTranslatable().getWrapper().dom.style.removeProperty('z-index'); // Remove this when we can remove translatable
// this.inItem.setTranslatable(null);
this.lastController.resume();
}
});
/**
* @private
*/
Ext.define('Ext.fx.layout.card.ScrollReveal', {
extend: 'Ext.fx.layout.card.Scroll',
alias: 'fx.layout.card.scrollreveal',
onActiveItemChange: function(cardLayout, inItem, outItem, options, controller) {
var containerElement, containerSize, xy, animConfig,
outTranslate, inTranslate;
this.lastController = controller;
this.outItem = outItem;
this.inItem = inItem;
if (inItem && outItem) {
containerElement = this.getLayout().container.innerElement;
containerSize = containerElement.getSize();
xy = this.calculateXY(containerSize);
animConfig = {
easing: this.getEasing(),
duration: this.getDuration()
};
outTranslate = outItem.setTranslatable(true).getTranslatable();
inTranslate = inItem.setTranslatable(true).getTranslatable();
outTranslate.getWrapper().dom.style.setProperty('z-index', '100', '!important');
outTranslate.translate({ x: 0, y: 0});
inTranslate.translate({ x: 0, y: 0});
inItem.show();
outTranslate.on({
animationend: 'onOutAnimationEnd',
scope: this
});
outTranslate.translateAnimated({ x: xy.x, y: xy.y}, animConfig);
controller.pause();
}
},
onOutAnimationEnd: function() {
this.outItem.getTranslatable().getWrapper().dom.style.removeProperty('z-index'); // Remove this when we can remove translatable
// this.outItem.setTranslatable(null);
this.lastController.resume();
}
});
/**
* @author Jacky Nguyen <jacky@sencha.com>
* @private
*/
Ext.define('Ext.fx.runner.CssAnimation', {
extend: 'Ext.fx.runner.Css',
constructor: function() {
this.runningAnimationsMap = {};
this.elementEndStates = {};
this.animationElementMap = {};
this.keyframesRulesCache = {};
this.uniqueId = 0;
return this.callParent(arguments);
},
attachListeners: function() {
var eventDispatcher = this.getEventDispatcher();
this.listenersAttached = true;
eventDispatcher.addListener('element', '*', 'animationstart', 'onAnimationStart', this);
eventDispatcher.addListener('element', '*', 'animationend', 'onAnimationEnd', this);
},
onAnimationStart: function(e) {
var name = e.browserEvent.animationName,
elementId = this.animationElementMap[name],
animation = this.runningAnimationsMap[elementId][name],
elementEndStates = this.elementEndStates,
elementEndState = elementEndStates[elementId],
data = {};
console.log("START============= " + name);
if (elementEndState) {
delete elementEndStates[elementId];
data[elementId] = elementEndState;
this.applyStyles(data);
}
if (animation.before) {
data[elementId] = animation.before;
this.applyStyles(data);
}
},
onAnimationEnd: function(e) {
var element = e.target,
name = e.browserEvent.animationName,
animationElementMap = this.animationElementMap,
elementId = animationElementMap[name],
runningAnimationsMap = this.runningAnimationsMap,
runningAnimations = runningAnimationsMap[elementId],
animation = runningAnimations[name];
console.log("END============= " + name);
if (animation.onBeforeEnd) {
animation.onBeforeEnd.call(animation.scope || this, element);
}
if (animation.onEnd) {
animation.onEnd.call(animation.scope || this, element);
}
delete animationElementMap[name];
delete runningAnimations[name];
this.removeKeyframesRule(name);
},
generateAnimationId: function() {
return 'animation-' + (++this.uniqueId);
},
run: function(animations) {
var data = {},
elementEndStates = this.elementEndStates,
animationElementMap = this.animationElementMap,
runningAnimationsMap = this.runningAnimationsMap,
runningAnimations, states,
elementId, animationId, i, ln, animation,
name, runningAnimation,
names, durations, easings, delays, directions, iterations;
if (!this.listenersAttached) {
this.attachListeners();
}
animations = Ext.Array.from(animations);
for (i = 0,ln = animations.length; i < ln; i++) {
animation = animations[i];
animation = Ext.factory(animation, Ext.fx.Animation);
elementId = animation.getElement().getId();
animationId = animation.getName() || this.generateAnimationId();
animationElementMap[animationId] = elementId;
animation = animation.getData();
states = animation.states;
this.addKeyframesRule(animationId, states);
runningAnimations = runningAnimationsMap[elementId];
if (!runningAnimations) {
runningAnimations = runningAnimationsMap[elementId] = {};
}
runningAnimations[animationId] = animation;
names = [];
durations = [];
easings = [];
delays = [];
directions = [];
iterations = [];
for (name in runningAnimations) {
if (runningAnimations.hasOwnProperty(name)) {
runningAnimation = runningAnimations[name];
names.push(name);
durations.push(runningAnimation.duration);
easings.push(runningAnimation.easing);
delays.push(runningAnimation.delay);
directions.push(runningAnimation.direction);
iterations.push(runningAnimation.iteration);
}
}
data[elementId] = {
'animation-name' : names,
'animation-duration' : durations,
'animation-timing-function' : easings,
'animation-delay' : delays,
'animation-direction' : directions,
'animation-iteration-count' : iterations
};
// Ext.apply(data[elementId], animation.origin);
if (animation.preserveEndState) {
elementEndStates[elementId] = states['100%'];
}
}
this.applyStyles(data);
},
addKeyframesRule: function(name, keyframes) {
var percentage, properties,
keyframesRule,
styleSheet, rules, styles, rulesLength, key, value;
styleSheet = this.getStyleSheet();
rules = styleSheet.cssRules;
rulesLength = rules.length;
styleSheet.insertRule('@' + this.vendorPrefix + 'keyframes ' + name + '{}', rulesLength);
keyframesRule = rules[rulesLength];
for (percentage in keyframes) {
properties = keyframes[percentage];
rules = keyframesRule.cssRules;
rulesLength = rules.length;
styles = [];
for (key in properties) {
value = this.formatValue(properties[key], key);
key = this.formatName(key);
styles.push(key + ':' + value);
}
keyframesRule.insertRule(percentage + '{' + styles.join(';') + '}', rulesLength);
}
return this;
},
removeKeyframesRule: function(name) {
var styleSheet = this.getStyleSheet(),
rules = styleSheet.cssRules,
i, ln, rule;
for (i = 0,ln = rules.length; i < ln; i++) {
rule = rules[i];
if (rule.name === name) {
styleSheet.removeRule(i);
break;
}
}
return this;
}
});
//<feature logger>
Ext.define('Ext.log.Base', {
config: {},
constructor: function(config) {
this.initConfig(config);
return this;
}
});
//</feature>
//<feature logger>
/**
* @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);
});
})();
//</feature>
//<feature logger>
Ext.define('Ext.log.filter.Filter', {
extend: 'Ext.log.Base',
accept: function(event) {
return true;
}
});
//</feature>
//<feature logger>
Ext.define('Ext.log.filter.Priority', {
extend: 'Ext.log.filter.Filter',
config: {
minPriority: 1
},
accept: function(event) {
return event.priority >= this.getMinPriority();
}
});
//</feature>
//<feature logger>
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;
}
});
//</feature>
//<feature logger>
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]);
}
});
//</feature>
//<feature logger>
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);
}
});
//</feature>
//<feature logger>
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
});
//</feature>
//<feature logger>
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);
}
}
});
//</feature>
//<feature logger>
Ext.define('Ext.log.writer.DocumentTitle', {
extend: 'Ext.log.writer.Writer',
doWrite: function(event) {
var message = event.message;
document.title = message;
}
});
//</feature>
//<feature logger>
Ext.define('Ext.log.writer.Remote', {
extend: 'Ext.log.writer.Writer',
requires: [
'Ext.Ajax'
],
config: {
batchSendDelay: 100,
onFailureRetryDelay: 500,
url: ''
},
isSending: false,
sendingTimer: null,
constructor: function() {
this.queue = [];
this.send = Ext.Function.bind(this.send, this);
return this.callParent(arguments);
},
doWrite: function(event) {
var queue = this.queue;
queue.push(event.message);
if (!this.isSending && this.sendingTimer === null) {
this.sendingTimer = setTimeout(this.send, this.getBatchSendDelay());
}
},
send: function() {
var queue = this.queue,
messages = queue.slice();
queue.length = 0;
this.sendingTimer = null;
if (messages.length > 0) {
this.doSend(messages);
}
},
doSend: function(messages) {
var me = this;
me.isSending = true;
Ext.Ajax.request({
url: me.getUrl(),
method: 'POST',
params: {
messages: messages.join("\n")
},
success: function(){
me.isSending = false;
me.send();
},
failure: function() {
setTimeout(function() {
me.doSend(messages);
}, me.getOnFailureRetryDelay());
},
scope: me
});
}
});
//</feature>
/**
* This component is used in {@link Ext.navigation.View} to control animations in the toolbar. You should never need to
* interact with the component directly, unless you are subclassing it.
* @private
* @author Robert Dougan <rob@sencha.com>
*/
Ext.define('Ext.navigation.Bar', {
extend: 'Ext.TitleBar',
requires: [
'Ext.Button',
'Ext.Spacer'
],
// @private
isToolbar: true,
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'toolbar',
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'navigation-bar',
/**
* @cfg {String} ui
* Style options for Toolbar. Either 'light' or 'dark'.
* @accessor
*/
ui: 'dark',
/**
* @cfg {String} title
* The title of the toolbar. You should NEVER set this, it is used internally. You set the title of the
* navigation bar by giving a navigation views children a title configuration.
* @private
* @accessor
*/
title: null,
/**
* @cfg
* @hide
* @accessor
*/
defaultType: 'button',
/**
* @cfg
* @ignore
* @accessor
*/
layout: {
type: 'hbox'
},
/**
* @cfg {Array/Object} items The child items to add to this NavigationBar. The {@link #cfg-defaultType} of
* a NavigationBar is {@link Ext.Button}, so you do not need to specify an `xtype` if you are adding
* buttons.
*
* You can also give items a `align` configuration which will align the item to the `left` or `right` of
* the NavigationBar.
* @hide
* @accessor
*/
/**
* @cfg {String} defaultBackButtonText
* The text to be displayed on the back button if:
* a) The previous view does not have a title
* b) The {@link #useTitleForBackButtonText} configuration is true.
* @private
* @accessor
*/
defaultBackButtonText: 'Back',
/**
* @cfg {Object} animation
* @private
* @accessor
*/
animation: {
duration: 300
},
/**
* @cfg {Boolean} useTitleForBackButtonText
* Set to false if you always want to display the {@link #defaultBackButtonText} as the text
* on the back button. True if you want to use the previous views title.
* @private
* @accessor
*/
useTitleForBackButtonText: null,
/**
* @cfg {Ext.navigation.View} view A reference to the navigation view this bar is linked to.
* @private
* @accessor
*/
view: null,
/**
* @cfg {Boolean} androidAnimation Optionally enable CSS transforms on Android 2
* for NavigationBar animations. Note that this may cause flickering if the
* NavigationBar is hidden.
* @accessor
*/
android2Transforms: false,
/**
* @cfg {Ext.Button/Object} backButton The configuration for the back button
* @private
* @accessor
*/
backButton: {
align: 'left',
ui: 'back',
hidden: true
}
},
/**
* @event back
* Fires when the back button was tapped.
* @param {Ext.navigation.Bar} this This bar
*/
constructor: function(config) {
config = config || {};
if (!config.items) {
config.items = [];
}
this.backButtonStack = [];
this.activeAnimations = [];
this.callParent([config]);
},
/**
* @private
*/
applyBackButton: function(config) {
return Ext.factory(config, Ext.Button, this.getBackButton());
},
/**
* @private
*/
updateBackButton: function(newBackButton, oldBackButton) {
if (oldBackButton) {
this.remove(oldBackButton);
}
if (newBackButton) {
this.add(newBackButton);
newBackButton.on({
scope: this,
tap: this.onBackButtonTap
});
}
},
onBackButtonTap: function() {
this.fireEvent('back', this);
},
/**
* @private
*/
updateView: function(newView) {
var me = this,
backButton = me.getBackButton(),
innerItems, i, backButtonText, item, title;
me.getItems();
if (newView) {
//update the back button stack with the current inner items of the view
innerItems = newView.getInnerItems();
for (i = 0; i < innerItems.length; i++) {
item = innerItems[i];
title = (item.getTitle) ? item.getTitle() : item.config.title;
me.backButtonStack.push(title || '&nbsp;');
}
me.setTitle(me.getTitleText());
backButtonText = me.getBackButtonText();
if (backButtonText) {
backButton.setText(backButtonText);
backButton.show();
}
}
},
/**
* @private
*/
onViewAdd: function(view, item) {
var me = this,
backButtonStack = me.backButtonStack,
hasPrevious, title;
me.endAnimation();
title = (item.getTitle) ? item.getTitle() : item.config.title;
backButtonStack.push(title || '&nbsp;');
hasPrevious = backButtonStack.length > 1;
me.doChangeView(view, hasPrevious, false);
},
/**
* @private
*/
onViewRemove: function(view) {
var me = this,
backButtonStack = me.backButtonStack,
hasPrevious;
me.endAnimation();
backButtonStack.pop();
hasPrevious = backButtonStack.length > 1;
me.doChangeView(view, hasPrevious, true);
},
/**
* @private
*/
doChangeView: function(view, hasPrevious, reverse) {
var me = this,
leftBox = me.leftBox,
leftBoxElement = leftBox.element,
titleComponent = me.titleComponent,
titleElement = titleComponent.element,
backButton = me.getBackButton(),
titleText = me.getTitleText(),
backButtonText = me.getBackButtonText(),
animation = me.getAnimation() && view.getLayout().getAnimation(),
animated = animation && animation.isAnimation && view.isPainted(),
properties, leftGhost, titleGhost, leftProps, titleProps;
if (animated) {
leftGhost = me.createProxy(leftBox.element);
leftBoxElement.setStyle('opacity', '0');
backButton.setText(backButtonText);
backButton[hasPrevious ? 'show' : 'hide']();
titleGhost = me.createProxy(titleComponent.element.getParent());
titleElement.setStyle('opacity', '0');
me.setTitle(titleText);
me.refreshTitlePosition();
properties = me.measureView(leftGhost, titleGhost, reverse);
leftProps = properties.left;
titleProps = properties.title;
me.isAnimating = true;
me.animate(leftBoxElement, leftProps.element);
me.animate(titleElement, titleProps.element, function() {
titleElement.setLeft(properties.titleLeft);
me.isAnimating = false;
});
if (Ext.os.is.Android2 && !this.getAndroid2Transforms()) {
leftGhost.ghost.destroy();
titleGhost.ghost.destroy();
}
else {
me.animate(leftGhost.ghost, leftProps.ghost);
me.animate(titleGhost.ghost, titleProps.ghost, function() {
leftGhost.ghost.destroy();
titleGhost.ghost.destroy();
});
}
}
else {
if (hasPrevious) {
backButton.setText(backButtonText);
backButton.show();
}
else {
backButton.hide();
}
me.setTitle(titleText);
}
},
/**
* Calculates and returns the position values needed for the back button when you are pushing a title.
* @private
*/
measureView: function(oldLeft, oldTitle, reverse) {
var me = this,
barElement = me.element,
newLeftElement = me.leftBox.element,
titleElement = me.titleComponent.element,
minOffset = Math.min(barElement.getWidth() / 3, 200),
newLeftWidth = newLeftElement.getWidth(),
barX = barElement.getX(),
barWidth = barElement.getWidth(),
titleX = titleElement.getX(),
titleLeft = titleElement.getLeft(),
titleWidth = titleElement.getWidth(),
oldLeftX = oldLeft.x,
oldLeftWidth = oldLeft.width,
oldLeftLeft = oldLeft.left,
useLeft = Ext.os.is.Android2 && !this.getAndroid2Transforms(),
newOffset, oldOffset, leftAnims, titleAnims, omega, theta;
theta = barX - oldLeftX - oldLeftWidth;
if (reverse) {
newOffset = theta;
oldOffset = Math.min(titleX - oldLeftWidth, minOffset);
}
else {
oldOffset = theta;
newOffset = Math.min(titleX - barX, minOffset);
}
if (useLeft) {
leftAnims = {
element: {
from: {
left: newOffset,
opacity: 1
},
to: {
left: 0,
opacity: 1
}
}
};
}
else {
leftAnims = {
element: {
from: {
transform: {
translateX: newOffset
},
opacity: 0
},
to: {
transform: {
translateX: 0
},
opacity: 1
}
},
ghost: {
to: {
transform: {
translateX: oldOffset
},
opacity: 0
}
}
};
}
theta = barX - titleX + newLeftWidth;
if ((oldLeftLeft + titleWidth) > titleX) {
omega = barX - titleX - titleWidth;
}
if (reverse) {
titleElement.setLeft(0);
oldOffset = barX + barWidth;
if (omega !== undefined) {
newOffset = omega;
}
else {
newOffset = theta;
}
}
else {
newOffset = barWidth - titleX;
if (omega !== undefined) {
oldOffset = omega;
}
else {
oldOffset = theta;
}
}
if (useLeft) {
titleAnims = {
element: {
from: {
left: newOffset,
opacity: 1
},
to: {
left: titleLeft,
opacity: 1
}
}
};
}
else {
titleAnims = {
element: {
from: {
transform: {
translateX: newOffset
},
opacity: 0
},
to: {
transform: {
translateX: titleLeft
},
opacity: 1
}
},
ghost: {
to: {
transform: {
translateX: oldOffset
},
opacity: 0
}
}
};
}
return {
left: leftAnims,
title: titleAnims,
titleLeft: titleLeft
};
},
/**
* Helper method used to animate elements.
* You pass it an element, objects for the from and to positions an option onEnd callback called when the animation is over.
* Normally this method is passed configurations returned from the methods such as #measureTitle(true) etc.
* It is called from the #pushLeftBoxAnimated, #pushTitleAnimated, #popBackButtonAnimated and #popTitleAnimated
* methods.
*
* If the current device is Android, it will use top/left to animate.
* If it is anything else, it will use transform.
* @private
*/
animate: function(element, config, callback) {
var me = this,
animation;
//reset the left of the element
element.setLeft(0);
config = Ext.apply(config, {
element: element,
easing: 'ease-in-out',
duration: me.getAnimation().duration || 250,
preserveEndState: true
});
animation = new Ext.fx.Animation(config);
animation.on('animationend', function() {
if (callback) {
callback.call(me);
}
}, me);
Ext.Animator.run(animation);
me.activeAnimations.push(animation);
},
endAnimation: function() {
var activeAnimations = this.activeAnimations,
animation, i, ln;
if (activeAnimations) {
ln = activeAnimations.length;
for (i = 0; i < ln; i++) {
animation = activeAnimations[i];
if (animation.isAnimating) {
animation.stopAnimation();
}
else {
animation.destroy();
}
}
this.activeAnimations = [];
}
},
refreshTitlePosition: function() {
if (!this.isAnimating) {
this.callParent();
}
},
/**
* Returns the text needed for the current back button at anytime.
* @private
*/
getBackButtonText: function() {
var text = this.backButtonStack[this.backButtonStack.length - 2],
useTitleForBackButtonText = this.getUseTitleForBackButtonText();
if (!useTitleForBackButtonText) {
if (text) {
text = this.getDefaultBackButtonText();
}
}
return text;
},
/**
* Returns the text needed for the current title at anytime.
* @private
*/
getTitleText: function() {
return this.backButtonStack[this.backButtonStack.length - 1];
},
/**
* Handles removing back button stacks from this bar
* @private
*/
beforePop: function(count) {
count--;
for (var i = 0; i < count; i++) {
this.backButtonStack.pop();
}
},
/**
* We override the hidden method because we don't want to remove it from the view using display:none. Instead we just position it off
* the screen, much like the navigation bar proxy. This means that all animations, pushing, popping etc. all still work when if you hide/show
* this bar at any time.
* @private
*/
doSetHidden: function(hidden) {
if (!hidden) {
this.element.setStyle({
position: 'relative',
top: 'auto',
left: 'auto',
width: 'auto'
});
} else {
this.element.setStyle({
position: 'absolute',
top: '-1000px',
left: '-1000px',
width: this.element.getWidth() + 'px'
});
}
},
/**
* Creates a proxy element of the passed element, and positions it in the same position, using absolute positioning.
* The createNavigationBarProxy method uses this to create proxies of the backButton and the title elements.
* @private
*/
createProxy: function(element) {
var ghost, x, y, left, width;
ghost = element.dom.cloneNode(true);
ghost.id = element.id + '-proxy';
//insert it into the toolbar
element.getParent().dom.appendChild(ghost);
//set the x/y
ghost = Ext.get(ghost);
x = element.getX();
y = element.getY();
left = element.getLeft();
width = element.getWidth();
ghost.setStyle('position', 'absolute');
ghost.setX(x);
ghost.setY(y);
ghost.setHeight(element.getHeight());
ghost.setWidth(width);
return {
x: x,
y: y,
left: left,
width: width,
ghost: ghost
};
}
});
/**
* @author Robert Dougan <rob@sencha.com>
*
* NavigationView is basically a {@link Ext.Container} with a {@link Ext.layout.Card card} layout, so only one view
* can be visible at a time. However, NavigationView also adds extra functionality on top of this to allow
* you to `push` and `pop` views at any time. When you do this, your NavigationView will automatically animate
* between your current active view, and the new view you want to `push`, or the previous view you want to `pop`.
*
* Using the NavigationView is very simple. Here is a basic example of it in action:
*
* @example
* var view = Ext.create('Ext.NavigationView', {
* fullscreen: true,
*
* items: [{
* title: 'First',
* items: [{
* xtype: 'button',
* text: 'Push a new view!',
* handler: function() {
* // use the push() method to push another view. It works much like
* // add() or setActiveItem(). it accepts a view instance, or you can give it
* // a view config.
* view.push({
* title: 'Second',
* html: 'Second view!'
* });
* }
* }]
* }]
* });
*
* Now, here comes the fun part: you can push any view/item into the NavigationView, at any time, and it will
* automatically handle the animations between the two views, including adding a back button (if necessary)
* and showing the new title.
*
* view.push({
* title: 'A new view',
* html: 'Some new content'
* });
*
* As you can see, it is as simple as calling the {@link #method-push} method, with a new view (instance or object). Done.
*
* You can also `pop` a view at any time. This will remove the top-most view from the NavigationView, and animate back
* to the previous view. You can do this using the {@link #method-pop} method (which requires no arguments).
*
* view.pop();
*
* @aside guide navigation_view
*/
Ext.define('Ext.navigation.View', {
extend: 'Ext.Container',
alternateClassName: 'Ext.NavigationView',
xtype: 'navigationview',
requires: ['Ext.navigation.Bar'],
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'navigationview',
/**
* @cfg {Boolean/Object} navigationBar
* The NavigationBar used in this navigation view. It defaults to be docked to the top.
*
* You can just pass in a normal object if you want to customize the NavigationBar. For example:
*
* navigationBar: {
* ui: 'dark',
* docked: 'bottom'
* }
*
* You **cannot** specify a *title* property in this configuration. The title of the navigationBar is taken
* from the configuration of this views children:
*
* view.push({
* title: 'This views title which will be shown in the navigation bar',
* html: 'Some HTML'
* });
*
* @accessor
*/
navigationBar: {
docked: 'top'
},
/**
* @cfg {String} defaultBackButtonText
* The text to be displayed on the back button if:
*
* - The previous view does not have a title.
* - The {@link #useTitleForBackButtonText} configuration is `true`.
* @accessor
*/
defaultBackButtonText: 'Back',
/**
* @cfg {Boolean} useTitleForBackButtonText
* Set to `false` if you always want to display the {@link #defaultBackButtonText} as the text
* on the back button. `true` if you want to use the previous views title.
* @accessor
*/
useTitleForBackButtonText: false,
/**
* @cfg {Array/Object} items The child items to add to this NavigationView. This is usually an array of Component
* configurations or instances, for example:
*
* Ext.create('Ext.Container', {
* items: [
* {
* xtype: 'panel',
* title: 'My title',
* html: 'This is an item'
* }
* ]
* });
*
* If you want a title to be displayed in the {@link #navigationBar}, you must specify a `title` configuration in your
* view, like above.
*
* __Note:__ Only one view will be visible at a time. If you want to change to another view, use the {@link #method-push} or
* {@link #setActiveItem} methods.
* @accessor
*/
/**
* @cfg
* @hide
*/
layout: {
type: 'card',
animation: {
duration: 300,
easing: 'ease-out',
type: 'slide',
direction: 'left'
}
}
// See https://sencha.jira.com/browse/TOUCH-1568
// If you do, add to #navigationBar config docs:
//
// If you want to add a button on the right of the NavigationBar,
// use the {@link #rightButton} configuration.
},
/**
* @event push
* Fires when a view is pushed into this navigation view
* @param {Ext.navigation.View} this The component instance
* @param {Mixed} view The view that has been pushed
*/
/**
* @event pop
* Fires when a view is popped from this navigation view
* @param {Ext.navigation.View} this The component instance
* @param {Mixed} view The view that has been popped
*/
/**
* @event back
* Fires when the back button in the navigation view was tapped.
* @param {Ext.navigation.View} this The component instance\
*/
// @private
initialize: function() {
var me = this,
navBar = me.getNavigationBar();
//add a listener onto the back button in the navigationbar
navBar.on({
back: me.onBackButtonTap,
scope: me
});
me.relayEvents(navBar, 'rightbuttontap');
me.relayEvents(me, {
add: 'push',
remove: 'pop'
});
//<debug>
var layout = me.getLayout();
if (layout && !layout.isCard) {
Ext.Logger.error('The base layout for a NavigationView must always be a Card Layout');
}
//</debug>
},
/**
* @private
*/
applyLayout: function(config) {
config = config || {};
return config;
},
/**
* @private
* Called when the user taps on the back button
*/
onBackButtonTap: function() {
this.pop();
this.fireEvent('back', this);
},
/**
* Pushes a new view into this navigation view using the default animation that this view has.
* @param {Object} view The view to push.
* @return {Ext.Component} The new item you just pushed.
*/
push: function(view) {
return this.add(view);
},
/**
* Removes the current active view from the stack and sets the previous view using the default animation
* of this view. You can also pass a {@link Ext.ComponentQuery} selector to target what inner item to pop to.
* @param {Number} count The number of views you want to pop.
* @return {Ext.Component} The new active item.
*/
pop: function(count) {
if (this.beforePop(count)) {
return this.doPop();
}
},
/**
* @private
* Calculates whether it needs to remove any items from the stack when you are popping more than 1
* item. If it does, it removes those views from the stack and returns `true`.
* @return {Boolean} `true` if it has removed views.
*/
beforePop: function(count) {
var me = this,
innerItems = me.getInnerItems();
if (Ext.isString(count) || Ext.isObject(count)) {
var last = innerItems.length - 1,
i;
for (i = last; i >= 0; i--) {
if ((Ext.isString(count) && Ext.ComponentQuery.is(innerItems[i], count)) || (Ext.isObject(count) && count == innerItems[i])) {
count = last - i;
break;
}
}
if (!Ext.isNumber(count)) {
return false;
}
}
var ln = innerItems.length,
toRemove;
//default to 1 pop
if (!Ext.isNumber(count) || count < 1) {
count = 1;
}
//check if we are trying to remove more items than we have
count = Math.min(count, ln - 1);
if (count) {
//we need to reset the backButtonStack in the navigation bar
me.getNavigationBar().beforePop(count);
//get the items we need to remove from the view and remove theme
toRemove = innerItems.splice(-count, count - 1);
for (i = 0; i < toRemove.length; i++) {
this.remove(toRemove[i]);
}
return true;
}
return false;
},
/**
* @private
*/
doPop: function() {
var me = this,
innerItems = this.getInnerItems();
//set the new active item to be the new last item of the stack
me.remove(innerItems[innerItems.length - 1]);
return this.getActiveItem();
},
/**
* Returns the previous item, if one exists.
* @return {Mixed} The previous view
*/
getPreviousItem: function() {
var innerItems = this.getInnerItems();
return innerItems[innerItems.length - 2];
},
/**
* Updates the backbutton text accordingly in the {@link #navigationBar}
* @private
*/
updateUseTitleForBackButtonText: function(useTitleForBackButtonText) {
var navigationBar = this.getNavigationBar();
if (navigationBar) {
navigationBar.setUseTitleForBackButtonText(useTitleForBackButtonText);
}
},
/**
* Updates the backbutton text accordingly in the {@link #navigationBar}
* @private
*/
updateDefaultBackButtonText: function(defaultBackButtonText) {
var navigationBar = this.getNavigationBar();
if (navigationBar) {
navigationBar.setDefaultBackButtonText(defaultBackButtonText);
}
},
// @private
applyNavigationBar: function(config) {
if (!config) {
config = {
hidden: true,
docked: 'top'
};
}
if (config.title) {
delete config.title;
//<debug>
Ext.Logger.warn("Ext.navigation.View: The 'navigationBar' configuration does not accept a 'title' property. You " +
"set the title of the navigationBar by giving this navigation view's children a 'title' property.");
//</debug>
}
config.view = this;
config.useTitleForBackButtonText = this.getUseTitleForBackButtonText();
return Ext.factory(config, Ext.navigation.Bar, this.getNavigationBar());
},
// @private
updateNavigationBar: function(newNavigationBar, oldNavigationBar) {
if (oldNavigationBar) {
this.remove(oldNavigationBar, true);
}
if (newNavigationBar) {
this.add(newNavigationBar);
}
},
/**
* @private
*/
applyActiveItem: function(activeItem, currentActiveItem) {
var me = this,
innerItems = me.getInnerItems();
// Make sure the items are already initialized
me.getItems();
// If we are not initialzed yet, we should set the active item to the last item in the stack
if (!me.initialized) {
activeItem = innerItems.length - 1;
}
return this.callParent([activeItem, currentActiveItem]);
},
doResetActiveItem: function(innerIndex) {
var me = this,
innerItems = me.getInnerItems(),
animation = me.getLayout().getAnimation();
if (innerIndex > 0) {
if (animation && animation.isAnimation) {
animation.setReverse(true);
}
me.setActiveItem(innerIndex - 1);
me.getNavigationBar().onViewRemove(me, innerItems[innerIndex], innerIndex);
}
},
/**
* @private
*/
doRemove: function() {
var animation = this.getLayout().getAnimation();
if (animation && animation.isAnimation) {
animation.setReverse(false);
}
this.callParent(arguments);
},
/**
* @private
*/
onItemAdd: function(item, index) {
this.doItemLayoutAdd(item, index);
if (!this.isItemsInitializing && item.isInnerItem()) {
this.setActiveItem(item);
this.getNavigationBar().onViewAdd(this, item, index);
}
if (this.initialized) {
this.fireEvent('add', this, item, index);
}
},
/**
* Resets the view by removing all items between the first and last item.
* @return {Ext.Component} The view that is now active
*/
reset: function() {
return this.pop(this.getInnerItems().length);
}
});
/**
* Adds a Load More button at the bottom of the list. When the user presses this button,
* the next page of data will be loaded into the store and appended to the List.
*
* By specifying `{@link #autoPaging}: true`, an 'infinite scroll' effect can be achieved,
* i.e., the next page of content will load automatically when the user scrolls to the
* bottom of the list.
*
* ## Example
*
* Ext.create('Ext.dataview.List', {
*
* store: Ext.create('TweetStore'),
*
* plugins: [
* {
* xclass: 'Ext.plugin.ListPaging',
* autoPaging: true
* }
* ],
*
* itemTpl: [
* '<img src="{profile_image_url}" />',
* '<div class="tweet">{text}</div>'
* ]
* });
*/
Ext.define('Ext.plugin.ListPaging', {
extend: 'Ext.Component',
alias: 'plugin.listpaging',
config: {
/**
* @cfg {Boolean} autoPaging
* True to automatically load the next page when you scroll to the bottom of the list.
*/
autoPaging: false,
/**
* @cfg {String} loadMoreText The text used as the label of the Load More button.
*/
loadMoreText: 'Load More...',
/**
* @cfg {String} noMoreRecordsText The text used as the label of the Load More button when the Store's
* {@link Ext.data.Store#totalCount totalCount} indicates that all of the records available on the server are
* already loaded
*/
noMoreRecordsText: 'No More Records',
/**
* @private
* @cfg {String} loadTpl The template used to render the load more text
*/
loadTpl: [
'<div class="{cssPrefix}loading-spinner" style="font-size: 180%; margin: 10px auto;">',
'<span class="{cssPrefix}loading-top"></span>',
'<span class="{cssPrefix}loading-right"></span>',
'<span class="{cssPrefix}loading-bottom"></span>',
'<span class="{cssPrefix}loading-left"></span>',
'</div>',
'<div class="{cssPrefix}list-paging-msg">{message}</div>'
].join(''),
/**
* @cfg {Object} loadMoreCmp
* @private
*/
loadMoreCmp: {
xtype: 'component',
baseCls: Ext.baseCSSPrefix + 'list-paging',
scrollDock: 'bottom',
docked: 'bottom',
hidden: true
},
/**
* @private
* @cfg {Boolean} loadMoreCmpAdded Indicates whether or not the load more component has been added to the List
* yet.
*/
loadMoreCmpAdded: false,
/**
* @private
* @cfg {String} loadingCls The CSS class that is added to the {@link #loadMoreCmp} while the Store is loading
*/
loadingCls: Ext.baseCSSPrefix + 'loading',
/**
* @private
* @cfg {Ext.List} list Local reference to the List this plugin is bound to
*/
list: null,
/**
* @private
* @cfg {Ext.scroll.Scroller} scroller Local reference to the List's Scroller
*/
scroller: null,
/**
* @private
* @cfg {Boolean} loading True if the plugin has initiated a Store load that has not yet completed
*/
loading: false
},
/**
* @private
* Sets up all of the references the plugin needs
*/
init: function(list) {
var scroller = list.getScrollable().getScroller(),
store = list.getStore();
this.setList(list);
this.setScroller(scroller);
this.bindStore(list.getStore());
list.setScrollToTopOnRefresh(false);
this.addLoadMoreCmp();
// We provide our own load mask so if the Store is autoLoading already disable the List's mask straight away,
// otherwise if the Store loads later allow the mask to show once then remove it thereafter
if (store) {
this.disableDataViewMask(store);
}
// The List's Store could change at any time so make sure we are informed when that happens
list.updateStore = Ext.Function.createInterceptor(list.updateStore, this.bindStore, this);
if (this.getAutoPaging()) {
scroller.on({
scrollend: this.onScrollEnd,
scope: this
});
}
},
/**
* @private
*/
bindStore: function(newStore, oldStore) {
if (oldStore) {
oldStore.un({
beforeload: this.onStoreBeforeLoad,
load: this.onStoreLoad,
scope: this
});
}
if (newStore) {
newStore.on({
beforeload: this.onStoreBeforeLoad,
load: this.onStoreLoad,
scope: this
});
}
},
/**
* @private
* Removes the List/DataView's loading mask because we show our own in the plugin. The logic here disables the
* loading mask immediately if the store is autoloading. If it's not autoloading, allow the mask to show the first
* time the Store loads, then disable it and use the plugin's loading spinner.
* @param {Ext.data.Store} store The store that is bound to the DataView
*/
disableDataViewMask: function(store) {
var list = this.getList();
if (store.isAutoLoading()) {
list.setLoadingText(null);
} else {
store.on({
load: {
single: true,
fn: function() {
list.setLoadingText(null);
}
}
});
}
},
/**
* @private
*/
applyLoadTpl: function(config) {
return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
},
/**
* @private
*/
applyLoadMoreCmp: function(config) {
config = Ext.merge(config, {
html: this.getLoadTpl().apply({
cssPrefix: Ext.baseCSSPrefix,
message: this.getLoadMoreText()
}),
listeners: {
tap: {
fn: this.loadNextPage,
scope: this,
element: 'element'
}
}
});
return Ext.factory(config, Ext.Component, this.getLoadMoreCmp());
},
/**
* @private
* If we're using autoPaging and detect that the user has scrolled to the bottom, kick off loading of the next page
*/
onScrollEnd: function(scroller, x, y) {
if (!this.getLoading() && y >= scroller.maxPosition.y) {
this.loadNextPage();
}
},
/**
* @private
* Makes sure we add/remove the loading CSS class while the Store is loading
*/
updateLoading: function(isLoading) {
var loadMoreCmp = this.getLoadMoreCmp(),
loadMoreCls = this.getLoadingCls();
if (isLoading) {
loadMoreCmp.addCls(loadMoreCls);
} else {
loadMoreCmp.removeCls(loadMoreCls);
}
},
/**
* @private
* If the Store is just about to load but it's currently empty, we hide the load more button because this is
* usually an outcome of setting a new Store on the List so we don't want the load more button to flash while
* the new Store loads
*/
onStoreBeforeLoad: function(store) {
if (store.getCount() === 0) {
this.getLoadMoreCmp().hide();
}
},
/**
* @private
*/
onStoreLoad: function(store) {
var loadCmp = this.getLoadMoreCmp(),
template = this.getLoadTpl(),
message = this.storeFullyLoaded() ? this.getNoMoreRecordsText() : this.getLoadMoreText();
if (store.getCount()) {
loadCmp.show();
this.getList().scrollDockHeightRefresh();
}
this.setLoading(false);
//if we've reached the end of the data set, switch to the noMoreRecordsText
loadCmp.setHtml(template.apply({
cssPrefix: Ext.baseCSSPrefix,
message: message
}));
},
/**
* @private
* Because the attached List's inner list element is rendered after our init function is called,
* we need to dynamically add the loadMoreCmp later. This does this once and caches the result.
*/
addLoadMoreCmp: function() {
var list = this.getList(),
cmp = this.getLoadMoreCmp();
if (!this.getLoadMoreCmpAdded()) {
list.add(cmp);
/**
* @event loadmorecmpadded Fired when the Load More component is added to the list. Fires on the List.
* @param {Ext.plugin.ListPaging} this The list paging plugin
* @param {Ext.List} list The list
*/
list.fireEvent('loadmorecmpadded', this, list);
this.setLoadMoreCmpAdded(true);
}
return cmp;
},
/**
* @private
* Returns true if the Store is detected as being fully loaded, or the server did not return a total count, which
* means we're in 'infinite' mode
* @return {Boolean}
*/
storeFullyLoaded: function() {
var store = this.getList().getStore(),
total = store.getTotalCount();
return total !== null ? store.getTotalCount() <= (store.currentPage * store.getPageSize()) : false;
},
/**
* @private
*/
loadNextPage: function() {
var me = this;
if (!me.storeFullyLoaded()) {
me.setLoading(true);
me.getList().getStore().nextPage({ addRecords: true });
}
}
});
/**
* This plugin adds pull to refresh functionality to the List.
*
* ## Example
*
* @example
* var store = Ext.create('Ext.data.Store', {
* fields: ['name', 'img', 'text'],
* data: [
* {
* name: 'rdougan',
* img: 'http://a0.twimg.com/profile_images/1261180556/171265_10150129602722922_727937921_7778997_8387690_o_reasonably_small.jpg',
* text: 'JavaScript development'
* }
* ]
* });
*
* Ext.create('Ext.dataview.List', {
* fullscreen: true,
*
* store: store,
*
* plugins: [
* {
* xclass: 'Ext.plugin.PullRefresh',
* pullRefreshText: 'Pull down for more new Tweets!'
* }
* ],
*
* itemTpl: [
* '<img src="{img}" alt="{name} photo" />',
* '<div class="tweet"><b>{name}:</b> {text}</div>'
* ]
* });
*/
Ext.define('Ext.plugin.PullRefresh', {
extend: 'Ext.Component',
alias: 'plugin.pullrefresh',
requires: ['Ext.DateExtras'],
config: {
/**
* @cfg {Ext.dataview.List} list
* The list to which this PullRefresh plugin is connected.
* This will usually by set automatically when configuring the list with this plugin.
* @accessor
*/
list: null,
/**
* @cfg {String} pullRefreshText The text that will be shown while you are pulling down.
* @accessor
*/
pullRefreshText: 'Pull down to refresh...',
/**
* @cfg {String} releaseRefreshText The text that will be shown after you have pulled down enough to show the release message.
* @accessor
*/
releaseRefreshText: 'Release to refresh...',
/**
* @cfg {String} lastUpdatedText The text to be shown in front of the last updated time.
* @accessor
*/
lastUpdatedText: 'Last Updated:',
/**
* @cfg {String} loadingText The text that will be shown while the list is refreshing.
* @accessor
*/
loadingText: 'Loading...',
/**
* @cfg {Number} snappingAnimationDuration The duration for snapping back animation after the data has been refreshed
* @accessor
*/
snappingAnimationDuration: 150,
/**
* @cfg {Function} refreshFn The function that will be called to refresh the list.
* If this is not defined, the store's load function will be called.
* The refresh function gets called with a reference to this plugin instance.
* @accessor
*/
refreshFn: null,
/**
* @cfg {Ext.XTemplate/String/Array} pullTpl The template being used for the pull to refresh markup.
* @accessor
*/
pullTpl: [
'<div class="x-list-pullrefresh">',
'<div class="x-list-pullrefresh-arrow"></div>',
'<div class="x-loading-spinner">',
'<span class="x-loading-top"></span>',
'<span class="x-loading-right"></span>',
'<span class="x-loading-bottom"></span>',
'<span class="x-loading-left"></span>',
'</div>',
'<div class="x-list-pullrefresh-wrap">',
'<h3 class="x-list-pullrefresh-message">{message}</h3>',
'<div class="x-list-pullrefresh-updated">{lastUpdatedText}&nbsp;{lastUpdated:date("m/d/Y h:iA")}</div>',
'</div>',
'</div>'
].join(''),
translatable: true
},
isRefreshing: false,
currentViewState: '',
initialize: function() {
this.callParent();
this.on({
painted: 'onPainted',
scope: this
});
},
init: function(list) {
var me = this;
me.setList(list);
me.initScrollable();
},
initScrollable: function() {
var me = this,
list = me.getList(),
store = list.getStore(),
pullTpl = me.getPullTpl(),
element = me.element,
scrollable = list.getScrollable(),
scroller;
if (!scrollable) {
return;
}
scroller = scrollable.getScroller();
me.lastUpdated = new Date();
list.container.insert(0, me);
// We provide our own load mask so if the Store is autoLoading already disable the List's mask straight away,
// otherwise if the Store loads later allow the mask to show once then remove it thereafter
if (store) {
if (store.isAutoLoading()) {
list.setLoadingText(null);
} else {
store.on({
load: {
single: true,
fn: function() {
list.setLoadingText(null);
}
}
});
}
}
pullTpl.overwrite(element, {
message: me.getPullRefreshText(),
lastUpdatedText: me.getLastUpdatedText(),
lastUpdated: me.lastUpdated
}, true);
me.loadingElement = element.getFirstChild();
me.messageEl = element.down('.x-list-pullrefresh-message');
me.updatedEl = element.down('.x-list-pullrefresh-updated');
me.maxScroller = scroller.getMaxPosition();
scroller.on({
maxpositionchange: me.setMaxScroller,
scroll: me.onScrollChange,
scope: me
});
},
onScrollableChange: function() {
this.initScrollable();
},
updateList: function(newList, oldList) {
var me = this;
if (newList && newList != oldList) {
newList.on({
order: 'after',
scrollablechange: me.onScrollableChange,
scope: me
});
} else if (oldList) {
oldList.un({
order: 'after',
scrollablechange: me.onScrollableChange,
scope: me
});
}
},
/**
* @private
* Attempts to load the newest posts via the attached List's Store's Proxy
*/
fetchLatest: function() {
var store = this.getList().getStore(),
proxy = store.getProxy(),
operation;
operation = Ext.create('Ext.data.Operation', {
page: 1,
start: 0,
model: store.getModel(),
limit: store.getPageSize(),
action: 'read',
filters: store.getRemoteFilter() ? store.getFilters() : []
});
proxy.read(operation, this.onLatestFetched, this);
},
/**
* @private
* Called after fetchLatest has finished grabbing data. Matches any returned records against what is already in the
* Store. If there is an overlap, updates the existing records with the new data and inserts the new items at the
* front of the Store. If there is no overlap, insert the new records anyway and record that there's a break in the
* timeline between the new and the old records.
*/
onLatestFetched: function(operation) {
var store = this.getList().getStore(),
oldRecords = store.getData(),
newRecords = operation.getRecords(),
length = newRecords.length,
toInsert = [],
newRecord, oldRecord, i;
for (i = 0; i < length; i++) {
newRecord = newRecords[i];
oldRecord = oldRecords.getByKey(newRecord.getId());
if (oldRecord) {
oldRecord.set(newRecord.getData());
} else {
toInsert.push(newRecord);
}
oldRecord = undefined;
}
store.insert(0, toInsert);
},
onPainted: function() {
this.pullHeight = this.loadingElement.getHeight();
},
setMaxScroller: function(scroller, position) {
this.maxScroller = position;
},
onScrollChange: function(scroller, x, y) {
if (y < 0) {
this.onBounceTop(y);
}
if (y > this.maxScroller.y) {
this.onBounceBottom(y);
}
},
/**
* @private
*/
applyPullTpl: function(config) {
return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
},
onBounceTop: function(y) {
var me = this,
pullHeight = me.pullHeight,
list = me.getList(),
scroller = list.getScrollable().getScroller();
if (!me.isReleased) {
if (!pullHeight) {
me.onPainted();
pullHeight = me.pullHeight;
}
if (!me.isRefreshing && -y >= pullHeight + 10) {
me.isRefreshing = true;
me.setViewState('release');
scroller.getContainer().onBefore({
dragend: 'onScrollerDragEnd',
single: true,
scope: me
});
}
else if (me.isRefreshing && -y < pullHeight + 10) {
me.isRefreshing = false;
me.setViewState('pull');
}
}
me.getTranslatable().translate(0, -y);
},
onScrollerDragEnd: function() {
var me = this;
if (me.isRefreshing) {
var list = me.getList(),
scroller = list.getScrollable().getScroller();
scroller.minPosition.y = -me.pullHeight;
scroller.on({
scrollend: 'loadStore',
single: true,
scope: me
});
me.isReleased = true;
}
},
loadStore: function() {
var me = this,
list = me.getList(),
scroller = list.getScrollable().getScroller();
me.setViewState('loading');
me.isReleased = false;
Ext.defer(function() {
scroller.on({
scrollend: function() {
if (me.getRefreshFn()) {
me.getRefreshFn().call(me, me);
} else {
me.fetchLatest();
}
me.resetRefreshState();
},
delay: 100,
single: true,
scope: me
});
scroller.minPosition.y = 0;
scroller.scrollTo(null, 0, true);
}, 500, me);
},
onBounceBottom: Ext.emptyFn,
setViewState: function(state) {
var me = this,
prefix = Ext.baseCSSPrefix,
messageEl = me.messageEl,
loadingElement = me.loadingElement;
if (state === me.currentViewState) {
return me;
}
me.currentViewState = state;
if (messageEl && loadingElement) {
switch (state) {
case 'pull':
messageEl.setHtml(me.getPullRefreshText());
loadingElement.removeCls([prefix + 'list-pullrefresh-release', prefix + 'list-pullrefresh-loading']);
break;
case 'release':
messageEl.setHtml(me.getReleaseRefreshText());
loadingElement.addCls(prefix + 'list-pullrefresh-release');
break;
case 'loading':
messageEl.setHtml(me.getLoadingText());
loadingElement.addCls(prefix + 'list-pullrefresh-loading');
break;
}
}
return me;
},
resetRefreshState: function() {
var me = this;
me.isRefreshing = false;
me.lastUpdated = new Date();
me.setViewState('pull');
me.updatedEl.setHtml(this.getLastUpdatedText() + '&nbsp;' + Ext.util.Format.date(me.lastUpdated, "m/d/Y h:iA"));
}
});
/**
* Used in the {@link Ext.tab.Bar} component. This shouldn't be used directly, instead use
* {@link Ext.tab.Bar} or {@link Ext.tab.Panel}.
* @private
*/
Ext.define('Ext.tab.Tab', {
extend: 'Ext.Button',
xtype: 'tab',
alternateClassName: 'Ext.Tab',
// @private
isTab: true,
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'tab',
/**
* @cfg {String} pressedCls
* The CSS class to be applied to a Tab when it is pressed.
* Providing your own CSS for this class enables you to customize the pressed state.
* @accessor
*/
pressedCls: Ext.baseCSSPrefix + 'tab-pressed',
/**
* @cfg {String} activeCls
* The CSS class to be applied to a Tab when it is active.
* Providing your own CSS for this class enables you to customize the active state.
* @accessor
*/
activeCls: Ext.baseCSSPrefix + 'tab-active',
/**
* @cfg {Boolean} active
* Set this to `true` to have the tab be active by default.
* @accessor
*/
active: false,
/**
* @cfg {String} title
* The title of the card that this tab is bound to.
* @accessor
*/
title: '&nbsp;'
},
// We need to override this so the `iconElement` is properly hidden using visibility
// when we render it.
template: [
{
tag: 'span',
reference: 'badgeElement',
hidden: true
},
{
tag: 'span',
className: Ext.baseCSSPrefix + 'button-icon',
reference: 'iconElement',
style: 'visibility: hidden !important'
},
{
tag: 'span',
reference: 'textElement',
hidden: true
}
],
updateIconCls : function(newCls, oldCls) {
this.callParent([newCls, oldCls]);
if (oldCls) {
this.removeCls('x-tab-icon');
}
if (newCls) {
this.addCls('x-tab-icon');
}
},
/**
* @event activate
* Fires when a tab is activated
* @param {Ext.tab.Tab} this
*/
/**
* @event deactivate
* Fires when a tab is deactivated
* @param {Ext.tab.Tab} this
*/
updateTitle: function(title) {
this.setText(title);
},
hideIconElement: function() {
this.iconElement.dom.style.setProperty('visibility', 'hidden', '!important');
},
showIconElement: function() {
this.iconElement.dom.style.setProperty('visibility', 'visible', '!important');
},
updateActive: function(active, oldActive) {
var activeCls = this.getActiveCls();
if (active && !oldActive) {
this.element.addCls(activeCls);
this.fireEvent('activate', this);
} else if (oldActive) {
this.element.removeCls(activeCls);
this.fireEvent('deactivate', this);
}
}
}, function() {
this.override({
activate: function() {
this.setActive(true);
},
deactivate: function() {
this.setActive(false);
}
});
});
/**
* Ext.tab.Bar is used internally by {@link Ext.tab.Panel} to create the bar of tabs that appears at the top of the tab
* panel. It's unusual to use it directly, instead see the {@link Ext.tab.Panel tab panel docs} for usage instructions.
*
* Used in the {@link Ext.tab.Panel} component to display {@link Ext.tab.Tab} components.
*
* @private
*/
Ext.define('Ext.tab.Bar', {
extend: 'Ext.Toolbar',
alternateClassName: 'Ext.TabBar',
xtype : 'tabbar',
requires: ['Ext.tab.Tab'],
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'tabbar',
// @private
defaultType: 'tab',
// @private
layout: {
type: 'hbox',
align: 'middle'
}
},
eventedConfig: {
/**
* @cfg {Number/String/Ext.Component} activeTab
* The initially activated tab. Can be specified as numeric index,
* component ID or as the component instance itself.
* @accessor
* @evented
*/
activeTab: null
},
/**
* @event tabchange
* Fired when active tab changes.
* @param {Ext.tab.Bar} this
* @param {Ext.tab.Tab} newTab The new Tab
* @param {Ext.tab.Tab} oldTab The old Tab
*/
initialize: function() {
var me = this;
me.callParent();
me.on({
tap: 'onTabTap',
delegate: '> tab',
scope : me
});
},
// @private
onTabTap: function(tab) {
this.setActiveTab(tab);
},
/**
* @private
*/
applyActiveTab: function(newActiveTab, oldActiveTab) {
if (!newActiveTab && newActiveTab !== 0) {
return;
}
var newTabInstance = this.parseActiveTab(newActiveTab);
if (!newTabInstance) {
// <debug warn>
if (oldActiveTab) {
Ext.Logger.warn('Trying to set a non-existent activeTab');
}
// </debug>
return;
}
return newTabInstance;
},
/**
* @private
* Default pack to center when docked to the bottom, otherwise default pack to left
*/
doSetDocked: function(newDocked) {
var layout = this.getLayout(),
initialConfig = this.getInitialConfig(),
pack;
if (!initialConfig.layout || !initialConfig.layout.pack) {
pack = (newDocked == 'bottom') ? 'center' : 'left';
//layout isn't guaranteed to be instantiated so must test
if (layout.isLayout) {
layout.setPack(pack);
} else {
layout.pack = (layout && layout.pack) ? layout.pack : pack;
}
}
},
/**
* @private
* Sets the active tab
*/
doSetActiveTab: function(newTab, oldTab) {
if (newTab) {
newTab.setActive(true);
}
//Check if the parent is present, if not it is destroyed
if (oldTab && oldTab.parent) {
oldTab.setActive(false);
}
},
/**
* @private
* Parses the active tab, which can be a number or string
*/
parseActiveTab: function(tab) {
//we need to call getItems to initialize the items, otherwise they will not exist yet.
if (typeof tab == 'number') {
return this.getInnerItems()[tab];
}
else if (typeof tab == 'string') {
tab = Ext.getCmp(tab);
}
return tab;
}
});
/**
* @aside guide tabs
* @aside video tabs-toolbars
* @aside example tabs
* @aside example tabs-bottom
*
* Tab Panels are a great way to allow the user to switch between several pages that are all full screen. Each
* Component in the Tab Panel gets its own Tab, which shows the Component when tapped on. Tabs can be positioned at
* the top or the bottom of the Tab Panel, and can optionally accept title and icon configurations.
*
* Here's how we can set up a simple Tab Panel with tabs at the bottom. Use the controls at the top left of the example
* to toggle between code mode and live preview mode (you can also edit the code and see your changes in the live
* preview):
*
* @example miniphone preview
* Ext.create('Ext.TabPanel', {
* fullscreen: true,
* tabBarPosition: 'bottom',
*
* defaults: {
* styleHtmlContent: true
* },
*
* items: [
* {
* title: 'Home',
* iconCls: 'home',
* html: 'Home Screen'
* },
* {
* title: 'Contact',
* iconCls: 'user',
* html: 'Contact Screen'
* }
* ]
* });
* One tab was created for each of the {@link Ext.Panel panels} defined in the items array. Each tab automatically uses
* the title and icon defined on the item configuration, and switches to that item when tapped on. We can also position
* the tab bar at the top, which makes our Tab Panel look like this:
*
* @example miniphone preview
* Ext.create('Ext.TabPanel', {
* fullscreen: true,
*
* defaults: {
* styleHtmlContent: true
* },
*
* items: [
* {
* title: 'Home',
* html: 'Home Screen'
* },
* {
* title: 'Contact',
* html: 'Contact Screen'
* }
* ]
* });
*
*/
Ext.define('Ext.tab.Panel', {
extend: 'Ext.Container',
xtype : 'tabpanel',
alternateClassName: 'Ext.TabPanel',
requires: ['Ext.tab.Bar'],
config: {
/**
* @cfg {String} ui
* Sets the UI of this component.
* Available values are: `light` and `dark`.
* @accessor
*/
ui: 'dark',
/**
* @cfg {Object} tabBar
* An Ext.tab.Bar configuration.
* @accessor
*/
tabBar: true,
/**
* @cfg {String} tabBarPosition
* The docked position for the {@link #tabBar} instance.
* Possible values are 'top' and 'bottom'.
* @accessor
*/
tabBarPosition: 'top',
/**
* @cfg layout
* @inheritdoc
*/
layout: {
type: 'card',
animation: {
type: 'slide',
direction: 'left'
}
},
/**
* @cfg cls
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'tabpanel'
/**
* @cfg {Boolean/String/Object} scrollable
* @accessor
* @hide
*/
/**
* @cfg {Boolean/String/Object} scroll
* @hide
*/
},
delegateListeners: {
delegate: '> component',
centeredchange: 'onItemCenteredChange',
dockedchange: 'onItemDockedChange',
floatingchange: 'onItemFloatingChange',
disabledchange: 'onItemDisabledChange'
},
initialize: function() {
this.callParent();
this.on({
order: 'before',
activetabchange: 'doTabChange',
delegate: '> tabbar',
scope : this
});
},
/**
* Tab panels should not be scrollable. Instead, you should add scrollable to any item that
* you want to scroll.
* @private
*/
applyScrollable: function() {
return false;
},
/**
* Updates the Ui for this component and the {@link #tabBar}.
*/
updateUi: function(newUi, oldUi) {
this.callParent(arguments);
if (this.initialized) {
this.getTabBar().setUi(newUi);
}
},
/**
* @private
*/
doSetActiveItem: function(newActiveItem, oldActiveItem) {
if (newActiveItem) {
var items = this.getInnerItems(),
oldIndex = items.indexOf(oldActiveItem),
newIndex = items.indexOf(newActiveItem),
reverse = oldIndex > newIndex,
animation = this.getLayout().getAnimation(),
tabBar = this.getTabBar(),
oldTab = tabBar.parseActiveTab(oldIndex),
newTab = tabBar.parseActiveTab(newIndex);
if (animation && animation.setReverse) {
animation.setReverse(reverse);
}
this.callParent(arguments);
if (newIndex != -1) {
this.forcedChange = true;
tabBar.setActiveTab(newIndex);
this.forcedChange = false;
if (oldTab) {
oldTab.setActive(false);
}
if (newTab) {
newTab.setActive(true);
}
}
}
},
/**
* Updates this container with the new active item.
* @param {Object} tabBar
* @param {Object} newTab
* @return {Boolean}
*/
doTabChange: function(tabBar, newTab) {
var oldActiveItem = this.getActiveItem(),
newActiveItem;
this.setActiveItem(tabBar.indexOf(newTab));
newActiveItem = this.getActiveItem();
return this.forcedChange || oldActiveItem !== newActiveItem;
},
/**
* Creates a new {@link Ext.tab.Bar} instance using {@link Ext#factory}.
* @param {Object} config
* @return {Object}
* @private
*/
applyTabBar: function(config) {
if (config === true) {
config = {};
}
if (config) {
Ext.applyIf(config, {
ui: this.getUi(),
docked: this.getTabBarPosition()
});
}
return Ext.factory(config, Ext.tab.Bar, this.getTabBar());
},
/**
* Adds the new {@link Ext.tab.Bar} instance into this container.
* @private
*/
updateTabBar: function(newTabBar) {
if (newTabBar) {
this.add(newTabBar);
this.setTabBarPosition(newTabBar.getDocked());
}
},
/**
* Updates the docked position of the {@link #tabBar}.
* @private
*/
updateTabBarPosition: function(position) {
var tabBar = this.getTabBar();
if (tabBar) {
tabBar.setDocked(position);
}
},
onItemAdd: function(card) {
var me = this;
if (!card.isInnerItem()) {
return me.callParent(arguments);
}
var tabBar = me.getTabBar(),
initialConfig = card.getInitialConfig(),
tabConfig = initialConfig.tab || {},
tabTitle = (card.getTitle) ? card.getTitle() : initialConfig.title,
tabIconCls = (card.getIconCls) ? card.getIconCls() : initialConfig.iconCls,
tabHidden = (card.getHidden) ? card.getHidden() : initialConfig.hidden,
tabDisabled = (card.getDisabled) ? card.getDisabled() : initialConfig.disabled,
tabBadgeText = (card.getBadgeText) ? card.getBadgeText() : initialConfig.badgeText,
innerItems = me.getInnerItems(),
index = innerItems.indexOf(card),
tabs = tabBar.getItems(),
activeTab = tabBar.getActiveTab(),
currentTabInstance = (tabs.length >= innerItems.length) && tabs.getAt(index),
tabInstance;
if (tabTitle && !tabConfig.title) {
tabConfig.title = tabTitle;
}
if (tabIconCls && !tabConfig.iconCls) {
tabConfig.iconCls = tabIconCls;
}
if (tabHidden && !tabConfig.hidden) {
tabConfig.hidden = tabHidden;
}
if (tabDisabled && !tabConfig.disabled) {
tabConfig.disabled = tabDisabled;
}
if (tabBadgeText && !tabConfig.badgeText) {
tabConfig.badgeText = tabBadgeText;
}
//<debug warn>
if (!currentTabInstance && !tabConfig.title && !tabConfig.iconCls) {
if (!tabConfig.title && !tabConfig.iconCls) {
Ext.Logger.error('Adding a card to a tab container without specifying any tab configuration');
}
}
//</debug>
tabInstance = Ext.factory(tabConfig, Ext.tab.Tab, currentTabInstance);
if (!currentTabInstance) {
tabBar.insert(index, tabInstance);
}
card.tab = tabInstance;
me.callParent(arguments);
if (!activeTab && activeTab !== 0) {
tabBar.setActiveTab(tabBar.getActiveItem());
}
},
/**
* If an item gets enabled/disabled and it has an tab, we should also enable/disable that tab
* @private
*/
onItemDisabledChange: function(item, newDisabled) {
if (item && item.tab) {
item.tab.setDisabled(newDisabled);
}
},
// @private
onItemRemove: function(item, index) {
this.getTabBar().remove(item.tab, this.getAutoDestroy());
this.callParent(arguments);
}
}, function() {
});
Ext.define('Ext.table.Cell', {
extend: 'Ext.Container',
xtype: 'tablecell',
config: {
baseCls: 'x-table-cell'
},
getElementConfig: function() {
var config = this.callParent();
config.children.length = 0;
return config;
}
});
Ext.define('Ext.table.Row', {
extend: 'Ext.table.Cell',
xtype: 'tablerow',
config: {
baseCls: 'x-table-row',
defaultType: 'tablecell'
}
});
Ext.define('Ext.table.Table', {
extend: 'Ext.Container',
requires: ['Ext.table.Row'],
xtype: 'table',
config: {
baseCls: 'x-table',
defaultType: 'tablerow'
},
cachedConfig: {
fixedLayout: false
},
fixedLayoutCls: 'x-table-fixed',
updateFixedLayout: function(fixedLayout) {
this.innerElement[fixedLayout ? 'addCls' : 'removeCls'](this.fixedLayoutCls);
}
});
/**
*
*/
Ext.define('Ext.util.Droppable', {
mixins: {
observable: 'Ext.mixin.Observable'
},
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'droppable'
},
/**
* @cfg {String} activeCls
* The CSS added to a Droppable when a Draggable in the same group is being
* dragged.
*/
activeCls: Ext.baseCSSPrefix + 'drop-active',
/**
* @cfg {String} invalidCls
* The CSS class to add to the droppable when dragging a draggable that is
* not in the same group.
*/
invalidCls: Ext.baseCSSPrefix + 'drop-invalid',
/**
* @cfg {String} hoverCls
* The CSS class to add to the droppable when hovering over a valid drop.
*/
hoverCls: Ext.baseCSSPrefix + 'drop-hover',
/**
* @cfg {String} validDropMode
* Determines when a drop is considered 'valid' whether it simply need to
* intersect the region or if it needs to be contained within the region.
* Valid values are: 'intersects' or 'contains'
*/
validDropMode: 'intersect',
/**
* @cfg {Boolean} disabled
*/
disabled: false,
/**
* @cfg {String} group
* Draggable and Droppable objects can participate in a group which are
* capable of interacting.
*/
group: 'base',
// not yet implemented
tolerance: null,
// @private
monitoring: false,
/**
* Creates new Droppable.
* @param {Mixed} el String, HtmlElement or Ext.Element representing an
* element on the page.
* @param {Object} config Configuration options for this class.
*/
constructor: function(el, config) {
var me = this;
config = config || {};
Ext.apply(me, config);
/**
* @event dropactivate
* @param {Ext.util.Droppable} this
* @param {Ext.util.Draggable} draggable
* @param {Ext.event.Event} e
*/
/**
* @event dropdeactivate
* @param {Ext.util.Droppable} this
* @param {Ext.util.Draggable} draggable
* @param {Ext.event.Event} e
*/
/**
* @event dropenter
* @param {Ext.util.Droppable} this
* @param {Ext.util.Draggable} draggable
* @param {Ext.event.Event} e
*/
/**
* @event dropleave
* @param {Ext.util.Droppable} this
* @param {Ext.util.Draggable} draggable
* @param {Ext.event.Event} e
*/
/**
* @event drop
* @param {Ext.util.Droppable} this
* @param {Ext.util.Draggable} draggable
* @param {Ext.event.Event} e
*/
me.el = Ext.get(el);
me.callParent();
me.mixins.observable.constructor.call(me);
if (!me.disabled) {
me.enable();
}
me.el.addCls(me.baseCls);
},
// @private
onDragStart: function(draggable, e) {
if (draggable.group === this.group) {
this.monitoring = true;
this.el.addCls(this.activeCls);
this.region = this.el.getPageBox(true);
draggable.on({
drag: this.onDrag,
beforedragend: this.onBeforeDragEnd,
dragend: this.onDragEnd,
scope: this
});
if (this.isDragOver(draggable)) {
this.setCanDrop(true, draggable, e);
}
this.fireEvent('dropactivate', this, draggable, e);
}
else {
draggable.on({
dragend: function() {
this.el.removeCls(this.invalidCls);
},
scope: this,
single: true
});
this.el.addCls(this.invalidCls);
}
},
// @private
isDragOver: function(draggable, region) {
return this.region[this.validDropMode](draggable.region);
},
// @private
onDrag: function(draggable, e) {
this.setCanDrop(this.isDragOver(draggable), draggable, e);
},
// @private
setCanDrop: function(canDrop, draggable, e) {
if (canDrop && !this.canDrop) {
this.canDrop = true;
this.el.addCls(this.hoverCls);
this.fireEvent('dropenter', this, draggable, e);
}
else if (!canDrop && this.canDrop) {
this.canDrop = false;
this.el.removeCls(this.hoverCls);
this.fireEvent('dropleave', this, draggable, e);
}
},
// @private
onBeforeDragEnd: function(draggable, e) {
draggable.cancelRevert = this.canDrop;
},
// @private
onDragEnd: function(draggable, e) {
this.monitoring = false;
this.el.removeCls(this.activeCls);
draggable.un({
drag: this.onDrag,
beforedragend: this.onBeforeDragEnd,
dragend: this.onDragEnd,
scope: this
});
if (this.canDrop) {
this.canDrop = false;
this.el.removeCls(this.hoverCls);
this.fireEvent('drop', this, draggable, e);
}
this.fireEvent('dropdeactivate', this, draggable, e);
},
/**
* Enable the Droppable target.
* This is invoked immediately after constructing a Droppable if the
* disabled parameter is NOT set to true.
*/
enable: function() {
if (!this.mgr) {
this.mgr = Ext.util.Observable.observe(Ext.util.Draggable);
}
this.mgr.on({
dragstart: this.onDragStart,
scope: this
});
this.disabled = false;
},
/**
* Disable the Droppable target.
*/
disable: function() {
this.mgr.un({
dragstart: this.onDragStart,
scope: this
});
this.disabled = true;
},
/**
* Method to determine whether this Component is currently disabled.
* @return {Boolean} the disabled state of this Component.
*/
isDisabled: function() {
return this.disabled;
},
/**
* Method to determine whether this Droppable is currently monitoring drag operations of Draggables.
* @return {Boolean} the monitoring state of this Droppable
*/
isMonitoring: function() {
return this.monitoring;
}
});
/*
http://www.JSON.org/json2.js
2010-03-20
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, strict: false */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
if (!this.JSON) {
this.JSON = {};
}
(function () {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ?
'"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' :
'"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' :
gap ? '[\n' + gap +
partial.join(',\n' + gap) + '\n' +
mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
k = rep[i];
if (typeof k === 'string') {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' :
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
mind + '}' : '{' + partial.join(',') + '}';
gap = mind;
return v;
}
return v;
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());
/**
* @class Ext.util.JSON
* Modified version of Douglas Crockford"s json.js that doesn"t
* mess with the Object prototype
* http://www.json.org/js.html
* @singleton
* @ignore
*/
Ext.util.JSON = {
encode: function(o) {
return JSON.stringify(o);
},
decode: function(s) {
return JSON.parse(s);
}
};
/**
* Shorthand for {@link Ext.util.JSON#encode}
* @param {Mixed} o The variable to encode
* @return {String} The JSON string
* @member Ext
* @method encode
* @ignore
*/
Ext.encode = Ext.util.JSON.encode;
/**
* Shorthand for {@link Ext.util.JSON#decode}
* @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
* @member Ext
* @method decode
* @ignore
*/
Ext.decode = Ext.util.JSON.decode;
/**
* @class Ext.util.translatable.CssPosition
* @private
*/
Ext.define('Ext.util.translatable.CssPosition', {
extend: 'Ext.util.translatable.Dom',
doTranslate: function(x, y) {
var domStyle = this.getElement().dom.style;
if (typeof x == 'number') {
domStyle.left = x + 'px';
}
if (typeof y == 'number') {
domStyle.top = y + 'px';
}
},
destroy: function() {
var domStyle = this.getElement().dom.style;
domStyle.left = null;
domStyle.top = null;
this.callParent(arguments);
}
});
Ext.define('Ext.ux.Faker', {
config: {
names: ['Ed Spencer', 'Tommy Maintz', 'Rob Dougan', 'Jamie Avins', 'Jacky Nguyen'],
emails: ['ed@sencha.com', 'tommy@sencha.com', 'rob@sencha.com', 'jamie@sencha.com', 'jacky@sencha.com'],
lorem: [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus eget neque nec sem semper cursus. Fusce ",
"molestie nibh nec ligula gravida et porta enim luctus. Curabitur id accumsan dolor. Vestibulum ultricies ",
"vehicula erat vel elementum. Mauris urna odio, dignissim sit amet molestie sit amet, sodales vel metus. Ut eu ",
"volutpat nulla. Morbi ut est sed eros egestas gravida quis eget eros. Proin sit amet massa nunc. Proin congue ",
"mollis mollis. Morbi sollicitudin nisl at diam placerat eu dignissim magna rutrum.\n",
"Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus eu ",
"vestibulum lectus. Fusce a eros metus. Vivamus vel aliquet neque. Ut eu purus ipsum. Nullam id leo hendrerit ",
"augue imperdiet malesuada ac eget velit. Quisque congue turpis eget ante mollis ut sollicitudin massa dapibus. ",
"Sed magna dolor, dictum sit amet aliquam eu, ultricies sit amet diam. Fusce tempor porta tellus vitae ",
"pulvinar. Aenean velit ligula, fermentum non imperdiet et, suscipit sed libero. Aliquam ac ligula ut dui ",
"pharetra dictum vel vel nunc. Phasellus semper, ligula id tristique ullamcorper, tortor diam mollis erat, sed ",
"feugiat nisl nisi sit amet sem. Maecenas nec mi vitae ligula malesuada pellentesque.\n",
"Quisque diam velit, suscipit sit amet ornare eu, congue sed quam. Integer rhoncus luctus mi, sed pulvinar ",
"lectus lobortis non. Sed egestas orci nec elit sagittis eu condimentum massa volutpat. Fusce blandit congue ",
"enim venenatis lacinia. Donec enim sapien, sollicitudin at placerat non, vehicula ut nisi. Aliquam volutpat ",
"metus sit amet lacus condimentum fermentum. Aliquam congue scelerisque leo ut tristique."
].join(""),
subjects: [
"Order more widgets",
"You're crazy",
"Jacky is not his real name",
"Why am I here?",
"This is totally broken",
"When do we ship?",
"Top Secret",
"There's always money in the banana stand"
]
},
oneOf: function(set) {
return set[Math.floor(Math.random() * set.length)];
},
name: function() {
return this.oneOf(this.getNames());
},
email: function() {
return this.oneOf(this.getSubjects());
},
subject: function() {
return this.oneOf(this.getSubjects());
},
lorem: function(paragraphs) {
var lorem = this.getLorem();
if (paragraphs) {
return lorem.split("\n").slice(0, paragraphs).join("\n");
} else {
return lorem;
}
}
});
Ext.define('Ext.ux.auth.Session', {
constructor: function(credentials) {
credentials = {
username: 'ed',
password: 'secret'
}
},
validate: function(options) {
options = {
success: function(session) {
},
failure: function(session) {
},
callback: function(session) {
},
scope: me
}
},
destroy: function() {
}
});
Ext.define('Ext.ux.auth.model.Session', {
fields: ['username', 'created_at', 'expires_at'],
validate: function() {
},
destroy: function() {
}
});
/**
* @private
* Base class for iOS and Android viewports.
*/
Ext.define('Ext.viewport.Default', {
extend: 'Ext.Container',
xtype: 'viewport',
PORTRAIT: 'portrait',
LANDSCAPE: 'landscape',
requires: [
'Ext.LoadMask',
'Ext.layout.Card'
],
/**
* @event ready
* Fires when the Viewport is in the DOM and ready.
* @param {Ext.Viewport} this
*/
/**
* @event maximize
* Fires when the Viewport is maximized.
* @param {Ext.Viewport} this
*/
/**
* @event orientationchange
* Fires when the Viewport orientation has changed.
* @param {Ext.Viewport} this
* @param {String} newOrientation The new orientation.
* @param {Number} width The width of the Viewport.
* @param {Number} height The height of the Viewport.
*/
config: {
/**
* @cfg {Boolean} autoMaximize
* Whether or not to always automatically maximize the viewport on first load and all subsequent orientation changes.
*
* This is set to `false` by default for a number of reasons:
*
* - Orientation change performance is drastically reduced when this is enabled, on all devices.
* - On some devices (mostly Android) this can sometimes cause issues when the default browser zoom setting is changed.
* - When wrapping your phone in a native shell, you may get a blank screen.
* - When bookmarked to the homescreen (iOS), you may get a blank screen.
*
* @accessor
*/
autoMaximize: false,
/**
* @private
*/
autoBlurInput: true,
/**
* @cfg {Boolean} preventPanning
* Whether or not to always prevent default panning behavior of the
* browser's viewport.
* @accessor
*/
preventPanning: true,
/**
* @cfg {Boolean} preventZooming
* `true` to attempt to stop zooming when you double tap on the screen on mobile devices,
* typically HTC devices with HTC Sense UI.
* @accessor
*/
preventZooming: false,
/**
* @cfg
* @private
*/
autoRender: true,
/**
* @cfg {Object/String} layout Configuration for this Container's layout. Example:
*
* Ext.create('Ext.Container', {
* layout: {
* type: 'hbox',
* align: 'middle'
* },
* items: [
* {
* xtype: 'panel',
* flex: 1,
* style: 'background-color: red;'
* },
* {
* xtype: 'panel',
* flex: 2,
* style: 'background-color: green'
* }
* ]
* });
*
* See the [layouts guide](#!/guides/layouts) for more information.
*
* @accessor
*/
layout: 'card',
/**
* @cfg
* @private
*/
width: '100%',
/**
* @cfg
* @private
*/
height: '100%',
useBodyElement: true
},
/**
* @property {Boolean} isReady
* `true` if the DOM is ready.
*/
isReady: false,
isViewport: true,
isMaximizing: false,
id: 'ext-viewport',
isInputRegex: /^(input|textarea|select|a)$/i,
focusedElement: null,
/**
* @private
*/
fullscreenItemCls: Ext.baseCSSPrefix + 'fullscreen',
constructor: function(config) {
var bind = Ext.Function.bind;
this.doPreventPanning = bind(this.doPreventPanning, this);
this.doPreventZooming = bind(this.doPreventZooming, this);
this.doBlurInput = bind(this.doBlurInput, this);
this.maximizeOnEvents = ['ready', 'orientationchange'];
this.orientation = this.determineOrientation();
this.windowWidth = this.getWindowWidth();
this.windowHeight = this.getWindowHeight();
this.windowOuterHeight = this.getWindowOuterHeight();
if (!this.stretchHeights) {
this.stretchHeights = {};
}
this.callParent([config]);
// Android is handled separately
if (!Ext.os.is.Android || Ext.browser.name == 'ChromeMobile') {
if (this.supportsOrientation()) {
this.addWindowListener('orientationchange', bind(this.onOrientationChange, this));
}
else {
this.addWindowListener('resize', bind(this.onResize, this));
}
}
document.addEventListener('focus', bind(this.onElementFocus, this), true);
document.addEventListener('blur', bind(this.onElementBlur, this), true);
Ext.onDocumentReady(this.onDomReady, this);
this.on('ready', this.onReady, this, {single: true});
this.getEventDispatcher().addListener('component', '*', 'fullscreen', 'onItemFullscreenChange', this);
return this;
},
onDomReady: function() {
this.isReady = true;
this.updateSize();
this.fireEvent('ready', this);
},
onReady: function() {
if (this.getAutoRender()) {
this.render();
}
},
onElementFocus: function(e) {
this.focusedElement = e.target;
},
onElementBlur: function() {
this.focusedElement = null;
},
render: function() {
if (!this.rendered) {
var body = Ext.getBody(),
clsPrefix = Ext.baseCSSPrefix,
classList = [],
osEnv = Ext.os,
osName = osEnv.name.toLowerCase(),
browserName = Ext.browser.name.toLowerCase(),
osMajorVersion = osEnv.version.getMajor(),
orientation = this.getOrientation();
this.renderTo(body);
classList.push(clsPrefix + osEnv.deviceType.toLowerCase());
if (osEnv.is.iPad) {
classList.push(clsPrefix + 'ipad');
}
classList.push(clsPrefix + osName);
classList.push(clsPrefix + browserName);
if (osMajorVersion) {
classList.push(clsPrefix + osName + '-' + osMajorVersion);
}
if (osEnv.is.BlackBerry) {
classList.push(clsPrefix + 'bb');
}
if (Ext.browser.is.Standalone) {
classList.push(clsPrefix + 'standalone');
}
classList.push(clsPrefix + orientation);
body.addCls(classList);
}
},
applyAutoBlurInput: function(autoBlurInput) {
var touchstart = (Ext.feature.has.Touch) ? 'touchstart' : 'mousedown';
if (autoBlurInput) {
this.addWindowListener(touchstart, this.doBlurInput, false);
}
else {
this.removeWindowListener(touchstart, this.doBlurInput, false);
}
return autoBlurInput;
},
applyAutoMaximize: function(autoMaximize) {
if (Ext.browser.is.WebView) {
autoMaximize = false;
}
if (autoMaximize) {
this.on('ready', 'doAutoMaximizeOnReady', this, { single: true });
this.on('orientationchange', 'doAutoMaximizeOnOrientationChange', this);
}
else {
this.un('ready', 'doAutoMaximizeOnReady', this);
this.un('orientationchange', 'doAutoMaximizeOnOrientationChange', this);
}
return autoMaximize;
},
applyPreventPanning: function(preventPanning) {
if (preventPanning) {
this.addWindowListener('touchmove', this.doPreventPanning, false);
}
else {
this.removeWindowListener('touchmove', this.doPreventPanning, false);
}
return preventPanning;
},
applyPreventZooming: function(preventZooming) {
var touchstart = (Ext.feature.has.Touch) ? 'touchstart' : 'mousedown';
if (preventZooming) {
this.addWindowListener(touchstart, this.doPreventZooming, false);
}
else {
this.removeWindowListener(touchstart, this.doPreventZooming, false);
}
return preventZooming;
},
doAutoMaximizeOnReady: function() {
var controller = arguments[arguments.length - 1];
controller.pause();
this.isMaximizing = true;
this.on('maximize', function() {
this.isMaximizing = false;
this.updateSize();
controller.resume();
this.fireEvent('ready', this);
}, this, { single: true });
this.maximize();
},
doAutoMaximizeOnOrientationChange: function() {
var controller = arguments[arguments.length - 1],
firingArguments = controller.firingArguments;
controller.pause();
this.isMaximizing = true;
this.on('maximize', function() {
this.isMaximizing = false;
this.updateSize();
firingArguments[2] = this.windowWidth;
firingArguments[3] = this.windowHeight;
controller.resume();
}, this, { single: true });
this.maximize();
},
doBlurInput: function(e) {
var target = e.target,
focusedElement = this.focusedElement;
if (focusedElement && !this.isInputRegex.test(target.tagName)) {
delete this.focusedElement;
focusedElement.blur();
}
},
doPreventPanning: function(e) {
e.preventDefault();
},
doPreventZooming: function(e) {
// Don't prevent right mouse event
if ('button' in e && e.button !== 0) {
return;
}
var target = e.target;
if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName)) {
e.preventDefault();
}
},
addWindowListener: function(eventName, fn, capturing) {
window.addEventListener(eventName, fn, Boolean(capturing));
},
removeWindowListener: function(eventName, fn, capturing) {
window.removeEventListener(eventName, fn, Boolean(capturing));
},
doAddListener: function(eventName, fn, scope, options) {
if (eventName === 'ready' && this.isReady && !this.isMaximizing) {
fn.call(scope);
return this;
}
return this.callSuper(arguments);
},
supportsOrientation: function() {
return Ext.feature.has.Orientation;
},
onResize: function() {
var oldWidth = this.windowWidth,
oldHeight = this.windowHeight,
width = this.getWindowWidth(),
height = this.getWindowHeight(),
currentOrientation = this.getOrientation(),
newOrientation = this.determineOrientation();
// Determine orientation change via resize. BOTH width AND height much change, otherwise
// this is a keyboard popping up.
if ((oldWidth !== width && oldHeight !== height) && currentOrientation !== newOrientation) {
this.fireOrientationChangeEvent(newOrientation, currentOrientation);
}
},
onOrientationChange: function() {
var currentOrientation = this.getOrientation(),
newOrientation = this.determineOrientation();
if (newOrientation !== currentOrientation) {
this.fireOrientationChangeEvent(newOrientation, currentOrientation);
}
},
fireOrientationChangeEvent: function(newOrientation, oldOrientation) {
var clsPrefix = Ext.baseCSSPrefix;
Ext.getBody().replaceCls(clsPrefix + oldOrientation, clsPrefix + newOrientation);
this.orientation = newOrientation;
this.updateSize();
this.fireEvent('orientationchange', this, newOrientation, this.windowWidth, this.windowHeight);
},
updateSize: function(width, height) {
this.windowWidth = width !== undefined ? width : this.getWindowWidth();
this.windowHeight = height !== undefined ? height : this.getWindowHeight();
return this;
},
waitUntil: function(condition, onSatisfied, onTimeout, delay, timeoutDuration) {
if (!delay) {
delay = 50;
}
if (!timeoutDuration) {
timeoutDuration = 2000;
}
var scope = this,
elapse = 0;
setTimeout(function repeat() {
elapse += delay;
if (condition.call(scope) === true) {
if (onSatisfied) {
onSatisfied.call(scope);
}
}
else {
if (elapse >= timeoutDuration) {
if (onTimeout) {
onTimeout.call(scope);
}
}
else {
setTimeout(repeat, delay);
}
}
}, delay);
},
maximize: function() {
this.fireMaximizeEvent();
},
fireMaximizeEvent: function() {
this.updateSize();
this.fireEvent('maximize', this);
},
doSetHeight: function(height) {
Ext.getBody().setHeight(height);
this.callParent(arguments);
},
doSetWidth: function(width) {
Ext.getBody().setWidth(width);
this.callParent(arguments);
},
scrollToTop: function() {
window.scrollTo(0, -1);
},
/**
* Retrieves the document width.
* @return {Number} width in pixels.
*/
getWindowWidth: function() {
return window.innerWidth;
},
/**
* Retrieves the document height.
* @return {Number} height in pixels.
*/
getWindowHeight: function() {
return window.innerHeight;
},
getWindowOuterHeight: function() {
return window.outerHeight;
},
getWindowOrientation: function() {
return window.orientation;
},
/**
* Returns the current orientation.
* @return {String} `portrait` or `landscape`
*/
getOrientation: function() {
return this.orientation;
},
getSize: function() {
return {
width: this.windowWidth,
height: this.windowHeight
};
},
determineOrientation: function() {
var portrait = this.PORTRAIT,
landscape = this.LANDSCAPE;
if (this.supportsOrientation()) {
if (this.getWindowOrientation() % 180 === 0) {
return portrait;
}
return landscape;
}
else {
if (this.getWindowHeight() >= this.getWindowWidth()) {
return portrait;
}
return landscape;
}
},
onItemFullscreenChange: function(item) {
item.addCls(this.fullscreenItemCls);
this.add(item);
}
});
/**
* @private
* Android version of viewport.
*/
Ext.define('Ext.viewport.Android', {
extend: 'Ext.viewport.Default',
constructor: function() {
this.on('orientationchange', 'doFireOrientationChangeEvent', this, { prepend: true });
this.on('orientationchange', 'hideKeyboardIfNeeded', this, { prepend: true });
this.callParent(arguments);
this.addWindowListener('resize', Ext.Function.bind(this.onResize, this));
},
getDummyInput: function() {
var input = this.dummyInput,
focusedElement = this.focusedElement,
box = Ext.fly(focusedElement).getPageBox();
if (!input) {
this.dummyInput = input = document.createElement('input');
input.style.position = 'absolute';
input.style.opacity = '0';
document.body.appendChild(input);
}
input.style.left = box.left + 'px';
input.style.top = box.top + 'px';
input.style.display = '';
return input;
},
doBlurInput: function(e) {
var target = e.target,
focusedElement = this.focusedElement,
dummy;
if (focusedElement && !this.isInputRegex.test(target.tagName)) {
dummy = this.getDummyInput();
delete this.focusedElement;
dummy.focus();
setTimeout(function() {
dummy.style.display = 'none';
}, 100);
}
},
hideKeyboardIfNeeded: function() {
var eventController = arguments[arguments.length - 1],
focusedElement = this.focusedElement;
if (focusedElement) {
delete this.focusedElement;
eventController.pause();
if (Ext.os.version.lt('4')) {
focusedElement.style.display = 'none';
}
else {
focusedElement.blur();
}
setTimeout(function() {
focusedElement.style.display = '';
eventController.resume();
}, 1000);
}
},
doFireOrientationChangeEvent: function() {
var eventController = arguments[arguments.length - 1];
this.orientationChanging = true;
eventController.pause();
this.waitUntil(function() {
return this.getWindowOuterHeight() !== this.windowOuterHeight;
}, function() {
this.windowOuterHeight = this.getWindowOuterHeight();
this.updateSize();
eventController.firingArguments[2] = this.windowWidth;
eventController.firingArguments[3] = this.windowHeight;
eventController.resume();
this.orientationChanging = false;
}, function() {
//<debug error>
Ext.Logger.error("Timeout waiting for viewport's outerHeight to change before firing orientationchange", this);
//</debug>
});
return this;
},
applyAutoMaximize: function(autoMaximize) {
autoMaximize = this.callParent(arguments);
this.on('add', 'fixSize', this, { single: true });
if (!autoMaximize) {
this.on('ready', 'fixSize', this, { single: true });
this.onAfter('orientationchange', 'doFixSize', this, { buffer: 100 });
}
else {
this.un('ready', 'fixSize', this);
this.unAfter('orientationchange', 'doFixSize', this);
}
},
fixSize: function() {
this.doFixSize();
},
doFixSize: function() {
this.setHeight(this.getWindowHeight());
},
determineOrientation: function() {
return (this.getWindowHeight() >= this.getWindowWidth()) ? this.PORTRAIT : this.LANDSCAPE;
},
getActualWindowOuterHeight: function() {
return Math.round(this.getWindowOuterHeight() / window.devicePixelRatio);
},
maximize: function() {
var stretchHeights = this.stretchHeights,
orientation = this.orientation,
height;
height = stretchHeights[orientation];
if (!height) {
stretchHeights[orientation] = height = this.getActualWindowOuterHeight();
}
if (!this.addressBarHeight) {
this.addressBarHeight = height - this.getWindowHeight();
}
this.setHeight(height);
var isHeightMaximized = Ext.Function.bind(this.isHeightMaximized, this, [height]);
this.scrollToTop();
this.waitUntil(isHeightMaximized, this.fireMaximizeEvent, this.fireMaximizeEvent);
},
isHeightMaximized: function(height) {
this.scrollToTop();
return this.getWindowHeight() === height;
}
}, function() {
if (!Ext.os.is.Android) {
return;
}
var version = Ext.os.version,
userAgent = Ext.browser.userAgent,
// These Android devices have a nasty bug which causes JavaScript timers to be completely frozen
// when the browser's viewport is being panned.
isBuggy = /(htc|desire|incredible|ADR6300)/i.test(userAgent) && version.lt('2.3');
if (isBuggy) {
this.override({
constructor: function(config) {
if (!config) {
config = {};
}
config.autoMaximize = false;
this.watchDogTick = Ext.Function.bind(this.watchDogTick, this);
setInterval(this.watchDogTick, 1000);
return this.callParent([config]);
},
watchDogTick: function() {
this.watchDogLastTick = Ext.Date.now();
},
doPreventPanning: function() {
var now = Ext.Date.now(),
lastTick = this.watchDogLastTick,
deltaTime = now - lastTick;
// Timers are frozen
if (deltaTime >= 2000) {
return;
}
return this.callParent(arguments);
},
doPreventZooming: function() {
var now = Ext.Date.now(),
lastTick = this.watchDogLastTick,
deltaTime = now - lastTick;
// Timers are frozen
if (deltaTime >= 2000) {
return;
}
return this.callParent(arguments);
}
});
}
if (version.match('2')) {
this.override({
onReady: function() {
this.addWindowListener('resize', Ext.Function.bind(this.onWindowResize, this));
this.callParent(arguments);
},
scrollToTop: function() {
document.body.scrollTop = 100;
},
onWindowResize: function() {
var oldWidth = this.windowWidth,
oldHeight = this.windowHeight,
width = this.getWindowWidth(),
height = this.getWindowHeight();
if (this.getAutoMaximize() && !this.isMaximizing && !this.orientationChanging
&& window.scrollY === 0
&& oldWidth === width
&& height < oldHeight
&& ((height >= oldHeight - this.addressBarHeight) || !this.focusedElement)) {
this.scrollToTop();
}
},
fixSize: function() {
var orientation = this.getOrientation(),
outerHeight = window.outerHeight,
outerWidth = window.outerWidth,
actualOuterHeight;
// On some Android 2 devices such as the Kindle Fire, outerWidth and outerHeight are reported wrongly
// when navigating from another page that has larger size.
if (orientation === 'landscape' && (outerHeight < outerWidth)
|| orientation === 'portrait' && (outerHeight >= outerWidth)) {
actualOuterHeight = this.getActualWindowOuterHeight();
}
else {
actualOuterHeight = this.getWindowHeight();
}
this.waitUntil(function() {
return actualOuterHeight > this.getWindowHeight();
}, this.doFixSize, this.doFixSize, 50, 1000);
}
});
}
else if (version.gtEq('3.1')) {
this.override({
isHeightMaximized: function(height) {
this.scrollToTop();
return this.getWindowHeight() === height - 1;
}
});
}
else if (version.match('3')) {
this.override({
isHeightMaximized: function() {
this.scrollToTop();
return true;
}
})
}
if (version.gtEq('4')) {
this.override({
doBlurInput: Ext.emptyFn
});
}
});
/**
* @private
* iOS version of viewport.
*/
Ext.define('Ext.viewport.Ios', {
extend: 'Ext.viewport.Default',
isFullscreen: function() {
return this.isHomeScreen();
},
isHomeScreen: function() {
return window.navigator.standalone === true;
},
constructor: function() {
this.callParent(arguments);
if (this.getAutoMaximize() && !this.isFullscreen()) {
this.addWindowListener('touchstart', Ext.Function.bind(this.onTouchStart, this));
}
},
maximize: function() {
if (this.isFullscreen()) {
return this.callParent();
}
var stretchHeights = this.stretchHeights,
orientation = this.orientation,
currentHeight = this.getWindowHeight(),
height = stretchHeights[orientation];
if (window.scrollY > 0) {
this.scrollToTop();
if (!height) {
stretchHeights[orientation] = height = this.getWindowHeight();
}
this.setHeight(height);
this.fireMaximizeEvent();
}
else {
if (!height) {
height = this.getScreenHeight();
}
this.setHeight(height);
this.waitUntil(function() {
this.scrollToTop();
return currentHeight !== this.getWindowHeight();
}, function() {
if (!stretchHeights[orientation]) {
height = stretchHeights[orientation] = this.getWindowHeight();
this.setHeight(height);
}
this.fireMaximizeEvent();
}, function() {
//<debug error>
Ext.Logger.error("Timeout waiting for window.innerHeight to change", this);
//</debug>
height = stretchHeights[orientation] = this.getWindowHeight();
this.setHeight(height);
this.fireMaximizeEvent();
}, 50, 1000);
}
},
getScreenHeight: function() {
return window.screen[this.orientation === this.PORTRAIT ? 'height' : 'width'];
},
onElementFocus: function() {
if (this.getAutoMaximize() && !this.isFullscreen()) {
clearTimeout(this.scrollToTopTimer);
}
this.callParent(arguments);
},
onElementBlur: function() {
if (this.getAutoMaximize() && !this.isFullscreen()) {
this.scrollToTopTimer = setTimeout(this.scrollToTop, 500);
}
this.callParent(arguments);
},
onTouchStart: function() {
if (this.focusedElement === null) {
this.scrollToTop();
}
},
scrollToTop: function() {
window.scrollTo(0, 0);
}
}, function() {
if (!Ext.os.is.iOS) {
return;
}
if (Ext.os.version.lt('3.2')) {
this.override({
constructor: function() {
var stretchHeights = this.stretchHeights = {};
stretchHeights[this.PORTRAIT] = 416;
stretchHeights[this.LANDSCAPE] = 268;
return this.callOverridden(arguments);
}
});
}
if (Ext.os.version.lt('5')) {
this.override({
fieldMaskClsTest: '-field-mask',
doPreventZooming: function(e) {
var target = e.target;
if (target && target.nodeType === 1 &&
!this.isInputRegex.test(target.tagName) &&
target.className.indexOf(this.fieldMaskClsTest) == -1) {
e.preventDefault();
}
}
});
}
if (Ext.os.is.iPad) {
this.override({
isFullscreen: function() {
return true;
}
});
}
});
/**
* This class acts as a factory for environment-specific viewport implementations.
*
* Please refer to the {@link Ext.Viewport} documentation about using the global instance.
* @private
*/
Ext.define('Ext.viewport.Viewport', {
requires: [
'Ext.viewport.Ios',
'Ext.viewport.Android'
],
constructor: function(config) {
var osName = Ext.os.name,
viewportName, viewport;
switch (osName) {
case 'Android':
viewportName = (Ext.browser.name == 'ChromeMobile') ? 'Default' : 'Android';
break;
case 'iOS':
viewportName = 'Ios';
break;
default:
viewportName = 'Default';
}
viewport = Ext.create('Ext.viewport.' + viewportName, config);
return viewport;
}
});
// Docs for the singleton instance created by above factory:
/**
* @class Ext.Viewport
* @extends Ext.viewport.Default
* @singleton
*
* Ext.Viewport is a instance created when you use {@link Ext#setup}. Because {@link Ext.Viewport} extends from
* {@link Ext.Container}, it has as {@link #layout} (which defaults to {@link Ext.layout.Card}). This means you
* can add items to it at any time, from anywhere in your code. The {@link Ext.Viewport} {@link #cfg-fullscreen}
* configuration is `true` by default, so it will take up your whole screen.
*
* @example raw
* Ext.setup({
* onReady: function() {
* Ext.Viewport.add({
* xtype: 'container',
* html: 'My new container!'
* });
* }
* });
*
* If you want to customize anything about this {@link Ext.Viewport} instance, you can do so by adding a property
* called `viewport` into your {@link Ext#setup} object:
*
* @example raw
* Ext.setup({
* viewport: {
* layout: 'vbox'
* },
* onReady: function() {
* //do something
* }
* });
*
* **Note** if you use {@link Ext#onReady}, this instance of {@link Ext.Viewport} will **not** be created. Though, in most cases,
* you should **not** use {@link Ext#onReady}.
*/