init repo

This commit is contained in:
nikolay ivanov
2014-07-05 18:22:49 +00:00
commit a8be6b9e72
17348 changed files with 9229832 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
/**
* @private
* This is the abstract class for {@link Ext.Component}.
*
* This should never be overridden.
*/
Ext.define('Ext.AbstractComponent', {
extend: 'Ext.Evented',
onClassExtended: function(Class, members) {
if (!members.hasOwnProperty('cachedConfig')) {
return;
}
var prototype = Class.prototype,
config = members.config,
cachedConfig = members.cachedConfig,
cachedConfigList = prototype.cachedConfigList,
hasCachedConfig = prototype.hasCachedConfig,
name, value;
delete members.cachedConfig;
prototype.cachedConfigList = cachedConfigList = (cachedConfigList) ? cachedConfigList.slice() : [];
prototype.hasCachedConfig = hasCachedConfig = (hasCachedConfig) ? Ext.Object.chain(hasCachedConfig) : {};
if (!config) {
members.config = config = {};
}
for (name in cachedConfig) {
if (cachedConfig.hasOwnProperty(name)) {
value = cachedConfig[name];
if (!hasCachedConfig[name]) {
hasCachedConfig[name] = true;
cachedConfigList.push(name);
}
config[name] = value;
}
}
},
getElementConfig: Ext.emptyFn,
referenceAttributeName: 'reference',
referenceSelector: '[reference]',
/**
* @private
* Significantly improve instantiation time for Component with multiple references
* Ext.Element instance of the reference domNode is only created the very first time
* it's ever used.
*/
addReferenceNode: function(name, domNode) {
Ext.Object.defineProperty(this, name, {
get: function() {
var reference;
delete this[name];
this[name] = reference = new Ext.Element(domNode);
return reference;
},
configurable: true
});
},
initElement: function() {
var prototype = this.self.prototype,
id = this.getId(),
referenceList = [],
cleanAttributes = true,
referenceAttributeName = this.referenceAttributeName,
needsOptimization = false,
renderTemplate, renderElement, element,
referenceNodes, i, ln, referenceNode, reference,
configNameCache, defaultConfig, cachedConfigList, initConfigList, initConfigMap, configList,
elements, name, nameMap, internalName;
if (prototype.hasOwnProperty('renderTemplate')) {
renderTemplate = this.renderTemplate.cloneNode(true);
renderElement = renderTemplate.firstChild;
}
else {
cleanAttributes = false;
needsOptimization = true;
renderTemplate = document.createDocumentFragment();
renderElement = Ext.Element.create(this.getElementConfig(), true);
renderTemplate.appendChild(renderElement);
}
referenceNodes = renderTemplate.querySelectorAll(this.referenceSelector);
for (i = 0,ln = referenceNodes.length; i < ln; i++) {
referenceNode = referenceNodes[i];
reference = referenceNode.getAttribute(referenceAttributeName);
if (cleanAttributes) {
referenceNode.removeAttribute(referenceAttributeName);
}
if (reference == 'element') {
referenceNode.id = id;
this.element = element = new Ext.Element(referenceNode);
}
else {
this.addReferenceNode(reference, referenceNode);
}
referenceList.push(reference);
}
this.referenceList = referenceList;
if (!this.innerElement) {
this.innerElement = element;
}
if (!this.bodyElement) {
this.bodyElement = this.innerElement;
}
if (renderElement === element.dom) {
this.renderElement = element;
}
else {
this.addReferenceNode('renderElement', renderElement);
}
// This happens only *once* per class, during the very first instantiation
// to optimize renderTemplate based on cachedConfig
if (needsOptimization) {
configNameCache = Ext.Class.configNameCache;
defaultConfig = this.config;
cachedConfigList = this.cachedConfigList;
initConfigList = this.initConfigList;
initConfigMap = this.initConfigMap;
configList = [];
for (i = 0,ln = cachedConfigList.length; i < ln; i++) {
name = cachedConfigList[i];
nameMap = configNameCache[name];
if (initConfigMap[name]) {
initConfigMap[name] = false;
Ext.Array.remove(initConfigList, name);
}
if (defaultConfig[name] !== null) {
configList.push(name);
this[nameMap.get] = this[nameMap.initGet];
}
}
for (i = 0,ln = configList.length; i < ln; i++) {
name = configList[i];
nameMap = configNameCache[name];
internalName = nameMap.internal;
this[internalName] = null;
this[nameMap.set].call(this, defaultConfig[name]);
delete this[nameMap.get];
prototype[internalName] = this[internalName];
}
renderElement = this.renderElement.dom;
prototype.renderTemplate = renderTemplate = document.createDocumentFragment();
renderTemplate.appendChild(renderElement.cloneNode(true));
elements = renderTemplate.querySelectorAll('[id]');
for (i = 0,ln = elements.length; i < ln; i++) {
element = elements[i];
element.removeAttribute('id');
}
for (i = 0,ln = referenceList.length; i < ln; i++) {
reference = referenceList[i];
this[reference].dom.removeAttribute('reference');
}
}
return this;
}
});

View File

@@ -0,0 +1,136 @@
/**
* @private
*/
Ext.define('Ext.AbstractManager', {
/* Begin Definitions */
requires: ['Ext.util.HashMap'],
/* End Definitions */
typeName: 'type',
constructor: function(config) {
Ext.apply(this, config || {});
/**
* @property {Ext.util.HashMap} all
* Contains all of the items currently managed
*/
this.all = Ext.create('Ext.util.HashMap');
this.types = {};
},
/**
* Returns an item by id.
* For additional details see {@link Ext.util.HashMap#get}.
* @param {String} id The `id` of the item.
* @return {Object} The item, `undefined` if not found.
*/
get : function(id) {
return this.all.get(id);
},
/**
* Registers an item to be managed.
* @param {Object} item The item to register.
*/
register: function(item) {
this.all.add(item);
},
/**
* Unregisters an item by removing it from this manager.
* @param {Object} item The item to unregister.
*/
unregister: function(item) {
this.all.remove(item);
},
/**
* Registers a new item constructor, keyed by a type key.
* @param {String} type The mnemonic string by which the class may be looked up.
* @param {Function} cls The new instance class.
*/
registerType : function(type, cls) {
this.types[type] = cls;
cls[this.typeName] = type;
},
/**
* Checks if an item type is registered.
* @param {String} type The mnemonic string by which the class may be looked up.
* @return {Boolean} Whether the type is registered.
*/
isRegistered : function(type){
return this.types[type] !== undefined;
},
/**
* Creates and returns an instance of whatever this manager manages, based on the supplied type and
* config object.
* @param {Object} config The config object.
* @param {String} defaultType If no type is discovered in the config object, we fall back to this type.
* @return {Object} The instance of whatever this manager is managing.
*/
create: function(config, defaultType) {
var type = config[this.typeName] || config.type || defaultType,
Constructor = this.types[type];
//<debug>
if (Constructor == undefined) {
Ext.Error.raise("The '" + type + "' type has not been registered with this manager");
}
//</debug>
return new Constructor(config);
},
/**
* Registers a function that will be called when an item with the specified id is added to the manager.
* This will happen on instantiation.
* @param {String} id The item `id`.
* @param {Function} fn The callback function. Called with a single parameter, the item.
* @param {Object} scope The scope (`this` reference) in which the callback is executed.
* Defaults to the item.
*/
onAvailable : function(id, fn, scope){
var all = this.all,
item;
if (all.containsKey(id)) {
item = all.get(id);
fn.call(scope || item, item);
} else {
all.on('add', function(map, key, item){
if (key == id) {
fn.call(scope || item, item);
all.un('add', fn, scope);
}
});
}
},
/**
* Executes the specified function once for each item in the collection.
* @param {Function} fn The function to execute.
* @param {String} fn.key The key of the item
* @param {Number} fn.value The value of the item
* @param {Number} fn.length The total number of items in the collection
* @param {Boolean} fn.return False to cease iteration.
* @param {Object} [scope=this] The scope to execute in.
*/
each: function(fn, scope){
this.all.each(fn, scope || this);
},
/**
* Gets the number of items in the collection.
* @return {Number} The number of items in the collection.
*/
getCount: function(){
return this.all.getCount();
}
});

View File

@@ -0,0 +1,78 @@
/**
* {@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'
}
});

46
OfficeWeb/3rdparty/touch/src/Ajax.js vendored Normal file
View File

@@ -0,0 +1,46 @@
/**
* @aside guide ajax
*
* A singleton instance of an {@link Ext.data.Connection}. This class
* is used to communicate with your server side code. It can be used as follows:
*
* Ext.Ajax.request({
* url: 'page.php',
* params: {
* id: 1
* },
* success: function(response){
* var text = response.responseText;
* // process server response here
* }
* });
*
* Default options for all requests can be set by changing a property on the Ext.Ajax class:
*
* Ext.Ajax.setTimeout(60000); // 60 seconds
*
* Any options specified in the request method for the Ajax request will override any
* defaults set on the Ext.Ajax class. In the code sample below, the timeout for the
* request will be 60 seconds.
*
* Ext.Ajax.setTimeout(120000); // 120 seconds
* Ext.Ajax.request({
* url: 'page.aspx',
* timeout: 60000
* });
*
* In general, this class will be used for all Ajax requests in your application.
* The main reason for creating a separate {@link Ext.data.Connection} is for a
* series of requests that share common settings that are different to all other
* requests in the application.
*/
Ext.define('Ext.Ajax', {
extend: 'Ext.data.Connection',
singleton: true,
/**
* @property {Boolean} autoAbort
* Whether a new request should abort any pending requests.
*/
autoAbort : false
});

631
OfficeWeb/3rdparty/touch/src/Anim.js vendored Normal file
View File

@@ -0,0 +1,631 @@
/**
* Ext.Anim is used to execute simple animations defined in {@link Ext.anims}. The {@link #run} method can take any of the
* properties defined below.
*
* Ext.Anim.run(this, 'fade', {
* out: false,
* autoClear: true
* });
*
* When using {@link Ext.Anim#run}, ensure you require {@link Ext.Anim} in your application. Either do this using {@link Ext#require}:
*
* Ext.requires('Ext.Anim');
*
* when using {@link Ext#setup}:
*
* Ext.setup({
* requires: ['Ext.Anim'],
* onReady: function() {
* //do something
* }
* });
*
* or when using {@link Ext#application}:
*
* Ext.application({
* requires: ['Ext.Anim'],
* launch: function() {
* //do something
* }
* });
*
* @singleton
*/
Ext.define('Ext.Anim', {
isAnim: true,
/**
* @cfg {Boolean} disableAnimations
* `true` to disable animations.
*/
disableAnimations: false,
defaultConfig: {
/**
* @cfg {Object} from
* An object of CSS values which the animation begins with. If you define a CSS property here, you must also
* define it in the {@link #to} config.
*/
from: {},
/**
* @cfg {Object} to
* An object of CSS values which the animation ends with. If you define a CSS property here, you must also
* define it in the {@link #from} config.
*/
to: {},
/**
* @cfg {Number} duration
* Time in milliseconds for the animation to last.
*/
duration: 250,
/**
* @cfg {Number} delay Time to delay before starting the animation.
*/
delay: 0,
/**
* @cfg {String} easing
* Valid values are 'ease', 'linear', ease-in', 'ease-out', 'ease-in-out', or a cubic-bezier curve as defined by CSS.
*/
easing: 'ease-in-out',
/**
* @cfg {Boolean} autoClear
* `true` to remove all custom CSS defined in the {@link #to} config when the animation is over.
*/
autoClear: true,
/**
* @cfg {Boolean} out
* `true` if you want the animation to slide out of the screen.
*/
out: true,
/**
* @cfg {String} direction
* Valid values are: 'left', 'right', 'up', 'down', and `null`.
*/
direction: null,
/**
* @cfg {Boolean} reverse
* `true` to reverse the animation direction. For example, if the animation direction was set to 'left', it would
* then use 'right'.
*/
reverse: false
},
/**
* @cfg {Function} before
* Code to execute before starting the animation.
*/
/**
* @cfg {Function} after
* Code to execute after the animation ends.
*/
/**
* @cfg {Object} scope
* Scope to run the {@link #before} function in.
*/
opposites: {
'left': 'right',
'right': 'left',
'up': 'down',
'down': 'up'
},
constructor: function(config) {
config = Ext.apply({}, config || {}, this.defaultConfig);
this.config = config;
this.callSuper([config]);
this.running = [];
},
initConfig: function(el, runConfig) {
var me = this,
config = Ext.apply({}, runConfig || {}, me.config);
config.el = el = Ext.get(el);
if (config.reverse && me.opposites[config.direction]) {
config.direction = me.opposites[config.direction];
}
if (me.config.before) {
me.config.before.call(config, el, config);
}
if (runConfig.before) {
runConfig.before.call(config.scope || config, el, config);
}
return config;
},
/**
* @ignore
*/
run: function(el, config) {
el = Ext.get(el);
config = config || {};
var me = this,
style = el.dom.style,
property,
after = config.after;
if (me.running[el.id]) {
me.onTransitionEnd(null, el, {
config: config,
after: after
});
}
config = this.initConfig(el, config);
if (this.disableAnimations) {
for (property in config.to) {
if (!config.to.hasOwnProperty(property)) {
continue;
}
style[property] = config.to[property];
}
this.onTransitionEnd(null, el, {
config: config,
after: after
});
return me;
}
el.un('transitionend', me.onTransitionEnd, me);
style.webkitTransitionDuration = '0ms';
for (property in config.from) {
if (!config.from.hasOwnProperty(property)) {
continue;
}
style[property] = config.from[property];
}
setTimeout(function() {
// If this element has been destroyed since the timeout started, do nothing
if (!el.dom) {
return;
}
// If this is a 3d animation we have to set the perspective on the parent
if (config.is3d === true) {
el.parent().setStyle({
// See https://sencha.jira.com/browse/TOUCH-1498
'-webkit-perspective': '1200',
'-webkit-transform-style': 'preserve-3d'
});
}
style.webkitTransitionDuration = config.duration + 'ms';
style.webkitTransitionProperty = 'all';
style.webkitTransitionTimingFunction = config.easing;
// Bind our listener that fires after the animation ends
el.on('transitionend', me.onTransitionEnd, me, {
single: true,
config: config,
after: after
});
for (property in config.to) {
if (!config.to.hasOwnProperty(property)) {
continue;
}
style[property] = config.to[property];
}
}, config.delay || 5);
me.running[el.id] = config;
return me;
},
onTransitionEnd: function(ev, el, o) {
el = Ext.get(el);
if (this.running[el.id] === undefined) {
return;
}
var style = el.dom.style,
config = o.config,
me = this,
property;
if (config.autoClear) {
for (property in config.to) {
if (!config.to.hasOwnProperty(property) || config[property] === false) {
continue;
}
style[property] = '';
}
}
style.webkitTransitionDuration = null;
style.webkitTransitionProperty = null;
style.webkitTransitionTimingFunction = null;
if (config.is3d) {
el.parent().setStyle({
'-webkit-perspective': '',
'-webkit-transform-style': ''
});
}
if (me.config.after) {
me.config.after.call(config, el, config);
}
if (o.after) {
o.after.call(config.scope || me, el, config);
}
delete me.running[el.id];
}
}, function() {
Ext.Anim.seed = 1000;
/**
* Used to run an animation on a specific element. Use the config argument to customize the animation.
* @param {Ext.Element/HTMLElement} el The element to animate.
* @param {String} anim The animation type, defined in {@link Ext.anims}.
* @param {Object} config The config object for the animation.
* @method run
*/
Ext.Anim.run = function(el, anim, config) {
if (el.isComponent) {
el = el.element;
}
config = config || {};
if (anim.isAnim) {
anim.run(el, config);
}
else {
if (Ext.isObject(anim)) {
if (config.before && anim.before) {
config.before = Ext.createInterceptor(config.before, anim.before, anim.scope);
}
if (config.after && anim.after) {
config.after = Ext.createInterceptor(config.after, anim.after, anim.scope);
}
config = Ext.apply({}, config, anim);
anim = anim.type;
}
if (!Ext.anims[anim]) {
throw anim + ' is not a valid animation type.';
}
else {
// add el check to make sure dom exists.
if (el && el.dom) {
Ext.anims[anim].run(el, config);
}
}
}
};
/**
* @class Ext.anims
* Defines different types of animations.
*
* __Note:__ _flip_, _cube_, and _wipe_ animations do not work on Android.
*
* Please refer to {@link Ext.Anim} on how to use animations.
* @singleton
*/
Ext.anims = {
/**
* Fade Animation
*/
fade: new Ext.Anim({
type: 'fade',
before: function(el) {
var fromOpacity = 1,
toOpacity = 1,
curZ = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
zIndex = curZ;
if (this.out) {
toOpacity = 0;
} else {
zIndex = Math.abs(curZ) + 1;
fromOpacity = 0;
}
this.from = {
'opacity': fromOpacity,
'z-index': zIndex
};
this.to = {
'opacity': toOpacity,
'z-index': zIndex
};
}
}),
/**
* Slide Animation
*/
slide: new Ext.Anim({
direction: 'left',
cover: false,
reveal: false,
opacity: false,
'z-index': false,
before: function(el) {
var currentZIndex = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
currentOpacity = el.getStyle('opacity'),
zIndex = currentZIndex + 1,
out = this.out,
direction = this.direction,
toX = 0,
toY = 0,
fromX = 0,
fromY = 0,
elH = el.getHeight(),
elW = el.getWidth();
if (direction == 'left' || direction == 'right') {
if (out) {
toX = -elW;
}
else {
fromX = elW;
}
}
else if (direction == 'up' || direction == 'down') {
if (out) {
toY = -elH;
}
else {
fromY = elH;
}
}
if (direction == 'right' || direction == 'down') {
toY *= -1;
toX *= -1;
fromY *= -1;
fromX *= -1;
}
if (this.cover && out) {
toX = 0;
toY = 0;
zIndex = currentZIndex;
}
else if (this.reveal && !out) {
fromX = 0;
fromY = 0;
zIndex = currentZIndex;
}
this.from = {
'-webkit-transform': 'translate3d(' + fromX + 'px, ' + fromY + 'px, 0)',
'z-index': zIndex,
'opacity': currentOpacity - 0.01
};
this.to = {
'-webkit-transform': 'translate3d(' + toX + 'px, ' + toY + 'px, 0)',
'z-index': zIndex,
'opacity': currentOpacity
};
}
}),
/**
* Pop Animation
*/
pop: new Ext.Anim({
scaleOnExit: true,
before: function(el) {
var fromScale = 1,
toScale = 1,
fromOpacity = 1,
toOpacity = 1,
curZ = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
fromZ = curZ,
toZ = curZ;
if (!this.out) {
fromScale = 0.01;
fromZ = curZ + 1;
toZ = curZ + 1;
fromOpacity = 0;
}
else {
if (this.scaleOnExit) {
toScale = 0.01;
toOpacity = 0;
} else {
toOpacity = 0.8;
}
}
this.from = {
'-webkit-transform': 'scale(' + fromScale + ')',
'-webkit-transform-origin': '50% 50%',
'opacity': fromOpacity,
'z-index': fromZ
};
this.to = {
'-webkit-transform': 'scale(' + toScale + ')',
'-webkit-transform-origin': '50% 50%',
'opacity': toOpacity,
'z-index': toZ
};
}
}),
/**
* Flip Animation
*/
flip: new Ext.Anim({
is3d: true,
direction: 'left',
before: function(el) {
var rotateProp = 'Y',
fromScale = 1,
toScale = 1,
fromRotate = 0,
toRotate = 0;
if (this.out) {
toRotate = -180;
toScale = 0.8;
}
else {
fromRotate = 180;
fromScale = 0.8;
}
if (this.direction == 'up' || this.direction == 'down') {
rotateProp = 'X';
}
if (this.direction == 'right' || this.direction == 'left') {
toRotate *= -1;
fromRotate *= -1;
}
this.from = {
'-webkit-transform': 'rotate' + rotateProp + '(' + fromRotate + 'deg) scale(' + fromScale + ')',
'-webkit-backface-visibility': 'hidden'
};
this.to = {
'-webkit-transform': 'rotate' + rotateProp + '(' + toRotate + 'deg) scale(' + toScale + ')',
'-webkit-backface-visibility': 'hidden'
};
}
}),
/**
* Cube Animation
*/
cube: new Ext.Anim({
is3d: true,
direction: 'left',
style: 'outer',
before: function(el) {
var origin = '0% 0%',
fromRotate = 0,
toRotate = 0,
rotateProp = 'Y',
fromZ = 0,
toZ = 0,
elW = el.getWidth(),
elH = el.getHeight(),
showTranslateZ = true,
fromTranslate = ' translateX(0)',
toTranslate = '';
if (this.direction == 'left' || this.direction == 'right') {
if (this.out) {
origin = '100% 100%';
toZ = elW;
toRotate = -90;
} else {
origin = '0% 0%';
fromZ = elW;
fromRotate = 90;
}
} else if (this.direction == 'up' || this.direction == 'down') {
rotateProp = 'X';
if (this.out) {
origin = '100% 100%';
toZ = elH;
toRotate = 90;
} else {
origin = '0% 0%';
fromZ = elH;
fromRotate = -90;
}
}
if (this.direction == 'down' || this.direction == 'right') {
fromRotate *= -1;
toRotate *= -1;
origin = (origin == '0% 0%') ? '100% 100%': '0% 0%';
}
if (this.style == 'inner') {
fromZ *= -1;
toZ *= -1;
fromRotate *= -1;
toRotate *= -1;
if (!this.out) {
toTranslate = ' translateX(0px)';
origin = '0% 50%';
} else {
toTranslate = fromTranslate;
origin = '100% 50%';
}
}
this.from = {
'-webkit-transform': 'rotate' + rotateProp + '(' + fromRotate + 'deg)' + (showTranslateZ ? ' translateZ(' + fromZ + 'px)': '') + fromTranslate,
'-webkit-transform-origin': origin
};
this.to = {
'-webkit-transform': 'rotate' + rotateProp + '(' + toRotate + 'deg) translateZ(' + toZ + 'px)' + toTranslate,
'-webkit-transform-origin': origin
};
},
duration: 250
}),
/**
* Wipe Animation.
* Because of the amount of calculations involved, this animation is best used on small display
* changes or specifically for phone environments. Does not currently accept any parameters.
*/
wipe: new Ext.Anim({
before: function(el) {
var curZ = el.getStyle('z-index'),
zIndex,
mask = '';
if (!this.out) {
zIndex = curZ + 1;
mask = '-webkit-gradient(linear, left bottom, right bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
this.from = {
'-webkit-mask-image': mask,
'-webkit-mask-size': el.getWidth() * 3 + 'px ' + el.getHeight() + 'px',
'z-index': zIndex,
'-webkit-mask-position-x': 0
};
this.to = {
'-webkit-mask-image': mask,
'-webkit-mask-size': el.getWidth() * 3 + 'px ' + el.getHeight() + 'px',
'z-index': zIndex,
'-webkit-mask-position-x': -el.getWidth() * 2 + 'px'
};
}
},
duration: 500
})
};
});

138
OfficeWeb/3rdparty/touch/src/Audio.js vendored Normal file
View File

@@ -0,0 +1,138 @@
/**
* {@link Ext.Audio} is a simple class which provides a container for the [HTML5 Audio element](http://developer.mozilla.org/en-US/docs/Using_HTML5_audio_and_video).
*
* ## Recommended File Types/Compression:
* * Uncompressed WAV and AIF audio
* * MP3 audio
* * AAC-LC
* * HE-AAC audio
*
* ## Notes
* On Android devices, the audio tags controls do not show. You must use the {@link #method-play}, {@link #method-pause} and
* {@link #toggle} methods to control the audio (example below).
*
* ## Examples
*
* Here is an example of the {@link Ext.Audio} component in a fullscreen container:
*
* @example preview
* Ext.create('Ext.Container', {
* fullscreen: true,
* layout: {
* type : 'vbox',
* pack : 'center',
* align: 'stretch'
* },
* items: [
* {
* xtype : 'toolbar',
* docked: 'top',
* title : 'Ext.Audio'
* },
* {
* xtype: 'audio',
* url : 'touch/examples/audio/crash.mp3'
* }
* ]
* });
*
* You can also set the {@link #hidden} configuration of the {@link Ext.Audio} component to true by default,
* and then control the audio by using the {@link #method-play}, {@link #method-pause} and {@link #toggle} methods:
*
* @example preview
* Ext.create('Ext.Container', {
* fullscreen: true,
* layout: {
* type: 'vbox',
* pack: 'center'
* },
* items: [
* {
* xtype : 'toolbar',
* docked: 'top',
* title : 'Ext.Audio'
* },
* {
* xtype: 'toolbar',
* docked: 'bottom',
* defaults: {
* xtype: 'button',
* handler: function() {
* var container = this.getParent().getParent(),
* // use ComponentQuery to get the audio component (using its xtype)
* audio = container.down('audio');
*
* audio.toggle();
* this.setText(audio.isPlaying() ? 'Pause' : 'Play');
* }
* },
* items: [
* { text: 'Play', flex: 1 }
* ]
* },
* {
* html: 'Hidden audio!',
* styleHtmlContent: true
* },
* {
* xtype : 'audio',
* hidden: true,
* url : 'touch/examples/audio/crash.mp3'
* }
* ]
* });
* @aside example audio
*/
Ext.define('Ext.Audio', {
extend: 'Ext.Media',
xtype : 'audio',
config: {
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'audio'
/**
* @cfg {String} url
* The location of the audio to play.
*
* ### Recommended file types are:
* * Uncompressed WAV and AIF audio
* * MP3 audio
* * AAC-LC
* * HE-AAC audio
* @accessor
*/
},
// @private
onActivate: function() {
var me = this;
me.callParent();
if (Ext.os.is.Phone) {
me.element.show();
}
},
// @private
onDeactivate: function() {
var me = this;
me.callParent();
if (Ext.os.is.Phone) {
me.element.hide();
}
},
template: [{
reference: 'media',
preload: 'auto',
tag: 'audio',
cls: Ext.baseCSSPrefix + 'component'
}]
});

781
OfficeWeb/3rdparty/touch/src/Button.js vendored Normal file
View File

@@ -0,0 +1,781 @@
/**
* 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,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) 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,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) delete
* - ![download](data:image/png;base64,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) 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,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) 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,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) 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,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) search
* - ![settings](data:image/png;base64,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) 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,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) 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() {
//<deprecated product=touch since=2.0>
/**
* Updates the badge text.
* @method setBadge
* @param {String} text
* @deprecated 2.0.0 Please use {@link #setBadgeText} instead.
*/
Ext.deprecateClassMethod(this, 'setBadge', 'setBadgeText');
/**
* Updates the icon class
* @method setIconClass
* @param {String} iconClass
* @deprecated 2.0.0 Please use {@link #setIconCls} instead.
*/
Ext.deprecateClassMethod(this, 'setIconClass', 'setIconCls');
this.override({
constructor: function(config) {
if (config) {
/**
* @cfg {String} badge
* Optional badge text.
* @deprecated 2.0.0 Please use {@link #badgeText} instead
*/
if (config.hasOwnProperty('badge')) {
//<debug warn>
Ext.Logger.deprecate("'badge' config is deprecated, please use 'badgeText' config instead", this);
//</debug>
config.badgeText = config.badge;
delete config.badge;
}
}
this.callParent([config]);
}
});
//</deprecated>
});

2916
OfficeWeb/3rdparty/touch/src/Component.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,111 @@
/**
* @private
*
* Provides a registry of all Components (instances of {@link Ext.Component} or any subclass
* thereof) on a page so that they can be easily accessed by {@link Ext.Component component}
* {@link Ext.Component#getId id} (see {@link #get}, or the convenience method {@link Ext#getCmp Ext.getCmp}).
*
* This object also provides a registry of available Component _classes_
* indexed by a mnemonic code known as the Component's `xtype`.
* The `xtype` provides a way to avoid instantiating child Components
* when creating a full, nested config object for a complete Ext page.
*
* A child Component may be specified simply as a _config object_
* as long as the correct `xtype` is specified so that if and when the Component
* needs rendering, the correct type can be looked up for lazy instantiation.
*
* For a list of all available `xtype`, see {@link Ext.Component}.
*/
Ext.define('Ext.ComponentManager', {
alternateClassName: 'Ext.ComponentMgr',
singleton: true,
constructor: function() {
var map = {};
// The sole reason for this is just to support the old code of ComponentQuery
this.all = {
map: map,
getArray: function() {
var list = [],
id;
for (id in map) {
list.push(map[id]);
}
return list;
}
};
this.map = map;
},
/**
* Registers an item to be managed.
* @param {Object} component The item to register.
*/
register: function(component) {
var id = component.getId();
// <debug>
if (this.map[id]) {
Ext.Logger.warn('Registering a component with a id (`' + id + '`) which has already been used. Please ensure the existing component has been destroyed (`Ext.Component#destroy()`.');
}
// </debug>
this.map[component.getId()] = component;
},
/**
* Unregisters an item by removing it from this manager.
* @param {Object} component The item to unregister.
*/
unregister: function(component) {
delete this.map[component.getId()];
},
/**
* Checks if an item type is registered.
* @param {String} component The mnemonic string by which the class may be looked up.
* @return {Boolean} Whether the type is registered.
*/
isRegistered : function(component){
return this.map[component] !== undefined;
},
/**
* Returns an item by id.
* For additional details see {@link Ext.util.HashMap#get}.
* @param {String} id The `id` of the item.
* @return {Object} The item, or `undefined` if not found.
*/
get: function(id) {
return this.map[id];
},
/**
* Creates a new Component from the specified config object using the
* config object's `xtype` to determine the class to instantiate.
* @param {Object} config A configuration object for the Component you wish to create.
* @param {Function} defaultType (optional) The constructor to provide the default Component type if
* the config object does not contain a `xtype`. (Optional if the config contains an `xtype`).
* @return {Ext.Component} The newly instantiated Component.
*/
create: function(component, defaultType) {
if (component.isComponent) {
return component;
}
else if (Ext.isString(component)) {
return Ext.createByAlias('widget.' + component);
}
else {
var type = component.xtype || defaultType;
return Ext.createByAlias('widget.' + type, component);
}
},
registerType: Ext.emptyFn
});

View File

@@ -0,0 +1,530 @@
/**
* @class Ext.ComponentQuery
* @extends Object
* @singleton
*
* Provides searching of Components within {@link Ext.ComponentManager} (globally) or a specific
* {@link Ext.Container} on the document with a similar syntax to a CSS selector.
*
* Components can be retrieved by using their {@link Ext.Component xtype} with an optional '.' prefix
*
* - `component` or `.component`
* - `gridpanel` or `.gridpanel`
*
* An itemId or id must be prefixed with a #
*
* - `#myContainer`
*
* Attributes must be wrapped in brackets
*
* - `component[autoScroll]`
* - `panel[title="Test"]`
*
* Member expressions from candidate Components may be tested. If the expression returns a *truthy* value,
* the candidate Component will be included in the query:
*
* var disabledFields = myFormPanel.query("{isDisabled()}");
*
* Pseudo classes may be used to filter results in the same way as in {@link Ext.DomQuery DomQuery}:
*
* // Function receives array and returns a filtered array.
* Ext.ComponentQuery.pseudos.invalid = function(items) {
* var i = 0, l = items.length, c, result = [];
* for (; i < l; i++) {
* if (!(c = items[i]).isValid()) {
* result.push(c);
* }
* }
* return result;
* };
*
* var invalidFields = myFormPanel.query('field:invalid');
* if (invalidFields.length) {
* invalidFields[0].getEl().scrollIntoView(myFormPanel.body);
* for (var i = 0, l = invalidFields.length; i < l; i++) {
* invalidFields[i].getEl().frame("red");
* }
* }
*
* Default pseudos include:
*
* - not
*
* Queries return an array of components.
* Here are some example queries.
*
* // retrieve all Ext.Panels in the document by xtype
* var panelsArray = Ext.ComponentQuery.query('panel');
*
* // retrieve all Ext.Panels within the container with an id myCt
* var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');
*
* // retrieve all direct children which are Ext.Panels within myCt
* var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');
*
* // retrieve all grids and trees
* var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');
*
* For easy access to queries based from a particular Container see the {@link Ext.Container#query},
* {@link Ext.Container#down} and {@link Ext.Container#child} methods. Also see
* {@link Ext.Component#up}.
*/
Ext.define('Ext.ComponentQuery', {
singleton: true,
uses: ['Ext.ComponentManager']
}, function() {
var cq = this,
// A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied
// as a member on each item in the passed array.
filterFnPattern = [
'var r = [],',
'i = 0,',
'it = items,',
'l = it.length,',
'c;',
'for (; i < l; i++) {',
'c = it[i];',
'if (c.{0}) {',
'r.push(c);',
'}',
'}',
'return r;'
].join(''),
filterItems = function(items, operation) {
// Argument list for the operation is [ itemsArray, operationArg1, operationArg2...]
// The operation's method loops over each item in the candidate array and
// returns an array of items which match its criteria
return operation.method.apply(this, [ items ].concat(operation.args));
},
getItems = function(items, mode) {
var result = [],
i = 0,
length = items.length,
candidate,
deep = mode !== '>';
for (; i < length; i++) {
candidate = items[i];
if (candidate.getRefItems) {
result = result.concat(candidate.getRefItems(deep));
}
}
return result;
},
getAncestors = function(items) {
var result = [],
i = 0,
length = items.length,
candidate;
for (; i < length; i++) {
candidate = items[i];
while (!!(candidate = (candidate.ownerCt || candidate.floatParent))) {
result.push(candidate);
}
}
return result;
},
// Filters the passed candidate array and returns only items which match the passed xtype
filterByXType = function(items, xtype, shallow) {
if (xtype === '*') {
return items.slice();
}
else {
var result = [],
i = 0,
length = items.length,
candidate;
for (; i < length; i++) {
candidate = items[i];
if (candidate.isXType(xtype, shallow)) {
result.push(candidate);
}
}
return result;
}
},
// Filters the passed candidate array and returns only items which have the passed className
filterByClassName = function(items, className) {
var EA = Ext.Array,
result = [],
i = 0,
length = items.length,
candidate;
for (; i < length; i++) {
candidate = items[i];
if (candidate.el ? candidate.el.hasCls(className) : EA.contains(candidate.initCls(), className)) {
result.push(candidate);
}
}
return result;
},
// Filters the passed candidate array and returns only items which have the specified property match
filterByAttribute = function(items, property, operator, value) {
var result = [],
i = 0,
length = items.length,
candidate, getter, getValue;
for (; i < length; i++) {
candidate = items[i];
getter = Ext.Class.getConfigNameMap(property).get;
if (candidate[getter]) {
getValue = candidate[getter]();
if (!value ? !!getValue : (String(getValue) === value)) {
result.push(candidate);
}
}
else if (candidate.config && candidate.config[property]) {
if (!value ? !!candidate.config[property] : (String(candidate.config[property]) === value)) {
result.push(candidate);
}
}
else if (!value ? !!candidate[property] : (String(candidate[property]) === value)) {
result.push(candidate);
}
}
return result;
},
// Filters the passed candidate array and returns only items which have the specified itemId or id
filterById = function(items, id) {
var result = [],
i = 0,
length = items.length,
candidate;
for (; i < length; i++) {
candidate = items[i];
if (candidate.getId() === id || candidate.getItemId() === id) {
result.push(candidate);
}
}
return result;
},
// Filters the passed candidate array and returns only items which the named pseudo class matcher filters in
filterByPseudo = function(items, name, value) {
return cq.pseudos[name](items, value);
},
// Determines leading mode
// > for direct child, and ^ to switch to ownerCt axis
modeRe = /^(\s?([>\^])\s?|\s|$)/,
// Matches a token with possibly (true|false) appended for the "shallow" parameter
tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/,
matchers = [{
// Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
re: /^\.([\w\-]+)(?:\((true|false)\))?/,
method: filterByXType
},{
// checks for [attribute=value]
re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/,
method: filterByAttribute
}, {
// checks for #cmpItemId
re: /^#([\w\-]+)/,
method: filterById
}, {
// checks for :<pseudo_class>(<selector>)
re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
method: filterByPseudo
}, {
// checks for {<member_expression>}
re: /^(?:\{([^\}]+)\})/,
method: filterFnPattern
}];
cq.Query = Ext.extend(Object, {
constructor: function(cfg) {
cfg = cfg || {};
Ext.apply(this, cfg);
},
/**
* @private
* Executes this Query upon the selected root.
* The root provides the initial source of candidate Component matches which are progressively
* filtered by iterating through this Query's operations cache.
* If no root is provided, all registered Components are searched via the ComponentManager.
* root may be a Container who's descendant Components are filtered
* root may be a Component with an implementation of getRefItems which provides some nested Components such as the
* docked items within a Panel.
* root may be an array of candidate Components to filter using this Query.
*/
execute : function(root) {
var operations = this.operations,
i = 0,
length = operations.length,
operation,
workingItems;
// no root, use all Components in the document
if (!root) {
workingItems = Ext.ComponentManager.all.getArray();
}
// Root is a candidate Array
else if (Ext.isArray(root)) {
workingItems = root;
}
// We are going to loop over our operations and take care of them
// one by one.
for (; i < length; i++) {
operation = operations[i];
// The mode operation requires some custom handling.
// All other operations essentially filter down our current
// working items, while mode replaces our current working
// items by getting children from each one of our current
// working items. The type of mode determines the type of
// children we get. (e.g. > only gets direct children)
if (operation.mode === '^') {
workingItems = getAncestors(workingItems || [root]);
}
else if (operation.mode) {
workingItems = getItems(workingItems || [root], operation.mode);
}
else {
workingItems = filterItems(workingItems || getItems([root]), operation);
}
// If this is the last operation, it means our current working
// items are the final matched items. Thus return them!
if (i === length -1) {
return workingItems;
}
}
return [];
},
is: function(component) {
var operations = this.operations,
components = Ext.isArray(component) ? component : [component],
originalLength = components.length,
lastOperation = operations[operations.length-1],
ln, i;
components = filterItems(components, lastOperation);
if (components.length === originalLength) {
if (operations.length > 1) {
for (i = 0, ln = components.length; i < ln; i++) {
if (Ext.Array.indexOf(this.execute(), components[i]) === -1) {
return false;
}
}
}
return true;
}
return false;
}
});
Ext.apply(this, {
// private cache of selectors and matching ComponentQuery.Query objects
cache: {},
// private cache of pseudo class filter functions
pseudos: {
not: function(components, selector){
var CQ = Ext.ComponentQuery,
i = 0,
length = components.length,
results = [],
index = -1,
component;
for(; i < length; ++i) {
component = components[i];
if (!CQ.is(component, selector)) {
results[++index] = component;
}
}
return results;
}
},
/**
* Returns an array of matched Components from within the passed root object.
*
* This method filters returned Components in a similar way to how CSS selector based DOM
* queries work using a textual selector string.
*
* See class summary for details.
*
* @param {String} selector The selector string to filter returned Components
* @param {Ext.Container} root The Container within which to perform the query.
* If omitted, all Components within the document are included in the search.
*
* This parameter may also be an array of Components to filter according to the selector.</p>
* @return {Ext.Component[]} The matched Components.
*
* @member Ext.ComponentQuery
*/
query: function(selector, root) {
var selectors = selector.split(','),
length = selectors.length,
i = 0,
results = [],
noDupResults = [],
dupMatcher = {},
query, resultsLn, cmp;
for (; i < length; i++) {
selector = Ext.String.trim(selectors[i]);
query = this.parse(selector);
// query = this.cache[selector];
// if (!query) {
// this.cache[selector] = query = this.parse(selector);
// }
results = results.concat(query.execute(root));
}
// multiple selectors, potential to find duplicates
// lets filter them out.
if (length > 1) {
resultsLn = results.length;
for (i = 0; i < resultsLn; i++) {
cmp = results[i];
if (!dupMatcher[cmp.id]) {
noDupResults.push(cmp);
dupMatcher[cmp.id] = true;
}
}
results = noDupResults;
}
return results;
},
/**
* Tests whether the passed Component matches the selector string.
* @param {Ext.Component} component The Component to test.
* @param {String} selector The selector string to test against.
* @return {Boolean} `true` if the Component matches the selector.
* @member Ext.ComponentQuery
*/
is: function(component, selector) {
if (!selector) {
return true;
}
var query = this.cache[selector];
if (!query) {
this.cache[selector] = query = this.parse(selector);
}
return query.is(component);
},
parse: function(selector) {
var operations = [],
length = matchers.length,
lastSelector,
tokenMatch,
matchedChar,
modeMatch,
selectorMatch,
i, matcher, method;
// We are going to parse the beginning of the selector over and
// over again, slicing off the selector any portions we converted into an
// operation, until it is an empty string.
while (selector && lastSelector !== selector) {
lastSelector = selector;
// First we check if we are dealing with a token like #, * or an xtype
tokenMatch = selector.match(tokenRe);
if (tokenMatch) {
matchedChar = tokenMatch[1];
// If the token is prefixed with a # we push a filterById operation to our stack
if (matchedChar === '#') {
operations.push({
method: filterById,
args: [Ext.String.trim(tokenMatch[2])]
});
}
// If the token is prefixed with a . we push a filterByClassName operation to our stack
// FIXME: Not enabled yet. just needs \. adding to the tokenRe prefix
else if (matchedChar === '.') {
operations.push({
method: filterByClassName,
args: [Ext.String.trim(tokenMatch[2])]
});
}
// If the token is a * or an xtype string, we push a filterByXType
// operation to the stack.
else {
operations.push({
method: filterByXType,
args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])]
});
}
// Now we slice of the part we just converted into an operation
selector = selector.replace(tokenMatch[0], '');
}
// If the next part of the query is not a space or > or ^, it means we
// are going to check for more things that our current selection
// has to comply to.
while (!(modeMatch = selector.match(modeRe))) {
// Lets loop over each type of matcher and execute it
// on our current selector.
for (i = 0; selector && i < length; i++) {
matcher = matchers[i];
selectorMatch = selector.match(matcher.re);
method = matcher.method;
// If we have a match, add an operation with the method
// associated with this matcher, and pass the regular
// expression matches are arguments to the operation.
if (selectorMatch) {
operations.push({
method: Ext.isString(matcher.method)
// Turn a string method into a function by formatting the string with our selector matche expression
// A new method is created for different match expressions, eg {id=='textfield-1024'}
// Every expression may be different in different selectors.
? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1))))
: matcher.method,
args: selectorMatch.slice(1)
});
selector = selector.replace(selectorMatch[0], '');
break; // Break on match
}
//<debug>
// Exhausted all matches: It's an error
if (i === (length - 1)) {
Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
}
//</debug>
}
}
// Now we are going to check for a mode change. This means a space
// or a > to determine if we are going to select all the children
// of the currently matched items, or a ^ if we are going to use the
// ownerCt axis as the candidate source.
if (modeMatch[1]) { // Assignment, and test for truthiness!
operations.push({
mode: modeMatch[2]||modeMatch[1]
});
selector = selector.replace(modeMatch[0], '');
}
}
// Now that we have all our operations in an array, we are going
// to create a new Query using these operations.
return new cq.Query({
operations: operations
});
}
});
});

1642
OfficeWeb/3rdparty/touch/src/Container.js vendored Normal file

File diff suppressed because it is too large Load Diff

1525
OfficeWeb/3rdparty/touch/src/DateExtras.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
/**
* @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();
}
});

105
OfficeWeb/3rdparty/touch/src/Evented.js vendored Normal file
View File

@@ -0,0 +1,105 @@
/**
* @private
*/
Ext.define('Ext.Evented', {
alternateClassName: 'Ext.EventedBase',
mixins: ['Ext.mixin.Observable'],
statics: {
generateSetter: function(nameMap) {
var internalName = nameMap.internal,
applyName = nameMap.apply,
changeEventName = nameMap.changeEvent,
doSetName = nameMap.doSet;
return function(value) {
var initialized = this.initialized,
oldValue = this[internalName],
applier = this[applyName];
if (applier) {
value = applier.call(this, value, oldValue);
if (typeof value == 'undefined') {
return this;
}
}
// The old value might have been changed at this point
// (after the apply call chain) so it should be read again
oldValue = this[internalName];
if (value !== oldValue) {
if (initialized) {
this.fireAction(changeEventName, [this, value, oldValue], this.doSet, this, {
nameMap: nameMap
});
}
else {
this[internalName] = value;
if (this[doSetName]) {
this[doSetName].call(this, value, oldValue);
}
}
}
return this;
}
}
},
initialized: false,
constructor: function(config) {
this.initialConfig = config;
this.initialize();
},
initialize: function() {
this.initConfig(this.initialConfig);
this.initialized = true;
},
doSet: function(me, value, oldValue, options) {
var nameMap = options.nameMap;
me[nameMap.internal] = value;
if (me[nameMap.doSet]) {
me[nameMap.doSet].call(this, value, oldValue);
}
},
onClassExtended: function(Class, data) {
if (!data.hasOwnProperty('eventedConfig')) {
return;
}
var ExtClass = Ext.Class,
config = data.config,
eventedConfig = data.eventedConfig,
name, nameMap;
data.config = (config) ? Ext.applyIf(config, eventedConfig) : eventedConfig;
/*
* These are generated setters for eventedConfig
*
* If the component is initialized, it invokes fireAction to fire the event as well,
* which indicate something has changed. Otherwise, it just executes the action
* (happens during initialization)
*
* This is helpful when we only want the event to be fired for subsequent changes.
* Also it's a major performance improvement for instantiation when fired events
* are mostly useless since there's no listeners
*/
for (name in eventedConfig) {
if (eventedConfig.hasOwnProperty(name)) {
nameMap = ExtClass.getConfigNameMap(name);
data[nameMap.set] = this.generateSetter(nameMap);
}
}
}
});

234
OfficeWeb/3rdparty/touch/src/Img.js vendored Normal file
View File

@@ -0,0 +1,234 @@
/**
* This is a simple way to add an image of any size to your application and have it participate in the layout system
* like any other component. This component typically takes between 1 and 3 configurations - a {@link #src}, and
* optionally a {@link #height} and a {@link #width}:
*
* @example miniphone
* var img = Ext.create('Ext.Img', {
* src: 'http://www.sencha.com/assets/images/sencha-avatar-64x64.png',
* height: 64,
* width: 64
* });
* Ext.Viewport.add(img);
*
* It's also easy to add an image into a panel or other container using its xtype:
*
* @example miniphone
* Ext.create('Ext.Panel', {
* fullscreen: true,
* layout: 'hbox',
* items: [
* {
* xtype: 'image',
* src: 'http://www.sencha.com/assets/images/sencha-avatar-64x64.png',
* flex: 1
* },
* {
* xtype: 'panel',
* flex: 2,
* html: 'Sencha Inc.<br/>1700 Seaport Boulevard Suite 120, Redwood City, CA'
* }
* ]
* });
*
* Here we created a panel which contains an image (a profile picture in this case) and a text area to allow the user
* to enter profile information about themselves. In this case we used an {@link Ext.layout.HBox hbox layout} and
* flexed the image to take up one third of the width and the text area to take two thirds of the width. See the
* {@link Ext.layout.HBox hbox docs} for more information on flexing items.
*/
Ext.define('Ext.Img', {
extend: 'Ext.Component',
xtype: ['image', 'img'],
/**
* @event tap
* Fires whenever the component is tapped
* @param {Ext.Img} this The Image instance
* @param {Ext.EventObject} e The event object
*/
/**
* @event load
* Fires when the image is loaded
* @param {Ext.Img} this The Image instance
* @param {Ext.EventObject} e The event object
*/
/**
* @event error
* Fires if an error occured when trying to load the image
* @param {Ext.Img} this The Image instance
* @param {Ext.EventObject} e The event object
*/
config: {
/**
* @cfg {String} src The source of this image
* @accessor
*/
src: null,
/**
* @cfg
* @inheritdoc
*/
baseCls : Ext.baseCSSPrefix + 'img',
/**
* @cfg {String} imageCls The CSS class to be used when {@link #mode} is not set to 'background'
* @accessor
*/
imageCls : Ext.baseCSSPrefix + 'img-image',
/**
* @cfg {String} backgroundCls The CSS class to be used when {@link #mode} is set to 'background'
* @accessor
*/
backgroundCls : Ext.baseCSSPrefix + 'img-background',
/**
* @cfg {String} mode If set to 'background', uses a background-image CSS property instead of an
* `<img>` tag to display the image.
*/
mode: 'background'
},
beforeInitialize: function() {
var me = this;
me.onLoad = Ext.Function.bind(me.onLoad, me);
me.onError = Ext.Function.bind(me.onError, me);
},
initialize: function() {
var me = this;
me.callParent();
me.relayEvents(me.renderElement, '*');
me.element.on({
tap: 'onTap',
scope: me
});
},
hide: function() {
this.callParent();
this.hiddenSrc = this.hiddenSrc || this.getSrc();
this.setSrc(null);
},
show: function() {
this.callParent();
if (this.hiddenSrc) {
this.setSrc(this.hiddenSrc);
delete this.hiddenSrc;
}
},
updateMode: function(mode) {
var me = this,
imageCls = me.getImageCls(),
backgroundCls = me.getBackgroundCls();
if (mode === 'background') {
if (me.imageElement) {
me.imageElement.destroy();
delete me.imageElement;
me.updateSrc(me.getSrc());
}
me.replaceCls(imageCls, backgroundCls);
} else {
me.imageElement = me.element.createChild({ tag: 'img' });
me.replaceCls(backgroundCls, imageCls);
}
},
updateImageCls : function (newCls, oldCls) {
this.replaceCls(oldCls, newCls);
},
updateBackgroundCls : function (newCls, oldCls) {
this.replaceCls(oldCls, newCls);
},
onTap: function(e) {
this.fireEvent('tap', this, e);
},
onAfterRender: function() {
this.updateSrc(this.getSrc());
},
/**
* @private
*/
updateSrc: function(newSrc) {
var me = this,
dom;
if (me.getMode() === 'background') {
dom = this.imageObject || new Image();
}
else {
dom = me.imageElement.dom;
}
this.imageObject = dom;
dom.setAttribute('src', Ext.isString(newSrc) ? newSrc : '');
dom.addEventListener('load', me.onLoad, false);
dom.addEventListener('error', me.onError, false);
},
detachListeners: function() {
var dom = this.imageObject;
if (dom) {
dom.removeEventListener('load', this.onLoad, false);
dom.removeEventListener('error', this.onError, false);
}
},
onLoad : function(e) {
this.detachListeners();
if (this.getMode() === 'background') {
this.element.dom.style.backgroundImage = 'url("' + this.imageObject.src + '")';
}
this.fireEvent('load', this, e);
},
onError : function(e) {
this.detachListeners();
this.fireEvent('error', this, e);
},
doSetWidth: function(width) {
var sizingElement = (this.getMode() === 'background') ? this.element : this.imageElement;
sizingElement.setWidth(width);
this.callParent(arguments);
},
doSetHeight: function(height) {
var sizingElement = (this.getMode() === 'background') ? this.element : this.imageElement;
sizingElement.setHeight(height);
this.callParent(arguments);
},
destroy: function() {
this.detachListeners();
Ext.destroy(this.imageObject, this.imageElement);
delete this.imageObject;
delete this.imageElement;
this.callParent();
}
});

View File

@@ -0,0 +1,14 @@
/**
* @private
*/
Ext.define('Ext.ItemCollection', {
extend: 'Ext.util.MixedCollection',
getKey: function(item) {
return item.getItemId();
},
has: function(item) {
return this.map.hasOwnProperty(item.getId());
}
});

22
OfficeWeb/3rdparty/touch/src/Label.js vendored Normal file
View File

@@ -0,0 +1,22 @@
/**
* 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.
*/
}
});

179
OfficeWeb/3rdparty/touch/src/LoadMask.js vendored Normal file
View File

@@ -0,0 +1,179 @@
/**
* 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() {
//<deprecated product=touch since=2.0>
this.override({
constructor: function(config, other) {
if (typeof other !== "undefined") {
config = other;
Ext.Logger.deprecate("You no longer need to pass an element to create a Ext.LoadMask. " +
"It is a component and can be shown using the Ext.Container.masked configuration.", this);
}
if (config) {
/**
* @member Ext.LoadMask
* @cfg {String} msg The message to display on the {@link Ext.LoadMask}
* @deprecated 2.0.0 Please use the {@link #message} configuration
*/
if (config.hasOwnProperty('msg')) {
config.message = config.msg;
Ext.Logger.deprecate("'msg' config is deprecated, please use 'message' config instead", this);
delete config.msg;
}
/**
* @member Ext.LoadMask
* @cfg {String} msgCls The message cls used on the element which displays the {@link #message}
* @deprecated 2.0.0 Please use the {@link #messageCls} configuration
*/
if (config.hasOwnProperty('msgCls')) {
config.messageCls = config.msgCls;
Ext.Logger.deprecate("'msgCls' config is deprecated, please use 'messageCls' config instead", this);
delete config.msgCls;
}
/**
* @cfg {Ext.data.Store} store
* Optional Store to which the mask is bound. The mask is displayed when a load request is issued, and
* hidden on either load success, or load fail.
* @removed 2.0.0 You can no longer bind a store to a {@link Ext.LoadMask}
*/
if (config.hasOwnProperty('store')) {
Ext.Logger.deprecate("'store' config has been removed. You can no longer bind a store to a Ext.LoadMask", this);
delete config.store;
}
}
this.callParent([config]);
},
/**
* Changes the data store bound to this LoadMask.
* @param {Ext.data.Store} store The store to bind to this LoadMask
* @removed 2.0.0 You can no longer bind a store to a {@link Ext.LoadMask}.
*/
bindStore: function() {
Ext.Logger.deprecate("You can no longer bind a store to a Ext.LoadMask", this);
}
});
//</deprecated>
});

388
OfficeWeb/3rdparty/touch/src/Map.js vendored Normal file
View File

@@ -0,0 +1,388 @@
/**
* Wraps a Google Map in an Ext.Component using the [Google Maps API](http://code.google.com/apis/maps/documentation/v3/introduction.html).
*
* To use this component you must include an additional JavaScript file from Google:
*
* <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
*
* ## Example
*
* Ext.Viewport.add({
* xtype: 'map',
* useCurrentLocation: true
* });
*
* @aside example maps
*/
Ext.define('Ext.Map', {
extend: 'Ext.Container',
xtype : 'map',
requires: ['Ext.util.Geolocation'],
isMap: true,
config: {
/**
* @event maprender
* Fired when Map initially rendered.
* @param {Ext.Map} this
* @param {google.maps.Map} map The rendered google.map.Map instance
*/
/**
* @event centerchange
* Fired when map is panned around.
* @param {Ext.Map} this
* @param {google.maps.Map} map The rendered google.map.Map instance
* @param {google.maps.LatLng} center The current LatLng center of the map
*/
/**
* @event typechange
* Fired when display type of the map changes.
* @param {Ext.Map} this
* @param {google.maps.Map} map The rendered google.map.Map instance
* @param {Number} mapType The current display type of the map
*/
/**
* @event zoomchange
* Fired when map is zoomed.
* @param {Ext.Map} this
* @param {google.maps.Map} map The rendered google.map.Map instance
* @param {Number} zoomLevel The current zoom level of the map
*/
/**
* @cfg {String} baseCls
* The base CSS class to apply to the Map's element
* @accessor
*/
baseCls: Ext.baseCSSPrefix + 'map',
/**
* @cfg {Boolean/Ext.util.Geolocation} useCurrentLocation
* Pass in true to center the map based on the geolocation coordinates or pass a
* {@link Ext.util.Geolocation GeoLocation} config to have more control over your GeoLocation options
* @accessor
*/
useCurrentLocation: false,
/**
* @cfg {google.maps.Map} map
* The wrapped map.
* @accessor
*/
map: null,
/**
* @cfg {Ext.util.Geolocation} geo
* Geolocation provider for the map.
* @accessor
*/
geo: null,
/**
* @cfg {Object} mapOptions
* MapOptions as specified by the Google Documentation:
* [http://code.google.com/apis/maps/documentation/v3/reference.html](http://code.google.com/apis/maps/documentation/v3/reference.html)
* @accessor
*/
mapOptions: {}
},
constructor: function() {
this.callParent(arguments);
// this.element.setVisibilityMode(Ext.Element.OFFSETS);
if (!(window.google || {}).maps) {
this.setHtml('Google Maps API is required');
}
},
initialize: function() {
this.callParent();
this.on({
painted: 'doResize',
scope: this
});
this.innerElement.on('touchstart', 'onTouchStart', this);
},
getElementConfig: function() {
return {
reference: 'element',
className: 'x-container',
children: [{
reference: 'innerElement',
className: 'x-inner',
children: [{
reference: 'mapContainer',
className: Ext.baseCSSPrefix + 'map-container'
}]
}]
};
},
onTouchStart: function(e) {
e.makeUnpreventable();
},
applyMapOptions: function(options) {
return Ext.merge({}, this.options, options);
},
updateMapOptions: function(newOptions) {
var me = this,
gm = (window.google || {}).maps,
map = this.getMap();
if (gm && map) {
map.setOptions(newOptions);
}
if (newOptions.center && !me.isPainted()) {
me.un('painted', 'setMapCenter', this);
me.on('painted', 'setMapCenter', this, { delay: 150, single: true, args: [newOptions.center] });
}
},
getMapOptions: function() {
return Ext.merge({}, this.options || this.getInitialConfig('mapOptions'));
},
updateUseCurrentLocation: function(useCurrentLocation) {
this.setGeo(useCurrentLocation);
if (!useCurrentLocation) {
this.renderMap();
}
},
applyGeo: function(config) {
return Ext.factory(config, Ext.util.Geolocation, this.getGeo());
},
updateGeo: function(newGeo, oldGeo) {
var events = {
locationupdate : 'onGeoUpdate',
locationerror : 'onGeoError',
scope : this
};
if (oldGeo) {
oldGeo.un(events);
}
if (newGeo) {
newGeo.on(events);
newGeo.updateLocation();
}
},
doResize: function() {
var gm = (window.google || {}).maps,
map = this.getMap();
if (gm && map) {
gm.event.trigger(map, "resize");
}
},
// @private
renderMap: function() {
var me = this,
gm = (window.google || {}).maps,
element = me.mapContainer,
mapOptions = me.getMapOptions(),
map = me.getMap(),
event;
if (gm) {
if (Ext.os.is.iPad) {
Ext.merge({
navigationControlOptions: {
style: gm.NavigationControlStyle.ZOOM_PAN
}
}, mapOptions);
}
mapOptions = Ext.merge({
zoom: 12,
mapTypeId: gm.MapTypeId.ROADMAP
}, mapOptions);
// This is done separately from the above merge so we don't have to instantiate
// a new LatLng if we don't need to
if (!mapOptions.hasOwnProperty('center')) {
mapOptions.center = new gm.LatLng(37.381592, -122.135672); // Palo Alto
}
if (element.dom.firstChild) {
Ext.fly(element.dom.firstChild).destroy();
}
if (map) {
gm.event.clearInstanceListeners(map);
}
me.setMap(new gm.Map(element.dom, mapOptions));
map = me.getMap();
//Track zoomLevel and mapType changes
event = gm.event;
event.addListener(map, 'zoom_changed', Ext.bind(me.onZoomChange, me));
event.addListener(map, 'maptypeid_changed', Ext.bind(me.onTypeChange, me));
event.addListener(map, 'center_changed', Ext.bind(me.onCenterChange, me));
me.fireEvent('maprender', me, map);
}
},
// @private
onGeoUpdate: function(geo) {
if (geo) {
this.setMapCenter(new google.maps.LatLng(geo.getLatitude(), geo.getLongitude()));
}
},
// @private
onGeoError: Ext.emptyFn,
/**
* Moves the map center to the designated coordinates hash of the form:
*
* { latitude: 37.381592, longitude: -122.135672 }
*
* or a google.maps.LatLng object representing to the target location.
*
* @param {Object/google.maps.LatLng} coordinates Object representing the desired Latitude and
* longitude upon which to center the map.
*/
setMapCenter: function(coordinates) {
var me = this,
map = me.getMap(),
gm = (window.google || {}).maps;
if (gm) {
if (!me.isPainted()) {
me.un('painted', 'setMapCenter', this);
me.on('painted', 'setMapCenter', this, { delay: 150, single: true, args: [coordinates] });
return;
}
coordinates = coordinates || new gm.LatLng(37.381592, -122.135672);
if (coordinates && !(coordinates instanceof gm.LatLng) && 'longitude' in coordinates) {
coordinates = new gm.LatLng(coordinates.latitude, coordinates.longitude);
}
if (!map) {
me.renderMap();
map = me.getMap();
}
if (map && coordinates instanceof gm.LatLng) {
map.panTo(coordinates);
}
else {
this.options = Ext.apply(this.getMapOptions(), {
center: coordinates
});
}
}
},
// @private
onZoomChange : function() {
var mapOptions = this.getMapOptions(),
map = this.getMap(),
zoom;
zoom = (map && map.getZoom) ? map.getZoom() : mapOptions.zoom || 10;
this.options = Ext.apply(mapOptions, {
zoom: zoom
});
this.fireEvent('zoomchange', this, map, zoom);
},
// @private
onTypeChange : function() {
var mapOptions = this.getMapOptions(),
map = this.getMap(),
mapTypeId;
mapTypeId = (map && map.getMapTypeId) ? map.getMapTypeId() : mapOptions.mapTypeId;
this.options = Ext.apply(mapOptions, {
mapTypeId: mapTypeId
});
this.fireEvent('typechange', this, map, mapTypeId);
},
// @private
onCenterChange: function() {
var mapOptions = this.getMapOptions(),
map = this.getMap(),
center;
center = (map && map.getCenter) ? map.getCenter() : mapOptions.center;
this.options = Ext.apply(mapOptions, {
center: center
});
this.fireEvent('centerchange', this, map, center);
},
// @private
destroy: function() {
Ext.destroy(this.getGeo());
var map = this.getMap();
if (map && (window.google || {}).maps) {
google.maps.event.clearInstanceListeners(map);
}
this.callParent();
}
}, function() {
//<deprecated product=touch since=2.0>
/**
* @cfg {Boolean} maskMap
* Masks the map
* @removed 2.0.0 Please mask this components container instead.
*/
/**
* @cfg {String} maskMapCls
* CSS class to add to the map when maskMap is set to true.
* @removed 2.0.0 Please mask this components container instead.
*/
/**
* @method getState
* Returns the state of the Map.
* @deprecated 2.0.0 Please use {@link #getMapOptions} instead.
* @return {Object} mapOptions
*/
Ext.deprecateClassMethod(this, 'getState', 'getMapOptions');
/**
* @method update
* 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.
*
* @deprecated 2.0.0 Please use the {@link #setMapCenter}
* @param {Object/google.maps.LatLng} coordinates Object representing the desired Latitude and
* longitude upon which to center the map.
*/
Ext.deprecateClassMethod(this, 'update', 'setMapCenter');
//</deprecated>
});

91
OfficeWeb/3rdparty/touch/src/Mask.js vendored Normal file
View File

@@ -0,0 +1,91 @@
/**
* 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');
}
});

347
OfficeWeb/3rdparty/touch/src/Media.js vendored Normal file
View File

@@ -0,0 +1,347 @@
/**
* Provides a base class for audio/visual controls. Should not be used directly.
*
* Please see the {@link Ext.Audio} and {@link Ext.Video} classes for more information.
* @private
*/
Ext.define('Ext.Media', {
extend: 'Ext.Component',
xtype: 'media',
/**
* @event play
* Fires whenever the media is played.
* @param {Ext.Media} this
*/
/**
* @event pause
* Fires whenever the media is paused.
* @param {Ext.Media} this
* @param {Number} time The time at which the media was paused at in seconds.
*/
/**
* @event ended
* Fires whenever the media playback has ended.
* @param {Ext.Media} this
* @param {Number} time The time at which the media ended at in seconds.
*/
/**
* @event stop
* Fires whenever the media is stopped.
* The `pause` event will also fire after the `stop` event if the media is currently playing.
* The `timeupdate` event will also fire after the `stop` event regardless of playing status.
* @param {Ext.Media} this
*/
/**
* @event volumechange
* Fires whenever the volume is changed.
* @param {Ext.Media} this
* @param {Number} volume The volume level from 0 to 1.
*/
/**
* @event mutedchange
* Fires whenever the muted status is changed.
* The volumechange event will also fire after the `mutedchange` event fires.
* @param {Ext.Media} this
* @param {Boolean} muted The muted status.
*/
/**
* @event timeupdate
* Fires when the media is playing every 15 to 250ms.
* @param {Ext.Media} this
* @param {Number} time The current time in seconds.
*/
config: {
/**
* @cfg {String} url
* Location of the media to play.
* @accessor
*/
url: '',
/**
* @cfg {Boolean} enableControls
* Set this to `false` to turn off the native media controls.
* Defaults to `false` when you are on Android, as it doesn't support controls.
* @accessor
*/
enableControls: Ext.os.is.Android ? false : true,
/**
* @cfg {Boolean} autoResume
* Will automatically start playing the media when the container is activated.
* @accessor
*/
autoResume: false,
/**
* @cfg {Boolean} autoPause
* Will automatically pause the media when the container is deactivated.
* @accessor
*/
autoPause: true,
/**
* @cfg {Boolean} preload
* Will begin preloading the media immediately.
* @accessor
*/
preload: true,
/**
* @cfg {Boolean} loop
* Will loop the media forever.
* @accessor
*/
loop: false,
/**
* @cfg {Ext.Element} media
* A reference to the underlying audio/video element.
* @accessor
*/
media: null,
/**
* @cfg {Number} volume
* The volume of the media from 0.0 to 1.0.
* @accessor
*/
volume: 1,
/**
* @cfg {Boolean} muted
* Whether or not the media is muted. This will also set the volume to zero.
* @accessor
*/
muted: false
},
constructor: function() {
this.mediaEvents = {};
this.callSuper(arguments);
},
initialize: function() {
var me = this;
me.callParent();
me.on({
scope: me,
activate : me.onActivate,
deactivate: me.onDeactivate
});
me.addMediaListener({
canplay: 'onCanPlay',
play: 'onPlay',
pause: 'onPause',
ended: 'onEnd',
volumechange: 'onVolumeChange',
timeupdate: 'onTimeUpdate'
});
},
addMediaListener: function(event, fn) {
var me = this,
dom = me.media.dom,
bind = Ext.Function.bind;
Ext.Object.each(event, function(e, fn) {
fn = bind(me[fn], me);
me.mediaEvents[e] = fn;
dom.addEventListener(e, fn);
});
},
onPlay: function() {
this.fireEvent('play', this);
},
onCanPlay: function() {
this.fireEvent('canplay', this);
},
onPause: function() {
this.fireEvent('pause', this, this.getCurrentTime());
},
onEnd: function() {
this.fireEvent('ended', this, this.getCurrentTime());
},
onVolumeChange: function() {
this.fireEvent('volumechange', this, this.media.dom.volume);
},
onTimeUpdate: function() {
this.fireEvent('timeupdate', this, this.getCurrentTime());
},
/**
* Returns if the media is currently playing.
* @return {Boolean} playing `true` if the media is playing.
*/
isPlaying: function() {
return !Boolean(this.media.dom.paused);
},
// @private
onActivate: function() {
var me = this;
if (me.getAutoResume() && !me.isPlaying()) {
me.play();
}
},
// @private
onDeactivate: function() {
var me = this;
if (me.getAutoPause() && me.isPlaying()) {
me.pause();
}
},
/**
* Sets the URL of the media element. If the media element already exists, it is update the src attribute of the
* element. If it is currently playing, it will start the new video.
*/
updateUrl: function(newUrl) {
var dom = this.media.dom;
//when changing the src, we must call load:
//http://developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/ControllingMediaWithJavaScript/ControllingMediaWithJavaScript.html
dom.src = newUrl;
if ('load' in dom) {
dom.load();
}
if (this.isPlaying()) {
this.play();
}
},
/**
* Updates the controls of the video element.
*/
updateEnableControls: function(enableControls) {
this.media.dom.controls = enableControls ? 'controls' : false;
},
/**
* Updates the loop setting of the media element.
*/
updateLoop: function(loop) {
this.media.dom.loop = loop ? 'loop' : false;
},
/**
* Starts or resumes media playback.
*/
play: function() {
var dom = this.media.dom;
if ('play' in dom) {
dom.play();
setTimeout(function() {
dom.play();
}, 10);
}
},
/**
* Pauses media playback.
*/
pause: function() {
var dom = this.media.dom;
if ('pause' in dom) {
dom.pause();
}
},
/**
* Toggles the media playback state.
*/
toggle: function() {
if (this.isPlaying()) {
this.pause();
} else {
this.play();
}
},
/**
* Stops media playback and returns to the beginning.
*/
stop: function() {
var me = this;
me.setCurrentTime(0);
me.fireEvent('stop', me);
me.pause();
},
//@private
updateVolume: function(volume) {
this.media.dom.volume = volume;
},
//@private
updateMuted: function(muted) {
this.fireEvent('mutedchange', this, muted);
this.media.dom.muted = muted;
},
/**
* Returns the current time of the media, in seconds.
* @return {Number}
*/
getCurrentTime: function() {
return this.media.dom.currentTime;
},
/*
* Set the current time of the media.
* @param {Number} time The time, in seconds.
* @return {Number}
*/
setCurrentTime: function(time) {
this.media.dom.currentTime = time;
return time;
},
/**
* Returns the duration of the media, in seconds.
* @return {Number}
*/
getDuration: function() {
return this.media.dom.duration;
},
destroy: function() {
var me = this,
dom = me.media.dom,
mediaEvents = me.mediaEvents;
Ext.Object.each(mediaEvents, function(event, fn) {
dom.removeEventListener(event, fn);
});
this.callSuper();
}
});

View File

@@ -0,0 +1,741 @@
/**
* Utility class for generating different styles of message boxes. The framework provides a global singleton
* {@link Ext.Msg} for common usage which you should use in most cases.
*
* If you want to use {@link Ext.MessageBox} directly, just think of it as a modal {@link Ext.Container}.
*
* Note that the MessageBox is asynchronous. Unlike a regular JavaScript `alert` (which will halt browser execution),
* showing a MessageBox will not cause the code to stop. For this reason, if you have code that should only run _after_
* some user feedback from the MessageBox, you must use a callback function (see the `fn` configuration option parameter
* for the {@link #method-show show} method for more details).
*
* @example preview
* Ext.Msg.alert('Title', 'The quick brown fox jumped over the lazy dog.', Ext.emptyFn);
*
* Checkout {@link Ext.Msg} for more examples.
*
*/
Ext.define('Ext.MessageBox', {
extend : 'Ext.Sheet',
requires: [
'Ext.Toolbar',
'Ext.field.Text',
'Ext.field.TextArea'
],
config: {
/**
* @cfg
* @inheritdoc
*/
ui: 'dark',
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'msgbox',
/**
* @cfg {String} iconCls
* CSS class for the icon. The icon should be 40px x 40px.
* @accessor
*/
iconCls: null,
/**
* @cfg
* @inheritdoc
*/
showAnimation: {
type: 'popIn',
duration: 250,
easing: 'ease-out'
},
/**
* @cfg
* @inheritdoc
*/
hideAnimation: {
type: 'popOut',
duration: 250,
easing: 'ease-out'
},
/**
* Override the default `zIndex` so it is normally always above floating components.
*/
zIndex: 999,
/**
* @cfg {Number} defaultTextHeight
* The default height in pixels of the message box's multiline textarea if displayed.
* @accessor
*/
defaultTextHeight: 75,
/**
* @cfg {String} title
* The title of this {@link Ext.MessageBox}.
* @accessor
*/
title: null,
/**
* @cfg {Array/Object} buttons
* An array of buttons, or an object of a button to be displayed in the toolbar of this {@link Ext.MessageBox}.
*/
buttons: null,
/**
* @cfg {String} message
* The message to be displayed in the {@link Ext.MessageBox}.
* @accessor
*/
message: null,
/**
* @cfg {String} msg
* The message to be displayed in the {@link Ext.MessageBox}.
* @removed 2.0.0 Please use {@link #message} instead.
*/
/**
* @cfg {Object} prompt
* The configuration to be passed if you want an {@link Ext.field.Text} or {@link Ext.field.TextArea} field
* in your {@link Ext.MessageBox}.
*
* Pass an object with the property `multiLine` with a value of `true`, if you want the prompt to use a TextArea.
*
* Alternatively, you can just pass in an object which has an xtype/xclass of another component.
*
* prompt: {
* xtype: 'textareafield',
* value: 'test'
* }
*
* @accessor
*/
prompt: null,
/**
* @private
*/
modal: true,
/**
* @cfg
* @inheritdoc
*/
layout: {
type: 'vbox',
pack: 'center'
}
},
statics: {
OK : {text: 'OK', itemId: 'ok', ui: 'action'},
YES : {text: 'Yes', itemId: 'yes', ui: 'action'},
NO : {text: 'No', itemId: 'no'},
CANCEL: {text: 'Cancel', itemId: 'cancel'},
INFO : Ext.baseCSSPrefix + 'msgbox-info',
WARNING : Ext.baseCSSPrefix + 'msgbox-warning',
QUESTION: Ext.baseCSSPrefix + 'msgbox-question',
ERROR : Ext.baseCSSPrefix + 'msgbox-error',
OKCANCEL: [
{text: 'Cancel', itemId: 'cancel'},
{text: 'OK', itemId: 'ok', ui : 'action'}
],
YESNOCANCEL: [
{text: 'Cancel', itemId: 'cancel'},
{text: 'No', itemId: 'no'},
{text: 'Yes', itemId: 'yes', ui: 'action'}
],
YESNO: [
{text: 'No', itemId: 'no'},
{text: 'Yes', itemId: 'yes', ui: 'action'}
]
},
constructor: function(config) {
config = config || {};
if (config.hasOwnProperty('promptConfig')) {
//<debug warn>
Ext.Logger.deprecate("'promptConfig' config is deprecated, please use 'prompt' config instead", this);
//</debug>
Ext.applyIf(config, {
prompt: config.promptConfig
});
delete config.promptConfig;
}
if (config.hasOwnProperty('multiline') || config.hasOwnProperty('multiLine')) {
config.prompt = config.prompt || {};
Ext.applyIf(config.prompt, {
multiLine: config.multiline || config.multiLine
});
delete config.multiline;
delete config.multiLine;
}
this.defaultAllowedConfig = {};
var allowedConfigs = ['ui', 'showAnimation', 'hideAnimation', 'title', 'message', 'prompt', 'iconCls', 'buttons', 'defaultTextHeight'],
ln = allowedConfigs.length,
i, allowedConfig;
for (i = 0; i < ln; i++) {
allowedConfig = allowedConfigs[i];
this.defaultAllowedConfig[allowedConfig] = this.defaultConfig[allowedConfig];
}
this.callParent([config]);
},
/**
* Creates a new {@link Ext.Toolbar} instance using {@link Ext#factory}.
* @private
*/
applyTitle: function(config) {
if (typeof config == "string") {
config = {
title: config
};
}
Ext.applyIf(config, {
docked: 'top',
minHeight: '1.3em',
cls : this.getBaseCls() + '-title'
});
return Ext.factory(config, Ext.Toolbar, this.getTitle());
},
/**
* Adds the new {@link Ext.Toolbar} instance into this container.
* @private
*/
updateTitle: function(newTitle) {
if (newTitle) {
this.add(newTitle);
}
},
/**
* Adds the new {@link Ext.Toolbar} instance into this container.
* @private
*/
updateButtons: function(newButtons) {
var me = this;
if (newButtons) {
if (me.buttonsToolbar) {
me.buttonsToolbar.removeAll();
me.buttonsToolbar.setItems(newButtons);
} else {
me.buttonsToolbar = Ext.create('Ext.Toolbar', {
docked : 'bottom',
defaultType: 'button',
layout : {
type: 'hbox',
pack: 'center'
},
ui : me.getUi(),
cls : me.getBaseCls() + '-buttons',
items : newButtons
});
me.add(me.buttonsToolbar);
}
}
},
/**
* @private
*/
applyMessage: function(config) {
config = {
html : config,
cls : this.getBaseCls() + '-text'
};
return Ext.factory(config, Ext.Component, this._message);
},
/**
* @private
*/
updateMessage: function(newMessage) {
if (newMessage) {
this.add(newMessage);
}
},
getMessage: function() {
if (this._message) {
return this._message.getHtml();
}
return null;
},
/**
* @private
*/
applyIconCls: function(config) {
config = {
xtype : 'component',
docked: 'left',
width : 40,
height: 40,
baseCls: Ext.baseCSSPrefix + 'icon',
hidden: (config) ? false : true,
cls: config
};
return Ext.factory(config, Ext.Component, this._iconCls);
},
/**
* @private
*/
updateIconCls: function(newIconCls, oldIconCls) {
var me = this;
//ensure the title and button elements are added first
this.getTitle();
this.getButtons();
if (newIconCls) {
this.add(newIconCls);
} else {
this.remove(oldIconCls);
}
},
getIconCls: function() {
var icon = this._iconCls,
iconCls;
if (icon) {
iconCls = icon.getCls();
return (iconCls) ? iconCls[0] : null;
}
return null;
},
/**
* @private
*/
applyPrompt: function(prompt) {
if (prompt) {
var config = {
label: false
};
if (Ext.isObject(prompt)) {
Ext.apply(config, prompt);
}
if (config.multiLine) {
config.height = Ext.isNumber(config.multiLine) ? parseFloat(config.multiLine) : this.getDefaultTextHeight();
return Ext.factory(config, Ext.field.TextArea, this.getPrompt());
} else {
return Ext.factory(config, Ext.field.Text, this.getPrompt());
}
}
return prompt;
},
/**
* @private
*/
updatePrompt: function(newPrompt, oldPrompt) {
if (newPrompt) {
this.add(newPrompt);
}
if (oldPrompt) {
this.remove(oldPrompt);
}
},
// @private
// pass `fn` config to show method instead
onClick: function(button) {
if (button) {
var config = button.config.userConfig || {},
initialConfig = button.getInitialConfig(),
prompt = this.getPrompt();
if (typeof config.fn == 'function') {
this.on({
hiddenchange: function() {
config.fn.call(
config.scope || null,
initialConfig.itemId || initialConfig.text,
prompt ? prompt.getValue() : null,
config
);
},
single: true,
scope: this
});
}
if (config.input) {
config.input.dom.blur();
}
}
this.hide();
},
/**
* Displays the {@link Ext.MessageBox} with a specified configuration. All
* display functions (e.g. {@link #method-prompt}, {@link #alert}, {@link #confirm})
* on MessageBox call this function internally, although those calls
* are basic shortcuts and do not support all of the config options allowed here.
*
* Example usage:
*
* @example
* Ext.Msg.show({
* title: 'Address',
* message: 'Please enter your address:',
* width: 300,
* buttons: Ext.MessageBox.OKCANCEL,
* multiLine: true,
* prompt : { maxlength : 180, autocapitalize : true },
* fn: function(buttonId) {
* alert('You pressed the "' + buttonId + '" button.');
* }
* });
*
* @param {Object} config An object with the following config options:
*
* @param {Object/Array} [config.buttons=false]
* A button config object or Array of the same(e.g., `Ext.MessageBox.OKCANCEL` or `{text:'Foo', itemId:'cancel'}`),
* or false to not show any buttons.
*
* @param {String} config.cls
* A custom CSS class to apply to the message box's container element.
*
* @param {Function} config.fn
* A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
*
* @param {String} config.fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
* @param {String} config.fn.value Value of the input field if either `prompt` or `multiline` option is `true`.
* @param {Object} config.fn.opt The config object passed to show.
*
* @param {Number} [config.width=auto]
* A fixed width for the MessageBox.
*
* @param {Number} [config.height=auto]
* A fixed height for the MessageBox.
*
* @param {Object} config.scope
* The scope of the callback function
*
* @param {String} config.icon
* A CSS class that provides a background image to be used as the body icon for the dialog
* (e.g. Ext.MessageBox.WARNING or 'custom-class').
*
* @param {Boolean} [config.modal=true]
* `false` to allow user interaction with the page while the message box is displayed.
*
* @param {String} [config.message=&#160;]
* A string that will replace the existing message box body text.
* Defaults to the XHTML-compliant non-breaking space character `&#160;`.
*
* @param {Number} [config.defaultTextHeight=75]
* The default height in pixels of the message box's multiline textarea if displayed.
*
* @param {Boolean} [config.prompt=false]
* `true` to prompt the user to enter single-line text. Please view the {@link Ext.MessageBox#method-prompt} documentation in {@link Ext.MessageBox}.
* for more information.
*
* @param {Boolean} [config.multiline=false]
* `true` to prompt the user to enter multi-line text.
*
* @param {String} config.title
* The title text.
*
* @param {String} config.value
* The string value to set into the active textbox element if displayed.
*
* @return {Ext.MessageBox} this
*/
show: function(initialConfig) {
//if it has not been added to a container, add it to the Viewport.
if (!this.getParent() && Ext.Viewport) {
Ext.Viewport.add(this);
}
if (!initialConfig) {
return this.callParent();
}
var config = Ext.Object.merge({}, {
value: ''
}, initialConfig);
var buttons = initialConfig.buttons || Ext.MessageBox.OK || [],
buttonBarItems = [],
userConfig = initialConfig;
Ext.each(buttons, function(buttonConfig) {
if (!buttonConfig) {
return;
}
buttonBarItems.push(Ext.apply({
userConfig: userConfig,
scope : this,
handler : 'onClick'
}, buttonConfig));
}, this);
config.buttons = buttonBarItems;
if (config.promptConfig) {
//<debug warn>
Ext.Logger.deprecate("'promptConfig' config is deprecated, please use 'prompt' config instead", this);
//</debug>
}
config.prompt = (config.promptConfig || config.prompt) || null;
if (config.multiLine) {
config.prompt = config.prompt || {};
config.prompt.multiLine = config.multiLine;
delete config.multiLine;
}
config = Ext.merge({}, this.defaultAllowedConfig, config);
this.setConfig(config);
var prompt = this.getPrompt();
if (prompt) {
prompt.setValue(initialConfig.value || '');
}
this.callParent();
return this;
},
/**
* Displays a standard read-only message box with an OK button (comparable to the basic JavaScript alert prompt). If
* a callback function is passed it will be called after the user clicks the button, and the `itemId` of the button
* that was clicked will be passed as the only parameter to the callback.
*
* @param {String} title The title bar text.
* @param {String} message The message box body text.
* @param {Function} fn A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
* @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
* @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
* @param {Object} fn.opt The config object passed to show.
* @param {Object} scope The scope (`this` reference) in which the callback is executed.
* Defaults to: the browser window
*
* @return {Ext.MessageBox} this
*/
alert: function(title, message, fn, scope) {
return this.show({
title: title || null,
message: message || null,
buttons: Ext.MessageBox.OK,
promptConfig: false,
fn: function() {
if (fn) {
fn.apply(scope, arguments);
}
},
scope: scope
});
},
/**
* Displays a confirmation message box with Yes and No buttons (comparable to JavaScript's confirm). If a callback
* function is passed it will be called after the user clicks either button, and the id of the button that was
* clicked will be passed as the only parameter to the callback (could also be the top-right close button).
*
* @param {String} title The title bar text.
* @param {String} message The message box body text.
* @param {Function} fn A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
* @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
* @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
* @param {Object} fn.opt The config object passed to show.
* @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
*
* Defaults to: the browser window
*
* @return {Ext.MessageBox} this
*/
confirm: function(title, message, fn, scope) {
return this.show({
title : title || null,
message : message || null,
buttons : Ext.MessageBox.YESNO,
promptConfig: false,
scope : scope,
fn: function() {
if (fn) {
fn.apply(scope, arguments);
}
}
});
},
/**
* Displays a message box with OK and Cancel buttons prompting the user to enter some text (comparable to
* JavaScript's prompt). The prompt can be a single-line or multi-line textbox. If a callback function is passed it
* will be called after the user clicks either button, and the id of the button that was clicked (could also be the
* top-right close button) and the text that was entered will be passed as the two parameters to the callback.
*
* Example usage:
*
* @example
* Ext.Msg.prompt(
* 'Welcome!',
* 'What\'s your name going to be today?',
* function (buttonId, value) {
* console.log(value);
* },
* null,
* false,
* null,
* {
* autoCapitalize: true,
* placeHolder: 'First-name please...'
* }
* );
*
* @param {String} title The title bar text.
* @param {String} message The message box body text.
* @param {Function} fn A callback function which is called when the dialog is dismissed by clicking on the configured buttons.
* @param {String} fn.buttonId The `itemId` of the button pressed, one of: 'ok', 'yes', 'no', 'cancel'.
* @param {String} fn.value Value of the input field if either `prompt` or `multiLine` option is `true`.
* @param {Object} fn.opt The config object passed to show.
* @param {Object} scope The scope (`this` reference) in which the callback is executed.
*
* Defaults to: the browser window.
*
* @param {Boolean/Number} [multiLine=false] `true` to create a multiline textbox using the `defaultTextHeight` property,
* or the height in pixels to create the textbox.
*
* @param {String} [value] Default value of the text input element.
*
* @param {Object} [prompt=true]
* The configuration for the prompt. See the {@link Ext.MessageBox#cfg-prompt prompt} documentation in {@link Ext.MessageBox}
* for more information.
*
* @return {Ext.MessageBox} this
*/
prompt: function(title, message, fn, scope, multiLine, value, prompt) {
return this.show({
title : title || null,
message : message || null,
buttons : Ext.MessageBox.OKCANCEL,
scope : scope,
prompt : prompt || true,
multiLine: multiLine,
value : value,
fn: function() {
if (fn) {
fn.apply(scope, arguments);
}
}
});
}
}, function(MessageBox) {
// <deprecated product=touch since=2.0>
this.override({
/**
* @cfg {String} icon
* Sets CSS class for icon.
* @removed 2.0 Use #iconCls instead.
*/
/**
* Sets #icon.
* @deprecated 2.0 Please use #setIconCls instead.
* @param {String} icon A CSS class name or empty string to clear the icon.
* @return {Ext.MessageBox} this
*/
setIcon: function(iconCls, doLayout){
//<debug warn>
Ext.Logger.deprecate("Ext.MessageBox#setIcon is deprecated, use setIconCls instead", 2);
//</debug>
this.setIconCls(iconCls);
return this;
},
/**
* @inheritdoc Ext.MessageBox#setMessage
* @deprecated 2.0.0 Please use #setMessage instead.
*/
updateText: function(text){
//<debug warn>
Ext.Logger.deprecate("Ext.MessageBox#updateText is deprecated, use setMessage instead", 2);
//</debug>
this.setMessage(text);
return this;
}
});
// </deprecated>
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;
});
});

213
OfficeWeb/3rdparty/touch/src/Panel.js vendored Normal file
View File

@@ -0,0 +1,213 @@
/**
* @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);
}
}
});

View File

@@ -0,0 +1,361 @@
/**
* 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() {
//<deprecated product=touch since=2.0>
var me = this;
/**
* Activates a button.
* @param {Number/String/Ext.Button} button The button to activate.
* @param {Boolean} pressed If defined, sets the pressed state of the button,
* otherwise the pressed state is toggled.
* @param {Boolean} suppressEvents `true` to suppress toggle events during the action.
* If {@link #allowMultiple} is `true`, then {@link #setPressed} will toggle the button state.
* @method setPressed
* @deprecated 2.0.0 Please use {@link #setPressedButtons} instead
*/
Ext.deprecateClassMethod(me, 'setPressed', 'setPressedButtons');
/**
* Gets the currently pressed button(s).
* @method getPressed
* @deprecated 2.0.0 Please use {@link #getPressedButtons} instead
*/
Ext.deprecateClassMethod(me, 'getPressed', 'getPressedButtons');
//</deprecated>
});

167
OfficeWeb/3rdparty/touch/src/Sheet.js vendored Normal file
View File

@@ -0,0 +1,167 @@
/**
* 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);
}
}
});

328
OfficeWeb/3rdparty/touch/src/Sortable.js vendored Normal file
View File

@@ -0,0 +1,328 @@
/**
* 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;
}
});

144
OfficeWeb/3rdparty/touch/src/Spacer.js vendored Normal file
View File

@@ -0,0 +1,144 @@
/**
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]);
}
});

View File

@@ -0,0 +1,120 @@
(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);
}
}
});

329
OfficeWeb/3rdparty/touch/src/Template.js vendored Normal file
View File

@@ -0,0 +1,329 @@
/**
* Represents an HTML fragment template. Templates may be {@link #compile precompiled} for greater performance.
*
* An instance of this class may be created by passing to the constructor either a single argument, or multiple
* arguments:
*
* # Single argument: String/Array
*
* The single argument may be either a String or an Array:
*
* - String:
*
* var t = new Ext.Template("<div>Hello {0}.</div>");
* t.{@link #append}('some-element', ['foo']);
*
* - Array: An Array will be combined with `join('')`.
*
* var t = new Ext.Template([
* '<div name="{id}">',
* '<span class="{cls}">{name:trim} {value:ellipsis(10)}</span>',
* '</div>'
* ]);
* t.{@link #compile}();
* t.{@link #append}('some-element', {id: 'myid', cls: 'myclass', name: 'foo', value: 'bar'});
*
* # Multiple arguments: String, Object, Array, ...
*
* Multiple arguments will be combined with `join('')`.
*
* var t = new Ext.Template(
* '<div name="{id}">',
* '<span class="{cls}">{name} {value}</span>',
* '</div>',
* // a configuration object:
* {
* compiled: true // {@link #compile} immediately
* }
* );
*
* # Notes
*
* - For a list of available format functions, see {@link Ext.util.Format}.
* - `disableFormats` reduces `{@link #apply}` time when no formatting is required.
*/
Ext.define('Ext.Template', {
/* Begin Definitions */
requires: ['Ext.dom.Helper', 'Ext.util.Format'],
inheritableStatics: {
/**
* Creates a template from the passed element's value (_display:none_ textarea, preferred) or `innerHTML`.
* @param {String/HTMLElement} el A DOM element or its `id`.
* @param {Object} config (optional) Config object.
* @return {Ext.Template} The created template.
* @static
* @inheritable
*/
from: function(el, config) {
el = Ext.getDom(el);
return new this(el.value || el.innerHTML, config || '');
}
},
/* End Definitions */
/**
* Creates new template.
*
* @param {String...} html List of strings to be concatenated into template.
* Alternatively an array of strings can be given, but then no config object may be passed.
* @param {Object} config (optional) Config object.
*/
constructor: function(html) {
var me = this,
args = arguments,
buffer = [],
i = 0,
length = args.length,
value;
me.initialConfig = {};
// Allow an array to be passed here so we can
// pass an array of strings and an object
// at the end
if (length === 1 && Ext.isArray(html)) {
args = html;
length = args.length;
}
if (length > 1) {
for (; i < length; i++) {
value = args[i];
if (typeof value == 'object') {
Ext.apply(me.initialConfig, value);
Ext.apply(me, value);
} else {
buffer.push(value);
}
}
} else {
buffer.push(html);
}
// @private
me.html = buffer.join('');
if (me.compiled) {
me.compile();
}
},
/**
* @property {Boolean} isTemplate
* `true` in this class to identify an object as an instantiated Template, or subclass thereof.
*/
isTemplate: true,
/**
* @cfg {Boolean} [compiled=false]
* `true` to immediately compile the template.
*/
/**
* @cfg {Boolean} [disableFormats=false]
* `true` to disable format functions in the template. If the template doesn't contain
* format functions, setting `disableFormats` to `true` will reduce apply time.
*/
disableFormats: false,
re: /\{([\w\-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g,
/**
* Returns an HTML fragment of this template with the specified values applied.
*
* @param {Object/Array} values The template values. Can be an array if your params are numeric:
*
* var tpl = new Ext.Template('Name: {0}, Age: {1}');
* tpl.apply(['John', 25]);
*
* or an object:
*
* var tpl = new Ext.Template('Name: {name}, Age: {age}');
* tpl.apply({name: 'John', age: 25});
*
* @return {String} The HTML fragment.
*/
apply: function(values) {
var me = this,
useFormat = me.disableFormats !== true,
fm = Ext.util.Format,
tpl = me,
ret;
if (me.compiled) {
return me.compiled(values).join('');
}
function fn(m, name, format, args) {
if (format && useFormat) {
if (args) {
args = [values[name]].concat(Ext.functionFactory('return ['+ args +'];')());
} else {
args = [values[name]];
}
if (format.substr(0, 5) == "this.") {
return tpl[format.substr(5)].apply(tpl, args);
}
else {
return fm[format].apply(fm, args);
}
}
else {
return values[name] !== undefined ? values[name] : "";
}
}
ret = me.html.replace(me.re, fn);
return ret;
},
/**
* Appends the result of this template to the provided output array.
* @param {Object/Array} values The template values. See {@link #apply}.
* @param {Array} out The array to which output is pushed.
* @return {Array} The given out array.
*/
applyOut: function(values, out) {
var me = this;
if (me.compiled) {
out.push.apply(out, me.compiled(values));
} else {
out.push(me.apply(values));
}
return out;
},
/**
* @method applyTemplate
* @member Ext.Template
* Alias for {@link #apply}.
* @inheritdoc Ext.Template#apply
*/
applyTemplate: function () {
return this.apply.apply(this, arguments);
},
/**
* Sets the HTML used as the template and optionally compiles it.
* @param {String} html
* @param {Boolean} compile (optional) `true` to compile the template.
* @return {Ext.Template} this
*/
set: function(html, compile) {
var me = this;
me.html = html;
me.compiled = null;
return compile ? me.compile() : me;
},
compileARe: /\\/g,
compileBRe: /(\r\n|\n)/g,
compileCRe: /'/g,
/**
* Compiles the template into an internal function, eliminating the RegEx overhead.
* @return {Ext.Template} this
*/
compile: function() {
var me = this,
fm = Ext.util.Format,
useFormat = me.disableFormats !== true,
body, bodyReturn;
function fn(m, name, format, args) {
if (format && useFormat) {
args = args ? ',' + args: "";
if (format.substr(0, 5) != "this.") {
format = "fm." + format + '(';
}
else {
format = 'this.' + format.substr(5) + '(';
}
}
else {
args = '';
format = "(values['" + name + "'] == undefined ? '' : ";
}
return "'," + format + "values['" + name + "']" + args + ") ,'";
}
bodyReturn = me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn);
body = "this.compiled = function(values){ return ['" + bodyReturn + "'];};";
eval(body);
return me;
},
/**
* Applies the supplied values to the template and inserts the new node(s) as the first child of el.
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) `true` to return a Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element.
*/
insertFirst: function(el, values, returnElement) {
return this.doInsert('afterBegin', el, values, returnElement);
},
/**
* Applies the supplied values to the template and inserts the new node(s) before el.
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) `true` to return an Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element
*/
insertBefore: function(el, values, returnElement) {
return this.doInsert('beforeBegin', el, values, returnElement);
},
/**
* Applies the supplied values to the template and inserts the new node(s) after el.
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) `true` to return a Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element.
*/
insertAfter: function(el, values, returnElement) {
return this.doInsert('afterEnd', el, values, returnElement);
},
/**
* Applies the supplied `values` to the template and appends the new node(s) to the specified `el`.
*
* For example usage see {@link Ext.Template Ext.Template class docs}.
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) true to return an Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element.
*/
append: function(el, values, returnElement) {
return this.doInsert('beforeEnd', el, values, returnElement);
},
doInsert: function(where, el, values, returnElement) {
var newNode = Ext.DomHelper.insertHtml(where, Ext.getDom(el), this.apply(values));
return returnElement ? Ext.get(newNode) : newNode;
},
/**
* Applies the supplied values to the template and overwrites the content of el with the new node(s).
*
* @param {String/HTMLElement/Ext.Element} el The context element.
* @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
* @param {Boolean} returnElement (optional) true to return a Ext.Element.
* @return {HTMLElement/Ext.Element} The new node or Element.
*/
overwrite: function(el, values, returnElement) {
var newNode = Ext.DomHelper.overwrite(Ext.getDom(el), this.apply(values));
return returnElement ? Ext.get(newNode) : newNode;
}
});

26
OfficeWeb/3rdparty/touch/src/Title.js vendored Normal file
View File

@@ -0,0 +1,26 @@
/**
* {@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);
}
});

331
OfficeWeb/3rdparty/touch/src/TitleBar.js vendored Normal file
View File

@@ -0,0 +1,331 @@
/**
* {@link Ext.TitleBar}'s are most commonly used as a docked item within an {@link Ext.Container}.
*
* The main difference between a {@link Ext.TitleBar} and an {@link Ext.Toolbar} is that
* the {@link #title} configuration is **always** centered horizontally in a {@link Ext.TitleBar} between
* any items aligned left or right.
*
* You can also give items of a {@link Ext.TitleBar} an `align` configuration of `left` or `right`
* which will dock them to the `left` or `right` of the bar.
*
* ## Examples
*
* @example preview
* Ext.Viewport.add({
* xtype: 'titlebar',
* docked: 'top',
* title: 'Navigation',
* items: [
* {
* iconCls: 'add',
* iconMask: true,
* align: 'left'
* },
* {
* iconCls: 'home',
* iconMask: true,
* align: 'right'
* }
* ]
* });
*
* Ext.Viewport.setStyleHtmlContent(true);
* Ext.Viewport.setHtml('This shows the title being centered and buttons using align <i>left</i> and <i>right</i>.');
*
* <br />
*
* @example preview
* Ext.Viewport.add({
* xtype: 'titlebar',
* docked: 'top',
* title: 'Navigation',
* items: [
* {
* align: 'left',
* text: 'This button has a super long title'
* },
* {
* iconCls: 'home',
* iconMask: true,
* align: 'right'
* }
* ]
* });
*
* Ext.Viewport.setStyleHtmlContent(true);
* Ext.Viewport.setHtml('This shows how the title is automatically moved to the right when one of the aligned buttons is very wide.');
*
* <br />
*
* @example preview
* Ext.Viewport.add({
* xtype: 'titlebar',
* docked: 'top',
* title: 'A very long title',
* items: [
* {
* align: 'left',
* text: 'This button has a super long title'
* },
* {
* align: 'right',
* text: 'Another button'
* }
* ]
* });
*
* Ext.Viewport.setStyleHtmlContent(true);
* Ext.Viewport.setHtml('This shows how the title and buttons will automatically adjust their size when the width of the items are too wide..');
*
* The {@link #defaultType} of Toolbar's is {@link Ext.Button button}.
*/
Ext.define('Ext.TitleBar', {
extend: 'Ext.Container',
xtype: 'titlebar',
requires: [
'Ext.Button',
'Ext.Title',
'Ext.Spacer'
],
// @private
isToolbar: true,
config: {
/**
* @cfg
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'toolbar',
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'navigation-bar',
/**
* @cfg {String} ui
* Style options for Toolbar. Either 'light' or 'dark'.
* @accessor
*/
ui: 'dark',
/**
* @cfg {String} title
* The title of the toolbar.
* @accessor
*/
title: null,
/**
* @cfg {String} defaultType
* The default xtype to create.
* @accessor
*/
defaultType: 'button',
height: '2.6em',
/**
* @cfg
* @hide
*/
layout: {
type: 'hbox'
},
/**
* @cfg {Array/Object} items The child items to add to this TitleBar. The {@link #defaultType} of
* a TitleBar is {@link Ext.Button}, so you do not need to specify an `xtype` if you are adding
* buttons.
*
* You can also give items a `align` configuration which will align the item to the `left` or `right` of
* the TitleBar.
* @accessor
*/
items: []
},
/**
* The max button width in this toolbar
* @private
*/
maxButtonWidth: '40%',
constructor: function() {
this.refreshTitlePosition = Ext.Function.createThrottled(this.refreshTitlePosition, 50, this);
this.callParent(arguments);
},
beforeInitialize: function() {
this.applyItems = this.applyInitialItems;
},
initialize: function() {
delete this.applyItems;
this.add(this.initialItems);
delete this.initialItems;
this.on({
painted: 'refreshTitlePosition',
single: true
});
},
applyInitialItems: function(items) {
var me = this,
defaults = me.getDefaults() || {};
me.initialItems = items;
me.leftBox = me.add({
xtype: 'container',
style: 'position: relative',
layout: {
type: 'hbox',
align: 'center'
},
listeners: {
resize: 'refreshTitlePosition',
scope: me
}
});
me.spacer = me.add({
xtype: 'component',
style: 'position: relative',
flex: 1,
listeners: {
resize: 'refreshTitlePosition',
scope: me
}
});
me.rightBox = me.add({
xtype: 'container',
style: 'position: relative',
layout: {
type: 'hbox',
align: 'center'
},
listeners: {
resize: 'refreshTitlePosition',
scope: me
}
});
me.titleComponent = me.add({
xtype: 'title',
hidden: defaults.hidden,
centered: true
});
me.doAdd = me.doBoxAdd;
me.remove = me.doBoxRemove;
me.doInsert = me.doBoxInsert;
},
doBoxAdd: function(item) {
if (item.config.align == 'right') {
this.rightBox.add(item);
}
else {
this.leftBox.add(item);
}
},
doBoxRemove: function(item) {
if (item.config.align == 'right') {
this.rightBox.remove(item);
}
else {
this.leftBox.remove(item);
}
},
doBoxInsert: function(index, item) {
if (item.config.align == 'right') {
this.rightBox.add(item);
}
else {
this.leftBox.add(item);
}
},
getMaxButtonWidth: function() {
var value = this.maxButtonWidth;
//check if it is a percentage
if (Ext.isString(this.maxButtonWidth)) {
value = parseInt(value.replace('%', ''), 10);
value = Math.round((this.element.getWidth() / 100) * value);
}
return value;
},
refreshTitlePosition: function() {
var titleElement = this.titleComponent.renderElement;
titleElement.setWidth(null);
titleElement.setLeft(null);
//set the min/max width of the left button
var leftBox = this.leftBox,
leftButton = leftBox.down('button'),
singleButton = leftBox.getItems().getCount() == 1,
leftBoxWidth, maxButtonWidth;
if (leftButton && singleButton) {
if (leftButton.getWidth() == null) {
leftButton.renderElement.setWidth('auto');
}
leftBoxWidth = leftBox.renderElement.getWidth();
maxButtonWidth = this.getMaxButtonWidth();
if (leftBoxWidth > maxButtonWidth) {
leftButton.renderElement.setWidth(maxButtonWidth);
}
}
var spacerBox = this.spacer.renderElement.getPageBox(),
titleBox = titleElement.getPageBox(),
widthDiff = titleBox.width - spacerBox.width,
titleLeft = titleBox.left,
titleRight = titleBox.right,
halfWidthDiff, leftDiff, rightDiff;
if (widthDiff > 0) {
titleElement.setWidth(spacerBox.width);
halfWidthDiff = widthDiff / 2;
titleLeft += halfWidthDiff;
titleRight -= halfWidthDiff;
}
leftDiff = spacerBox.left - titleLeft;
rightDiff = titleRight - spacerBox.right;
if (leftDiff > 0) {
titleElement.setLeft(leftDiff);
}
else if (rightDiff > 0) {
titleElement.setLeft(-rightDiff);
}
titleElement.repaint();
},
// @private
updateTitle: function(newTitle) {
this.titleComponent.setTitle(newTitle);
if (this.isPainted()) {
this.refreshTitlePosition();
}
}
});

260
OfficeWeb/3rdparty/touch/src/Toolbar.js vendored Normal file
View File

@@ -0,0 +1,260 @@
/**
* @aside video tabs-toolbars
*
* {@link Ext.Toolbar}s are most commonly used as docked items as within a {@link Ext.Container}. They can be docked either `top` or `bottom` using the {@link #docked} configuration.
*
* They allow you to insert items (normally {@link Ext.Button buttons}) and also add a {@link #title}.
*
* The {@link #defaultType} of {@link Ext.Toolbar} is {@link Ext.Button}.
*
* You can alternatively use {@link Ext.TitleBar} if you want the title to automatically adjust the size of its items.
*
* ## Examples
*
* @example miniphone preview
* Ext.create('Ext.Container', {
* fullscreen: true,
* layout: {
* type: 'vbox',
* pack: 'center'
* },
* items: [
* {
* xtype : 'toolbar',
* docked: 'top',
* title: 'My Toolbar'
* },
* {
* xtype: 'container',
* defaults: {
* xtype: 'button',
* margin: '10 10 0 10'
* },
* items: [
* {
* text: 'Toggle docked',
* handler: function() {
* var toolbar = Ext.ComponentQuery.query('toolbar')[0],
* newDocked = (toolbar.getDocked() === 'top') ? 'bottom' : 'top';
*
* toolbar.setDocked(newDocked);
* }
* },
* {
* text: 'Toggle UI',
* handler: function() {
* var toolbar = Ext.ComponentQuery.query('toolbar')[0],
* newUi = (toolbar.getUi() === 'light') ? 'dark' : 'light';
*
* toolbar.setUi(newUi);
* }
* },
* {
* text: 'Change title',
* handler: function() {
* var toolbar = Ext.ComponentQuery.query('toolbar')[0],
* titles = ['My Toolbar', 'Ext.Toolbar', 'Configurations are awesome!', 'Beautiful.'],
//internally, the title configuration gets converted into a {@link Ext.Title} component,
//so you must get the title configuration of that component
* title = toolbar.getTitle().getTitle(),
* newTitle = titles[titles.indexOf(title) + 1] || titles[0];
*
* toolbar.setTitle(newTitle);
* }
* }
* ]
* }
* ]
* });
*
* ## Notes
*
* You must use a HTML5 doctype for {@link #docked} `bottom` to work. To do this, simply add the following code to the HTML file:
*
* <!doctype html>
*
* So your index.html file should look a little like this:
*
* <!doctype html>
* <html>
* <head>
* <title>MY application title</title>
* ...
*
*/
Ext.define('Ext.Toolbar', {
extend: 'Ext.Container',
xtype : 'toolbar',
requires: [
'Ext.Button',
'Ext.Title',
'Ext.Spacer',
'Ext.layout.HBox'
],
// @private
isToolbar: true,
config: {
/**
* @cfg baseCls
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'toolbar',
/**
* @cfg {String} ui
* The ui for this {@link Ext.Toolbar}. Either 'light' or 'dark'. You can create more UIs by using using the CSS Mixin {@link #sencha-toolbar-ui}
* @accessor
*/
ui: 'dark',
/**
* @cfg {String/Ext.Title} title
* The title of the toolbar.
* @accessor
*/
title: null,
/**
* @cfg {String} defaultType
* The default xtype to create.
* @accessor
*/
defaultType: 'button',
/**
* @cfg {String} docked
* The docked position for this {@link Ext.Toolbar}.
* If you specify `left` or `right`, the {@link #layout} configuration will automatically change to a `vbox`. It's also
* recommended to adjust the {@link #width} of the toolbar if you do this.
* @accessor
*/
/**
* @cfg {String} minHeight
* The minimum height height of the Toolbar.
* @accessor
*/
minHeight: '2.6em',
/**
* @cfg {Object/String} layout Configuration for this Container's layout. Example:
*
* Ext.create('Ext.Container', {
* layout: {
* type: 'hbox',
* align: 'middle'
* },
* items: [
* {
* xtype: 'panel',
* flex: 1,
* style: 'background-color: red;'
* },
* {
* xtype: 'panel',
* flex: 2,
* style: 'background-color: green'
* }
* ]
* });
*
* See the [layouts guide](#!/guides/layouts) for more information
*
* __Note:__ If you set the {@link #docked} configuration to `left` or `right`, the default layout will change from the
* `hbox` to a `vbox`.
*
* @accessor
*/
layout: {
type: 'hbox',
align: 'center'
}
},
constructor: function(config) {
config = config || {};
if (config.docked == "left" || config.docked == "right") {
config.layout = {
type: 'vbox',
align: 'stretch'
};
}
this.callParent([config]);
},
// @private
applyTitle: function(title) {
if (typeof title == 'string') {
title = {
title: title,
centered: true
};
}
return Ext.factory(title, Ext.Title, this.getTitle());
},
// @private
updateTitle: function(newTitle, oldTitle) {
if (newTitle) {
this.add(newTitle);
}
if (oldTitle) {
oldTitle.destroy();
}
},
/**
* Shows the title, if it exists.
*/
showTitle: function() {
var title = this.getTitle();
if (title) {
title.show();
}
},
/**
* Hides the title, if it exists.
*/
hideTitle: function() {
var title = this.getTitle();
if (title) {
title.hide();
}
}
/**
* Returns an {@link Ext.Title} component.
* @member Ext.Toolbar
* @method getTitle
* @return {Ext.Title}
*/
/**
* Use this to update the {@link #title} configuration.
* @member Ext.Toolbar
* @method setTitle
* @param {String/Ext.Title} title You can either pass a String, or a config/instance of {@link Ext.Title}.
*/
}, function() {
//<deprecated product=touch since=2.0>
/**
* @member Ext.Toolbar
* @cfg {Boolean} titleCls
* The CSS class to apply to the `titleEl`.
* @removed 2.0.0 Title class is now a config option of the title
*/
Ext.deprecateProperty(this, 'titleCls', null, "Ext.Toolbar.titleCls has been removed. Use #cls config of title instead.");
//</deprecated>
});

195
OfficeWeb/3rdparty/touch/src/Video.js vendored Normal file
View File

@@ -0,0 +1,195 @@
/**
* @aside example video
* Provides a simple Container for HTML5 Video.
*
* ## Notes
*
* - There are quite a few issues with the `<video>` tag on Android devices. On Android 2+, the video will
* appear and play on first attempt, but any attempt afterwards will not work.
*
* ## Useful Properties
*
* - {@link #url}
* - {@link #autoPause}
* - {@link #autoResume}
*
* ## Useful Methods
*
* - {@link #method-pause}
* - {@link #method-play}
* - {@link #toggle}
*
* ## Example
*
* var panel = Ext.create('Ext.Panel', {
* fullscreen: true,
* items: [
* {
* xtype : 'video',
* x : 600,
* y : 300,
* width : 175,
* height : 98,
* url : "porsche911.mov",
* posterUrl: 'porsche.png'
* }
* ]
* });
*/
Ext.define('Ext.Video', {
extend: 'Ext.Media',
xtype: 'video',
config: {
/**
* @cfg {String/Array} urls
* Location of the video to play. This should be in H.264 format and in a .mov file format.
* @accessor
*/
/**
* @cfg {String} posterUrl
* Location of a poster image to be shown before showing the video.
* @accessor
*/
posterUrl: null,
/**
* @cfg
* @inheritdoc
*/
cls: Ext.baseCSSPrefix + 'video'
},
template: [{
/**
* @property {Ext.dom.Element} ghost
* @private
*/
reference: 'ghost',
classList: [Ext.baseCSSPrefix + 'video-ghost']
}, {
tag: 'video',
reference: 'media',
classList: [Ext.baseCSSPrefix + 'media']
}],
initialize: function() {
var me = this;
me.callParent();
me.media.hide();
me.onBefore({
erased: 'onErased',
scope: me
});
me.ghost.on({
tap: 'onGhostTap',
scope: me
});
me.media.on({
pause: 'onPause',
scope: me
});
if (Ext.os.is.Android4 || Ext.os.is.iPad) {
this.isInlineVideo = true;
}
},
applyUrl: function(url) {
return [].concat(url);
},
updateUrl: function(newUrl) {
var me = this,
media = me.media,
newLn = newUrl.length,
existingSources = media.query('source'),
oldLn = existingSources.length,
i;
for (i = 0; i < oldLn; i++) {
Ext.fly(existingSources[i]).destroy();
}
for (i = 0; i < newLn; i++) {
media.appendChild(Ext.Element.create({
tag: 'source',
src: newUrl[i]
}));
}
if (me.isPlaying()) {
me.play();
}
},
onErased: function() {
this.pause();
this.media.setTop(-2000);
this.ghost.show();
},
/**
* @private
* Called when the {@link #ghost} element is tapped.
*/
onGhostTap: function() {
var me = this,
media = this.media,
ghost = this.ghost;
media.show();
if (Ext.os.is.Android2) {
setTimeout(function() {
me.play();
setTimeout(function() {
media.hide();
}, 10);
}, 10);
} else {
// Browsers which support native video tag display only, move the media down so
// we can control the Viewport
ghost.hide();
me.play();
}
},
/**
* @private
* native video tag display only, move the media down so we can control the Viewport
*/
onPause: function() {
this.callParent(arguments);
if (!this.isInlineVideo) {
this.media.setTop(-2000);
this.ghost.show();
}
},
/**
* @private
* native video tag display only, move the media down so we can control the Viewport
*/
onPlay: function() {
this.callParent(arguments);
this.media.setTop(0);
},
/**
* Updates the URL to the poster, even if it is rendered.
* @param {Object} newUrl
*/
updatePosterUrl: function(newUrl) {
var ghost = this.ghost;
if (ghost) {
ghost.setStyle('background-image', 'url(' + newUrl + ')');
}
}
});

View File

@@ -0,0 +1,377 @@
/**
* A template class that supports advanced functionality like:
*
* - Autofilling arrays using templates and sub-templates
* - Conditional processing with basic comparison operators
* - Basic math function support
* - Execute arbitrary inline code with special built-in template variables
* - Custom member functions
* - Many special tags and built-in operators that aren't defined as part of the API, but are supported in the templates that can be created
*
* XTemplate provides the templating mechanism built into {@link Ext.DataView}.
*
* The {@link Ext.Template} describes the acceptable parameters to pass to the constructor. The following examples
* demonstrate all of the supported features.
*
* # Sample Data
*
* This is the data object used for reference in each code example:
*
* var data = {
* name: 'Don Griffin',
* title: 'Senior Technomage',
* company: 'Sencha Inc.',
* drinks: ['Coffee', 'Water', 'More Coffee'],
* kids: [
* { name: 'Aubrey', age: 17 },
* { name: 'Joshua', age: 13 },
* { name: 'Cale', age: 10 },
* { name: 'Nikol', age: 5 },
* { name: 'Solomon', age: 0 }
* ]
* };
*
* # Auto filling of arrays
*
* The **tpl** tag and the **for** operator are used to process the provided data object:
*
* - If the value specified in for is an array, it will auto-fill, repeating the template block inside the tpl
* tag for each item in the array.
* - If for="." is specified, the data object provided is examined.
* - While processing an array, the special variable {#} will provide the current array index + 1 (starts at 1, not 0).
*
* Examples:
*
* <tpl for=".">...</tpl> // loop through array at root node
* <tpl for="foo">...</tpl> // loop through array at foo node
* <tpl for="foo.bar">...</tpl> // loop through array at foo.bar node
*
* Using the sample data above:
*
* var tpl = new Ext.XTemplate(
* '<p>Kids: ',
* '<tpl for=".">', // process the data.kids node
* '<p>{#}. {name}</p>', // use current array index to autonumber
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data.kids); // pass the kids property of the data object
*
* An example illustrating how the **for** property can be leveraged to access specified members of the provided data
* object to populate the template:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Title: {title}</p>',
* '<p>Company: {company}</p>',
* '<p>Kids: ',
* '<tpl for="kids">', // interrogate the kids property within the data
* '<p>{name}</p>',
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data); // pass the root node of the data object
*
* Flat arrays that contain values (and not objects) can be auto-rendered using the special **`{.}`** variable inside a
* loop. This variable will represent the value of the array at the current index:
*
* var tpl = new Ext.XTemplate(
* '<p>{name}\'s favorite beverages:</p>',
* '<tpl for="drinks">',
* '<div> - {.}</div>',
* '</tpl>'
* );
* tpl.overwrite(panel.body, data);
*
* When processing a sub-template, for example while looping through a child array, you can access the parent object's
* members via the **parent** object:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<tpl if="age &gt; 1">',
* '<p>{name}</p>',
* '<p>Dad: {parent.name}</p>',
* '</tpl>',
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data);
*
* # Conditional processing with basic comparison operators
*
* The **tpl** tag and the **if** operator are used to provide conditional checks for deciding whether or not to render
* specific parts of the template.
*
* Using the sample data above:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<tpl if="age &gt; 1">',
* '<p>{name}</p>',
* '</tpl>',
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data);
*
* More advanced conditionals are also supported:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<p>{name} is a ',
* '<tpl if="age &gt;= 13">',
* '<p>teenager</p>',
* '<tpl elseif="age &gt;= 2">',
* '<p>kid</p>',
* '<tpl else>',
* '<p>baby</p>',
* '</tpl>',
* '</tpl></p>'
* );
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<p>{name} is a ',
* '<tpl switch="name">',
* '<tpl case="Aubrey" case="Nikol">',
* '<p>girl</p>',
* '<tpl default">',
* '<p>boy</p>',
* '</tpl>',
* '</tpl></p>'
* );
*
* A `break` is implied between each case and default, however, multiple cases can be listed
* in a single &lt;tpl&gt; tag.
*
* # Using double quotes
*
* Examples:
*
* var tpl = new Ext.XTemplate(
* "<tpl if='age &gt; 1 && age &lt; 10'>Child</tpl>",
* "<tpl if='age &gt;= 10 && age &lt; 18'>Teenager</tpl>",
* "<tpl if='this.isGirl(name)'>...</tpl>",
* '<tpl if="id == \'download\'">...</tpl>',
* "<tpl if='needsIcon'><img src='{icon}' class='{iconCls}'/></tpl>",
* "<tpl if='name == \"Don\"'>Hello</tpl>"
* );
*
* # Basic math support
*
* The following basic math operators may be applied directly on numeric data values:
*
* + - * /
*
* For example:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<tpl if="age &gt; 1">', // <-- Note that the > is encoded
* '<p>{#}: {name}</p>', // <-- Auto-number each item
* '<p>In 5 Years: {age+5}</p>', // <-- Basic math
* '<p>Dad: {parent.name}</p>',
* '</tpl>',
* '</tpl></p>'
* );
* tpl.overwrite(panel.body, data);
*
* # Execute arbitrary inline code with special built-in template variables
*
* Anything between `{[ ... ]}` is considered code to be executed in the scope of the template.
* The expression is evaluated and the result is included in the generated result. There are
* some special variables available in that code:
*
* - **out**: The output array into which the template is being appended (using `push` to later
* `join`).
* - **values**: The values in the current scope. If you are using scope changing sub-templates,
* you can change what values is.
* - **parent**: The scope (values) of the ancestor template.
* - **xindex**: If you are in a looping template, the index of the loop you are in (1-based).
* - **xcount**: If you are in a looping template, the total length of the array you are looping.
*
* This example demonstrates basic row striping using an inline code block and the xindex variable:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<div class="{[xindex % 2 === 0 ? "even" : "odd"]}">',
* '{name}',
* '</div>',
* '</tpl></p>'
* );
*
* Any code contained in "verbatim" blocks (using "{% ... %}") will be inserted directly in
* the generated code for the template. These blocks are not included in the output. This
* can be used for simple things like break/continue in a loop, or control structures or
* method calls (when they don't produce output). The `this` references the template instance.
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '{% if (xindex % 2 === 0) continue; %}',
* '{name}',
* '{% if (xindex > 100) break; %}',
* '</div>',
* '</tpl></p>'
* );
*
* # Template member functions
*
* One or more member functions can be specified in a configuration object passed into the XTemplate constructor for
* more complex processing:
*
* var tpl = new Ext.XTemplate(
* '<p>Name: {name}</p>',
* '<p>Kids: ',
* '<tpl for="kids">',
* '<tpl if="this.isGirl(name)">',
* '<p>Girl: {name} - {age}</p>',
* '<tpl else>',
* '<p>Boy: {name} - {age}</p>',
* '</tpl>',
* '<tpl if="this.isBaby(age)">',
* '<p>{name} is a baby!</p>',
* '</tpl>',
* '</tpl></p>',
* {
* // XTemplate configuration:
* disableFormats: true,
* // member functions:
* isGirl: function(name){
* return name == 'Sara Grace';
* },
* isBaby: function(age){
* return age < 1;
* }
* }
* );
* tpl.overwrite(panel.body, data);
*/
Ext.define('Ext.XTemplate', {
extend: 'Ext.Template',
requires: 'Ext.XTemplateCompiler',
/**
* @private
*/
emptyObj: {},
/**
* @cfg {Boolean} compiled
* Only applies to {@link Ext.Template}, XTemplates are compiled automatically on the
* first call to {@link #apply} or {@link #applyOut}.
* @hide
*/
apply: function(values) {
return this.applyOut(values, []).join('');
},
/**
* Appends the result of this template to the provided output array.
* @param {Object/Array} values The template values. See {@link #apply}.
* @param {Array} out The array to which output is pushed.
* @param {Object} parent
* @return {Array} The given out array.
*/
applyOut: function(values, out, parent) {
var me = this,
xindex = values.xindex,
xcount = values.xcount,
compiler;
if (!me.fn) {
compiler = new Ext.XTemplateCompiler({
useFormat : me.disableFormats !== true,
definitions : me.definitions
});
me.fn = compiler.compile(me.html);
}
try {
xindex = typeof xindex === 'number' ? xindex : 1;
xcount = typeof xcount === 'number' ? xcount : 1;
me.fn.call(me, out, values, parent || me.emptyObj, xindex, xcount);
} catch (e) {
//<debug>
Ext.Logger.log('Error: ' + e.message);
//</debug>
}
return out;
},
/**
* Does nothing. XTemplates are compiled automatically, so this function simply returns this.
* @return {Ext.XTemplate} this
*/
compile: function() {
return this;
},
statics: {
/**
* Gets an `XTemplate` from an object (an instance of an {@link Ext#define}'d class).
* Many times, templates are configured high in the class hierarchy and are to be
* shared by all classes that derive from that base. To further complicate matters,
* these templates are seldom actual instances but are rather configurations. For
* example:
*
* Ext.define('MyApp.Class', {
* someTpl: [
* 'tpl text here'
* ]
* });
*
* The goal being to share that template definition with all instances and even
* instances of derived classes, until `someTpl` is overridden. This method will
* "upgrade" these configurations to be real `XTemplate` instances *in place* (to
* avoid creating one instance per object).
*
* @param {Object} instance The object from which to get the `XTemplate` (must be
* an instance of an {@link Ext#define}'d class).
* @param {String} name The name of the property by which to get the `XTemplate`.
* @return {Ext.XTemplate} The `XTemplate` instance or null if not found.
* @protected
*/
getTpl: function (instance, name) {
var tpl = instance[name], // go for it! 99% of the time we will get it!
proto;
if (tpl && !tpl.isTemplate) { // tpl is just a configuration (not an instance)
// create the template instance from the configuration:
tpl = Ext.ClassManager.dynInstantiate('Ext.XTemplate', tpl);
// and replace the reference with the new instance:
if (instance.hasOwnProperty(name)) { // the tpl is on the instance
instance[name] = tpl;
} else { // must be somewhere in the prototype chain
for (proto = instance.self.prototype; proto; proto = proto.superclass) {
if (proto.hasOwnProperty(name)) {
proto[name] = tpl;
break;
}
}
}
}
// else !tpl (no such tpl) or the tpl is an instance already... either way, tpl
// is ready to return
return tpl || null;
}
}
});

View File

@@ -0,0 +1,450 @@
/**
* This class compiles the XTemplate syntax into a function object. The function is used
* like so:
*
* function (out, values, parent, xindex, xcount) {
* // out is the output array to store results
* // values, parent, xindex and xcount have their historical meaning
* }
*
* @private
*/
Ext.define('Ext.XTemplateCompiler', {
extend: 'Ext.XTemplateParser',
// Chrome really likes "new Function" to realize the code block (as in it is
// 2x-3x faster to call it than using eval), but Firefox chokes on it badly.
// IE and Opera are also fine with the "new Function" technique.
useEval: Ext.isGecko,
// See [http://jsperf.com/nige-array-append](http://jsperf.com/nige-array-append) for quickest way to append to an array of unknown length
// (Due to arbitrary code execution inside a template, we cannot easily track the length in var)
// On IE6 and 7 `myArray[myArray.length]='foo'` is better. On other browsers `myArray.push('foo')` is better.
useIndex: Ext.isIE6 || Ext.isIE7,
useFormat: true,
propNameRe: /^[\w\d\$]*$/,
compile: function (tpl) {
var me = this,
code = me.generate(tpl);
// When using "new Function", we have to pass our "Ext" variable to it in order to
// support sandboxing. If we did not, the generated function would use the global
// "Ext", not the "Ext" from our sandbox (scope chain).
//
return me.useEval ? me.evalTpl(code) : (new Function('Ext', code))(Ext);
},
generate: function (tpl) {
var me = this,
// note: Ext here is properly sandboxed
definitions = 'var fm=Ext.util.Format,ts=Object.prototype.toString;',
code;
// Track how many levels we use, so that we only "var" each level's variables once
me.maxLevel = 0;
me.body = [
'var c0=values, a0=' + me.createArrayTest(0) + ', p0=parent, n0=xcount, i0=xindex, v;\n'
];
if (me.definitions) {
if (typeof me.definitions === 'string') {
me.definitions = [me.definitions, definitions ];
} else {
me.definitions.push(definitions);
}
} else {
me.definitions = [ definitions ];
}
me.switches = [];
me.parse(tpl);
me.definitions.push(
(me.useEval ? '$=' : 'return') + ' function (' + me.fnArgs + ') {',
me.body.join(''),
'}'
);
code = me.definitions.join('\n');
// Free up the arrays.
me.definitions.length = me.body.length = me.switches.length = 0;
delete me.definitions;
delete me.body;
delete me.switches;
return code;
},
//-----------------------------------
// XTemplateParser callouts
//
doText: function (text) {
var me = this,
out = me.body;
text = text.replace(me.aposRe, "\\'").replace(me.newLineRe, '\\n');
if (me.useIndex) {
out.push('out[out.length]=\'', text, '\'\n');
} else {
out.push('out.push(\'', text, '\')\n');
}
},
doExpr: function (expr) {
var out = this.body;
out.push('if ((v=' + expr + ')!==undefined && (v=' + expr + ')!==null) out');
// Coerce value to string using concatenation of an empty string literal.
// See http://jsperf.com/tostringvscoercion/5
if (this.useIndex) {
out.push('[out.length]=v+\'\'\n');
} else {
out.push('.push(v+\'\')\n');
}
},
doTag: function (tag) {
this.doExpr(this.parseTag(tag));
},
doElse: function () {
this.body.push('} else {\n');
},
doEval: function (text) {
this.body.push(text, '\n');
},
doIf: function (action, actions) {
var me = this;
// If it's just a propName, use it directly in the if
if (action === '.') {
me.body.push('if (values) {\n');
} else if (me.propNameRe.test(action)) {
me.body.push('if (', me.parseTag(action), ') {\n');
}
// Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
else {
me.body.push('if (', me.addFn(action), me.callFn, ') {\n');
}
if (actions.exec) {
me.doExec(actions.exec);
}
},
doElseIf: function (action, actions) {
var me = this;
// If it's just a propName, use it directly in the else if
if (action === '.') {
me.body.push('else if (values) {\n');
} else if (me.propNameRe.test(action)) {
me.body.push('} else if (', me.parseTag(action), ') {\n');
}
// Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
else {
me.body.push('} else if (', me.addFn(action), me.callFn, ') {\n');
}
if (actions.exec) {
me.doExec(actions.exec);
}
},
doSwitch: function (action) {
var me = this;
// If it's just a propName, use it directly in the switch
if (action === '.') {
me.body.push('switch (values) {\n');
} else if (me.propNameRe.test(action)) {
me.body.push('switch (', me.parseTag(action), ') {\n');
}
// Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
else {
me.body.push('switch (', me.addFn(action), me.callFn, ') {\n');
}
me.switches.push(0);
},
doCase: function (action) {
var me = this,
cases = Ext.isArray(action) ? action : [action],
n = me.switches.length - 1,
match, i;
if (me.switches[n]) {
me.body.push('break;\n');
} else {
me.switches[n]++;
}
for (i = 0, n = cases.length; i < n; ++i) {
match = me.intRe.exec(cases[i]);
cases[i] = match ? match[1] : ("'" + cases[i].replace(me.aposRe,"\\'") + "'");
}
me.body.push('case ', cases.join(': case '), ':\n');
},
doDefault: function () {
var me = this,
n = me.switches.length - 1;
if (me.switches[n]) {
me.body.push('break;\n');
} else {
me.switches[n]++;
}
me.body.push('default:\n');
},
doEnd: function (type, actions) {
var me = this,
L = me.level-1;
if (type == 'for') {
/*
To exit a for loop we must restore the outer loop's context. The code looks
like this (which goes with that produced by doFor:
for (...) { // the part generated by doFor
... // the body of the for loop
// ... any tpl for exec statement goes here...
}
parent = p1;
values = r2;
xcount = n1;
xindex = i1
*/
if (actions.exec) {
me.doExec(actions.exec);
}
me.body.push('}\n');
me.body.push('parent=p',L,';values=r',L+1,';xcount=n',L,';xindex=i',L,'\n');
} else if (type == 'if' || type == 'switch') {
me.body.push('}\n');
}
},
doFor: function (action, actions) {
var me = this,
s,
L = me.level,
up = L-1,
pL = 'p' + L,
parentAssignment;
// If it's just a propName, use it directly in the switch
if (action === '.') {
s = 'values';
} else if (me.propNameRe.test(action)) {
s = me.parseTag(action);
}
// Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
else {
s = me.addFn(action) + me.callFn;
}
/*
We are trying to produce a block of code that looks like below. We use the nesting
level to uniquely name the control variables.
// Omit "var " if we have already been through level 2
var i2 = 0,
n2 = 0,
c2 = values['propName'],
// c2 is the context object for the for loop
a2 = Array.isArray(c2);
p2 = c1,
// p2 is the parent context (of the outer for loop)
r2 = values
// r2 is the values object to
// If iterating over the current data, the parent is always set to c2
parent = c2;
// If iterating over a property in an object, set the parent to the object
parent = a1 ? c1[i1] : p2 // set parent
if (c2) {
if (a2) {
n2 = c2.length;
} else if (c2.isMixedCollection) {
c2 = c2.items;
n2 = c2.length;
} else if (c2.isStore) {
c2 = c2.data.items;
n2 = c2.length;
} else {
c2 = [ c2 ];
n2 = 1;
}
}
// i2 is the loop index and n2 is the number (xcount) of this for loop
for (xcount = n2; i2 < n2; ++i2) {
values = c2[i2] // adjust special vars to inner scope
xindex = i2 + 1 // xindex is 1-based
The body of the loop is whatever comes between the tpl and /tpl statements (which
is handled by doEnd).
*/
// Declare the vars for a particular level only if we have not already declared them.
if (me.maxLevel < L) {
me.maxLevel = L;
me.body.push('var ');
}
if (action == '.') {
parentAssignment = 'c' + L;
} else {
parentAssignment = 'a' + up + '?c' + up + '[i' + up + ']:p' + L;
}
me.body.push('i',L,'=0,n', L, '=0,c',L,'=',s,',a',L,'=', me.createArrayTest(L), ',p',L,'=c',up,',r',L,'=values;\n',
'parent=',parentAssignment,'\n',
'if (c',L,'){if(a',L,'){n', L,'=c', L, '.length;}else if (c', L, '.isMixedCollection){c',L,'=c',L,'.items;n',L,'=c',L,'.length;}else if(c',L,'.isStore){c',L,'=c',L,'.data.items;n',L,'=c',L,'.length;}else{c',L,'=[c',L,'];n',L,'=1;}}\n',
'for (xcount=n',L,';i',L,'<n'+L+';++i',L,'){\n',
'values=c',L,'[i',L,']');
if (actions.propName) {
me.body.push('.', actions.propName);
}
me.body.push('\n',
'xindex=i',L,'+1\n');
},
createArrayTest: ('isArray' in Array) ? function(L) {
return 'Array.isArray(c' + L + ')';
} : function(L) {
return 'ts.call(c' + L + ')==="[object Array]"';
},
doExec: function (action, actions) {
var me = this,
name = 'f' + me.definitions.length;
me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
' try { with(values) {',
' ' + action,
' }} catch(e) {',
//<debug>
'Ext.Logger.log("XTemplate Error: " + e.message);',
//</debug>
'}',
'}');
me.body.push(name + me.callFn + '\n');
},
//-----------------------------------
// Internal
//
addFn: function (body) {
var me = this,
name = 'f' + me.definitions.length;
if (body === '.') {
me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
' return values',
'}');
} else if (body === '..') {
me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
' return parent',
'}');
} else {
me.definitions.push('function ' + name + '(' + me.fnArgs + ') {',
' try { with(values) {',
' return(' + body + ')',
' }} catch(e) {',
//<debug>
'Ext.Logger.log("XTemplate Error: " + e.message);',
//</debug>
'}',
'}');
}
return name;
},
parseTag: function (tag) {
var me = this,
m = me.tagRe.exec(tag),
name = m[1],
format = m[2],
args = m[3],
math = m[4],
v;
// name = "." - Just use the values object.
if (name == '.') {
// filter to not include arrays/objects/nulls
if (!me.validTypes) {
me.definitions.push('var validTypes={string:1,number:1,boolean:1};');
me.validTypes = true;
}
v = 'validTypes[typeof values] || ts.call(values) === "[object Date]" ? values : ""';
}
// name = "#" - Use the xindex
else if (name == '#') {
v = 'xindex';
}
else if (name.substr(0, 7) == "parent.") {
v = name;
}
// compound JavaScript property name (e.g., "foo.bar")
else if (isNaN(name) && name.indexOf('-') == -1 && name.indexOf('.') != -1) {
v = "values." + name;
}
// number or a '-' in it or a single word (maybe a keyword): use array notation
// (http://jsperf.com/string-property-access/4)
else {
v = "values['" + name + "']";
}
if (math) {
v = '(' + v + math + ')';
}
if (format && me.useFormat) {
args = args ? ',' + args : "";
if (format.substr(0, 5) != "this.") {
format = "fm." + format + '(';
} else {
format += '(';
}
} else {
return v;
}
return format + v + args + ')';
},
// @private
evalTpl: function ($) {
// We have to use eval to realize the code block and capture the inner func we also
// don't want a deep scope chain. We only do this in Firefox and it is also unhappy
// with eval containing a return statement, so instead we assign to "$" and return
// that. Because we use "eval", we are automatically sandboxed properly.
eval($);
return $;
},
newLineRe: /\r\n|\r|\n/g,
aposRe: /[']/g,
intRe: /^\s*(\d+)\s*$/,
tagRe: /([\w-\.\#\$]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?/
}, function () {
var proto = this.prototype;
proto.fnArgs = 'out,values,parent,xindex,xcount';
proto.callFn = '.call(this,' + proto.fnArgs + ')';
});

View File

@@ -0,0 +1,250 @@
/**
* This class parses the XTemplate syntax and calls abstract methods to process the parts.
* @private
*/
Ext.define('Ext.XTemplateParser', {
constructor: function (config) {
Ext.apply(this, config);
},
/**
* @property {Number} level The 'for' loop context level. This is adjusted up by one
* prior to calling {@link #doFor} and down by one after calling the corresponding
* {@link #doEnd} that closes the loop. This will be 1 on the first {@link #doFor}
* call.
*/
/**
* This method is called to process a piece of raw text from the tpl.
* @param {String} text
* @method doText
*/
// doText: function (text)
/**
* This method is called to process expressions (like `{[expr]}`).
* @param {String} expr The body of the expression (inside "{[" and "]}").
* @method doExpr
*/
// doExpr: function (expr)
/**
* This method is called to process simple tags (like `{tag}`).
* @param {String} tag
* @method doTag
*/
// doTag: function (tag)
/**
* This method is called to process `<tpl else>`.
* @method doElse
*/
// doElse: function ()
/**
* This method is called to process `{% text %}`.
* @param {String} text
* @method doEval
*/
// doEval: function (text)
/**
* This method is called to process `<tpl if="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doIf
*/
// doIf: function (action, actions)
/**
* This method is called to process `<tpl elseif="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doElseIf
*/
// doElseIf: function (action, actions)
/**
* This method is called to process `<tpl switch="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doSwitch
*/
// doSwitch: function (action, actions)
/**
* This method is called to process `<tpl case="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doCase
*/
// doCase: function (action, actions)
/**
* This method is called to process `<tpl default>`.
* @method doDefault
*/
// doDefault: function ()
/**
* This method is called to process `</tpl>`. It is given the action type that started
* the tpl and the set of additional actions.
* @param {String} type The type of action that is being ended.
* @param {Object} actions The other actions keyed by the attribute name (such as 'exec').
* @method doEnd
*/
// doEnd: function (type, actions)
/**
* This method is called to process `<tpl for="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doFor
*/
// doFor: function (action, actions)
/**
* This method is called to process `<tpl exec="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name.
* @method doExec
*/
// doExec: function (action, actions)
/**
* This method is called to process an empty `<tpl>`. This is unlikely to need to be
* implemented, so a default (do nothing) version is provided.
* @method
*/
doTpl: Ext.emptyFn,
parse: function (str) {
var me = this,
len = str.length,
aliases = { elseif: 'elif' },
topRe = me.topRe,
actionsRe = me.actionsRe,
index, stack, s, m, t, prev, frame, subMatch, begin, end, actions,
prop;
me.level = 0;
me.stack = stack = [];
for (index = 0; index < len; index = end) {
topRe.lastIndex = index;
m = topRe.exec(str);
if (!m) {
me.doText(str.substring(index, len));
break;
}
begin = m.index;
end = topRe.lastIndex;
if (index < begin) {
me.doText(str.substring(index, begin));
}
if (m[1]) {
end = str.indexOf('%}', begin+2);
me.doEval(str.substring(begin+2, end));
end += 2;
} else if (m[2]) {
end = str.indexOf(']}', begin+2);
me.doExpr(str.substring(begin+2, end));
end += 2;
} else if (m[3]) { // if ('{' token)
me.doTag(m[3]);
} else if (m[4]) { // content of a <tpl xxxxxx xxx> tag
actions = null;
while ((subMatch = actionsRe.exec(m[4])) !== null) {
s = subMatch[2] || subMatch[3];
if (s) {
s = Ext.String.htmlDecode(s); // decode attr value
t = subMatch[1];
t = aliases[t] || t;
actions = actions || {};
prev = actions[t];
if (typeof prev == 'string') {
actions[t] = [prev, s];
} else if (prev) {
actions[t].push(s);
} else {
actions[t] = s;
}
}
}
if (!actions) {
if (me.elseRe.test(m[4])) {
me.doElse();
} else if (me.defaultRe.test(m[4])) {
me.doDefault();
} else {
me.doTpl();
stack.push({ type: 'tpl' });
}
}
else if (actions['if']) {
me.doIf(actions['if'], actions);
stack.push({ type: 'if' });
}
else if (actions['switch']) {
me.doSwitch(actions['switch'], actions);
stack.push({ type: 'switch' });
}
else if (actions['case']) {
me.doCase(actions['case'], actions);
}
else if (actions['elif']) {
me.doElseIf(actions['elif'], actions);
}
else if (actions['for']) {
++me.level;
// Extract property name to use from indexed item
if (prop = me.propRe.exec(m[4])) {
actions.propName = prop[1] || prop[2];
}
me.doFor(actions['for'], actions);
stack.push({ type: 'for', actions: actions });
}
else if (actions.exec) {
me.doExec(actions.exec, actions);
stack.push({ type: 'exec', actions: actions });
}
/*
else {
// todo - error
}
*/
} else if (m[0].length === 5) {
// if the length of m[0] is 5, assume that we're dealing with an opening tpl tag with no attributes (e.g. <tpl>...</tpl>)
// in this case no action is needed other than pushing it on to the stack
stack.push({ type: 'tpl' });
} else {
frame = stack.pop();
me.doEnd(frame.type, frame.actions);
if (frame.type == 'for') {
--me.level;
}
}
}
},
// Internal regexes
topRe: /(?:(\{\%)|(\{\[)|\{([^{}]*)\})|(?:<tpl([^>]*)\>)|(?:<\/tpl>)/g,
actionsRe: /\s*(elif|elseif|if|for|exec|switch|case|eval)\s*\=\s*(?:(?:"([^"]*)")|(?:'([^']*)'))\s*/g,
propRe: /prop=(?:(?:"([^"]*)")|(?:'([^']*)'))/,
defaultRe: /^\s*default\s*$/,
elseRe: /^\s*else\s*$/
});

View File

@@ -0,0 +1,136 @@
/**
* @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();
}
});

View File

@@ -0,0 +1,905 @@
/**
* @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];
}
// <deprecated product=touch since=2.0>
if (config.autoCreateViewport) {
Ext.Logger.deprecate(
'[Ext.app.Application] autoCreateViewport has been deprecated in Sencha Touch 2. Please implement a ' +
'launch function on your Application instead and use Ext.create("MyApp.view.Main") to create your initial UI.'
);
}
// </deprecated>
//<debug>
Ext.Loader.setConfig({ enabled: true });
//</debug>
Ext.require(this.getRequires(), function() {
if (this.getEnableLoader() !== false) {
Ext.require(this.getProfiles(), this.onProfilesLoaded, this);
}
}, this);
},
/**
* Dispatches a given {@link Ext.app.Action} to the relevant Controller instance. This is not usually called
* directly by the developer, instead Sencha Touch's History support picks up on changes to the browser's url
* and calls dispatch automatically.
* @param {Ext.app.Action} action The action to dispatch.
* @param {Boolean} [addToHistory=true] Sets the browser's url to the action's url.
*/
dispatch: function(action, addToHistory) {
action = action || {};
Ext.applyIf(action, {
application: this
});
action = Ext.factory(action, Ext.app.Action);
if (action) {
var profile = this.getCurrentProfile(),
profileNS = profile ? profile.getNamespace() : undefined,
controller = this.getController(action.getController(), profileNS);
if (controller) {
if (addToHistory !== false) {
this.getHistory().add(action, true);
}
controller.execute(action);
}
}
},
/**
* Redirects the browser to the given url. This only affects the url after the '#'. You can pass in either a String
* or a Model instance - if a Model instance is defined its {@link Ext.data.Model#toUrl toUrl} function is called,
* which returns a string representing the url for that model. Internally, this uses your application's
* {@link Ext.app.Router Router} to decode the url into a matching controller action and then calls
* {@link #dispatch}.
* @param {String/Ext.data.Model} url The String url to redirect to.
*/
redirectTo: function(url) {
if (Ext.data && Ext.data.Model && url instanceof Ext.data.Model) {
var record = url;
url = record.toUrl();
}
var decoded = this.getRouter().recognize(url);
if (decoded) {
decoded.url = url;
if (record) {
decoded.data = {};
decoded.data.record = record;
}
return this.dispatch(decoded);
}
},
/**
* @private
* (documented on Controller's control config)
*/
control: function(selectors, controller) {
//if the controller is not defined, use this instead (the application instance)
controller = controller || this;
var dispatcher = this.getEventDispatcher(),
refs = (controller) ? controller.getRefs() : {},
selector, eventName, listener, listeners, ref;
for (selector in selectors) {
if (selectors.hasOwnProperty(selector)) {
listeners = selectors[selector];
ref = refs[selector];
//refs can be used in place of selectors
if (ref) {
selector = ref.selector || ref;
}
for (eventName in listeners) {
listener = listeners[eventName];
if (Ext.isString(listener)) {
listener = controller[listener];
}
dispatcher.addListener('component', selector, eventName, listener, controller);
}
}
}
},
/**
* Returns the Controller instance for the given controller name.
* @param {String} name The name of the Controller.
* @param {String} [profileName] Optional profile name. If passed, this is the same as calling
* `getController('profileName.controllerName')`.
*/
getController: function(name, profileName) {
var instances = this.getControllerInstances(),
appName = this.getName(),
format = Ext.String.format,
topLevelName;
if (name instanceof Ext.app.Controller) {
return name;
}
if (instances[name]) {
return instances[name];
} else {
topLevelName = format("{0}.controller.{1}", appName, name);
profileName = format("{0}.controller.{1}.{2}", appName, profileName, name);
return instances[profileName] || instances[topLevelName];
}
},
/**
* @private
* Callback that is invoked when all of the configured Profiles have been loaded. Detects the current profile and
* gathers any additional dependencies from that profile, then loads all of those dependencies.
*/
onProfilesLoaded: function() {
var profiles = this.getProfiles(),
length = profiles.length,
instances = [],
requires = this.gatherDependencies(),
current, i, profileDeps;
for (i = 0; i < length; i++) {
instances[i] = Ext.create(profiles[i], {
application: this
});
/*
* Note that we actually require all of the dependencies for all Profiles - this is so that we can produce
* a single build file that will work on all defined Profiles. Although the other classes will be loaded,
* the correct Profile will still be identified and the other classes ignored. While this feels somewhat
* inefficient, the majority of the bulk of an application is likely to be the framework itself. The bigger
* the app though, the bigger the effect of this inefficiency so ideally we will create a way to create and
* load Profile-specific builds in a future release.
*/
profileDeps = instances[i].getDependencies();
requires = requires.concat(profileDeps.all);
if (instances[i].isActive() && !current) {
current = instances[i];
this.setCurrentProfile(current);
this.setControllers(this.getControllers().concat(profileDeps.controller));
this.setModels(this.getModels().concat(profileDeps.model));
this.setViews(this.getViews().concat(profileDeps.view));
this.setStores(this.getStores().concat(profileDeps.store));
}
}
this.setProfileInstances(instances);
Ext.require(requires, this.loadControllerDependencies, this);
},
/**
* @private
* Controllers can also specify dependencies, so we grab them all here and require them.
*/
loadControllerDependencies: function() {
this.instantiateControllers();
var controllers = this.getControllerInstances(),
classes = [],
stores = [],
i, controller, controllerStores, name;
for (name in controllers) {
controller = controllers[name];
controllerStores = controller.getStores();
stores = stores.concat(controllerStores);
classes = classes.concat(controller.getModels().concat(controller.getViews()).concat(controllerStores));
}
this.setStores(this.getStores().concat(stores));
Ext.require(classes, this.onDependenciesLoaded, this);
},
/**
* @private
* Callback that is invoked when all of the Application, Controller and Profile dependencies have been loaded.
* Launches the controllers, then the profile and application.
*/
onDependenciesLoaded: function() {
var me = this,
profile = this.getCurrentProfile(),
launcher = this.getLaunch(),
controllers, name;
this.instantiateStores();
//<deprecated product=touch since=2.0>
Ext.app.Application.appInstance = this;
if (Ext.Router) {
Ext.Router.setAppInstance(this);
}
//</deprecated>
controllers = this.getControllerInstances();
for (name in controllers) {
controllers[name].init(this);
}
if (profile) {
profile.launch();
}
launcher.call(me);
for (name in controllers) {
//<debug warn>
if (controllers[name] && !(controllers[name] instanceof Ext.app.Controller)) {
Ext.Logger.warn("The controller '" + name + "' doesn't have a launch method. Are you sure it extends from Ext.app.Controller?");
} else {
//</debug>
controllers[name].launch(this);
//<debug warn>
}
//</debug>
}
me.redirectTo(window.location.hash.substr(1));
},
/**
* @private
* Gathers up all of the previously computed MVCS dependencies into a single array that we can pass to {@link Ext#require}.
*/
gatherDependencies: function() {
var classes = this.getModels().concat(this.getViews()).concat(this.getControllers());
Ext.each(this.getStores(), function(storeName) {
if (Ext.isString(storeName)) {
classes.push(storeName);
}
}, this);
return classes;
},
/**
* @private
* Should be called after dependencies are loaded, instantiates all of the Stores specified in the {@link #stores}
* config. For each item in the stores array we make sure the Store is instantiated. When strings are specified,
* the corresponding _app/store/StoreName.js_ was loaded so we now instantiate a `MyApp.store.StoreName`, giving it the
* id `StoreName`.
*/
instantiateStores: function() {
var stores = this.getStores(),
length = stores.length,
store, storeClass, storeName, splits, i;
for (i = 0; i < length; i++) {
store = stores[i];
if (Ext.data && Ext.data.Store && !(store instanceof Ext.data.Store)) {
if (Ext.isString(store)) {
storeName = store;
storeClass = Ext.ClassManager.classes[store];
store = {
xclass: store
};
//we don't want to wipe out a configured storeId in the app's Store subclass so need
//to check for this first
if (storeClass.prototype.defaultConfig.storeId === undefined) {
splits = storeName.split('.');
store.id = splits[splits.length - 1];
}
}
stores[i] = Ext.factory(store, Ext.data.Store);
}
}
this.setStores(stores);
},
/**
* @private
* Called once all of our controllers have been loaded
*/
instantiateControllers: function() {
var controllerNames = this.getControllers(),
instances = {},
length = controllerNames.length,
name, i;
for (i = 0; i < length; i++) {
name = controllerNames[i];
instances[name] = Ext.create(name, {
application: this
});
}
return this.setControllerInstances(instances);
},
/**
* @private
* As a convenience developers can locally qualify controller names (e.g. 'MyController' vs
* 'MyApp.controller.MyController'). This just makes sure everything ends up fully qualified
*/
applyControllers: function(controllers) {
return this.getFullyQualified(controllers, 'controller');
},
/**
* @private
* As a convenience developers can locally qualify profile names (e.g. 'MyProfile' vs
* 'MyApp.profile.MyProfile'). This just makes sure everything ends up fully qualified
*/
applyProfiles: function(profiles) {
return this.getFullyQualified(profiles, 'profile');
},
/**
* @private
* Checks that the name configuration has any whitespace, and trims them if found.
*/
applyName: function(name) {
var oldName;
if (name && name.match(/ /g)) {
oldName = name;
name = name.replace(/ /g, "");
// <debug>
Ext.Logger.warn('Attempting to create an application with a name which contains whitespace ("' + oldName + '"). Renamed to "' + name + '".');
// </debug>
}
return name;
},
/**
* @private
* Makes sure the app namespace exists, sets the `app` property of the namespace to this application and sets its
* loading path (checks to make sure the path hadn't already been set via Ext.Loader.setPath)
*/
updateName: function(newName) {
Ext.ClassManager.setNamespace(newName + '.app', this);
if (!Ext.Loader.config.paths[newName]) {
Ext.Loader.setPath(newName, this.getAppFolder());
}
},
/**
* @private
*/
applyRouter: function(config) {
return Ext.factory(config, Ext.app.Router, this.getRouter());
},
/**
* @private
*/
applyHistory: function(config) {
var history = Ext.factory(config, Ext.app.History, this.getHistory());
history.on('change', this.onHistoryChange, this);
return history;
},
/**
* @private
*/
onHistoryChange: function(url) {
this.dispatch(this.getRouter().recognize(url), false);
}
}, function() {
// <deprecated product=touch since=2.0>
Ext.regApplication = function(config) {
Ext.Logger.deprecate(
'[Ext.app.Application] Ext.regApplication() is deprecated, please replace it with Ext.application()'
);
var appName = config.name,
format = Ext.String.format;
Ext.ns(
appName,
format("{0}.controllers", appName),
format("{0}.models", appName),
format("{0}.views", appName)
);
Ext.application(config);
};
Ext.define('Ext.data.ProxyMgr', {
singleton: true,
registerType: function(name, cls) {
Ext.Logger.deprecate(
'Ext.data.ProxyMgr no longer exists - instead of calling Ext.data.ProxyMgr.registerType just update ' +
'your custom Proxy class to set alias: "proxy.' + name + '"'
);
Ext.ClassManager.setAlias(cls, "proxy." + name);
}
});
Ext.reg = function(alias, cls) {
Ext.Logger.deprecate(
'Ext.reg is deprecated, please set xtype: "' + alias + '" directly in your subclass instead'
);
Ext.ClassManager.setAlias(cls, alias);
};
Ext.redirect = function() {
var app = Ext.app.Application.appInstance;
Ext.Logger.deprecate('[Ext.app.Application] Ext.redirect is deprecated, please use YourApp.redirectTo instead');
if (app) {
app.redirectTo.apply(app, arguments);
}
};
Ext.dispatch = function() {
var app = Ext.app.Application.appInstance;
Ext.Logger.deprecate('[Ext.app.Application] Ext.dispatch is deprecated, please use YourApp.dispatch instead');
if (app) {
app.dispatch.apply(app, arguments);
}
};
// </deprecated>
});

View File

@@ -0,0 +1,747 @@
/**
* @author Ed Spencer
*
* @aside guide controllers
* @aside guide apps_intro
* @aside guide history_support
* @aside video mvc-part-1
* @aside video mvc-part-2
*
* Controllers are responsible for responding to events that occur within your app. If your app contains a Logout
* {@link Ext.Button button} that your user can tap on, a Controller would listen to the Button's tap event and take
* the appropriate action. It allows the View classes to handle the display of data and the Model classes to handle the
* loading and saving of data - the Controller is the glue that binds them together.
*
* ## Relation to Ext.app.Application
*
* Controllers exist within the context of an {@link Ext.app.Application Application}. An Application usually consists
* of a number of Controllers, each of which handle a specific part of the app. For example, an Application that
* handles the orders for an online shopping site might have controllers for Orders, Customers and Products.
*
* All of the Controllers that an Application uses are specified in the Application's
* {@link Ext.app.Application#controllers} config. The Application automatically instantiates each Controller and keeps
* references to each, so it is unusual to need to instantiate Controllers directly. By convention each Controller is
* named after the thing (usually the Model) that it deals with primarily, usually in the plural - for example if your
* app is called 'MyApp' and you have a Controller that manages Products, convention is to create a
* MyApp.controller.Products class in the file app/controller/Products.js.
*
* ## Refs and Control
*
* The centerpiece of Controllers is the twin configurations {@link #refs} and {@link #cfg-control}. These are used to
* easily gain references to Components inside your app and to take action on them based on events that they fire.
* Let's look at {@link #refs} first:
*
* ### Refs
*
* Refs leverage the powerful {@link Ext.ComponentQuery ComponentQuery} syntax to easily locate Components on your
* page. We can define as many refs as we like for each Controller, for example here we define a ref called 'nav' that
* finds a Component on the page with the ID 'mainNav'. We then use that ref in the addLogoutButton beneath it:
*
* Ext.define('MyApp.controller.Main', {
* extend: 'Ext.app.Controller',
*
* config: {
* refs: {
* nav: '#mainNav'
* }
* },
*
* addLogoutButton: function() {
* this.getNav().add({
* text: 'Logout'
* });
* }
* });
*
* Usually, a ref is just a key/value pair - the key ('nav' in this case) is the name of the reference that will be
* generated, the value ('#mainNav' in this case) is the {@link Ext.ComponentQuery ComponentQuery} selector that will
* be used to find the Component.
*
* Underneath that, we have created a simple function called addLogoutButton which uses this ref via its generated
* 'getNav' function. These getter functions are generated based on the refs you define and always follow the same
* format - 'get' followed by the capitalized ref name. In this case we're treating the nav reference as though it's a
* {@link Ext.Toolbar Toolbar}, and adding a Logout button to it when our function is called. This ref would recognize
* a Toolbar like this:
*
* Ext.create('Ext.Toolbar', {
* id: 'mainNav',
*
* items: [
* {
* text: 'Some Button'
* }
* ]
* });
*
* Assuming this Toolbar has already been created by the time we run our 'addLogoutButton' function (we'll see how that
* is invoked later), it will get the second button added to it.
*
* ### Advanced Refs
*
* Refs can also be passed a couple of additional options, beyond name and selector. These are autoCreate and xtype,
* which are almost always used together:
*
* Ext.define('MyApp.controller.Main', {
* extend: 'Ext.app.Controller',
*
* config: {
* refs: {
* nav: '#mainNav',
*
* infoPanel: {
* selector: 'tabpanel panel[name=fish] infopanel',
* xtype: 'infopanel',
* autoCreate: true
* }
* }
* }
* });
*
* We've added a second ref to our Controller. Again the name is the key, 'infoPanel' in this case, but this time we've
* passed an object as the value instead. This time we've used a slightly more complex selector query - in this example
* imagine that your app contains a {@link Ext.tab.Panel tab panel} and that one of the items in the tab panel has been
* given the name 'fish'. Our selector matches any Component with the xtype 'infopanel' inside that tab panel item.
*
* The difference here is that if that infopanel does not exist already inside the 'fish' panel, it will be
* automatically created when you call this.getInfoPanel inside your Controller. The Controller is able to do this
* because we provided the xtype to instantiate with in the event that the selector did not return anything.
*
* ### Control
*
* The sister config to {@link #refs} is {@link #cfg-control}. {@link #cfg-control Control} is the means by which your listen
* to events fired by Components and have your Controller react in some way. Control accepts both ComponentQuery
* selectors and refs as its keys, and listener objects as values - for example:
*
* Ext.define('MyApp.controller.Main', {
* extend: 'Ext.app.Controller',
*
* config: {
* control: {
* loginButton: {
* tap: 'doLogin'
* },
* 'button[action=logout]': {
* tap: 'doLogout'
* }
* },
*
* refs: {
* loginButton: 'button[action=login]'
* }
* },
*
* doLogin: function() {
* //called whenever the Login button is tapped
* },
*
* doLogout: function() {
* //called whenever any Button with action=logout is tapped
* }
* });
*
* Here we have set up two control declarations - one for our loginButton ref and the other for any Button on the page
* that has been given the action 'logout'. For each declaration we passed in a single event handler - in each case
* listening for the 'tap' event, specifying the action that should be called when that Button fires the tap event.
* Note that we specified the 'doLogin' and 'doLogout' methods as strings inside the control block - this is important.
*
* You can listen to as many events as you like in each control declaration, and mix and match ComponentQuery selectors
* and refs as the keys.
*
* ## Routes
*
* As of Sencha Touch 2, Controllers can now directly specify which routes they are interested in. This enables us to
* provide history support within our app, as well as the ability to deeply link to any part of the application that we
* provide a route for.
*
* For example, let's say we have a Controller responsible for logging in and viewing user profiles, and want to make
* those screens accessible via urls. We could achieve that like this:
*
* Ext.define('MyApp.controller.Users', {
* extend: 'Ext.app.Controller',
*
* config: {
* routes: {
* 'login': 'showLogin',
* 'user/:id': 'showUserById'
* },
*
* refs: {
* main: '#mainTabPanel'
* }
* },
*
* //uses our 'main' ref above to add a loginpanel to our main TabPanel (note that
* //'loginpanel' is a custom xtype created for this application)
* showLogin: function() {
* this.getMain().add({
* xtype: 'loginpanel'
* });
* },
*
* //Loads the User then adds a 'userprofile' view to the main TabPanel
* showUserById: function(id) {
* MyApp.model.User.load(id, {
* scope: this,
* success: function(user) {
* this.getMain().add({
* xtype: 'userprofile',
* user: user
* });
* }
* });
* }
* });
*
* The routes we specified above simply map the contents of the browser address bar to a Controller function to call
* when that route is matched. The routes can be simple text like the login route, which matches against
* http://myapp.com/#login, or contain wildcards like the 'user/:id' route, which matches urls like
* http://myapp.com/#user/123. Whenever the address changes the Controller automatically calls the function specified.
*
* Note that in the showUserById function we had to first load the User instance. Whenever you use a route, the
* function that is called by that route is completely responsible for loading its data and restoring state. This is
* because your user could either send that url to another person or simply refresh the page, which we wipe clear any
* cached data you had already loaded. There is a more thorough discussion of restoring state with routes in the
* application architecture guides.
*
* ## Advanced Usage
*
* See [the Controllers guide](#!/guide/controllers) for advanced Controller usage including before filters
* and customizing for different devices.
*/
Ext.define('Ext.app.Controller', {
mixins: {
observable: "Ext.mixin.Observable"
},
config: {
/**
* @cfg {Object} refs A collection of named {@link Ext.ComponentQuery ComponentQuery} selectors that makes it
* easy to get references to key Components on your page. Example usage:
*
* refs: {
* main: '#mainTabPanel',
* loginButton: '#loginWindow button[action=login]',
*
* infoPanel: {
* selector: 'infopanel',
* xtype: 'infopanel',
* autoCreate: true
* }
* }
*
* The first two are simple ComponentQuery selectors, the third (infoPanel) also passes in the autoCreate and
* xtype options, which will first run the ComponentQuery to see if a Component matching that selector exists
* on the page. If not, it will automatically create one using the xtype provided:
*
* someControllerFunction: function() {
* //if the info panel didn't exist before, calling its getter will instantiate
* //it automatically and return the new instance
* this.getInfoPanel().show();
* }
*
* @accessor
*/
refs: {},
/**
* @cfg {Object} routes Provides a mapping of urls to Controller actions. Whenever the specified url is matched
* in the address bar, the specified Controller action is called. Example usage:
*
* routes: {
* 'login': 'showLogin',
* 'users/:id': 'showUserById'
* }
*
* The first route will match against http://myapp.com/#login and call the Controller's showLogin function. The
* second route contains a wildcard (':id') and will match all urls like http://myapp.com/#users/123, calling
* the showUserById function with the matched ID as the first argument.
*
* @accessor
*/
routes: {},
/**
* @cfg {Object} control Provides a mapping of Controller functions that should be called whenever certain
* Component events are fired. The Components can be specified using {@link Ext.ComponentQuery ComponentQuery}
* selectors or {@link #refs}. Example usage:
*
* control: {
* 'button[action=logout]': {
* tap: 'doLogout'
* },
* main: {
* activeitemchange: 'doUpdate'
* }
* }
*
* The first item uses a ComponentQuery selector to run the Controller's doLogout function whenever any Button
* with action=logout is tapped on. The second calls the Controller's doUpdate function whenever the
* activeitemchange event is fired by the Component referenced by our 'main' ref. In this case main is a tab
* panel (see {@link #refs} for how to set that reference up).
*
* @accessor
*/
control: {},
/**
* @cfg {Object} before Provides a mapping of Controller functions to filter functions that are run before them
* when dispatched to from a route. These are usually used to run pre-processing functions like authentication
* before a certain function is executed. They are only called when dispatching from a route. Example usage:
*
* Ext.define('MyApp.controller.Products', {
* config: {
* before: {
* editProduct: 'authenticate'
* },
*
* routes: {
* 'product/edit/:id': 'editProduct'
* }
* },
*
* //this is not directly because our before filter is called first
* editProduct: function() {
* //... performs the product editing logic
* },
*
* //this is run before editProduct
* authenticate: function(action) {
* MyApp.authenticate({
* success: function() {
* action.resume();
* },
* failure: function() {
* Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in");
* }
* });
* }
* });
*
* @accessor
*/
before: {},
/**
* @cfg {Ext.app.Application} application The Application instance this Controller is attached to. This is
* automatically provided when using the MVC architecture so should rarely need to be set directly.
* @accessor
*/
application: {},
/**
* @cfg {String[]} stores The set of stores to load for this Application. Each store is expected to
* exist inside the *app/store* directory and define a class following the convention
* AppName.store.StoreName. For example, in the code below, the *AppName.store.Users* class will be loaded.
* Note that we are able to specify either the full class name (as with *AppName.store.Groups*) or just the
* final part of the class name and leave Application to automatically prepend *AppName.store.'* to each:
*
* stores: [
* 'Users',
* 'AppName.store.Groups',
* 'SomeCustomNamespace.store.Orders'
* ]
* @accessor
*/
stores: [],
/**
* @cfg {String[]} models The set of models to load for this Application. Each model is expected to exist inside the
* *app/model* directory and define a class following the convention AppName.model.ModelName. For example, in the
* code below, the classes *AppName.model.User*, *AppName.model.Group* and *AppName.model.Product* will be loaded.
* Note that we are able to specify either the full class name (as with *AppName.model.Product*) or just the
* final part of the class name and leave Application to automatically prepend *AppName.model.* to each:
*
* models: [
* 'User',
* 'Group',
* 'AppName.model.Product',
* 'SomeCustomNamespace.model.Order'
* ]
* @accessor
*/
models: [],
/**
* @cfg {Array} views The set of views to load for this Application. Each view is expected to exist inside the
* *app/view* directory and define a class following the convention AppName.view.ViewName. For example, in the
* code below, the classes *AppName.view.Users*, *AppName.view.Groups* and *AppName.view.Products* will be loaded.
* Note that we are able to specify either the full class name (as with *AppName.view.Products*) or just the
* final part of the class name and leave Application to automatically prepend *AppName.view.* to each:
*
* views: [
* 'Users',
* 'Groups',
* 'AppName.view.Products',
* 'SomeCustomNamespace.view.Orders'
* ]
* @accessor
*/
views: []
},
/**
* Constructs a new Controller instance
*/
constructor: function(config) {
this.initConfig(config);
this.mixins.observable.constructor.call(this, config);
},
/**
* @cfg
* Called by the Controller's {@link #application} to initialize the Controller. This is always called before the
* {@link Ext.app.Application Application} launches, giving the Controller a chance to run any pre-launch logic.
* See also {@link #launch}, which is called after the {@link Ext.app.Application#launch Application's launch function}
*/
init: Ext.emptyFn,
/**
* @cfg
* Called by the Controller's {@link #application} immediately after the Application's own
* {@link Ext.app.Application#launch launch function} has been called. This is usually a good place to run any
* logic that has to run after the app UI is initialized. See also {@link #init}, which is called before the
* {@link Ext.app.Application#launch Application's launch function}.
*/
launch: Ext.emptyFn,
/**
* Convenient way to redirect to a new url. See {@link Ext.app.Application#redirectTo} for full usage information.
* @return {Object}
*/
redirectTo: function(place) {
return this.getApplication().redirectTo(place);
},
/**
* @private
* Executes an Ext.app.Action by giving it the correct before filters and kicking off execution
*/
execute: function(action, skipFilters) {
action.setBeforeFilters(this.getBefore()[action.getAction()]);
action.execute();
},
/**
* @private
* Massages the before filters into an array of function references for each controller action
*/
applyBefore: function(before) {
var filters, name, length, i;
for (name in before) {
filters = Ext.Array.from(before[name]);
length = filters.length;
for (i = 0; i < length; i++) {
filters[i] = this[filters[i]];
}
before[name] = filters;
}
return before;
},
/**
* @private
*/
applyControl: function(config) {
this.control(config, this);
return config;
},
/**
* @private
*/
applyRefs: function(refs) {
//<debug>
if (Ext.isArray(refs)) {
Ext.Logger.deprecate("In Sencha Touch 2 the refs config accepts an object but you have passed it an array.");
}
//</debug>
this.ref(refs);
return refs;
},
/**
* @private
* Adds any routes specified in this Controller to the global Application router
*/
applyRoutes: function(routes) {
var app = this instanceof Ext.app.Application ? this : this.getApplication(),
router = app.getRouter(),
route, url, config;
for (url in routes) {
route = routes[url];
config = {
controller: this.$className
};
if (Ext.isString(route)) {
config.action = route;
} else {
Ext.apply(config, route);
}
router.connect(url, config);
}
return routes;
},
/**
* @private
* As a convenience developers can locally qualify store names (e.g. 'MyStore' vs
* 'MyApp.store.MyStore'). This just makes sure everything ends up fully qualified
*/
applyStores: function(stores) {
return this.getFullyQualified(stores, 'store');
},
/**
* @private
* As a convenience developers can locally qualify model names (e.g. 'MyModel' vs
* 'MyApp.model.MyModel'). This just makes sure everything ends up fully qualified
*/
applyModels: function(models) {
return this.getFullyQualified(models, 'model');
},
/**
* @private
* As a convenience developers can locally qualify view names (e.g. 'MyView' vs
* 'MyApp.view.MyView'). This just makes sure everything ends up fully qualified
*/
applyViews: function(views) {
return this.getFullyQualified(views, 'view');
},
/**
* @private
* Returns the fully qualified name for any class name variant. This is used to find the FQ name for the model,
* view, controller, store and profiles listed in a Controller or Application.
* @param {String[]} items The array of strings to get the FQ name for
* @param {String} namespace If the name happens to be an application class, add it to this namespace
* @return {String} The fully-qualified name of the class
*/
getFullyQualified: function(items, namespace) {
var length = items.length,
appName = this.getApplication().getName(),
name, i;
for (i = 0; i < length; i++) {
name = items[i];
//we check name === appName to allow MyApp.profile.MyApp to exist
if (Ext.isString(name) && (Ext.Loader.getPrefix(name) === "" || name === appName)) {
items[i] = appName + '.' + namespace + '.' + name;
}
}
return items;
},
/**
* @private
*/
control: function(selectors) {
this.getApplication().control(selectors, this);
},
/**
* @private
* 1.x-inspired ref implementation
*/
ref: function(refs) {
var me = this,
refName, getterName, selector, info;
for (refName in refs) {
selector = refs[refName];
getterName = "get" + Ext.String.capitalize(refName);
if (!this[getterName]) {
if (Ext.isString(refs[refName])) {
info = {
ref: refName,
selector: selector
};
} else {
info = refs[refName];
}
this[getterName] = function(refName, info) {
var args = [refName, info];
return function() {
return me.getRef.apply(me, args.concat.apply(args, arguments));
};
}(refName, info);
}
this.references = this.references || [];
this.references.push(refName.toLowerCase());
}
},
/**
* @private
*/
getRef: function(ref, info, config) {
this.refCache = this.refCache || {};
info = info || {};
config = config || {};
Ext.apply(info, config);
if (info.forceCreate) {
return Ext.ComponentManager.create(info, 'component');
}
var me = this,
cached = me.refCache[ref];
if (!cached) {
me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
if (!cached && info.autoCreate) {
me.refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
}
if (cached) {
cached.on('destroy', function() {
me.refCache[ref] = null;
});
}
}
return cached;
},
/**
* @private
*/
hasRef: function(ref) {
return this.references && this.references.indexOf(ref.toLowerCase()) !== -1;
}
// <deprecated product=touch since=2.0>
,onClassExtended: function(cls, members) {
var prototype = this.prototype,
defaultConfig = prototype.config,
config = members.config || {},
arrayRefs = members.refs,
objectRefs = {},
stores = members.stores,
views = members.views,
format = Ext.String.format,
refItem, key, length, i, functionName;
// Convert deprecated properties in application into a config object
for (key in defaultConfig) {
if (key in members && key != "control") {
if (key == "refs") {
//we need to convert refs from the 1.x array-style to 2.x object-style
for (i = 0; i < arrayRefs.length; i++) {
refItem = arrayRefs[i];
objectRefs[refItem.ref] = refItem;
}
config.refs = objectRefs;
} else {
config[key] = members[key];
}
delete members[key];
// <debug warn>
Ext.Logger.deprecate(key + ' is deprecated as a property directly on the ' + this.$className + ' prototype. Please put it inside the config object.');
// </debug>
}
}
if (stores) {
length = stores.length;
config.stores = stores;
for (i = 0; i < length; i++) {
functionName = format("get{0}Store", Ext.String.capitalize(stores[i]));
prototype[functionName] = function(name) {
return function() {
return Ext.StoreManager.lookup(name);
};
}(stores[i]);
}
}
if (views) {
length = views.length;
config.views = views;
for (i = 0; i < length; i++) {
functionName = format("get{0}View", views[i]);
prototype[functionName] = function(name) {
return function() {
return Ext.ClassManager.classes[format("{0}.view.{1}", this.getApplication().getName(), name)];
};
}(views[i]);
}
}
members.config = config;
},
/**
* Returns a reference to a Model.
* @param modelName
* @return {Object}
* @deprecated 2.0.0 Considered bad practice - please just use the Model name instead
* (e.g. `MyApp.model.User` vs `this.getModel('User')`).
*/
getModel: function(modelName) {
//<debug warn>
Ext.Logger.deprecate("getModel() is deprecated and considered bad practice - please just use the Model " +
"name instead (e.g. MyApp.model.User vs this.getModel('User'))");
//</debug>
var appName = this.getApplication().getName(),
classes = Ext.ClassManager.classes;
return classes[appName + '.model.' + modelName];
},
/**
* Returns a reference to another Controller.
* @param controllerName
* @param profile
* @return {Object}
* @deprecated 2.0.0 Considered bad practice - if you need to do this
* please use this.getApplication().getController() instead
*/
getController: function(controllerName, profile) {
//<debug warn>
Ext.Logger.deprecate("Ext.app.Controller#getController is deprecated and considered bad practice - " +
"please use this.getApplication().getController('someController') instead");
//</debug>
return this.getApplication().getController(controllerName, profile);
}
// </deprecated>
}, function() {
// <deprecated product=touch since=2.0>
Ext.regController = function(name, config) {
Ext.apply(config, {
extend: 'Ext.app.Controller'
});
Ext.Logger.deprecate(
'[Ext.app.Controller] Ext.regController is deprecated, please use Ext.define to define a Controller as ' +
'with any other class. For more information see the Touch 1.x -> 2.x migration guide'
);
Ext.define('controller.' + name, config);
};
// </deprecated>
});

View File

@@ -0,0 +1,112 @@
/**
* @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));
}
});

View File

@@ -0,0 +1,251 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,208 @@
/**
* @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 + "$");
}
});

View File

@@ -0,0 +1,137 @@
/**
* @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() {
//<deprecated product=touch since=2.0>
/**
* Restores compatibility for the old `Ext.Router.draw` syntax. This needs to be here because apps often include
* _routes.js_ just after _app.js_, so this is our only opportunity to hook this in. There is a small piece of code
* inside Application's {@link Ext.app.Application#onDependenciesLoaded onDependenciesLoaded} that sets up the other end of this.
* @singleton
* @private
*/
Ext.Router = {};
var drawStack = [];
/**
* Application's {@link Ext.app.Application#onDependenciesLoaded onDependenciesLoaded} has a deprecated-wrapped line that calls this. Basic idea is that once an
* app has been instantiated we set that at Ext.Router's `appInstance` and then redirect any calls to
* {@link Ext.app.Router#draw Ext.Router.draw} to that app's Router. We keep a `drawStack` above so that we can call {@link Ext.app.Router#draw Ext.Router.draw} one or
* more times before the application is even instantiated and it will simply link it up once everything is
* present.
*/
Ext.Router.setAppInstance = function(app) {
Ext.Router.appInstance = app;
if (drawStack.length > 0) {
Ext.each(drawStack, Ext.Router.draw);
}
};
Ext.Router.draw = function(mapperFn) {
Ext.Logger.deprecate(
'Ext.Router.map is deprecated, please define your routes inline inside each Controller. ' +
'Please see the 1.x -> 2.x migration guide for more details.'
);
var app = Ext.Router.appInstance,
router;
if (app) {
router = app.getRouter();
mapperFn(router);
} else {
drawStack.push(mapperFn);
}
};
//</deprecated>
});

View File

@@ -0,0 +1,112 @@
/**
* 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'
}
}
});

View File

@@ -0,0 +1,12 @@
/**
* @private
*/
Ext.define('Ext.behavior.Behavior', {
constructor: function(component) {
this.component = component;
component.on('destroy', 'onComponentDestroy', this);
},
onComponentDestroy: Ext.emptyFn
});

View File

@@ -0,0 +1,52 @@
/**
* @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();
}
}
});

View File

View File

@@ -0,0 +1,89 @@
/**
* @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();
}
}
});

View File

View File

@@ -0,0 +1,48 @@
/**
* @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();
}
}
});

View File

@@ -0,0 +1,843 @@
/**
* @class Ext.carousel.Carousel
* @author Jacky Nguyen <jacky@sencha.com>
*
* Carousels, like [tabs](#!/guide/tabs), are a great way to allow the user to swipe through multiple full-screen pages.
* A Carousel shows only one of its pages at a time but allows you to swipe through with your finger.
*
* Carousels can be oriented either horizontally or vertically and are easy to configure - they just work like any other
* Container. Here's how to set up a simple horizontal Carousel:
*
* @example
* Ext.create('Ext.Carousel', {
* fullscreen: true,
*
* defaults: {
* styleHtmlContent: true
* },
*
* items: [
* {
* html : 'Item 1',
* style: 'background-color: #5E99CC'
* },
* {
* html : 'Item 2',
* style: 'background-color: #759E60'
* },
* {
* html : 'Item 3'
* }
* ]
* });
*
* We can also make Carousels orient themselves vertically:
*
* @example preview
* Ext.create('Ext.Carousel', {
* fullscreen: true,
* direction: 'vertical',
*
* defaults: {
* styleHtmlContent: true
* },
*
* items: [
* {
* html : 'Item 1',
* style: 'background-color: #759E60'
* },
* {
* html : 'Item 2',
* style: 'background-color: #5E99CC'
* }
* ]
* });
*
* ### Common Configurations
* * {@link #ui} defines the style of the carousel
* * {@link #direction} defines the direction of the carousel
* * {@link #indicator} defines if the indicator show be shown
*
* ### Useful Methods
* * {@link #next} moves to the next card
* * {@link #previous} moves to the previous card
* * {@link #setActiveItem} moves to the passed card
*
* ## Further Reading
*
* For more information about Carousels see the [Carousel guide](#!/guide/carousel).
*
* @aside guide carousel
* @aside example carousel
*/
Ext.define('Ext.carousel.Carousel', {
extend: 'Ext.Container',
alternateClassName: 'Ext.Carousel',
xtype: 'carousel',
requires: [
'Ext.fx.easing.EaseOut',
'Ext.carousel.Item',
'Ext.carousel.Indicator',
'Ext.util.TranslatableGroup'
],
config: {
/**
* @cfg layout
* Hide layout config in Carousel. It only causes confusion.
* @accessor
* @private
*/
/**
* @cfg
* @inheritdoc
*/
baseCls: 'x-carousel',
/**
* @cfg {String} direction
* The direction of the Carousel, either 'horizontal' or 'vertical'.
* @accessor
*/
direction: 'horizontal',
directionLock: false,
animation: {
duration: 250,
easing: {
type: 'ease-out'
}
},
/**
* @cfg {Boolean} indicator
* Provides an indicator while toggling between child items to let the user
* know where they are in the card stack.
* @accessor
*/
indicator: true,
/**
* @cfg {String} ui
* Style options for Carousel. Default is 'dark'. 'light' is also available.
* @accessor
*/
ui: 'dark',
itemConfig: {},
bufferSize: 1,
itemLength: null
},
itemLength: 0,
offset: 0,
flickStartOffset: 0,
flickStartTime: 0,
dragDirection: 0,
count: 0,
painted: false,
activeIndex: -1,
beforeInitialize: function() {
this.element.on({
dragstart: 'onDragStart',
drag: 'onDrag',
dragend: 'onDragEnd',
scope: this
});
this.element.on('resize', 'onSizeChange', this);
this.carouselItems = [];
this.orderedCarouselItems = [];
this.inactiveCarouselItems = [];
this.hiddenTranslation = 0;
},
updateBufferSize: function(size) {
var ItemClass = Ext.carousel.Item,
total = size * 2 + 1,
isRendered = this.isRendered(),
innerElement = this.innerElement,
items = this.carouselItems,
ln = items.length,
itemConfig = this.getItemConfig(),
itemLength = this.getItemLength(),
direction = this.getDirection(),
setterName = direction === 'horizontal' ? 'setWidth' : 'setHeight',
i, item;
for (i = ln; i < total; i++) {
item = Ext.factory(itemConfig, ItemClass);
if (itemLength) {
item[setterName].call(item, itemLength);
}
item.setLayoutSizeFlags(this.LAYOUT_BOTH);
items.push(item);
innerElement.append(item.renderElement);
if (isRendered && item.setRendered(true)) {
item.fireEvent('renderedchange', this, item, true);
}
}
this.getTranslatable().setActiveIndex(size);
},
setRendered: function(rendered) {
var wasRendered = this.rendered;
if (rendered !== wasRendered) {
this.rendered = rendered;
var items = this.items.items,
carouselItems = this.carouselItems,
i, ln, item;
for (i = 0,ln = items.length; i < ln; i++) {
item = items[i];
if (!item.isInnerItem()) {
item.setRendered(rendered);
}
}
for (i = 0,ln = carouselItems.length; i < ln; i++) {
carouselItems[i].setRendered(rendered);
}
return true;
}
return false;
},
onSizeChange: function() {
this.refreshSizing();
this.refreshCarouselItems();
this.refreshActiveItem();
},
onItemAdd: function(item, index) {
this.callParent(arguments);
var innerIndex = this.getInnerItems().indexOf(item),
indicator = this.getIndicator();
if (indicator && item.isInnerItem()) {
indicator.addIndicator();
}
if (innerIndex <= this.getActiveIndex()) {
this.refreshActiveIndex();
}
if (this.isIndexDirty(innerIndex) && !this.isItemsInitializing) {
this.refreshActiveItem();
}
},
doItemLayoutAdd: function(item) {
if (item.isInnerItem()) {
return;
}
this.callParent(arguments);
},
onItemRemove: function(item, index) {
this.callParent(arguments);
var innerIndex = this.getInnerItems().indexOf(item),
indicator = this.getIndicator(),
carouselItems = this.carouselItems,
i, ln, carouselItem;
if (item.isInnerItem() && indicator) {
indicator.removeIndicator();
}
if (innerIndex <= this.getActiveIndex()) {
this.refreshActiveIndex();
}
if (this.isIndexDirty(innerIndex)) {
for (i = 0,ln = carouselItems.length; i < ln; i++) {
carouselItem = carouselItems[i];
if (carouselItem.getComponent() === item) {
carouselItem.setComponent(null);
}
}
this.refreshActiveItem();
}
},
doItemLayoutRemove: function(item) {
if (item.isInnerItem()) {
return;
}
this.callParent(arguments);
},
onInnerItemMove: function(item, toIndex, fromIndex) {
if ((this.isIndexDirty(toIndex) || this.isIndexDirty(fromIndex))) {
this.refreshActiveItem();
}
},
doItemLayoutMove: function(item) {
if (item.isInnerItem()) {
return;
}
this.callParent(arguments);
},
isIndexDirty: function(index) {
var activeIndex = this.getActiveIndex(),
bufferSize = this.getBufferSize();
return (index >= activeIndex - bufferSize && index <= activeIndex + bufferSize);
},
getTranslatable: function() {
var translatable = this.translatable;
if (!translatable) {
this.translatable = translatable = new Ext.util.TranslatableGroup;
translatable.setItems(this.orderedCarouselItems);
translatable.on('animationend', 'onAnimationEnd', this);
}
return translatable;
},
onDragStart: function(e) {
var direction = this.getDirection(),
absDeltaX = e.absDeltaX,
absDeltaY = e.absDeltaY,
directionLock = this.getDirectionLock();
this.isDragging = true;
if (directionLock) {
if ((direction === 'horizontal' && absDeltaX > absDeltaY)
|| (direction === 'vertical' && absDeltaY > absDeltaX)) {
e.stopPropagation();
}
else {
this.isDragging = false;
return;
}
}
this.getTranslatable().stopAnimation();
this.dragStartOffset = this.offset;
this.dragDirection = 0;
},
onDrag: function(e) {
if (!this.isDragging) {
return;
}
var startOffset = this.dragStartOffset,
direction = this.getDirection(),
delta = direction === 'horizontal' ? e.deltaX : e.deltaY,
lastOffset = this.offset,
flickStartTime = this.flickStartTime,
dragDirection = this.dragDirection,
now = Ext.Date.now(),
currentActiveIndex = this.getActiveIndex(),
maxIndex = this.getMaxItemIndex(),
lastDragDirection = dragDirection,
offset;
if ((currentActiveIndex === 0 && delta > 0) || (currentActiveIndex === maxIndex && delta < 0)) {
delta *= 0.5;
}
offset = startOffset + delta;
if (offset > lastOffset) {
dragDirection = 1;
}
else if (offset < lastOffset) {
dragDirection = -1;
}
if (dragDirection !== lastDragDirection || (now - flickStartTime) > 300) {
this.flickStartOffset = lastOffset;
this.flickStartTime = now;
}
this.dragDirection = dragDirection;
this.setOffset(offset);
},
onDragEnd: function(e) {
if (!this.isDragging) {
return;
}
this.onDrag(e);
this.isDragging = false;
var now = Ext.Date.now(),
itemLength = this.itemLength,
threshold = itemLength / 2,
offset = this.offset,
activeIndex = this.getActiveIndex(),
maxIndex = this.getMaxItemIndex(),
animationDirection = 0,
flickDistance = offset - this.flickStartOffset,
flickDuration = now - this.flickStartTime,
indicator = this.getIndicator(),
velocity;
if (flickDuration > 0 && Math.abs(flickDistance) >= 10) {
velocity = flickDistance / flickDuration;
if (Math.abs(velocity) >= 1) {
if (velocity < 0 && activeIndex < maxIndex) {
animationDirection = -1;
}
else if (velocity > 0 && activeIndex > 0) {
animationDirection = 1;
}
}
}
if (animationDirection === 0) {
if (activeIndex < maxIndex && offset < -threshold) {
animationDirection = -1;
}
else if (activeIndex > 0 && offset > threshold) {
animationDirection = 1;
}
}
if (indicator) {
indicator.setActiveIndex(activeIndex - animationDirection);
}
this.animationDirection = animationDirection;
this.setOffsetAnimated(animationDirection * itemLength);
},
applyAnimation: function(animation) {
animation.easing = Ext.factory(animation.easing, Ext.fx.easing.EaseOut);
return animation;
},
updateDirection: function(direction) {
var indicator = this.getIndicator();
this.currentAxis = (direction === 'horizontal') ? 'x' : 'y';
if (indicator) {
indicator.setDirection(direction);
}
},
/**
* @private
* @chainable
*/
setOffset: function(offset) {
this.offset = offset;
this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset);
return this;
},
/**
* @private
* @return {Ext.carousel.Carousel} this
* @chainable
*/
setOffsetAnimated: function(offset) {
var indicator = this.getIndicator();
if (indicator) {
indicator.setActiveIndex(this.getActiveIndex() - this.animationDirection);
}
this.offset = offset;
this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset, this.getAnimation());
return this;
},
onAnimationEnd: function(translatable) {
var currentActiveIndex = this.getActiveIndex(),
animationDirection = this.animationDirection,
axis = this.currentAxis,
currentOffset = translatable[axis],
itemLength = this.itemLength,
offset;
if (animationDirection === -1) {
offset = itemLength + currentOffset;
}
else if (animationDirection === 1) {
offset = currentOffset - itemLength;
}
else {
offset = currentOffset;
}
offset -= this.itemOffset;
this.offset = offset;
this.setActiveItem(currentActiveIndex - animationDirection);
},
refresh: function() {
this.refreshSizing();
this.refreshActiveItem();
},
refreshSizing: function() {
var element = this.element,
itemLength = this.getItemLength(),
translatableItemLength = {
x: 0,
y: 0
},
itemOffset, containerSize;
if (this.getDirection() === 'horizontal') {
containerSize = element.getWidth();
}
else {
containerSize = element.getHeight();
}
this.hiddenTranslation = -containerSize;
if (itemLength === null) {
itemLength = containerSize;
itemOffset = 0;
}
else {
itemOffset = (containerSize - itemLength) / 2;
}
this.itemLength = itemLength;
this.itemOffset = itemOffset;
translatableItemLength[this.currentAxis] = itemLength;
this.getTranslatable().setItemLength(translatableItemLength);
},
refreshOffset: function() {
this.setOffset(this.offset);
},
refreshActiveItem: function() {
this.doSetActiveItem(this.getActiveItem());
},
/**
* Returns the index of the currently active card.
* @return {Number} The index of the currently active card.
*/
getActiveIndex: function() {
return this.activeIndex;
},
refreshActiveIndex: function() {
this.activeIndex = this.getInnerItemIndex(this.getActiveItem());
},
refreshCarouselItems: function() {
var items = this.carouselItems,
i, ln, item;
for (i = 0,ln = items.length; i < ln; i++) {
item = items[i];
item.getTranslatable().refresh();
}
this.refreshInactiveCarouselItems();
},
refreshInactiveCarouselItems: function() {
var items = this.inactiveCarouselItems,
hiddenTranslation = this.hiddenTranslation,
axis = this.currentAxis,
i, ln, item;
for (i = 0,ln = items.length; i < ln; i++) {
item = items[i];
item.translateAxis(axis, hiddenTranslation);
}
},
/**
* @private
* @return {Number}
*/
getMaxItemIndex: function() {
return this.innerItems.length - 1;
},
/**
* @private
* @return {Number}
*/
getInnerItemIndex: function(item) {
return this.innerItems.indexOf(item);
},
/**
* @private
* @return {Object}
*/
getInnerItemAt: function(index) {
return this.innerItems[index];
},
/**
* @private
* @return {Object}
*/
applyActiveItem: function() {
var activeItem = this.callParent(arguments),
activeIndex;
if (activeItem) {
activeIndex = this.getInnerItemIndex(activeItem);
if (activeIndex !== -1) {
this.activeIndex = activeIndex;
return activeItem;
}
}
},
doSetActiveItem: function(activeItem) {
var activeIndex = this.getActiveIndex(),
maxIndex = this.getMaxItemIndex(),
indicator = this.getIndicator(),
bufferSize = this.getBufferSize(),
carouselItems = this.carouselItems.slice(),
orderedCarouselItems = this.orderedCarouselItems,
visibleIndexes = {},
visibleItems = {},
visibleItem, component, id, i, index, ln, carouselItem;
if (carouselItems.length === 0) {
return;
}
this.callParent(arguments);
orderedCarouselItems.length = 0;
if (activeItem) {
id = activeItem.getId();
visibleItems[id] = activeItem;
visibleIndexes[id] = bufferSize;
if (activeIndex > 0) {
for (i = 1; i <= bufferSize; i++) {
index = activeIndex - i;
if (index >= 0) {
visibleItem = this.getInnerItemAt(index);
id = visibleItem.getId();
visibleItems[id] = visibleItem;
visibleIndexes[id] = bufferSize - i;
}
else {
break;
}
}
}
if (activeIndex < maxIndex) {
for (i = 1; i <= bufferSize; i++) {
index = activeIndex + i;
if (index <= maxIndex) {
visibleItem = this.getInnerItemAt(index);
id = visibleItem.getId();
visibleItems[id] = visibleItem;
visibleIndexes[id] = bufferSize + i;
}
else {
break;
}
}
}
for (i = 0,ln = carouselItems.length; i < ln; i++) {
carouselItem = carouselItems[i];
component = carouselItem.getComponent();
if (component) {
id = component.getId();
if (visibleIndexes.hasOwnProperty(id)) {
carouselItems.splice(i, 1);
i--;
ln--;
delete visibleItems[id];
orderedCarouselItems[visibleIndexes[id]] = carouselItem;
}
}
}
for (id in visibleItems) {
if (visibleItems.hasOwnProperty(id)) {
visibleItem = visibleItems[id];
carouselItem = carouselItems.pop();
carouselItem.setComponent(visibleItem);
orderedCarouselItems[visibleIndexes[id]] = carouselItem;
}
}
}
this.inactiveCarouselItems.length = 0;
this.inactiveCarouselItems = carouselItems;
this.refreshOffset();
this.refreshInactiveCarouselItems();
if (indicator) {
indicator.setActiveIndex(activeIndex);
}
},
/**
* Switches to the next card.
* @return {Ext.carousel.Carousel} this
* @chainable
*/
next: function() {
this.setOffset(0);
if (this.activeIndex === this.getMaxItemIndex()) {
return this;
}
this.animationDirection = -1;
this.setOffsetAnimated(-this.itemLength);
return this;
},
/**
* Switches to the previous card.
* @return {Ext.carousel.Carousel} this
* @chainable
*/
previous: function() {
this.setOffset(0);
if (this.activeIndex === 0) {
return this;
}
this.animationDirection = 1;
this.setOffsetAnimated(this.itemLength);
return this;
},
// @private
applyIndicator: function(indicator, currentIndicator) {
return Ext.factory(indicator, Ext.carousel.Indicator, currentIndicator);
},
// @private
updateIndicator: function(indicator) {
if (indicator) {
this.insertFirst(indicator);
indicator.setUi(this.getUi());
indicator.on({
next: 'next',
previous: 'previous',
scope: this
});
}
},
destroy: function() {
var carouselItems = this.carouselItems.slice();
this.carouselItems.length = 0;
Ext.destroy(carouselItems, this.getIndicator(), this.translatable);
this.callParent();
delete this.carouselItems;
}
}, function() {
//<deprecated product=touch since=2.0>
this.override({
constructor: function(config) {
if (config && 'activeIndex' in config) {
//<debug warn>
Ext.Logger.deprecate("'activeIndex' config is deprecated, please use 'activeItem' config instead }");
//</debug>
config.activeItem = config.activeIndex;
}
this.callParent([config]);
}
});
Ext.deprecateClassMethod(this, {
/**
* Returns `true` when {@link #direction} is 'vertical'.
* @return {Boolean}
* @deprecated 2.0.0 Use `getDirection() === 'vertical'` instead.
*/
isVertical: function getDirection() {
return this.getDirection() === 'vertical';
},
/**
* Returns `true` when {@link #direction} is 'horizontal'.
* @return {Boolean}
* @deprecated 2.0.0 Use `getDirection() === 'horizontal'` instead.
*/
isHorizontal: function getDirection() {
return this.getDirection() === 'horizontal';
},
/**
* @method
* @inheritdoc Ext.carousel.Carousel#previous
* @deprecated 2.0.0 Use {@link Ext.carousel.Carousel#previous} instead.
*/
prev: 'previous'
});
//</deprecated>
});

View File

@@ -0,0 +1,121 @@
/**
* 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();
}
});

View File

@@ -0,0 +1,164 @@
/**
* @class Ext.carousel.Infinite
* @author Jacky Nguyen <jacky@sencha.com>
* @private
*
* The true infinite implementation of Carousel, private for now until it's stable to be public
*/
Ext.define('Ext.carousel.Infinite', {
extend: 'Ext.carousel.Carousel',
config: {
indicator: null,
maxItemIndex: Infinity,
innerItemConfig: {}
},
applyIndicator: function(indicator) {
//<debug error>
if (indicator) {
Ext.Logger.error("'indicator' in Infinite Carousel implementation is not currently supported", this);
}
//</debug>
return;
},
updateBufferSize: function(size) {
this.callParent(arguments);
var total = size * 2 + 1,
ln = this.innerItems.length,
innerItemConfig = this.getInnerItemConfig(),
i;
this.isItemsInitializing = true;
for (i = ln; i < total; i++) {
this.doAdd(this.factoryItem(innerItemConfig));
}
this.isItemsInitializing = false;
this.rebuildInnerIndexes();
this.refreshActiveItem();
},
updateMaxItemIndex: function(maxIndex, oldMaxIndex) {
if (oldMaxIndex !== undefined) {
var activeIndex = this.getActiveIndex();
if (activeIndex > maxIndex) {
this.setActiveItem(maxIndex);
}
else {
this.rebuildInnerIndexes(activeIndex);
this.refreshActiveItem();
}
}
},
rebuildInnerIndexes: function(activeIndex) {
var indexToItem = this.innerIndexToItem,
idToIndex = this.innerIdToIndex,
items = this.innerItems.slice(),
ln = items.length,
bufferSize = this.getBufferSize(),
maxIndex = this.getMaxItemIndex(),
changedIndexes = [],
i, oldIndex, index, id, item;
if (activeIndex === undefined) {
this.innerIndexToItem = indexToItem = {};
this.innerIdToIndex = idToIndex = {};
for (i = 0; i < ln; i++) {
item = items[i];
id = item.getId();
idToIndex[id] = i;
indexToItem[i] = item;
this.fireEvent('itemindexchange', this, item, i, -1);
}
}
else {
for (i = activeIndex - bufferSize; i <= activeIndex + bufferSize; i++) {
if (i >= 0 && i <= maxIndex) {
if (indexToItem.hasOwnProperty(i)) {
Ext.Array.remove(items, indexToItem[i]);
continue;
}
changedIndexes.push(i);
}
}
for (i = 0,ln = changedIndexes.length; i < ln; i++) {
item = items[i];
id = item.getId();
index = changedIndexes[i];
oldIndex = idToIndex[id];
delete indexToItem[oldIndex];
idToIndex[id] = index;
indexToItem[index] = item;
this.fireEvent('itemindexchange', this, item, index, oldIndex);
}
}
},
reset: function() {
this.rebuildInnerIndexes();
this.setActiveItem(0);
},
refreshItems: function() {
var items = this.innerItems,
idToIndex = this.innerIdToIndex,
index, item, i, ln;
for (i = 0,ln = items.length; i < ln; i++) {
item = items[i];
index = idToIndex[item.getId()];
this.fireEvent('itemindexchange', this, item, index, -1);
}
},
getInnerItemIndex: function(item) {
var index = this.innerIdToIndex[item.getId()];
return (typeof index == 'number') ? index : -1;
},
getInnerItemAt: function(index) {
return this.innerIndexToItem[index];
},
applyActiveItem: function(activeItem) {
this.getItems();
this.getBufferSize();
var maxIndex = this.getMaxItemIndex(),
currentActiveIndex = this.getActiveIndex();
if (typeof activeItem == 'number') {
activeItem = Math.max(0, Math.min(activeItem, maxIndex));
if (activeItem === currentActiveIndex) {
return;
}
this.activeIndex = activeItem;
this.rebuildInnerIndexes(activeItem);
activeItem = this.getInnerItemAt(activeItem);
}
if (activeItem) {
return this.callParent([activeItem]);
}
}
});

View File

@@ -0,0 +1,12 @@
/**
* @private
*/
Ext.define('Ext.carousel.Item', {
extend: 'Ext.Decorator',
config: {
baseCls: 'x-carousel-item',
component: null,
translatable: true
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,272 @@
/**
* @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'
});
}
}
});

View File

@@ -0,0 +1,120 @@
/**
* @class Ext.chart.Legend
* @extends Ext.dataview.DataView
*
* A default legend for charts.
*
* @example preview
* var chart = new Ext.chart.Chart({
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* legend: {
* position: 'bottom'
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data1'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 0
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'area',
* title: ['Data1', 'Data2', 'Data3'],
* subStyle: {
* fill: ['blue', 'green', 'red']
* },
* xField: 'name',
* yField: ['data1', 'data2', 'data3']
*
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(chart);
*/
Ext.define("Ext.chart.Legend", {
xtype: 'legend',
extend: "Ext.dataview.DataView",
config: {
itemTpl: [
"<span class=\"x-legend-item-marker {[values.disabled?\'x-legend-inactive\':\'\']}\" style=\"background:{mark};\"></span>{name}"
],
baseCls: 'x-legend',
padding: 5,
disableSelection: true,
inline: true,
/**
* @cfg {String} position
* @deprecated Use `docked` instead.
* Delegates to `docked`
*/
position: 'top',
horizontalHeight: 48,
verticalWidth: 150
},
constructor: function () {
this.callSuper(arguments);
var scroller = this.getScrollable().getScroller(),
onDrag = scroller.onDrag;
scroller.onDrag = function (e) {
e.stopPropagation();
onDrag.call(this, e);
};
},
doSetDocked: function (docked) {
this.callSuper(arguments);
if (docked === 'top' || docked === 'bottom') {
this.setLayout({type: 'hbox', pack: 'center'});
this.setInline(true);
// TODO: Remove this when possible
this.setWidth(null);
this.setHeight(this.getHorizontalHeight());
this.setScrollable({direction: 'horizontal' });
} else {
this.setLayout({pack: 'center'});
this.setInline(false);
// TODO: Remove this when possible
this.setWidth(this.getVerticalWidth());
this.setHeight(null);
this.setScrollable({direction: 'vertical' });
}
},
updatePosition: function (position) {
this.setDocked(position);
},
onItemTap: function (container, target, index, e) {
this.callSuper(arguments);
var me = this,
store = me.getStore(),
record = store && store.getAt(index);
record.beginEdit();
record.set('disabled', !record.get('disabled'));
record.commit();
}
});

View File

@@ -0,0 +1,85 @@
/**
* @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);
}
}
}
});

View File

@@ -0,0 +1,97 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,161 @@
/**
* @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();
}
});

View File

@@ -0,0 +1,52 @@
/**
* @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();
}
});

View File

@@ -0,0 +1,894 @@
/**
* @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();
}
});

View File

@@ -0,0 +1,69 @@
/**
* @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'
}
});

View File

@@ -0,0 +1,73 @@
/**
* @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'
}
});

View File

@@ -0,0 +1,140 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,20 @@
/**
* @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];
}
});

View File

@@ -0,0 +1,40 @@
/**
* @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);
}
};
}
});

View File

@@ -0,0 +1,108 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,136 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,36 @@
/**
* @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
};
}
});

View File

@@ -0,0 +1,45 @@
/**
* @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
};
}
});

View File

@@ -0,0 +1,84 @@
/**
* @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
});

View File

@@ -0,0 +1,107 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,700 @@
/**
* @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();
}
}
});

View File

@@ -0,0 +1,19 @@
/**
* @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'
}
}
}
});

View File

@@ -0,0 +1,46 @@
/**
* @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();
}
});

View File

@@ -0,0 +1,44 @@
/**
* @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);
}
});

View File

@@ -0,0 +1,43 @@
/**
* @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();
}
});

View File

@@ -0,0 +1,230 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,372 @@
/**
* @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();
}
});

View File

@@ -0,0 +1,37 @@
/**
* @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();
}
});

View File

@@ -0,0 +1,106 @@
/**
* 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();
}
}
});

View File

@@ -0,0 +1,476 @@
/**
* The PanZoom interaction allows the user to navigate the data for one or more chart
* axes by panning and/or zooming. Navigation can be limited to particular axes. Zooming is
* performed by pinching on the chart or axis area; panning is performed by single-touch dragging.
*
* For devices which do not support multiple-touch events, zooming can not be done via pinch gestures; in this case the
* interaction will allow the user to perform both zooming and panning using the same single-touch drag gesture.
* {@link #modeToggleButton} provides a button to indicate and toggle between two modes.
*
* @example preview
* var lineChart = new Ext.chart.CartesianChart({
* interactions: [{
* type: 'panzoom',
* zoomOnPanGesture: true
* }],
* animate: true,
* store: {
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
* data: [
* {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
* {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
* {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
* {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
* {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data1'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* },
* grid: true,
* minimum: 0
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Sample Values',
* fontSize: 15
* }
* }],
* series: [{
* type: 'line',
* highlight: {
* size: 7,
* radius: 7
* },
* style: {
* stroke: 'rgb(143,203,203)'
* },
* xField: 'name',
* yField: 'data1',
* marker: {
* type: 'path',
* path: ['M', -2, 0, 0, 2, 2, 0, 0, -2, 'Z'],
* stroke: 'blue',
* lineWidth: 0
* }
* }, {
* type: 'line',
* highlight: {
* size: 7,
* radius: 7
* },
* fill: true,
* xField: 'name',
* yField: 'data3',
* marker: {
* type: 'circle',
* radius: 4,
* lineWidth: 0
* }
* }]
* });
* Ext.Viewport.setLayout('fit');
* Ext.Viewport.add(lineChart);
*
* The configuration object for the `panzoom` interaction type should specify which axes
* will be made navigable via the `axes` config. See the {@link #axes} config documentation
* for details on the allowed formats. If the `axes` config is not specified, it will default
* to making all axes navigable with the default axis options.
*
*/
Ext.define('Ext.chart.interactions.PanZoom', {
extend: 'Ext.chart.interactions.Abstract',
type: 'panzoom',
alias: 'interaction.panzoom',
requires: [
'Ext.util.Region',
'Ext.draw.Animator'
],
config: {
/**
* @cfg {Object/Array} axes
* Specifies which axes should be made navigable. The config value can take the following formats:
*
* - An Object whose keys correspond to the {@link Ext.chart.axis.Axis#position position} of each
* axis that should be made navigable. Each key's value can either be an Object with further
* configuration options for each axis or simply `true` for a default set of options.
* {
* type: 'panzoom',
* axes: {
* left: {
* maxZoom: 5,
* allowPan: false
* },
* bottom: true
* }
* }
*
* If using the full Object form, the following options can be specified for each axis:
*
* - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its natural size.
* - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`.
* - startZoom (Number) A starting zoom level for the axis. Defaults to `1`.
* - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`.
* - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`.
* - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`.
*
* - An Array of strings, each one corresponding to the {@link Ext.chart.axis.Axis#position position}
* of an axis that should be made navigable. The default options will be used for each named axis.
*
* {
* type: 'panzoom',
* axes: ['left', 'bottom']
* }
*
* If the `axes` config is not specified, it will default to making all axes navigable with the
* default axis options.
*/
axes: {
top: {},
right: {},
bottom: {},
left: {}
},
minZoom: 1,
maxZoom: 10000,
/**
* @cfg {Boolean} showOverflowArrows
* If `true`, arrows will be conditionally shown at either end of each axis to indicate that the
* axis is overflowing and can therefore be panned in that direction. Set this to `false` to
* prevent the arrows from being displayed.
*/
showOverflowArrows: true,
/**
* @cfg {Object} overflowArrowOptions
* A set of optional overrides for the overflow arrow sprites' options. Only relevant when
* {@link #showOverflowArrows} is `true`.
*/
gesture: 'pinch',
panGesture: 'drag',
zoomOnPanGesture: false,
modeToggleButton: {
cls: ['x-panzoom-toggle', 'x-zooming'],
iconCls: 'x-panzoom-toggle-icon',
iconMask: true
},
hideLabelInGesture: false //Ext.os.is.Android
},
stopAnimationBeforeSync: true,
applyAxes: function (axesConfig, oldAxesConfig) {
return Ext.merge(oldAxesConfig || {}, axesConfig);
},
applyZoomOnPanGesture: function (zoomOnPanGesture) {
this.getChart();
if (this.isMultiTouch()) {
return false;
}
return zoomOnPanGesture;
},
updateZoomOnPanGesture: function (zoomOnPanGesture) {
if (!this.isMultiTouch()) {
var button = this.getModeToggleButton(),
zoomModeCls = Ext.baseCSSPrefix + 'zooming';
if (zoomOnPanGesture) {
button.addCls(zoomModeCls);
if (!button.config.hideText) {
button.setText('&nbsp;Zoom');
}
} else {
button.removeCls(zoomModeCls);
if (!button.config.hideText) {
button.setText('&nbsp;Pan');
}
}
}
},
toggleMode: function () {
var me = this;
if (!me.isMultiTouch()) {
me.setZoomOnPanGesture(!me.getZoomOnPanGesture());
}
},
applyModeToggleButton: function (button, oldButton) {
var me = this,
result = Ext.factory(button, "Ext.Button", oldButton);
if (result && !oldButton) {
result.setHandler(function () {
me.toggleMode();
});
}
return result;
},
getGestures: function () {
var me = this,
gestures = {};
gestures[me.getGesture()] = 'onGesture';
gestures[me.getGesture() + 'start'] = 'onGestureStart';
gestures[me.getGesture() + 'end'] = 'onGestureEnd';
gestures[me.getPanGesture()] = 'onPanGesture';
gestures[me.getPanGesture() + 'start'] = 'onPanGestureStart';
gestures[me.getPanGesture() + 'end'] = 'onPanGestureEnd';
gestures.doubletap = 'onDoubleTap';
return gestures;
},
onDoubleTap: function (e) {
},
onPanGestureStart: function (e) {
if (!e || !e.touches || e.touches.length < 2) { //Limit drags to single touch
var me = this,
region = me.getChart().getInnerRegion(),
xy = me.getChart().element.getXY();
me.startX = e.pageX - xy[0] - region[0];
me.startY = e.pageY - xy[1] - region[1];
me.oldVisibleRanges = null;
me.hideLabels();
me.getChart().suspendThicknessChanged();
}
},
onPanGesture: function (e) {
if (!e.touches || e.touches.length < 2) { //Limit drags to single touch
var me = this,
region = me.getChart().getInnerRegion(),
xy = me.getChart().element.getXY();
if (me.getZoomOnPanGesture()) {
me.transformAxesBy(me.getZoomableAxes(e), 0, 0, (e.pageX - xy[0] - region[0]) / me.startX, me.startY / (e.pageY - xy[1] - region[1]));
} else {
me.transformAxesBy(me.getPannableAxes(e), e.pageX - xy[0] - region[0] - me.startX, e.pageY - xy[1] - region[1] - me.startY, 1, 1);
}
me.sync();
}
},
onPanGestureEnd: function (e) {
var me = this;
me.getChart().resumeThicknessChanged();
me.showLabels();
me.sync();
},
onGestureStart: function (e) {
if (e.touches && e.touches.length === 2) {
var me = this,
xy = me.getChart().element.getXY(),
region = me.getChart().getInnerRegion(),
x = xy[0] + region[0],
y = xy[1] + region[1],
newPoints = [e.touches[0].point.x - x, e.touches[0].point.y - y, e.touches[1].point.x - x, e.touches[1].point.y - y],
xDistance = Math.max(44, Math.abs(newPoints[2] - newPoints[0])),
yDistance = Math.max(44, Math.abs(newPoints[3] - newPoints[1]));
me.getChart().suspendThicknessChanged();
me.lastZoomDistances = [xDistance, yDistance];
me.lastPoints = newPoints;
me.oldVisibleRanges = null;
me.hideLabels();
}
},
onGesture: function (e) {
if (e.touches && e.touches.length === 2) {
var me = this,
region = me.getChart().getInnerRegion(),
xy = me.getChart().element.getXY(),
x = xy[0] + region[0],
y = xy[1] + region[1],
abs = Math.abs,
lastPoints = me.lastPoints,
newPoints = [e.touches[0].point.x - x, e.touches[0].point.y - y, e.touches[1].point.x - x, e.touches[1].point.y - y],
xDistance = Math.max(44, abs(newPoints[2] - newPoints[0])),
yDistance = Math.max(44, abs(newPoints[3] - newPoints[1])),
lastDistances = this.lastZoomDistances || [xDistance, yDistance],
zoomX = xDistance / lastDistances[0],
zoomY = yDistance / lastDistances[1];
me.transformAxesBy(me.getZoomableAxes(e),
region[2] * (zoomX - 1) / 2 + newPoints[2] - lastPoints[2] * zoomX,
region[3] * (zoomY - 1) / 2 + newPoints[3] - lastPoints[3] * zoomY,
zoomX,
zoomY);
me.sync();
}
},
onGestureEnd: function (e) {
var me = this;
me.showLabels();
me.sync();
},
hideLabels: function () {
if (this.getHideLabelInGesture()) {
this.eachInteractiveAxes(function (axis) {
axis.hideLabels();
});
}
},
showLabels: function () {
if (this.getHideLabelInGesture()) {
this.eachInteractiveAxes(function (axis) {
axis.showLabels();
});
}
},
isEventOnAxis: function (e, axis) {
// TODO: right now this uses the current event position but really we want to only
// use the gesture's start event. Pinch does not give that to us though.
var region = axis.getSurface().getRegion();
return region[0] <= e.pageX && e.pageX <= region[0] + region[2] && region[1] <= e.pageY && e.pageY <= region[1] + region[3];
},
getPannableAxes: function (e) {
var me = this,
axisConfigs = me.getAxes(),
axes = me.getChart().getAxes(),
i, ln = axes.length,
result = [], isEventOnAxis = false,
config;
if (e) {
for (i = 0; i < ln; i++) {
if (this.isEventOnAxis(e, axes[i])) {
isEventOnAxis = true;
break;
}
}
}
for (i = 0; i < ln; i++) {
config = axisConfigs[axes[i].getPosition()];
if (config && config.allowPan !== false && (!isEventOnAxis || this.isEventOnAxis(e, axes[i]))) {
result.push(axes[i]);
}
}
return result;
},
getZoomableAxes: function (e) {
var me = this,
axisConfigs = me.getAxes(),
axes = me.getChart().getAxes(),
result = [],
i, ln = axes.length, axis,
isEventOnAxis = false, config;
if (e) {
for (i = 0; i < ln; i++) {
if (this.isEventOnAxis(e, axes[i])) {
isEventOnAxis = true;
break;
}
}
}
for (i = 0; i < ln; i++) {
axis = axes[i];
config = axisConfigs[axis.getPosition()];
if (config && config.allowZoom !== false && (!isEventOnAxis || this.isEventOnAxis(e, axis))) {
result.push(axis);
}
}
return result;
},
eachInteractiveAxes: function (fn) {
var me = this,
axisConfigs = me.getAxes(),
axes = me.getChart().getAxes();
for (var i = 0; i < axes.length; i++) {
if (axisConfigs[axes[i].getPosition()]) {
if (false === fn.call(this, axes[i])) {
return;
}
}
}
},
transformAxesBy: function (axes, panX, panY, sx, sy) {
var region = this.getChart().getInnerRegion(),
axesCfg = this.getAxes(), axisCfg,
oldVisibleRanges = this.oldVisibleRanges;
if (!oldVisibleRanges) {
this.oldVisibleRanges = oldVisibleRanges = {};
this.eachInteractiveAxes(function (axis) {
oldVisibleRanges[axis.getId()] = axis.getVisibleRange();
});
}
if (!region) {
return;
}
for (var i = 0; i < axes.length; i++) {
axisCfg = axesCfg[axes[i].getPosition()];
this.transformAxisBy(axes[i], oldVisibleRanges[axes[i].getId()], panX, panY, sx, sy, axisCfg.minZoom, axisCfg.maxZoom);
}
},
transformAxisBy: function (axis, oldVisibleRange, panX, panY, sx, sy, minZoom, maxZoom) {
var me = this,
visibleLength = oldVisibleRange[1] - oldVisibleRange[0],
actualMinZoom = axis.config.minZoom || minZoom || me.getMinZoom(),
actualMaxZoom = axis.config.maxZoom || maxZoom || me.getMaxZoom(),
region = me.getChart().getInnerRegion();
if (!region) {
return;
}
var isSide = axis.isSide(),
length = isSide ? region[3] : region[2],
pan = isSide ? -panY : panX;
visibleLength /= isSide ? sy : sx;
if (visibleLength < 0) {
visibleLength = -visibleLength;
}
if (visibleLength * actualMinZoom > 1) {
visibleLength = 1;
}
if (visibleLength * actualMaxZoom < 1) {
visibleLength = 1 / actualMaxZoom;
}
axis.setVisibleRange([
(oldVisibleRange[0] + oldVisibleRange[1] - visibleLength) * 0.5 - pan / length * visibleLength,
(oldVisibleRange[0] + oldVisibleRange[1] + visibleLength) * 0.5 - pan / length * visibleLength
]);
},
destroy: function () {
this.setModeToggleButton(null);
this.callSuper();
}
});

View File

@@ -0,0 +1,101 @@
/**
* @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) {
}
});

View File

@@ -0,0 +1,22 @@
/**
* @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);
}
});

View File

@@ -0,0 +1,107 @@
/**
* @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);
}
}
});

View File

@@ -0,0 +1,91 @@
/**
* @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);
}
});

View File

@@ -0,0 +1,61 @@
/**
* @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']
});

View File

@@ -0,0 +1,104 @@
/**
* @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);
}
});

View File

@@ -0,0 +1,100 @@
/**
* @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']
});

View File

@@ -0,0 +1,144 @@
/**
* @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]];
}
})
;

View File

@@ -0,0 +1,257 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,335 @@
/**
* @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 () {
});

View File

@@ -0,0 +1,139 @@
/**
* @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
});

View File

@@ -0,0 +1,315 @@
/**
* @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
});
}
}
}
});

View File

@@ -0,0 +1,247 @@
/**
* @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;
}
});

View File

@@ -0,0 +1,103 @@
/**
* 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
});
}
}
});

View File

@@ -0,0 +1,168 @@
/**
* @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;
});

View File

@@ -0,0 +1,97 @@
/**
* @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);
}
});

View File

@@ -0,0 +1,843 @@
/**
* 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();
}
});

Some files were not shown because too many files have changed in this diff Show More