'Ext.Logger.log("XTemplate Error: " + e.message);',
//
'}',
'}');
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) {',
//
'Ext.Logger.log("XTemplate Error: " + e.message);',
//
'}',
'}');
}
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:
*
* ... // loop through array at root node
* ... // loop through array at foo node
* ... // loop through array at foo.bar node
*
* Using the sample data above:
*
* var tpl = new Ext.XTemplate(
* 'Kids: ',
* '', // process the data.kids node
* '{#}. {name}
', // use current array index to autonumber
* '
'
* );
* 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(
* 'Name: {name}
',
* 'Title: {title}
',
* 'Company: {company}
',
* 'Kids: ',
* '', // interrogate the kids property within the data
* '{name}
',
* '
'
* );
* 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(
* '{name}\'s favorite beverages:
',
* '',
* ' - {.}
',
* ' '
* );
* 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(
* 'Name: {name}
',
* 'Kids: ',
* '',
* '',
* '{name}
',
* 'Dad: {parent.name}
',
* ' ',
* '
'
* );
* 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(
* 'Name: {name}
',
* 'Kids: ',
* '',
* '',
* '{name}
',
* ' ',
* '
'
* );
* tpl.overwrite(panel.body, data);
*
* More advanced conditionals are also supported:
*
* var tpl = new Ext.XTemplate(
* 'Name: {name}
',
* 'Kids: ',
* '',
* '{name} is a ',
* '',
* 'teenager
',
* '',
* 'kid
',
* '',
* 'baby
',
* ' ',
* '
'
* );
*
* var tpl = new Ext.XTemplate(
* 'Name: {name}
',
* 'Kids: ',
* '',
* '{name} is a ',
* '',
* '',
* 'girl
',
* '',
* 'boy
',
* ' ',
* '
'
* );
*
* A `break` is implied between each case and default, however, multiple cases can be listed
* in a single <tpl> tag.
*
* # Using double quotes
*
* Examples:
*
* var tpl = new Ext.XTemplate(
* "Child ",
* "Teenager ",
* "... ",
* '... ',
* " ",
* "Hello "
* );
*
* # Basic math support
*
* The following basic math operators may be applied directly on numeric data values:
*
* + - * /
*
* For example:
*
* var tpl = new Ext.XTemplate(
* 'Name: {name}
',
* 'Kids: ',
* '',
* '', // <-- Note that the > is encoded
* '{#}: {name}
', // <-- Auto-number each item
* 'In 5 Years: {age+5}
', // <-- Basic math
* 'Dad: {parent.name}
',
* ' ',
* '
'
* );
* 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(
* 'Name: {name}
',
* 'Company: {[values.company.toUpperCase() + ", " + values.title]}
',
* 'Kids: ',
* '',
* '',
* '{name}',
* '
',
* '
'
* );
*
* 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(
* 'Name: {name}
',
* 'Company: {[values.company.toUpperCase() + ", " + values.title]}
',
* 'Kids: ',
* '',
* '{% if (xindex % 2 === 0) continue; %}',
* '{name}',
* '{% if (xindex > 100) break; %}',
* '',
* '
'
* );
*
* # 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(
* 'Name: {name}
',
* 'Kids: ',
* '',
* '',
* 'Girl: {name} - {age}
',
* '',
* 'Boy: {name} - {age}
',
* ' ',
* '',
* '{name} is a baby!
',
* ' ',
* '
',
* {
* // 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) {
//
Ext.Logger.log('Error: ' + e.message);
//
}
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).
*
*
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
*
*
* ## 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:
*
*
*
* So your index.html file should look a little like this:
*
*
*
*
* MY application 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: '{title}
',
* store: 'Items'
* }
* });
*
* Multiple plugins by alias:
*
* Ext.create('Ext.dataview.List', {
* config: {
* plugins: ['listpaging', 'pullrefresh'],
* itemTpl: '{title}
',
* 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: '{title}
',
* 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: '{title}
',
* 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;
}
//
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;
}
//
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) {
//
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);
}
//
// 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) {
//
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);
}
//
// 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);
//
if (!matches) {
Ext.Logger.error("Invalid alignment value of '" + alignment + "'");
}
//
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) {
//
if (orient !== 'horizontal' && orient !== 'vertical') {
Ext.Logger.error("Invalid box orient of: '" + orient + "', must be either 'horizontal' or 'vertical'");
}
//
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;
}
//
else if (name === this.STATE_TO) {
Ext.Logger.error("Setting and invalid '100%' / 'to' state of: " + state);
}
//
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
*/
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);
//
if (!defaultClass) {
Ext.Logger.error("Invalid animation type of: '" + type + "'");
}
//
}
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);
//
if (!defaultClass) {
Ext.Logger.error("Unknown card animation type: '" + type + "'");
}
//
}
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) {
//
Ext.Logger.error('A Filter requires either a property and value, or a filterFn to be set');
//
return Ext.emptyFn;
}
else {
return this.createFilterFn();
}
}
return filterFn;
},
/**
* @private
* Creates a filter function for the configured property/value/anyMatch/caseSensitive options for this Filter
*/
createFilterFn: function() {
var me = this,
matcher = me.createValueMatcher();
return function(item) {
var root = me.getRoot(),
property = me.getProperty();
if (root) {
item = item[root];
}
return matcher.test(item[property]);
};
},
/**
* @private
* Returns a regular expression based on the given value and matching options
*/
createValueMatcher: function() {
var me = this,
value = me.getValue(),
anyMatch = me.getAnyMatch(),
exactMatch = me.getExactMatch(),
caseSensitive = me.getCaseSensitive(),
escapeRe = Ext.String.escapeRegex;
if (value === null || value === undefined || !value.exec) { // not a regex
value = String(value);
if (anyMatch === true) {
value = escapeRe(value);
} else {
value = '^' + escapeRe(value);
if (exactMatch === true) {
value += '$';
}
}
value = new RegExp(value, caseSensitive ? '' : 'i');
}
return value;
}
});
/**
* @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: '{firstName} {lastName}
',
* 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);
},
//
applySorterFn: function(sorterFn) {
if (!sorterFn && !this.getProperty()) {
Ext.Logger.error("A Sorter requires either a property or a sorterFn.");
}
return sorterFn;
},
applyProperty: function(property) {
if (!property && !this.getSorterFn()) {
Ext.Logger.error("A Sorter requires either a property or a sorterFn.");
}
return property;
},
//
applyId: function(id) {
if (!id) {
id = this.getProperty();
if (!id) {
id = Ext.id(null, 'ext-sorter-');
}
}
return id;
},
/**
* @private
* Creates and returns a function which sorts an array by the given property and direction
* @return {Function} A function which sorts by the property/direction combination provided
*/
createSortFunction: function(sorterFn) {
var me = this,
modifier = me.getDirection().toUpperCase() == "DESC" ? -1 : 1;
//create a comparison function. Takes 2 objects, returns 1 if object 1 is greater,
//-1 if object 2 is greater or 0 if they are equal
return function(o1, o2) {
return modifier * sorterFn.call(me, o1, o2);
};
},
/**
* @private
* Basic default sorter function that just compares the defined property of each object
*/
defaultSortFn: function(item1, item2) {
var me = this,
transform = me._transform,
root = me._root,
value1, value2,
property = me._property;
if (root !== null) {
item1 = item1[root];
item2 = item2[root];
}
value1 = item1[property];
value2 = item2[property];
if (transform) {
value1 = transform(value1);
value2 = transform(value2);
}
return value1 > value2 ? 1 : (value1 < value2 ? -1 : 0);
},
updateDirection: function() {
this.updateSortFn();
},
updateSortFn: function() {
this.sort = this.createSortFunction(this.getSorterFn() || this.defaultSortFn);
},
/**
* Toggles the direction of this Sorter. Note that when you call this function,
* the Collection this Sorter is part of does not get refreshed automatically.
*/
toggle: function() {
this.setDirection(Ext.String.toggle(this.getDirection(), "ASC", "DESC"));
}
});
/**
* @docauthor Tommy Maintz
*
* 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
*
* 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();
//
if (!container) {
Ext.Logger.error("Making an element scrollable that doesn't have any container");
}
//
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);
//
if (this.isBenchmarking) {
this.framesCount++;
}
//
},
//
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;
},
//
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);
},
//
updateLayout: function(newLayout, oldLayout) {
if (oldLayout && oldLayout.isLayout) {
Ext.Logger.error('Replacing a layout after one has already been initialized is not currently supported.');
}
},
//
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);
//
if (!this.defaultItemClass) {
Ext.Logger.error("Invalid defaultType of: '" + defaultType + "', must be a valid component xtype");
}
//
},
applyDefaults: function(defaults) {
if (defaults) {
this.factoryItem = this.factoryItemWithDefaults;
return defaults;
}
},
factoryItem: function(item) {
//
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");
}
//
return Ext.factory(item, this.defaultItemClass);
},
factoryItemWithDefaults: function(item) {
//
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");
}
//
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;
//
if (!item.isInnerItem()) {
Ext.Logger.error("Setting activeItem to be a non-inner item");
}
//
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 `
* @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
}
//
// Exhausted all matches: It's an error
if (i === (length - 1)) {
Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
}
//
}
}
// Now we are going to check for a mode change. This means a space
// or a > to determine if we are going to select all the children
// of the currently matched items, or a ^ if we are going to use the
// ownerCt axis as the candidate source.
if (modeMatch[1]) { // Assignment, and test for truthiness!
operations.push({
mode: modeMatch[2]||modeMatch[1]
});
selector = selector.replace(modeMatch[0], '');
}
}
// Now that we have all our operations in an array, we are going
// to create a new Query using these operations.
return new cq.Query({
operations: operations
});
}
});
});
/**
* @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. 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
* ` ` 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.**
*
*/
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:
*
*
*
* ## 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:
*
*
*
* So your index.html file should look a little like this:
*
*
*
*
* MY application 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;
},
//
// @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;
},
//
/**
* 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;
},
//
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;
},
//
/**
* 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);
},
//
// @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;
},
//
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 ` `
* @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 ` `
* @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')) {
//
Ext.Logger.deprecate("'promptConfig' config is deprecated, please use 'prompt' config instead", this);
//
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= ]
* A string that will replace the existing message box body text.
* Defaults to the XHTML-compliant non-breaking space character ` `.
*
* @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) {
//
Ext.Logger.deprecate("'promptConfig' config is deprecated, please use 'prompt' config instead", this);
//
}
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 left and right .');
*
*
*
* @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.');
*
*
*
* @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 `` 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) {
//
if (Ext.isArray(refs)) {
Ext.Logger.deprecate("In Sencha Touch 2 the refs config accepts an object but you have passed it an array.");
}
//
this.ref(refs);
return refs;
},
/**
* @private
* Adds any routes specified in this Controller to the global Application router
*/
applyRoutes: function(routes) {
var app = this instanceof Ext.app.Application ? this : this.getApplication(),
router = app.getRouter(),
route, url, config;
for (url in routes) {
route = routes[url];
config = {
controller: this.$className
};
if (Ext.isString(route)) {
config.action = route;
} else {
Ext.apply(config, route);
}
router.connect(url, config);
}
return routes;
},
/**
* @private
* As a convenience developers can locally qualify store names (e.g. 'MyStore' vs
* 'MyApp.store.MyStore'). This just makes sure everything ends up fully qualified
*/
applyStores: function(stores) {
return this.getFullyQualified(stores, 'store');
},
/**
* @private
* As a convenience developers can locally qualify model names (e.g. 'MyModel' vs
* 'MyApp.model.MyModel'). This just makes sure everything ends up fully qualified
*/
applyModels: function(models) {
return this.getFullyQualified(models, 'model');
},
/**
* @private
* As a convenience developers can locally qualify view names (e.g. 'MyView' vs
* 'MyApp.view.MyView'). This just makes sure everything ends up fully qualified
*/
applyViews: function(views) {
return this.getFullyQualified(views, 'view');
},
/**
* @private
* Returns the fully qualified name for any class name variant. This is used to find the FQ name for the model,
* view, controller, store and profiles listed in a Controller or Application.
* @param {String[]} items The array of strings to get the FQ name for
* @param {String} namespace If the name happens to be an application class, add it to this namespace
* @return {String} The fully-qualified name of the class
*/
getFullyQualified: function(items, namespace) {
var length = items.length,
appName = this.getApplication().getName(),
name, i;
for (i = 0; i < length; i++) {
name = items[i];
//we check name === appName to allow MyApp.profile.MyApp to exist
if (Ext.isString(name) && (Ext.Loader.getPrefix(name) === "" || name === appName)) {
items[i] = appName + '.' + namespace + '.' + name;
}
}
return items;
},
/**
* @private
*/
control: function(selectors) {
this.getApplication().control(selectors, this);
},
/**
* @private
* 1.x-inspired ref implementation
*/
ref: function(refs) {
var 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];
}
//
Ext.Loader.setConfig({ enabled: true });
//
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) {
//
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 {
//
controllers[name].launch(this);
//
}
//
}
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, "");
//
Ext.Logger.warn('Attempting to create an application with a name which contains whitespace ("' + oldName + '"). Renamed to "' + name + '".');
//
}
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
*
* 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
* @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) {
//
if (indicator) {
Ext.Logger.error("'indicator' in Infinite Carousel implementation is not currently supported", this);
}
//
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);
//
var startLooping, frames;
//
function animationLoop() {
Ext.frameStartTime = animationStartTimePolyfill();
//
if (startLooping === undefined) {
startLooping = Ext.frameStartTime;
}
//
frame.step(frame.animationTime());
frame.fireFrameCallbacks();
if (frame.scheduled || !frame.empty()) {
requestAnimationFramePolyfill(animationLoop);
//
frames++;
//
} else {
looping = false;
//
startLooping = undefined;
//
}
//
frame.framerate = frames * 1000 / (frame.animationTime() - startLooping);
//
}
//
frame.clearCounter = function () {
startLooping = frame.animationTime();
frames = 0;
};
//
frame.ignite = function () {
if (!looping) {
//
frames = 0;
//
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 Sencha Touch GPL '
},
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
//
else {
Ext.Logger.warn('Invalid sorter specified:', sorter);
}
//
// 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
//
else {
Ext.Logger.warn('Invalid filter specified:', filter);
}
//
// If a sorter config was created, make it an instance
filter = Ext.create('Ext.util.Filter', filterConfig);
newFilters.push(filter);
}
// Now lets add the newly created sorters.
for (i = 0, ln = newFilters.length; i < ln; i++) {
currentFilters.insert(index + i, newFilters[i]);
}
this.dirtyFilterFn = true;
if (currentFilters.length) {
this.filtered = true;
}
return currentFilters;
},
/**
* This method removes all the filters in a passed array.
* @param {Array} filters Each value in the array can be a string (property name),
* function (sorterFn), an object containing a property and value keys or
* {@link Ext.util.Sorter Sorter} instance.
*/
removeFilters: function(filters) {
// We begin by making sure we are dealing with an array of sorters
if (!Ext.isArray(filters)) {
filters = [filters];
}
var ln = filters.length,
currentFilters = this.getFilters(),
i, filter;
for (i = 0; i < ln; i++) {
filter = filters[i];
if (typeof filter === 'string') {
currentFilters.each(function(item) {
if (item.getProperty() === filter) {
currentFilters.remove(item);
}
});
}
else if (typeof filter === 'function') {
currentFilters.each(function(item) {
if (item.getFilterFn() === filter) {
currentFilters.remove(item);
}
});
}
else {
if (filter.isFilter) {
currentFilters.remove(filter);
}
else if (filter.property !== undefined && filter.value !== undefined) {
currentFilters.each(function(item) {
if (item.getProperty() === filter.property && item.getValue() === filter.value) {
currentFilters.remove(item);
}
});
}
}
}
if (!currentFilters.length) {
this.filtered = false;
}
},
/**
* This updates the cached sortFn based on the current sorters.
* @return {Function} sortFn The generated sort function.
* @private
*/
updateFilterFn: function() {
var filters = this.getFilters().items;
this.filterFn = function(item) {
var isMatch = true,
length = filters.length,
i;
for (i = 0; i < length; i++) {
var filter = filters[i],
fn = filter.getFilterFn(),
scope = filter.getScope() || this;
isMatch = isMatch && fn.call(scope, item);
}
return isMatch;
};
this.dirtyFilterFn = false;
return this.filterFn;
},
/**
* This method will sort an array based on the currently configured {@link Ext.data.Store#sorters sorters}.
* @param {Array} data The array you want to have sorted.
* @return {Array} The array you passed after it is sorted.
*/
filter: function(data) {
return this.getFilters().length ? Ext.Array.filter(data, this.getFilterFn()) : data;
},
isFiltered: function(item) {
return this.getFilters().length ? !this.getFilterFn()(item) : false;
},
/**
* Returns an up to date sort function.
* @return {Function} sortFn The sort function.
*/
getFilterFn: function() {
if (this.dirtyFilterFn) {
return this.updateFilterFn();
}
return this.filterFn;
}
});
/**
* @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()) {
//
Ext.Logger.error('Inserting a collection of items into a sorted Collection is invalid. Please just add these items or remove the sorters.');
//
}
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
* @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: '{name} is {age} years old
'
* });
*
* 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: '{name} is {age} years old
'
* });
*
* 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: '{from_user} {text}
'
* });
*
* 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: '{text}
',
/**
* @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);
//
layout = this.getLayout();
if (layout && !layout.isAuto) {
Ext.Logger.error('The base layout for a DataView must always be an Auto Layout');
}
//
},
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);
}
}
}
//
else {
Ext.Logger.warn("The specified Store cannot be found", this);
}
//
}
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: [
" {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);
}
//
if (!model) {
Ext.Logger.warn('Unless you define your model using metadata, an Operation needs to have a model defined.');
}
//
return model;
},
getRecords: function() {
var resultSet = this.getResultSet();
return this._records || (resultSet ? resultSet.getRecords() : []);
},
/**
* Marks the Operation as started.
*/
setStarted: function() {
this.started = true;
this.running = true;
},
/**
* Marks the Operation as completed.
*/
setCompleted: function() {
this.complete = true;
this.running = false;
},
/**
* Marks the Operation as successful.
*/
setSuccessful: function() {
this.success = true;
},
/**
* Marks the Operation as having experienced an exception. Can be supplied with an option error message/object.
* @param {String/Object} error (optional) error string/object
*/
setException: function(error) {
this.exception = true;
this.success = false;
this.running = false;
this.error = error;
},
/**
* Returns `true` if this Operation encountered an exception (see also {@link #getError}).
* @return {Boolean} `true` if there was an exception.
*/
hasException: function() {
return this.exception === true;
},
/**
* Returns the error string or object that was set using {@link #setException}.
* @return {String/Object} The error object.
*/
getError: function() {
return this.error;
},
/**
* Returns `true` if the Operation has been started. Note that the Operation may have started AND completed, see
* {@link #isRunning} to test if the Operation is currently running.
* @return {Boolean} `true` if the Operation has started
*/
isStarted: function() {
return this.started === true;
},
/**
* Returns `true` if the Operation has been started but has not yet completed.
* @return {Boolean} `true` if the Operation is currently running
*/
isRunning: function() {
return this.running === true;
},
/**
* Returns `true` if the Operation has been completed
* @return {Boolean} `true` if the Operation is complete
*/
isComplete: function() {
return this.complete === true;
},
/**
* Returns `true` if the Operation has completed and was successful
* @return {Boolean} `true` if successful
*/
wasSuccessful: function() {
return this.isComplete() && this.success === true;
},
/**
* Checks whether this operation should cause writing to occur.
* @return {Boolean} Whether the operation should cause a write to occur.
*/
allowWrite: function() {
return this.getAction() != 'read';
},
process: function(action, resultSet, request, response) {
if (resultSet.getSuccess() !== false) {
this.setResponse(response);
this.setResultSet(resultSet);
this.setCompleted();
this.setSuccessful();
} else {
return false;
}
return this['process' + Ext.String.capitalize(action)].call(this, resultSet, request, response);
},
processRead: function(resultSet) {
var records = resultSet.getRecords(),
processedRecords = [],
Model = this.getModel(),
ln = records.length,
i, record;
for (i = 0; i < ln; i++) {
record = records[i];
processedRecords.push(new Model(record.data, record.id, record.node));
}
this.setRecords(processedRecords);
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);
}
//
else {
Ext.Logger.warn('Unable to match the record that came back from the server.');
}
//
}
return true;
},
processUpdate: function(resultSet) {
var updatedRecords = resultSet.getRecords(),
currentRecords = this.getRecords(),
ln = updatedRecords.length,
i, currentRecord, updatedRecord;
for (i = 0; i < ln; i++) {
updatedRecord = updatedRecords[i];
currentRecord = currentRecords[i];
if (currentRecord) {
this.updateRecord(currentRecord, updatedRecord);
}
//
else {
Ext.Logger.warn('Unable to match the updated record that came back from the server.');
}
//
}
return true;
},
processDestroy: function(resultSet) {
var updatedRecords = resultSet.getRecords(),
ln = updatedRecords.length,
i, currentRecord, updatedRecord;
for (i = 0; i < ln; i++) {
updatedRecord = updatedRecords[i];
currentRecord = this.findCurrentRecord(updatedRecord.id);
if (currentRecord) {
currentRecord.setIsErased(true);
currentRecord.notifyStores('afterErase', currentRecord);
}
//
else {
Ext.Logger.warn('Unable to match the destroyed record that came back from the server.');
}
//
}
},
findCurrentRecord: function(clientId) {
var currentRecords = this.getRecords(),
ln = currentRecords.length,
i, currentRecord;
for (i = 0; i < ln; i++) {
currentRecord = currentRecords[i];
if (currentRecord.getId() === clientId) {
return currentRecord;
}
}
},
updateRecord: function(currentRecord, updatedRecord) {
var recordData = updatedRecord.data,
recordId = updatedRecord.id;
currentRecord.beginEdit();
currentRecord.set(recordData);
if (recordId !== null) {
currentRecord.setId(recordId);
}
// We call endEdit with silent: true because the commit below already makes
// sure any store is notified of the record being updated.
currentRecord.endEdit(true);
currentRecord.commit();
}
});
/**
* @author Ed Spencer
*
* Simple 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(" "));
* }
* });
*
* 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);
}
//
if (!me.getModel()) {
Ext.Logger.warn('In order to read record data, a Reader needs to have a Model defined on it.');
}
//
// 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());
}
//
if (!data) {
this.fireEvent('exception', this, response, 'JSON object not found');
Ext.Logger.error('JSON object not found');
}
//
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 {
//
Ext.Logger.error('Must specify a root when using encode');
//
}
} else {
// send as jsonData
jsonData = request.getJsonData() || {};
if (root) {
jsonData[root] = data;
} else {
jsonData = data;
}
request.setJsonData(jsonData);
}
return request;
}
});
/*
* @allowSingle: true
* @encodeRequest: false
* Url: update.json?param1=test
* {'field1': 'test': 'field2': 'test'}
*
* @allowSingle: false
* @encodeRequest: false
* Url: update.json?param1=test
* [{'field1': 'test', 'field2': 'test'}]
*
* @allowSingle: true
* @root: 'data'
* @encodeRequest: true
* Url: update.json
* {
* 'param1': 'test',
* 'data': {'field1': 'test', 'field2': 'test'}
* }
*
* @allowSingle: false
* @root: 'data'
* @encodeRequest: true
* Url: update.json
* {
* 'param1': 'test',
* 'data': [{'field1': 'test', 'field2': 'test'}]
* }
*
* @allowSingle: true
* @root: data
* @encodeRequest: false
* Url: update.json
* param1=test&data={'field1': 'test', 'field2': 'test'}
*
* @allowSingle: false
* @root: data
* @encodeRequest: false
* @ncode: true
* Url: update.json
* param1=test&data=[{'field1': 'test', 'field2': 'test'}]
*
* @allowSingle: true
* @root: data
* @encodeRequest: false
* Url: update.json?param1=test&data={'field1': 'test', 'field2': 'test'}
*
* @allowSingle: false
* @root: data
* @encodeRequest: false
* Url: update.json?param1=test&data=[{'field1': 'test', 'field2': 'test'}]
*/
/**
* @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
};
//
Ext.Logger.deprecate('Passes old-style signature to Proxy.batch (operations, listeners). Please convert to single options argument syntax.');
//
}
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() {
//
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.");
//
}
});
/**
* @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
*/
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, "/")));
//
if (isNaN(parsed)) {
Ext.Logger.warn("Cannot parse the passed value (" + value + ") into a valid date");
}
//
}
}
return isNaN(parsed) ? null : parsed;
},
sortType: sortTypes.asDate,
type: 'date'
}
});
Ext.apply(Types, {
/**
* @property {Object} BOOLEAN
* 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() {
//
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: []});.');
//
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;
//
Ext.Logger.warn('nocache configuration on Ext.data.proxy.Server has been deprecated. Please use noCache.');
//
}
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);
//
if (!url) {
Ext.Logger.error("You are using a ServerProxy but have not supplied it with a url.");
}
//
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) {
//
Ext.Logger.error("The doRequest function has not been implemented on your Ext.data.proxy.Server subclass. See src/data/ServerProxy.js for details");
//
},
/**
* 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) {
//
Ext.Logger.warn('storeConfig is deprecated on an association. Instead use the store configuration.');
//
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);
},
//
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);
},
//
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();
}
//
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');
}
//
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) {
//
Ext.Logger.warn('Specifying getGroupString on a store has been deprecated. Please use grouper: {groupFn: yourFunction}');
//
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;
//
if (!grouper) {
Ext.Logger.error('Trying to get groups for a store that has no grouper');
}
//
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) {
//
if(!(offset instanceof this.statics())) {
Ext.Error.raise('Offset must be an instance of Ext.util.Offset');
}
//
return (this.x == offset.x && this.y == offset.y);
},
round: function(to) {
if (!isNaN(to)) {
var factor = Math.pow(10, to);
this.x = Math.round(this.x * factor) / factor;
this.y = Math.round(this.y * factor) / factor;
} else {
this.x = Math.round(this.x);
this.y = Math.round(this.y);
}
},
isZero: function() {
return this.x == 0 && this.y == 0;
}
});
/**
* 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(' Zoom');
}
} else {
button.removeCls(zoomModeCls);
if (!button.config.hideText) {
button.setText(' 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('{0}
', 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;
//
if (!fn) {
Ext.Logger.error('No direct function specified for this proxy');
}
//
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 ' + weather[0].tempMaxF + '° F ');
* }
* }
* });
* }
* });
*
* 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);
//
if (!options.url) {
Ext.Logger.error('A url must be specified for a JSONP request.');
}
//
var me = this,
disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching,
cacheParam = options.disableCachingParam || me.disableCachingParam,
id = ++me.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) {
//
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.");
}
//
},
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 `
*
* 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:
*
*
*
* # 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:
*
*
*
* # 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 `