X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/pkgs/classes.js?ds=sidebyside diff --git a/pkgs/classes.js b/pkgs/classes.js new file mode 100644 index 00000000..3e8f2d94 --- /dev/null +++ b/pkgs/classes.js @@ -0,0 +1,106524 @@ +/* +Ext JS - JavaScript Library +Copyright (c) 2006-2011, Sencha Inc. +All rights reserved. +licensing@sencha.com +*/ +/** + * @class Ext.util.Observable + * Base class that provides a common interface for publishing events. Subclasses are expected to + * to have a property "events" with all the events defined, and, optionally, a property "listeners" + * with configured listeners defined.
+ * For example: + *

+Employee = Ext.extend(Ext.util.Observable, {
+    constructor: function(config){
+        this.name = config.name;
+        this.addEvents({
+            "fired" : true,
+            "quit" : true
+        });
+
+        // Copy configured listeners into *this* object so that the base class's
+        // constructor will add them.
+        this.listeners = config.listeners;
+
+        // Call our superclass constructor to complete construction process.
+        Employee.superclass.constructor.call(this, config)
+    }
+});
+
+ * This could then be used like this:

+var newEmployee = new Employee({
+    name: employeeName,
+    listeners: {
+        quit: function() {
+            // By default, "this" will be the object that fired the event.
+            alert(this.name + " has quit!");
+        }
+    }
+});
+
+ */ + +Ext.define('Ext.util.Observable', { + + /* Begin Definitions */ + + requires: ['Ext.util.Event'], + + statics: { + /** + * Removes all added captures from the Observable. + * @param {Observable} o The Observable to release + * @static + */ + releaseCapture: function(o) { + o.fireEvent = this.prototype.fireEvent; + }, + + /** + * Starts capture on the specified Observable. All events will be passed + * to the supplied function with the event name + standard signature of the event + * before the event is fired. If the supplied function returns false, + * the event will not fire. + * @param {Observable} o The Observable to capture events from. + * @param {Function} fn The function to call when an event is fired. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the Observable firing the event. + * @static + */ + capture: function(o, fn, scope) { + o.fireEvent = Ext.Function.createInterceptor(o.fireEvent, fn, scope); + }, + + /** +Sets observability on the passed class constructor. + +This makes any event fired on any instance of the passed class also fire a single event through +the __class__ allowing for central handling of events on many instances at once. + +Usage: + + Ext.util.Observable.observe(Ext.data.Connection); + Ext.data.Connection.on('beforerequest', function(con, options) { + console.log('Ajax request made to ' + options.url); + }); + + * @param {Function} c The class constructor to make observable. + * @param {Object} listeners An object containing a series of listeners to add. See {@link #addListener}. + * @static + * @markdown + */ + observe: function(cls, listeners) { + if (cls) { + if (!cls.isObservable) { + Ext.applyIf(cls, new this()); + this.capture(cls.prototype, cls.fireEvent, cls); + } + if (Ext.isObject(listeners)) { + cls.on(listeners); + } + return cls; + } + } + }, + + /* End Definitions */ + + /** + * @cfg {Object} listeners (optional)

A config object containing one or more event handlers to be added to this + * object during initialization. This should be a valid listeners config object as specified in the + * {@link #addListener} example for attaching multiple handlers at once.

+ *

DOM events from ExtJs {@link Ext.Component Components}

+ *

While some ExtJs Component classes export selected DOM events (e.g. "click", "mouseover" etc), this + * is usually only done when extra value can be added. For example the {@link Ext.view.View DataView}'s + * {@link Ext.view.View#click click} event passing the node clicked on. To access DOM + * events directly from a child element of a Component, we need to specify the element option to + * identify the Component property to add a DOM listener to: + *


+new Ext.panel.Panel({
+    width: 400,
+    height: 200,
+    dockedItems: [{
+        xtype: 'toolbar'
+    }],
+    listeners: {
+        click: {
+            element: 'el', //bind to the underlying el property on the panel
+            fn: function(){ console.log('click el'); }
+        },
+        dblclick: {
+            element: 'body', //bind to the underlying body property on the panel
+            fn: function(){ console.log('dblclick body'); }
+        }
+    }
+});
+
+ *

+ */ + // @private + isObservable: true, + + constructor: function(config) { + var me = this; + + Ext.apply(me, config); + if (me.listeners) { + me.on(me.listeners); + delete me.listeners; + } + me.events = me.events || {}; + + if (me.bubbleEvents) { + me.enableBubble(me.bubbleEvents); + } + }, + + // @private + eventOptionsRe : /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate|element|vertical|horizontal)$/, + + /** + *

Adds listeners to any Observable object (or Element) which are automatically removed when this Component + * is destroyed. + * @param {Observable/Element} item The item to which to add a listener/listeners. + * @param {Object/String} ename The event name, or an object containing event name properties. + * @param {Function} fn Optional. If the ename parameter was an event name, this + * is the handler function. + * @param {Object} scope Optional. If the ename parameter was an event name, this + * is the scope (this reference) in which the handler function is executed. + * @param {Object} opt Optional. If the ename parameter was an event name, this + * is the {@link Ext.util.Observable#addListener addListener} options. + */ + addManagedListener : function(item, ename, fn, scope, options) { + var me = this, + managedListeners = me.managedListeners = me.managedListeners || [], + config; + + if (Ext.isObject(ename)) { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!me.eventOptionsRe.test(ename)) { + me.addManagedListener(item, ename, config.fn || config, config.scope || options.scope, config.fn ? config : options); + } + } + } + } + else { + managedListeners.push({ + item: item, + ename: ename, + fn: fn, + scope: scope, + options: options + }); + + item.on(ename, fn, scope, options); + } + }, + + /** + * Removes listeners that were added by the {@link #mon} method. + * @param {Observable|Element} item The item from which to remove a listener/listeners. + * @param {Object|String} ename The event name, or an object containing event name properties. + * @param {Function} fn Optional. If the ename parameter was an event name, this + * is the handler function. + * @param {Object} scope Optional. If the ename parameter was an event name, this + * is the scope (this reference) in which the handler function is executed. + */ + removeManagedListener : function(item, ename, fn, scope) { + var me = this, + options, + config, + managedListeners, + managedListener, + length, + i; + + if (Ext.isObject(ename)) { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!me.eventOptionsRe.test(ename)) { + me.removeManagedListener(item, ename, config.fn || config, config.scope || options.scope); + } + } + } + } + + managedListeners = me.managedListeners ? me.managedListeners.slice() : []; + length = managedListeners.length; + + for (i = 0; i < length; i++) { + managedListener = managedListeners[i]; + if (managedListener.item === item && managedListener.ename === ename && (!fn || managedListener.fn === fn) && (!scope || managedListener.scope === scope)) { + Ext.Array.remove(me.managedListeners, managedListener); + item.un(managedListener.ename, managedListener.fn, managedListener.scope); + } + } + }, + + /** + *

Fires the specified event with the passed parameters (minus the event name).

+ *

An event may be set to bubble up an Observable parent hierarchy (See {@link Ext.Component#getBubbleTarget}) + * by calling {@link #enableBubble}.

+ * @param {String} eventName The name of the event to fire. + * @param {Object...} args Variable number of parameters are passed to handlers. + * @return {Boolean} returns false if any of the handlers return false otherwise it returns true. + */ + fireEvent: function() { + var me = this, + args = Ext.Array.toArray(arguments), + ename = args[0].toLowerCase(), + ret = true, + event = me.events[ename], + queue = me.eventQueue, + parent; + + if (me.eventsSuspended === true) { + if (queue) { + queue.push(args); + } + } else if (event && Ext.isObject(event) && event.bubble) { + if (event.fire.apply(event, args.slice(1)) === false) { + return false; + } + parent = me.getBubbleTarget && me.getBubbleTarget(); + if (parent && parent.isObservable) { + if (!parent.events[ename] || !Ext.isObject(parent.events[ename]) || !parent.events[ename].bubble) { + parent.enableBubble(ename); + } + return parent.fireEvent.apply(parent, args); + } + } else if (event && Ext.isObject(event)) { + args.shift(); + ret = event.fire.apply(event, args); + } + return ret; + }, + + /** + * Appends an event handler to this object. + * @param {String} eventName The name of the event to listen for. May also be an object who's property names are event names. See + * @param {Function} handler The method the event invokes. + * @param {Object} scope (optional) The scope (this reference) in which the handler function is executed. + * If omitted, defaults to the object which fired the event. + * @param {Object} options (optional) An object containing handler configuration. + * properties. This may contain any of the following properties:
+ *

+ * Combining Options
+ * Using the options argument, it is possible to combine different types of listeners:
+ *
+ * A delayed, one-time listener. + *


+myPanel.on('hide', this.handleClick, this, {
+single: true,
+delay: 100
+});
+ *

+ * Attaching multiple handlers in 1 call
+ * The method also allows for a single argument to be passed which is a config object containing properties + * which specify multiple events. For example:


+myGridPanel.on({
+    cellClick: this.onCellClick,
+    mouseover: this.onMouseOver,
+    mouseout: this.onMouseOut,
+    scope: this // Important. Ensure "this" is correct during handler execution
+});
+
. + *

+ */ + addListener: function(ename, fn, scope, options) { + var me = this, + config, + event; + + if (Ext.isObject(ename)) { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!me.eventOptionsRe.test(ename)) { + me.addListener(ename, config.fn || config, config.scope || options.scope, config.fn ? config : options); + } + } + } + } + else { + ename = ename.toLowerCase(); + me.events[ename] = me.events[ename] || true; + event = me.events[ename] || true; + if (Ext.isBoolean(event)) { + me.events[ename] = event = new Ext.util.Event(me, ename); + } + event.addListener(fn, scope, Ext.isObject(options) ? options : {}); + } + }, + + /** + * Removes an event handler. + * @param {String} eventName The type of event the handler was associated with. + * @param {Function} handler The handler to remove. This must be a reference to the function passed into the {@link #addListener} call. + * @param {Object} scope (optional) The scope originally specified for the handler. + */ + removeListener: function(ename, fn, scope) { + var me = this, + config, + event, + options; + + if (Ext.isObject(ename)) { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!me.eventOptionsRe.test(ename)) { + me.removeListener(ename, config.fn || config, config.scope || options.scope); + } + } + } + } else { + ename = ename.toLowerCase(); + event = me.events[ename]; + if (event.isEvent) { + event.removeListener(fn, scope); + } + } + }, + + /** + * Removes all listeners for this object including the managed listeners + */ + clearListeners: function() { + var events = this.events, + event, + key; + + for (key in events) { + if (events.hasOwnProperty(key)) { + event = events[key]; + if (event.isEvent) { + event.clearListeners(); + } + } + } + + this.clearManagedListeners(); + }, + + // + purgeListeners : function() { + console.warn('Observable: purgeListeners has been deprecated. Please use clearListeners.'); + return this.clearListeners.apply(this, arguments); + }, + // + + /** + * Removes all managed listeners for this object. + */ + clearManagedListeners : function() { + var managedListeners = this.managedListeners || [], + i = 0, + len = managedListeners.length, + managedListener; + + for (; i < len; i++) { + managedListener = managedListeners[i]; + managedListener.item.un(managedListener.ename, managedListener.fn, managedListener.scope); + } + + this.managedListeners = []; + }, + + // + purgeManagedListeners : function() { + console.warn('Observable: purgeManagedListeners has been deprecated. Please use clearManagedListeners.'); + return this.clearManagedListeners.apply(this, arguments); + }, + // + + /** + * Adds the specified events to the list of events which this Observable may fire. + * @param {Object/String} o Either an object with event names as properties with a value of true + * or the first event name string if multiple event names are being passed as separate parameters. + * @param {String} [additional] Optional additional event names if multiple event names are being passed as separate parameters. + * Usage:


+this.addEvents('storeloaded', 'storecleared');
+
+ */ + addEvents: function(o) { + var me = this, + args, + len, + i; + + me.events = me.events || {}; + if (Ext.isString(o)) { + args = arguments; + i = args.length; + + while (i--) { + me.events[args[i]] = me.events[args[i]] || true; + } + } else { + Ext.applyIf(me.events, o); + } + }, + + /** + * Checks to see if this object has any listeners for a specified event + * @param {String} eventName The name of the event to check for + * @return {Boolean} True if the event is being listened for, else false + */ + hasListener: function(ename) { + var event = this.events[ename.toLowerCase()]; + return event && event.isEvent === true && event.listeners.length > 0; + }, + + /** + * Suspend the firing of all events. (see {@link #resumeEvents}) + * @param {Boolean} queueSuspended Pass as true to queue up suspended events to be fired + * after the {@link #resumeEvents} call instead of discarding all suspended events; + */ + suspendEvents: function(queueSuspended) { + this.eventsSuspended = true; + if (queueSuspended && !this.eventQueue) { + this.eventQueue = []; + } + }, + + /** + * Resume firing events. (see {@link #suspendEvents}) + * If events were suspended using the queueSuspended parameter, then all + * events fired during event suspension will be sent to any listeners now. + */ + resumeEvents: function() { + var me = this, + queued = me.eventQueue || []; + + me.eventsSuspended = false; + delete me.eventQueue; + + Ext.each(queued, + function(e) { + me.fireEvent.apply(me, e); + }); + }, + + /** + * Relays selected events from the specified Observable as if the events were fired by this. + * @param {Object} origin The Observable whose events this object is to relay. + * @param {Array} events Array of event names to relay. + */ + relayEvents : function(origin, events, prefix) { + prefix = prefix || ''; + var me = this, + len = events.length, + i = 0, + oldName, + newName; + + for (; i < len; i++) { + oldName = events[i].substr(prefix.length); + newName = prefix + oldName; + me.events[newName] = me.events[newName] || true; + origin.on(oldName, me.createRelayer(newName)); + } + }, + + /** + * @private + * Creates an event handling function which refires the event from this object as the passed event name. + * @param newName + * @returns {Function} + */ + createRelayer: function(newName){ + var me = this; + return function(){ + return me.fireEvent.apply(me, [newName].concat(Array.prototype.slice.call(arguments, 0, -1))); + }; + }, + + /** + *

Enables events fired by this Observable to bubble up an owner hierarchy by calling + * this.getBubbleTarget() if present. There is no implementation in the Observable base class.

+ *

This is commonly used by Ext.Components to bubble events to owner Containers. See {@link Ext.Component#getBubbleTarget}. The default + * implementation in Ext.Component returns the Component's immediate owner. But if a known target is required, this can be overridden to + * access the required target more quickly.

+ *

Example:


+Ext.override(Ext.form.field.Base, {
+//  Add functionality to Field's initComponent to enable the change event to bubble
+initComponent : Ext.Function.createSequence(Ext.form.field.Base.prototype.initComponent, function() {
+    this.enableBubble('change');
+}),
+
+//  We know that we want Field's events to bubble directly to the FormPanel.
+getBubbleTarget : function() {
+    if (!this.formPanel) {
+        this.formPanel = this.findParentByType('form');
+    }
+    return this.formPanel;
+}
+});
+
+var myForm = new Ext.formPanel({
+title: 'User Details',
+items: [{
+    ...
+}],
+listeners: {
+    change: function() {
+        // Title goes red if form has been modified.
+        myForm.header.setStyle('color', 'red');
+    }
+}
+});
+
+ * @param {String/Array} events The event name to bubble, or an Array of event names. + */ + enableBubble: function(events) { + var me = this; + if (!Ext.isEmpty(events)) { + events = Ext.isArray(events) ? events: Ext.Array.toArray(arguments); + Ext.each(events, + function(ename) { + ename = ename.toLowerCase(); + var ce = me.events[ename] || true; + if (Ext.isBoolean(ce)) { + ce = new Ext.util.Event(me, ename); + me.events[ename] = ce; + } + ce.bubble = true; + }); + } + } +}, function() { + /** + * Removes an event handler (shorthand for {@link #removeListener}.) + * @param {String} eventName The type of event the handler was associated with. + * @param {Function} handler The handler to remove. This must be a reference to the function passed into the {@link #addListener} call. + * @param {Object} scope (optional) The scope originally specified for the handler. + * @method un + */ + + /** + * Appends an event handler to this object (shorthand for {@link #addListener}.) + * @param {String} eventName The type of event to listen for + * @param {Function} handler The method the event invokes + * @param {Object} scope (optional) The scope (this reference) in which the handler function is executed. + * If omitted, defaults to the object which fired the event. + * @param {Object} options (optional) An object containing handler configuration. + * @method on + */ + + this.createAlias({ + on: 'addListener', + un: 'removeListener', + mon: 'addManagedListener', + mun: 'removeManagedListener' + }); + + //deprecated, will be removed in 5.0 + this.observeClass = this.observe; + + Ext.apply(Ext.util.Observable.prototype, function(){ + // this is considered experimental (along with beforeMethod, afterMethod, removeMethodListener?) + // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call + // private + function getMethodEvent(method){ + var e = (this.methodEvents = this.methodEvents || {})[method], + returnValue, + v, + cancel, + obj = this; + + if (!e) { + this.methodEvents[method] = e = {}; + e.originalFn = this[method]; + e.methodName = method; + e.before = []; + e.after = []; + + var makeCall = function(fn, scope, args){ + if((v = fn.apply(scope || obj, args)) !== undefined){ + if (typeof v == 'object') { + if(v.returnValue !== undefined){ + returnValue = v.returnValue; + }else{ + returnValue = v; + } + cancel = !!v.cancel; + } + else + if (v === false) { + cancel = true; + } + else { + returnValue = v; + } + } + }; + + this[method] = function(){ + var args = Array.prototype.slice.call(arguments, 0), + b, i, len; + returnValue = v = undefined; + cancel = false; + + for(i = 0, len = e.before.length; i < len; i++){ + b = e.before[i]; + makeCall(b.fn, b.scope, args); + if (cancel) { + return returnValue; + } + } + + if((v = e.originalFn.apply(obj, args)) !== undefined){ + returnValue = v; + } + + for(i = 0, len = e.after.length; i < len; i++){ + b = e.after[i]; + makeCall(b.fn, b.scope, args); + if (cancel) { + return returnValue; + } + } + return returnValue; + }; + } + return e; + } + + return { + // these are considered experimental + // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call + // adds an 'interceptor' called before the original method + beforeMethod : function(method, fn, scope){ + getMethodEvent.call(this, method).before.push({ + fn: fn, + scope: scope + }); + }, + + // adds a 'sequence' called after the original method + afterMethod : function(method, fn, scope){ + getMethodEvent.call(this, method).after.push({ + fn: fn, + scope: scope + }); + }, + + removeMethodListener: function(method, fn, scope){ + var e = this.getMethodEvent(method), + i, len; + for(i = 0, len = e.before.length; i < len; i++){ + if(e.before[i].fn == fn && e.before[i].scope == scope){ + e.before.splice(i, 1); + return; + } + } + for(i = 0, len = e.after.length; i < len; i++){ + if(e.after[i].fn == fn && e.after[i].scope == scope){ + e.after.splice(i, 1); + return; + } + } + }, + + toggleEventLogging: function(toggle) { + Ext.util.Observable[toggle ? 'capture' : 'releaseCapture'](this, function(en) { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.log(en, arguments); + } + }); + } + }; + }()); +}); + +/** + * @class Ext.util.Animate + * This animation class is a mixin. + * + * Ext.util.Animate provides an API for the creation of animated transitions of properties and styles. + * This class is used as a mixin and currently applied to {@link Ext.core.Element}, {@link Ext.CompositeElement}, + * {@link Ext.draw.Sprite}, {@link Ext.draw.CompositeSprite}, and {@link Ext.Component}. Note that Components + * have a limited subset of what attributes can be animated such as top, left, x, y, height, width, and + * opacity (color, paddings, and margins can not be animated). + * + * ## Animation Basics + * + * All animations require three things - `easing`, `duration`, and `to` (the final end value for each property) + * you wish to animate. Easing and duration are defaulted values specified below. + * Easing describes how the intermediate values used during a transition will be calculated. + * {@link Ext.fx.Anim#easing Easing} allows for a transition to change speed over its duration. + * You may use the defaults for easing and duration, but you must always set a + * {@link Ext.fx.Anim#to to} property which is the end value for all animations. + * + * Popular element 'to' configurations are: + * + * - opacity + * - x + * - y + * - color + * - height + * - width + * + * Popular sprite 'to' configurations are: + * + * - translation + * - path + * - scale + * - stroke + * - rotation + * + * The default duration for animations is 250 (which is a 1/4 of a second). Duration is denoted in + * milliseconds. Therefore 1 second is 1000, 1 minute would be 60000, and so on. The default easing curve + * used for all animations is 'ease'. Popular easing functions are included and can be found in {@link Ext.fx.Anim#easing Easing}. + * + * For example, a simple animation to fade out an element with a default easing and duration: + * + * var p1 = Ext.get('myElementId'); + * + * p1.animate({ + * to: { + * opacity: 0 + * } + * }); + * + * To make this animation fade out in a tenth of a second: + * + * var p1 = Ext.get('myElementId'); + * + * p1.animate({ + * duration: 100, + * to: { + * opacity: 0 + * } + * }); + * + * ## Animation Queues + * + * By default all animations are added to a queue which allows for animation via a chain-style API. + * For example, the following code will queue 4 animations which occur sequentially (one right after the other): + * + * p1.animate({ + * to: { + * x: 500 + * } + * }).animate({ + * to: { + * y: 150 + * } + * }).animate({ + * to: { + * backgroundColor: '#f00' //red + * } + * }).animate({ + * to: { + * opacity: 0 + * } + * }); + * + * You can change this behavior by calling the {@link Ext.util.Animate#syncFx syncFx} method and all + * subsequent animations for the specified target will be run concurrently (at the same time). + * + * p1.syncFx(); //this will make all animations run at the same time + * + * p1.animate({ + * to: { + * x: 500 + * } + * }).animate({ + * to: { + * y: 150 + * } + * }).animate({ + * to: { + * backgroundColor: '#f00' //red + * } + * }).animate({ + * to: { + * opacity: 0 + * } + * }); + * + * This works the same as: + * + * p1.animate({ + * to: { + * x: 500, + * y: 150, + * backgroundColor: '#f00' //red + * opacity: 0 + * } + * }); + * + * The {@link Ext.util.Animate#stopAnimation stopAnimation} method can be used to stop any + * currently running animations and clear any queued animations. + * + * ## Animation Keyframes + * + * You can also set up complex animations with {@link Ext.fx.Anim#keyframe keyframe} which follows the + * CSS3 Animation configuration pattern. Note rotation, translation, and scaling can only be done for sprites. + * The previous example can be written with the following syntax: + * + * p1.animate({ + * duration: 1000, //one second total + * keyframes: { + * 25: { //from 0 to 250ms (25%) + * x: 0 + * }, + * 50: { //from 250ms to 500ms (50%) + * y: 0 + * }, + * 75: { //from 500ms to 750ms (75%) + * backgroundColor: '#f00' //red + * }, + * 100: { //from 750ms to 1sec + * opacity: 0 + * } + * } + * }); + * + * ## Animation Events + * + * Each animation you create has events for {@link Ext.fx.Anim#beforeanimation beforeanimation}, + * {@link Ext.fx.Anim#afteranimate afteranimate}, and {@link Ext.fx.Anim#lastframe lastframe}. + * Keyframed animations adds an additional {@link Ext.fx.Animator#keyframe keyframe} event which + * fires for each keyframe in your animation. + * + * All animations support the {@link Ext.util.Observable#listeners listeners} configuration to attact functions to these events. + * + * startAnimate: function() { + * var p1 = Ext.get('myElementId'); + * p1.animate({ + * duration: 100, + * to: { + * opacity: 0 + * }, + * listeners: { + * beforeanimate: function() { + * // Execute my custom method before the animation + * this.myBeforeAnimateFn(); + * }, + * afteranimate: function() { + * // Execute my custom method after the animation + * this.myAfterAnimateFn(); + * }, + * scope: this + * }); + * }, + * myBeforeAnimateFn: function() { + * // My custom logic + * }, + * myAfterAnimateFn: function() { + * // My custom logic + * } + * + * Due to the fact that animations run asynchronously, you can determine if an animation is currently + * running on any target by using the {@link Ext.util.Animate#getActiveAnimation getActiveAnimation} + * method. This method will return false if there are no active animations or return the currently + * running {@link Ext.fx.Anim} instance. + * + * In this example, we're going to wait for the current animation to finish, then stop any other + * queued animations before we fade our element's opacity to 0: + * + * var curAnim = p1.getActiveAnimation(); + * if (curAnim) { + * curAnim.on('afteranimate', function() { + * p1.stopAnimation(); + * p1.animate({ + * to: { + * opacity: 0 + * } + * }); + * }); + * } + * + * @docauthor Jamie Avins + */ +Ext.define('Ext.util.Animate', { + + uses: ['Ext.fx.Manager', 'Ext.fx.Anim'], + + /** + *

Perform custom animation on this object.

+ *

This method is applicable to both the the {@link Ext.Component Component} class and the {@link Ext.core.Element Element} class. + * It performs animated transitions of certain properties of this object over a specified timeline.

+ *

The sole parameter is an object which specifies start property values, end property values, and properties which + * describe the timeline. Of the properties listed below, only to is mandatory.

+ *

Properties include

+ *

Animating an {@link Ext.core.Element Element}

+ * When animating an Element, the following properties may be specified in from, to, and keyframe objects: + *

Be aware than animating an Element which is being used by an Ext Component without in some way informing the Component about the changed element state + * will result in incorrect Component behaviour. This is because the Component will be using the old state of the element. To avoid this problem, it is now possible to + * directly animate certain properties of Components.

+ *

Animating a {@link Ext.Component Component}

+ * When animating an Element, the following properties may be specified in from, to, and keyframe objects: + *

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

+ *

+myWindow = Ext.create('Ext.window.Window', {
+    title: 'Test Component animation',
+    width: 500,
+    height: 300,
+    layout: {
+        type: 'hbox',
+        align: 'stretch'
+    },
+    items: [{
+        title: 'Left: 33%',
+        margins: '5 0 5 5',
+        flex: 1
+    }, {
+        title: 'Left: 66%',
+        margins: '5 5 5 5',
+        flex: 2
+    }]
+});
+myWindow.show();
+myWindow.header.el.on('click', function() {
+    myWindow.animate({
+        to: {
+            width: (myWindow.getWidth() == 500) ? 700 : 500,
+            height: (myWindow.getHeight() == 300) ? 400 : 300,
+        }
+    });
+});
+
+ *

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

+ * @param {Object} config An object containing properties which describe the animation's start and end states, and the timeline of the animation. + * @return {Object} this + */ + animate: function(animObj) { + var me = this; + if (Ext.fx.Manager.hasFxBlock(me.id)) { + return me; + } + Ext.fx.Manager.queueFx(Ext.create('Ext.fx.Anim', me.anim(animObj))); + return this; + }, + + // @private - process the passed fx configuration. + anim: function(config) { + if (!Ext.isObject(config)) { + return (config) ? {} : false; + } + + var me = this; + + if (config.stopAnimation) { + me.stopAnimation(); + } + + Ext.applyIf(config, Ext.fx.Manager.getFxDefaults(me.id)); + + return Ext.apply({ + target: me, + paused: true + }, config); + }, + + /** + * Stops any running effects and clears this object's internal effects queue if it contains + * any additional effects that haven't started yet. + * @return {Ext.core.Element} The Element + */ + stopFx: Ext.Function.alias(Ext.util.Animate, 'stopAnimation'), + + /** + * @deprecated 4.0 Replaced by {@link #stopAnimation} + * Stops any running effects and clears this object's internal effects queue if it contains + * any additional effects that haven't started yet. + * @return {Ext.core.Element} The Element + */ + stopAnimation: function() { + Ext.fx.Manager.stopAnimation(this.id); + }, + + /** + * Ensures that all effects queued after syncFx is called on this object are + * run concurrently. This is the opposite of {@link #sequenceFx}. + * @return {Ext.core.Element} The Element + */ + syncFx: function() { + Ext.fx.Manager.setFxDefaults(this.id, { + concurrent: true + }); + }, + + /** + * Ensures that all effects queued after sequenceFx is called on this object are + * run in sequence. This is the opposite of {@link #syncFx}. + * @return {Ext.core.Element} The Element + */ + sequenceFx: function() { + Ext.fx.Manager.setFxDefaults(this.id, { + concurrent: false + }); + }, + + /** + * @deprecated 4.0 Replaced by {@link #getActiveAnimation} + * Returns thq current animation if this object has any effects actively running or queued, else returns false. + * @return {Mixed} anim if element has active effects, else false + */ + hasActiveFx: Ext.Function.alias(Ext.util.Animate, 'getActiveAnimation'), + + /** + * Returns thq current animation if this object has any effects actively running or queued, else returns false. + * @return {Mixed} anim if element has active effects, else false + */ + getActiveAnimation: function() { + return Ext.fx.Manager.getActiveAnimation(this.id); + } +}); + +// Apply Animate mixin manually until Element is defined in the proper 4.x way +Ext.applyIf(Ext.core.Element.prototype, Ext.util.Animate.prototype); +/** + * @class Ext.state.Provider + *

Abstract base class for state provider implementations. The provider is responsible + * for setting values and extracting values to/from the underlying storage source. The + * storage source can vary and the details should be implemented in a subclass. For example + * a provider could use a server side database or the browser localstorage where supported.

+ * + *

This class provides methods for encoding and decoding typed variables including + * dates and defines the Provider interface. By default these methods put the value and the + * type information into a delimited string that can be stored. These should be overridden in + * a subclass if you want to change the format of the encoded value and subsequent decoding.

+ */ +Ext.define('Ext.state.Provider', { + mixins: { + observable: 'Ext.util.Observable' + }, + + /** + * @cfg {String} prefix A string to prefix to items stored in the underlying state store. + * Defaults to 'ext-' + */ + prefix: 'ext-', + + constructor : function(config){ + config = config || {}; + var me = this; + Ext.apply(me, config); + /** + * @event statechange + * Fires when a state change occurs. + * @param {Provider} this This state provider + * @param {String} key The state key which was changed + * @param {String} value The encoded value for the state + */ + me.addEvents("statechange"); + me.state = {}; + me.mixins.observable.constructor.call(me); + }, + + /** + * Returns the current value for a key + * @param {String} name The key name + * @param {Mixed} defaultValue A default value to return if the key's value is not found + * @return {Mixed} The state data + */ + get : function(name, defaultValue){ + return typeof this.state[name] == "undefined" ? + defaultValue : this.state[name]; + }, + + /** + * Clears a value from the state + * @param {String} name The key name + */ + clear : function(name){ + var me = this; + delete me.state[name]; + me.fireEvent("statechange", me, name, null); + }, + + /** + * Sets the value for a key + * @param {String} name The key name + * @param {Mixed} value The value to set + */ + set : function(name, value){ + var me = this; + me.state[name] = value; + me.fireEvent("statechange", me, name, value); + }, + + /** + * Decodes a string previously encoded with {@link #encodeValue}. + * @param {String} value The value to decode + * @return {Mixed} The decoded value + */ + decodeValue : function(value){ + + // a -> Array + // n -> Number + // d -> Date + // b -> Boolean + // s -> String + // o -> Object + // -> Empty (null) + + var me = this, + re = /^(a|n|d|b|s|o|e)\:(.*)$/, + matches = re.exec(unescape(value)), + all, + type, + value, + keyValue; + + if(!matches || !matches[1]){ + return; // non state + } + + type = matches[1]; + value = matches[2]; + switch (type) { + case 'e': + return null; + case 'n': + return parseFloat(value); + case 'd': + return new Date(Date.parse(value)); + case 'b': + return (value == '1'); + case 'a': + all = []; + if(value != ''){ + Ext.each(value.split('^'), function(val){ + all.push(me.decodeValue(val)); + }, me); + } + return all; + case 'o': + all = {}; + if(value != ''){ + Ext.each(value.split('^'), function(val){ + keyValue = val.split('='); + all[keyValue[0]] = me.decodeValue(keyValue[1]); + }, me); + } + return all; + default: + return value; + } + }, + + /** + * Encodes a value including type information. Decode with {@link #decodeValue}. + * @param {Mixed} value The value to encode + * @return {String} The encoded value + */ + encodeValue : function(value){ + var flat = '', + i = 0, + enc, + len, + key; + + if (value == null) { + return 'e:1'; + } else if(typeof value == 'number') { + enc = 'n:' + value; + } else if(typeof value == 'boolean') { + enc = 'b:' + (value ? '1' : '0'); + } else if(Ext.isDate(value)) { + enc = 'd:' + value.toGMTString(); + } else if(Ext.isArray(value)) { + for (len = value.length; i < len; i++) { + flat += this.encodeValue(value[i]); + if (i != len - 1) { + flat += '^'; + } + } + enc = 'a:' + flat; + } else if (typeof value == 'object') { + for (key in value) { + if (typeof value[key] != 'function' && value[key] !== undefined) { + flat += key + '=' + this.encodeValue(value[key]) + '^'; + } + } + enc = 'o:' + flat.substring(0, flat.length-1); + } else { + enc = 's:' + value; + } + return escape(enc); + } +}); +/** + * @class Ext.util.HashMap + *

+ * Represents a collection of a set of key and value pairs. Each key in the HashMap + * must be unique, the same key cannot exist twice. Access to items is provided via + * the key only. Sample usage: + *


+var map = new Ext.util.HashMap();
+map.add('key1', 1);
+map.add('key2', 2);
+map.add('key3', 3);
+
+map.each(function(key, value, length){
+    console.log(key, value, length);
+});
+ * 
+ *

+ * + *

The HashMap is an unordered class, + * there is no guarantee when iterating over the items that they will be in any particular + * order. If this is required, then use a {@link Ext.util.MixedCollection}. + *

+ * @constructor + * @param {Object} config The configuration options + */ +Ext.define('Ext.util.HashMap', { + + /** + * @cfg {Function} keyFn A function that is used to retrieve a default key for a passed object. + * A default is provided that returns the id property on the object. This function is only used + * if the add method is called with a single argument. + */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + constructor: function(config) { + var me = this; + + me.addEvents( + /** + * @event add + * Fires when a new item is added to the hash + * @param {Ext.util.HashMap} this. + * @param {String} key The key of the added item. + * @param {Object} value The value of the added item. + */ + 'add', + /** + * @event clear + * Fires when the hash is cleared. + * @param {Ext.util.HashMap} this. + */ + 'clear', + /** + * @event remove + * Fires when an item is removed from the hash. + * @param {Ext.util.HashMap} this. + * @param {String} key The key of the removed item. + * @param {Object} value The value of the removed item. + */ + 'remove', + /** + * @event replace + * Fires when an item is replaced in the hash. + * @param {Ext.util.HashMap} this. + * @param {String} key The key of the replaced item. + * @param {Object} value The new value for the item. + * @param {Object} old The old value for the item. + */ + 'replace' + ); + + me.mixins.observable.constructor.call(me, config); + me.clear(true); + }, + + /** + * Gets the number of items in the hash. + * @return {Number} The number of items in the hash. + */ + getCount: function() { + return this.length; + }, + + /** + * Implementation for being able to extract the key from an object if only + * a single argument is passed. + * @private + * @param {String} key The key + * @param {Object} value The value + * @return {Array} [key, value] + */ + getData: function(key, value) { + // if we have no value, it means we need to get the key from the object + if (value === undefined) { + value = key; + key = this.getKey(value); + } + + return [key, value]; + }, + + /** + * Extracts the key from an object. This is a default implementation, it may be overridden + * @private + * @param {Object} o The object to get the key from + * @return {String} The key to use. + */ + getKey: function(o) { + return o.id; + }, + + /** + * Adds an item to the collection. Fires the {@link #add} event when complete. + * @param {String} key

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

+ *

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

+ * @param {Object} o The item to add. + * @return {Object} The item added. + */ + add: function(key, value) { + var me = this, + data; + + if (arguments.length === 1) { + value = key; + key = me.getKey(value); + } + + if (me.containsKey(key)) { + me.replace(key, value); + } + + data = me.getData(key, value); + key = data[0]; + value = data[1]; + me.map[key] = value; + ++me.length; + me.fireEvent('add', me, key, value); + return value; + }, + + /** + * Replaces an item in the hash. If the key doesn't exist, the + * {@link #add} method will be used. + * @param {String} key The key of the item. + * @param {Object} value The new value for the item. + * @return {Object} The new value of the item. + */ + replace: function(key, value) { + var me = this, + map = me.map, + old; + + if (!me.containsKey(key)) { + me.add(key, value); + } + old = map[key]; + map[key] = value; + me.fireEvent('replace', me, key, value, old); + return value; + }, + + /** + * Remove an item from the hash. + * @param {Object} o The value of the item to remove. + * @return {Boolean} True if the item was successfully removed. + */ + remove: function(o) { + var key = this.findKey(o); + if (key !== undefined) { + return this.removeAtKey(key); + } + return false; + }, + + /** + * Remove an item from the hash. + * @param {String} key The key to remove. + * @return {Boolean} True if the item was successfully removed. + */ + removeAtKey: function(key) { + var me = this, + value; + + if (me.containsKey(key)) { + value = me.map[key]; + delete me.map[key]; + --me.length; + me.fireEvent('remove', me, key, value); + return true; + } + return false; + }, + + /** + * Retrieves an item with a particular key. + * @param {String} key The key to lookup. + * @return {Object} The value at that key. If it doesn't exist, undefined is returned. + */ + get: function(key) { + return this.map[key]; + }, + + /** + * Removes all items from the hash. + * @return {Ext.util.HashMap} this + */ + clear: function(/* private */ initial) { + var me = this; + me.map = {}; + me.length = 0; + if (initial !== true) { + me.fireEvent('clear', me); + } + return me; + }, + + /** + * Checks whether a key exists in the hash. + * @param {String} key The key to check for. + * @return {Boolean} True if they key exists in the hash. + */ + containsKey: function(key) { + return this.map[key] !== undefined; + }, + + /** + * Checks whether a value exists in the hash. + * @param {Object} value The value to check for. + * @return {Boolean} True if the value exists in the dictionary. + */ + contains: function(value) { + return this.containsKey(this.findKey(value)); + }, + + /** + * Return all of the keys in the hash. + * @return {Array} An array of keys. + */ + getKeys: function() { + return this.getArray(true); + }, + + /** + * Return all of the values in the hash. + * @return {Array} An array of values. + */ + getValues: function() { + return this.getArray(false); + }, + + /** + * Gets either the keys/values in an array from the hash. + * @private + * @param {Boolean} isKey True to extract the keys, otherwise, the value + * @return {Array} An array of either keys/values from the hash. + */ + getArray: function(isKey) { + var arr = [], + key, + map = this.map; + for (key in map) { + if (map.hasOwnProperty(key)) { + arr.push(isKey ? key: map[key]); + } + } + return arr; + }, + + /** + * Executes the specified function once for each item in the hash. + * Returning false from the function will cease iteration. + * + * The paramaters passed to the function are: + *
+ * @param {Function} fn The function to execute. + * @param {Object} scope The scope to execute in. Defaults to this. + * @return {Ext.util.HashMap} this + */ + each: function(fn, scope) { + // copy items so they may be removed during iteration. + var items = Ext.apply({}, this.map), + key, + length = this.length; + + scope = scope || this; + for (key in items) { + if (items.hasOwnProperty(key)) { + if (fn.call(scope, key, items[key], length) === false) { + break; + } + } + } + return this; + }, + + /** + * Performs a shallow copy on this hash. + * @return {Ext.util.HashMap} The new hash object. + */ + clone: function() { + var hash = new this.self(), + map = this.map, + key; + + hash.suspendEvents(); + for (key in map) { + if (map.hasOwnProperty(key)) { + hash.add(key, map[key]); + } + } + hash.resumeEvents(); + return hash; + }, + + /** + * @private + * Find the key for a value. + * @param {Object} value The value to find. + * @return {Object} The value of the item. Returns undefined if not found. + */ + findKey: function(value) { + var key, + map = this.map; + + for (key in map) { + if (map.hasOwnProperty(key) && map[key] === value) { + return key; + } + } + return undefined; + } +}); + +/** + * @class Ext.Template + *

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: + *
+ * @param {Mixed} config + */ + +Ext.define('Ext.Template', { + + /* Begin Definitions */ + + requires: ['Ext.core.DomHelper', 'Ext.util.Format'], + + statics: { + /** + * 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 A configuration object + * @return {Ext.Template} The created template + * @static + */ + from: function(el, config) { + el = Ext.getDom(el); + return new this(el.value || el.innerHTML, config || ''); + } + }, + + /* End Definitions */ + + constructor: function(html) { + var me = this, + args = arguments, + buffer = [], + i = 0, + length = args.length, + value; + + me.initialConfig = {}; + + 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); + } + } + html = buffer.join(''); + } else { + if (Ext.isArray(html)) { + buffer.push(html.join('')); + } else { + buffer.push(html); + } + } + + // @private + me.html = buffer.join(''); + + if (me.compiled) { + me.compile(); + } + }, + isTemplate: true, + /** + * @cfg {Boolean} disableFormats true to disable format functions in the template. If the template doesn't contain format functions, setting + * disableFormats to true will reduce apply time (defaults to false) + */ + 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 (i.e. {0}) or an object (i.e. {foo: 'bar'}) + * @return {String} The HTML fragment + * @hide repeat doc + */ + applyTemplate: function(values) { + var me = this, + useFormat = me.disableFormats !== true, + fm = Ext.util.Format, + tpl = me; + + if (me.compiled) { + return me.compiled(values); + } + 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] : ""; + } + } + return me.html.replace(me.re, fn); + }, + + /** + * Sets the HTML used as the template and optionally compiles it. + * @param {String} html + * @param {Boolean} compile (optional) True to compile the template (defaults to undefined) + * @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 + * @hide repeat doc + */ + 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 + "'].join('');};"; + eval(body); + return me; + }, + + /** + * Applies the supplied values to the template and inserts the new node(s) as the first child of el. + * @param {Mixed} el The context element + * @param {Object/Array} values The template values. Can be an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}) + * @param {Boolean} returnElement (optional) true to return a Ext.core.Element (defaults to undefined) + * @return {HTMLElement/Ext.core.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 {Mixed} el The context element + * @param {Object/Array} values The template values. Can be an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}) + * @param {Boolean} returnElement (optional) true to return a Ext.core.Element (defaults to undefined) + * @return {HTMLElement/Ext.core.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 {Mixed} el The context element + * @param {Object/Array} values The template values. Can be an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}) + * @param {Boolean} returnElement (optional) true to return a Ext.core.Element (defaults to undefined) + * @return {HTMLElement/Ext.core.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 {@link #Template see the constructor}.

+ * @param {Mixed} el The context element + * @param {Object/Array} values + * The template values. Can be an array if the params are numeric (i.e. {0}) + * or an object (i.e. {foo: 'bar'}). + * @param {Boolean} returnElement (optional) true to return an Ext.core.Element (defaults to undefined) + * @return {HTMLElement/Ext.core.Element} The new node or Element + */ + append: function(el, values, returnElement) { + return this.doInsert('beforeEnd', el, values, returnElement); + }, + + doInsert: function(where, el, values, returnEl) { + el = Ext.getDom(el); + var newNode = Ext.core.DomHelper.insertHtml(where, el, this.applyTemplate(values)); + return returnEl ? Ext.get(newNode, true) : newNode; + }, + + /** + * Applies the supplied values to the template and overwrites the content of el with the new node(s). + * @param {Mixed} el The context element + * @param {Object/Array} values The template values. Can be an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}) + * @param {Boolean} returnElement (optional) true to return a Ext.core.Element (defaults to undefined) + * @return {HTMLElement/Ext.core.Element} The new node or Element + */ + overwrite: function(el, values, returnElement) { + el = Ext.getDom(el); + el.innerHTML = this.applyTemplate(values); + return returnElement ? Ext.get(el.firstChild, true) : el.firstChild; + } +}, function() { + + /** + * Alias for {@link #applyTemplate} + * Returns an HTML fragment of this template with the specified values applied. + * @param {Object/Array} values + * The template values. Can be an array if the params are numeric (i.e. {0}) + * or an object (i.e. {foo: 'bar'}). + * @return {String} The HTML fragment + * @member Ext.Template + * @method apply + */ + this.createAlias('apply', 'applyTemplate'); +}); + +/** + * @class Ext.ComponentQuery + * @extends Object + * + * Provides searching of Components within Ext.ComponentManager (globally) or a specific + * Ext.container.Container on the document with a similar syntax to a CSS selector. + * + * Components can be retrieved by using their {@link Ext.Component xtype} with an optional . prefix + + * + * An itemId or id must be prefixed with a # + + * + * + * Attributes must be wrapped in brackets + + * + * 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 gridpanels and listviews
+    var gridsAndLists = Ext.ComponentQuery.query('gridpanel, listview');
+
+ +For easy access to queries based from a particular Container see the {@link Ext.container.Container#query}, +{@link Ext.container.Container#down} and {@link Ext.container.Container#child} methods. Also see +{@link Ext.Component#up}. + * @singleton + */ +Ext.define('Ext.ComponentQuery', { + singleton: true, + uses: ['Ext.ComponentManager'] +}, function() { + + var cq = this, + + // A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied + // as a member on each item in the passed array. + filterFnPattern = [ + 'var r = [],', + 'i = 0,', + 'it = items,', + 'l = it.length,', + 'c;', + 'for (; i < l; i++) {', + 'c = it[i];', + 'if (c.{0}) {', + 'r.push(c);', + '}', + '}', + 'return r;' + ].join(''), + + filterItems = function(items, operation) { + // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...] + // The operation's method loops over each item in the candidate array and + // returns an array of items which match its criteria + return operation.method.apply(this, [ items ].concat(operation.args)); + }, + + getItems = function(items, mode) { + var result = [], + i = 0, + length = items.length, + candidate, + deep = mode !== '>'; + + for (; i < length; i++) { + candidate = items[i]; + if (candidate.getRefItems) { + result = result.concat(candidate.getRefItems(deep)); + } + } + return result; + }, + + getAncestors = function(items) { + var result = [], + i = 0, + length = items.length, + candidate; + for (; i < length; i++) { + candidate = items[i]; + while (!!(candidate = (candidate.ownerCt || candidate.floatParent))) { + result.push(candidate); + } + } + return result; + }, + + // Filters the passed candidate array and returns only items which match the passed xtype + filterByXType = function(items, xtype, shallow) { + if (xtype === '*') { + return items.slice(); + } + else { + var result = [], + i = 0, + length = items.length, + candidate; + for (; i < length; i++) { + candidate = items[i]; + if (candidate.isXType(xtype, shallow)) { + result.push(candidate); + } + } + return result; + } + }, + + // Filters the passed candidate array and returns only items which have the passed className + filterByClassName = function(items, className) { + var EA = Ext.Array, + result = [], + i = 0, + length = items.length, + candidate; + for (; i < length; i++) { + candidate = items[i]; + if (candidate.el ? candidate.el.hasCls(className) : EA.contains(candidate.initCls(), className)) { + result.push(candidate); + } + } + return result; + }, + + // Filters the passed candidate array and returns only items which have the specified property match + filterByAttribute = function(items, property, operator, value) { + var result = [], + i = 0, + length = items.length, + candidate; + for (; i < length; i++) { + candidate = items[i]; + if (!value ? !!candidate[property] : (String(candidate[property]) === value)) { + result.push(candidate); + } + } + return result; + }, + + // Filters the passed candidate array and returns only items which have the specified itemId or id + filterById = function(items, id) { + var result = [], + i = 0, + length = items.length, + candidate; + for (; i < length; i++) { + candidate = items[i]; + if (candidate.getItemId() === id) { + result.push(candidate); + } + } + return result; + }, + + // Filters the passed candidate array and returns only items which the named pseudo class matcher filters in + filterByPseudo = function(items, name, value) { + return cq.pseudos[name](items, value); + }, + + // Determines leading mode + // > for direct child, and ^ to switch to ownerCt axis + modeRe = /^(\s?([>\^])\s?|\s|$)/, + + // Matches a token with possibly (true|false) appended for the "shallow" parameter + tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/, + + matchers = [{ + // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter + re: /^\.([\w\-]+)(?:\((true|false)\))?/, + method: filterByXType + },{ + // checks for [attribute=value] + re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/, + method: filterByAttribute + }, { + // checks for #cmpItemId + re: /^#([\w\-]+)/, + method: filterById + }, { + // checks for :() + re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/, + method: filterByPseudo + }, { + // checks for {} + re: /^(?:\{([^\}]+)\})/, + method: filterFnPattern + }]; + + /** + * @class Ext.ComponentQuery.Query + * @extends Object + * @private + */ + 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 selector The selector string to filter returned Components + * @param root

The Container within which to perform the query. If omitted, all Components + * within the document are included in the search.

+ *

This parameter may also be an array of Components to filter according to the selector.

+ * @returns {Array} The matched Components. + * @member Ext.ComponentQuery + * @method query + */ + query: function(selector, root) { + var selectors = selector.split(','), + length = selectors.length, + i = 0, + results = [], + noDupResults = [], + dupMatcher = {}, + query, resultsLn, cmp; + + for (; i < length; i++) { + selector = Ext.String.trim(selectors[i]); + query = this.cache[selector]; + if (!query) { + this.cache[selector] = query = this.parse(selector); + } + results = results.concat(query.execute(root)); + } + + // multiple selectors, potential to find duplicates + // lets filter them out. + if (length > 1) { + resultsLn = results.length; + for (i = 0; i < resultsLn; i++) { + cmp = results[i]; + if (!dupMatcher[cmp.id]) { + noDupResults.push(cmp); + dupMatcher[cmp.id] = true; + } + } + results = noDupResults; + } + return results; + }, + + /** + * Tests whether the passed Component matches the selector string. + * @param component The Component to test + * @param selector The selector string to test against. + * @return {Boolean} True if the Component matches the selector. + * @member Ext.ComponentQuery + * @method query + */ + is: function(component, selector) { + if (!selector) { + return true; + } + var query = this.cache[selector]; + if (!query) { + this.cache[selector] = query = this.parse(selector); + } + return query.is(component); + }, + + parse: function(selector) { + var operations = [], + length = matchers.length, + lastSelector, + tokenMatch, + matchedChar, + modeMatch, + selectorMatch, + i, matcher, method; + + // We are going to parse the beginning of the selector over and + // over again, slicing off the selector any portions we converted into an + // operation, until it is an empty string. + while (selector && lastSelector !== selector) { + lastSelector = selector; + + // First we check if we are dealing with a token like #, * or an xtype + tokenMatch = selector.match(tokenRe); + + if (tokenMatch) { + matchedChar = tokenMatch[1]; + + // If the token is prefixed with a # we push a filterById operation to our stack + if (matchedChar === '#') { + operations.push({ + method: filterById, + args: [Ext.String.trim(tokenMatch[2])] + }); + } + // If the token is prefixed with a . we push a filterByClassName operation to our stack + // FIXME: Not enabled yet. just needs \. adding to the tokenRe prefix + else if (matchedChar === '.') { + operations.push({ + method: filterByClassName, + args: [Ext.String.trim(tokenMatch[2])] + }); + } + // If the token is a * or an xtype string, we push a filterByXType + // operation to the stack. + else { + operations.push({ + method: filterByXType, + args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])] + }); + } + + // Now we slice of the part we just converted into an operation + selector = selector.replace(tokenMatch[0], ''); + } + + // If the next part of the query is not a space or > or ^, it means we + // are going to check for more things that our current selection + // has to comply to. + while (!(modeMatch = selector.match(modeRe))) { + // Lets loop over each type of matcher and execute it + // on our current selector. + for (i = 0; selector && i < length; i++) { + matcher = matchers[i]; + selectorMatch = selector.match(matcher.re); + method = matcher.method; + + // If we have a match, add an operation with the method + // associated with this matcher, and pass the regular + // expression matches are arguments to the operation. + if (selectorMatch) { + operations.push({ + method: Ext.isString(matcher.method) + // Turn a string method into a function by formatting the string with our selector matche expression + // A new method is created for different match expressions, eg {id=='textfield-1024'} + // Every expression may be different in different selectors. + ? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1)))) + : matcher.method, + args: selectorMatch.slice(1) + }); + selector = selector.replace(selectorMatch[0], ''); + break; // Break on match + } + // + // Exhausted all matches: It's an error + if (i === (length - 1)) { + Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"'); + } + // + } + } + + // Now we are going to check for a mode change. This means a space + // or a > to determine if we are going to select all the children + // of the currently matched items, or a ^ if we are going to use the + // ownerCt axis as the candidate source. + if (modeMatch[1]) { // Assignment, and test for truthiness! + operations.push({ + mode: modeMatch[2]||modeMatch[1] + }); + selector = selector.replace(modeMatch[0], ''); + } + } + + // Now that we have all our operations in an array, we are going + // to create a new Query using these operations. + return new cq.Query({ + operations: operations + }); + } + }); +}); +/** + * @class Ext.util.Filter + * @extends Object + *

Represents a filter that can be applied to a {@link Ext.util.MixedCollection MixedCollection}. Can either simply + * filter on a property/value pair or pass in a filter function with custom logic. Filters are always used in the context + * of MixedCollections, though {@link Ext.data.Store Store}s frequently create them when filtering and searching on their + * records. Example usage:

+

+//set up a fictional MixedCollection containing a few people to filter on
+var allNames = new Ext.util.MixedCollection();
+allNames.addAll([
+    {id: 1, name: 'Ed',    age: 25},
+    {id: 2, name: 'Jamie', age: 37},
+    {id: 3, name: 'Abe',   age: 32},
+    {id: 4, name: 'Aaron', age: 26},
+    {id: 5, name: 'David', age: 32}
+]);
+
+var ageFilter = new Ext.util.Filter({
+    property: 'age',
+    value   : 32
+});
+
+var longNameFilter = new Ext.util.Filter({
+    filterFn: function(item) {
+        return item.name.length > 4;
+    }
+});
+
+//a new MixedCollection with the 3 names longer than 4 characters
+var longNames = allNames.filter(longNameFilter);
+
+//a new MixedCollection with the 2 people of age 24:
+var youngFolk = allNames.filter(ageFilter);
+
+ * @constructor + * @param {Object} config Config object + */ +Ext.define('Ext.util.Filter', { + + /* Begin Definitions */ + + /* End Definitions */ + /** + * @cfg {String} property The property to filter on. Required unless a {@link #filter} is passed + */ + + /** + * @cfg {Function} filterFn A custom filter function which is passed each item in the {@link Ext.util.MixedCollection} + * in turn. Should return true to accept each item or false to reject it + */ + + /** + * @cfg {Boolean} anyMatch True to allow any match - no regex start/end line anchors will be added. Defaults to false + */ + anyMatch: false, + + /** + * @cfg {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. + * Ignored if anyMatch is true. + */ + exactMatch: false, + + /** + * @cfg {Boolean} caseSensitive True to make the regex case sensitive (adds 'i' switch to regex). Defaults to false. + */ + caseSensitive: false, + + /** + * @cfg {String} root Optional root property. This is mostly useful when filtering a Store, in which case we set the + * root to 'data' to make the filter pull the {@link #property} out of the data object of each item + */ + + constructor: function(config) { + Ext.apply(this, config); + + //we're aliasing filter to filterFn mostly for API cleanliness reasons, despite the fact it dirties the code here. + //Ext.util.Sorter takes a sorterFn property but allows .sort to be called - we do the same here + this.filter = this.filter || this.filterFn; + + if (this.filter == undefined) { + if (this.property == undefined || this.value == undefined) { + // Commented this out temporarily because it stops us using string ids in models. TODO: Remove this once + // Model has been updated to allow string ids + + // Ext.Error.raise("A Filter requires either a property or a filterFn to be set"); + } else { + this.filter = this.createFilterFn(); + } + + this.filterFn = this.filter; + } + }, + + /** + * @private + * Creates a filter function for the configured property/value/anyMatch/caseSensitive options for this Filter + */ + createFilterFn: function() { + var me = this, + matcher = me.createValueMatcher(), + property = me.property; + + return function(item) { + return matcher.test(me.getRoot.call(me, item)[property]); + }; + }, + + /** + * @private + * Returns the root property of the given item, based on the configured {@link #root} property + * @param {Object} item The item + * @return {Object} The root property of the object + */ + getRoot: function(item) { + return this.root == undefined ? item : item[this.root]; + }, + + /** + * @private + * Returns a regular expression based on the given value and matching options + */ + createValueMatcher : function() { + var me = this, + value = me.value, + anyMatch = me.anyMatch, + exactMatch = me.exactMatch, + caseSensitive = me.caseSensitive, + escapeRe = Ext.String.escapeRegex; + + if (!value.exec) { // not a regex + value = String(value); + + if (anyMatch === true) { + value = escapeRe(value); + } else { + value = '^' + escapeRe(value); + if (exactMatch === true) { + value += '$'; + } + } + value = new RegExp(value, caseSensitive ? '' : 'i'); + } + + return value; + } +}); +/** + * @class Ext.util.Sorter + * @extends Object + * Represents a single sorter that can be applied to a Store + */ +Ext.define('Ext.util.Sorter', { + + /** + * @cfg {String} property The property to sort by. Required unless {@link #sorter} is provided + */ + + /** + * @cfg {Function} sorterFn A specific sorter function to execute. Can be passed instead of {@link #property} + */ + + /** + * @cfg {String} root Optional root property. This is mostly useful when sorting a Store, in which case we set the + * root to 'data' to make the filter pull the {@link #property} out of the data object of each item + */ + + /** + * @cfg {Function} transform A function that will be run on each value before + * it is compared in the sorter. The function will receive a single argument, + * the value. + */ + + /** + * @cfg {String} direction The direction to sort by. Defaults to ASC + */ + direction: "ASC", + + constructor: function(config) { + var me = this; + + Ext.apply(me, config); + + // + if (me.property == undefined && me.sorterFn == undefined) { + Ext.Error.raise("A Sorter requires either a property or a sorter function"); + } + // + + me.updateSortFunction(); + }, + + /** + * @private + * Creates and returns a function which sorts an array by the given property and direction + * @return {Function} A function which sorts by the property/direction combination provided + */ + createSortFunction: function(sorterFn) { + var me = this, + property = me.property, + direction = me.direction || "ASC", + modifier = direction.toUpperCase() == "DESC" ? -1 : 1; + + //create a comparison function. Takes 2 objects, returns 1 if object 1 is greater, + //-1 if object 2 is greater or 0 if they are equal + return function(o1, o2) { + return modifier * sorterFn.call(me, o1, o2); + }; + }, + + /** + * @private + * Basic default sorter function that just compares the defined property of each object + */ + defaultSorterFn: function(o1, o2) { + var me = this, + transform = me.transform, + v1 = me.getRoot(o1)[me.property], + v2 = me.getRoot(o2)[me.property]; + + if (transform) { + v1 = transform(v1); + v2 = transform(v2); + } + + return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0); + }, + + /** + * @private + * Returns the root property of the given item, based on the configured {@link #root} property + * @param {Object} item The item + * @return {Object} The root property of the object + */ + getRoot: function(item) { + return this.root == undefined ? item : item[this.root]; + }, + + // @TODO: Add docs for these three methods + setDirection: function(direction) { + var me = this; + me.direction = direction; + me.updateSortFunction(); + }, + + toggle: function() { + var me = this; + me.direction = Ext.String.toggle(me.direction, "ASC", "DESC"); + me.updateSortFunction(); + }, + + updateSortFunction: function() { + var me = this; + me.sort = me.createSortFunction(me.sorterFn || me.defaultSorterFn); + } +}); +/** + * @class Ext.ElementLoader + * A class used to load remote content to an Element. Sample usage: + *

+Ext.get('el').load({
+    url: 'myPage.php',
+    scripts: true,
+    params: {
+        id: 1
+    }
+});
+ * 
+ *

+ * In general this class will not be instanced directly, rather the {@link Ext.core.Element#load} method + * will be used. + *

+ */ +Ext.define('Ext.ElementLoader', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + uses: [ + 'Ext.data.Connection', + 'Ext.Ajax' + ], + + statics: { + Renderer: { + Html: function(loader, response, active){ + loader.getTarget().update(response.responseText, active.scripts === true); + return true; + } + } + }, + + /* End Definitions */ + + /** + * @cfg {String} url The url to retrieve the content from. Defaults to null. + */ + url: null, + + /** + * @cfg {Object} params Any params to be attached to the Ajax request. These parameters will + * be overridden by any params in the load options. Defaults to null. + */ + params: null, + + /** + * @cfg {Object} baseParams Params that will be attached to every request. These parameters + * will not be overridden by any params in the load options. Defaults to null. + */ + baseParams: null, + + /** + * @cfg {Boolean/Object} autoLoad True to have the loader make a request as soon as it is created. Defaults to false. + * This argument can also be a set of options that will be passed to {@link #load} is called. + */ + autoLoad: false, + + /** + * @cfg {Mixed} target The target element for the loader. It can be the DOM element, the id or an Ext.Element. + */ + target: null, + + /** + * @cfg {Mixed} loadMask True or a string to show when the element is loading. + */ + loadMask: false, + + /** + * @cfg {Object} ajaxOptions Any additional options to be passed to the request, for example timeout or headers. Defaults to null. + */ + ajaxOptions: null, + + /** + * @cfg {Boolean} scripts True to parse any inline script tags in the response. + */ + scripts: false, + + /** + * @cfg {Function} success A function to be called when a load request is successful. + */ + + /** + * @cfg {Function} failure A function to be called when a load request fails. + */ + + /** + * @cfg {Object} scope The scope to execute the {@link #success} and {@link #failure} functions in. + */ + + /** + * @cfg {Function} renderer A custom function to render the content to the element. The passed parameters + * are + *
    + *
  • The loader
  • + *
  • The response
  • + *
  • The active request
  • + *
+ */ + + isLoader: true, + + constructor: function(config) { + var me = this, + autoLoad; + + config = config || {}; + Ext.apply(me, config); + me.setTarget(me.target); + me.addEvents( + /** + * @event beforeload + * Fires before a load request is made to the server. + * Returning false from an event listener can prevent the load + * from occurring. + * @param {Ext.ElementLoader} this + * @param {Object} options The options passed to the request + */ + 'beforeload', + + /** + * @event exception + * Fires after an unsuccessful load. + * @param {Ext.ElementLoader} this + * @param {Object} response The response from the server + * @param {Object} options The options passed to the request + */ + 'exception', + + /** + * @event exception + * Fires after a successful load. + * @param {Ext.ElementLoader} this + * @param {Object} response The response from the server + * @param {Object} options The options passed to the request + */ + 'load' + ); + + // don't pass config because we have already applied it. + me.mixins.observable.constructor.call(me); + + if (me.autoLoad) { + autoLoad = me.autoLoad; + if (autoLoad === true) { + autoLoad = {}; + } + me.load(autoLoad); + } + }, + + /** + * Set an {Ext.Element} as the target of this loader. Note that if the target is changed, + * any active requests will be aborted. + * @param {Mixed} target The element + */ + setTarget: function(target){ + var me = this; + target = Ext.get(target); + if (me.target && me.target != target) { + me.abort(); + } + me.target = target; + }, + + /** + * Get the target of this loader. + * @return {Ext.Component} target The target, null if none exists. + */ + getTarget: function(){ + return this.target || null; + }, + + /** + * Aborts the active load request + */ + abort: function(){ + var active = this.active; + if (active !== undefined) { + Ext.Ajax.abort(active.request); + if (active.mask) { + this.removeMask(); + } + delete this.active; + } + }, + + /** + * Remove the mask on the target + * @private + */ + removeMask: function(){ + this.target.unmask(); + }, + + /** + * Add the mask on the target + * @private + * @param {Mixed} mask The mask configuration + */ + addMask: function(mask){ + this.target.mask(mask === true ? null : mask); + }, + + /** + * Load new data from the server. + * @param {Object} options The options for the request. They can be any configuration option that can be specified for + * the class, with the exception of the target option. Note that any options passed to the method will override any + * class defaults. + */ + load: function(options) { + // + if (!this.target) { + Ext.Error.raise('A valid target is required when loading content'); + } + // + + options = Ext.apply({}, options); + + var me = this, + target = me.target, + mask = Ext.isDefined(options.loadMask) ? options.loadMask : me.loadMask, + params = Ext.apply({}, options.params), + ajaxOptions = Ext.apply({}, options.ajaxOptions), + callback = options.callback || me.callback, + scope = options.scope || me.scope || me, + request; + + Ext.applyIf(ajaxOptions, me.ajaxOptions); + Ext.applyIf(options, ajaxOptions); + + Ext.applyIf(params, me.params); + Ext.apply(params, me.baseParams); + + Ext.applyIf(options, { + url: me.url + }); + + // + if (!options.url) { + Ext.Error.raise('You must specify the URL from which content should be loaded'); + } + // + + Ext.apply(options, { + scope: me, + params: params, + callback: me.onComplete + }); + + if (me.fireEvent('beforeload', me, options) === false) { + return; + } + + if (mask) { + me.addMask(mask); + } + + request = Ext.Ajax.request(options); + me.active = { + request: request, + options: options, + mask: mask, + scope: scope, + callback: callback, + success: options.success || me.success, + failure: options.failure || me.failure, + renderer: options.renderer || me.renderer, + scripts: Ext.isDefined(options.scripts) ? options.scripts : me.scripts + }; + me.setOptions(me.active, options); + }, + + /** + * Set any additional options on the active request + * @private + * @param {Object} active The active request + * @param {Object} options The initial options + */ + setOptions: Ext.emptyFn, + + /** + * Parse the response after the request completes + * @private + * @param {Object} options Ajax options + * @param {Boolean} success Success status of the request + * @param {Object} response The response object + */ + onComplete: function(options, success, response) { + var me = this, + active = me.active, + scope = active.scope, + renderer = me.getRenderer(active.renderer); + + + if (success) { + success = renderer.call(me, me, response, active); + } + + if (success) { + Ext.callback(active.success, scope, [me, response, options]); + me.fireEvent('load', me, response, options); + } else { + Ext.callback(active.failure, scope, [me, response, options]); + me.fireEvent('exception', me, response, options); + } + Ext.callback(active.callback, scope, [me, success, response, options]); + + if (active.mask) { + me.removeMask(); + } + + delete me.active; + }, + + /** + * Gets the renderer to use + * @private + * @param {String/Function} renderer The renderer to use + * @return {Function} A rendering function to use. + */ + getRenderer: function(renderer){ + if (Ext.isFunction(renderer)) { + return renderer; + } + return this.statics().Renderer.Html; + }, + + /** + * Automatically refreshes the content over a specified period. + * @param {Number} interval The interval to refresh in ms. + * @param {Object} options (optional) The options to pass to the load method. See {@link #load} + */ + startAutoRefresh: function(interval, options){ + var me = this; + me.stopAutoRefresh(); + me.autoRefresh = setInterval(function(){ + me.load(options); + }, interval); + }, + + /** + * Clears any auto refresh. See {@link #startAutoRefresh}. + */ + stopAutoRefresh: function(){ + clearInterval(this.autoRefresh); + delete this.autoRefresh; + }, + + /** + * Checks whether the loader is automatically refreshing. See {@link #startAutoRefresh}. + * @return {Boolean} True if the loader is automatically refreshing + */ + isAutoRefreshing: function(){ + return Ext.isDefined(this.autoRefresh); + }, + + /** + * Destroys the loader. Any active requests will be aborted. + */ + destroy: function(){ + var me = this; + me.stopAutoRefresh(); + delete me.target; + me.abort(); + me.clearListeners(); + } +}); + +/** + * @class Ext.layout.Layout + * @extends Object + * @private + * Base Layout class - extended by ComponentLayout and ContainerLayout + */ + +Ext.define('Ext.layout.Layout', { + + /* Begin Definitions */ + + /* End Definitions */ + + isLayout: true, + initialized: false, + + statics: { + create: function(layout, defaultType) { + var type; + if (layout instanceof Ext.layout.Layout) { + return Ext.createByAlias('layout.' + layout); + } else { + if (Ext.isObject(layout)) { + type = layout.type; + } + else { + type = layout || defaultType; + layout = {}; + } + return Ext.createByAlias('layout.' + type, layout || {}); + } + } + }, + + constructor : function(config) { + this.id = Ext.id(null, this.type + '-'); + Ext.apply(this, config); + }, + + /** + * @private + */ + layout : function() { + var me = this; + me.layoutBusy = true; + me.initLayout(); + + if (me.beforeLayout.apply(me, arguments) !== false) { + me.layoutCancelled = false; + me.onLayout.apply(me, arguments); + me.childrenChanged = false; + me.owner.needsLayout = false; + me.layoutBusy = false; + me.afterLayout.apply(me, arguments); + } + else { + me.layoutCancelled = true; + } + me.layoutBusy = false; + me.doOwnerCtLayouts(); + }, + + beforeLayout : function() { + this.renderItems(this.getLayoutItems(), this.getRenderTarget()); + return true; + }, + + /** + * @private + * Iterates over all passed items, ensuring they are rendered. If the items are already rendered, + * also determines if the items are in the proper place dom. + */ + renderItems : function(items, target) { + var ln = items.length, + i = 0, + item; + + for (; i < ln; i++) { + item = items[i]; + if (item && !item.rendered) { + this.renderItem(item, target, i); + } + else if (!this.isValidParent(item, target, i)) { + this.moveItem(item, target, i); + } + } + }, + + // @private - Validates item is in the proper place in the dom. + isValidParent : function(item, target, position) { + var dom = item.el ? item.el.dom : Ext.getDom(item); + if (dom && target && target.dom) { + if (Ext.isNumber(position) && dom !== target.dom.childNodes[position]) { + return false; + } + return (dom.parentNode == (target.dom || target)); + } + return false; + }, + + /** + * @private + * Renders the given Component into the target Element. + * @param {Ext.Component} item The Component to render + * @param {Ext.core.Element} target The target Element + * @param {Number} position The position within the target to render the item to + */ + renderItem : function(item, target, position) { + if (!item.rendered) { + item.render(target, position); + this.configureItem(item); + this.childrenChanged = true; + } + }, + + /** + * @private + * Moved Component to the provided target instead. + */ + moveItem : function(item, target, position) { + // Make sure target is a dom element + target = target.dom || target; + if (typeof position == 'number') { + position = target.childNodes[position]; + } + target.insertBefore(item.el.dom, position || null); + item.container = Ext.get(target); + this.configureItem(item); + this.childrenChanged = true; + }, + + /** + * @private + * Adds the layout's targetCls if necessary and sets + * initialized flag when complete. + */ + initLayout : function() { + if (!this.initialized && !Ext.isEmpty(this.targetCls)) { + this.getTarget().addCls(this.targetCls); + } + this.initialized = true; + }, + + // @private Sets the layout owner + setOwner : function(owner) { + this.owner = owner; + }, + + // @private - Returns empty array + getLayoutItems : function() { + return []; + }, + + /** + * @private + * Applies itemCls + */ + configureItem: function(item) { + var me = this, + el = item.el, + owner = me.owner; + + if (me.itemCls) { + el.addCls(me.itemCls); + } + if (owner.itemCls) { + el.addCls(owner.itemCls); + } + }, + + // Placeholder empty functions for subclasses to extend + onLayout : Ext.emptyFn, + afterLayout : Ext.emptyFn, + onRemove : Ext.emptyFn, + onDestroy : Ext.emptyFn, + doOwnerCtLayouts : Ext.emptyFn, + + /** + * @private + * Removes itemCls + */ + afterRemove : function(item) { + var me = this, + el = item.el, + owner = me.owner; + + if (item.rendered) { + if (me.itemCls) { + el.removeCls(me.itemCls); + } + if (owner.itemCls) { + el.removeCls(owner.itemCls); + } + } + }, + + /* + * Destroys this layout. This is a template method that is empty by default, but should be implemented + * by subclasses that require explicit destruction to purge event handlers or remove DOM nodes. + * @protected + */ + destroy : function() { + if (!Ext.isEmpty(this.targetCls)) { + var target = this.getTarget(); + if (target) { + target.removeCls(this.targetCls); + } + } + this.onDestroy(); + } +}); +/** + * @class Ext.layout.component.Component + * @extends Ext.layout.Layout + * @private + *

This class is intended to be extended or created via the {@link Ext.Component#componentLayout layout} + * configuration property. See {@link Ext.Component#componentLayout} for additional details.

+ */ + +Ext.define('Ext.layout.component.Component', { + + /* Begin Definitions */ + + extend: 'Ext.layout.Layout', + + /* End Definitions */ + + type: 'component', + + monitorChildren: true, + + initLayout : function() { + var me = this, + owner = me.owner, + ownerEl = owner.el; + + if (!me.initialized) { + if (owner.frameSize) { + me.frameSize = owner.frameSize; + } + else { + owner.frameSize = me.frameSize = { + top: 0, + left: 0, + bottom: 0, + right: 0 + }; + } + } + me.callParent(arguments); + }, + + beforeLayout : function(width, height, isSetSize, layoutOwner) { + this.callParent(arguments); + + var me = this, + owner = me.owner, + ownerCt = owner.ownerCt, + layout = owner.layout, + isVisible = owner.isVisible(true), + ownerElChild = owner.el.child, + layoutCollection; + + /** + * Do not layout calculatedSized components for fixedLayouts unless the ownerCt == layoutOwner + * fixedLayouts means layouts which are never auto/auto in the sizing that comes from their ownerCt. + * Currently 3 layouts MAY be auto/auto (Auto, Border, and Box) + * The reason for not allowing component layouts is to stop component layouts from things such as Updater and + * form Validation. + */ + if (!isSetSize && !(Ext.isNumber(width) && Ext.isNumber(height)) && ownerCt && ownerCt.layout && ownerCt.layout.fixedLayout && ownerCt != layoutOwner) { + me.doContainerLayout(); + return false; + } + + // If an ownerCt is hidden, add my reference onto the layoutOnShow stack. Set the needsLayout flag. + // If the owner itself is a directly hidden floater, set the needsLayout object on that for when it is shown. + if (!isVisible && (owner.hiddenAncestor || owner.floating)) { + if (owner.hiddenAncestor) { + layoutCollection = owner.hiddenAncestor.layoutOnShow; + layoutCollection.remove(owner); + layoutCollection.add(owner); + } + owner.needsLayout = { + width: width, + height: height, + isSetSize: false + }; + } + + if (isVisible && this.needsLayout(width, height)) { + me.rawWidth = width; + me.rawHeight = height; + return owner.beforeComponentLayout(width, height, isSetSize, layoutOwner); + } + else { + return false; + } + }, + + /** + * Check if the new size is different from the current size and only + * trigger a layout if it is necessary. + * @param {Mixed} width The new width to set. + * @param {Mixed} height The new height to set. + */ + needsLayout : function(width, height) { + this.lastComponentSize = this.lastComponentSize || { + width: -Infinity, + height: -Infinity + }; + return (this.childrenChanged || this.lastComponentSize.width !== width || this.lastComponentSize.height !== height); + }, + + /** + * Set the size of any element supporting undefined, null, and values. + * @param {Mixed} width The new width to set. + * @param {Mixed} height The new height to set. + */ + setElementSize: function(el, width, height) { + if (width !== undefined && height !== undefined) { + el.setSize(width, height); + } + else if (height !== undefined) { + el.setHeight(height); + } + else if (width !== undefined) { + el.setWidth(width); + } + }, + + /** + * Returns the owner component's resize element. + * @return {Ext.core.Element} + */ + getTarget : function() { + return this.owner.el; + }, + + /** + *

Returns the element into which rendering must take place. Defaults to the owner Component's encapsulating element.

+ * May be overridden in Component layout managers which implement an inner element. + * @return {Ext.core.Element} + */ + getRenderTarget : function() { + return this.owner.el; + }, + + /** + * Set the size of the target element. + * @param {Mixed} width The new width to set. + * @param {Mixed} height The new height to set. + */ + setTargetSize : function(width, height) { + var me = this; + me.setElementSize(me.owner.el, width, height); + + if (me.owner.frameBody) { + var targetInfo = me.getTargetInfo(), + padding = targetInfo.padding, + border = targetInfo.border, + frameSize = me.frameSize; + + me.setElementSize(me.owner.frameBody, + Ext.isNumber(width) ? (width - frameSize.left - frameSize.right - padding.left - padding.right - border.left - border.right) : width, + Ext.isNumber(height) ? (height - frameSize.top - frameSize.bottom - padding.top - padding.bottom - border.top - border.bottom) : height + ); + } + + me.autoSized = { + width: !Ext.isNumber(width), + height: !Ext.isNumber(height) + }; + + me.lastComponentSize = { + width: width, + height: height + }; + }, + + getTargetInfo : function() { + if (!this.targetInfo) { + var target = this.getTarget(), + body = this.owner.getTargetEl(); + + this.targetInfo = { + padding: { + top: target.getPadding('t'), + right: target.getPadding('r'), + bottom: target.getPadding('b'), + left: target.getPadding('l') + }, + border: { + top: target.getBorderWidth('t'), + right: target.getBorderWidth('r'), + bottom: target.getBorderWidth('b'), + left: target.getBorderWidth('l') + }, + bodyMargin: { + top: body.getMargin('t'), + right: body.getMargin('r'), + bottom: body.getMargin('b'), + left: body.getMargin('l') + } + }; + } + return this.targetInfo; + }, + + // Start laying out UP the ownerCt's layout when flagged to do so. + doOwnerCtLayouts: function() { + var owner = this.owner, + ownerCt = owner.ownerCt, + ownerCtComponentLayout, ownerCtContainerLayout; + + if (!ownerCt) { + return; + } + + ownerCtComponentLayout = ownerCt.componentLayout; + ownerCtContainerLayout = ownerCt.layout; + + if (!owner.floating && ownerCtComponentLayout && ownerCtComponentLayout.monitorChildren && !ownerCtComponentLayout.layoutBusy) { + if (!ownerCt.suspendLayout && ownerCtContainerLayout && !ownerCtContainerLayout.layoutBusy) { + // AutoContainer Layout and Dock with auto in some dimension + if (ownerCtContainerLayout.bindToOwnerCtComponent === true) { + ownerCt.doComponentLayout(); + } + // Box Layouts + else if (ownerCtContainerLayout.bindToOwnerCtContainer === true) { + ownerCtContainerLayout.layout(); + } + } + } + }, + + doContainerLayout: function() { + var me = this, + owner = me.owner, + ownerCt = owner.ownerCt, + layout = owner.layout, + ownerCtComponentLayout; + + // Run the container layout if it exists (layout for child items) + // **Unless automatic laying out is suspended, or the layout is currently running** + if (!owner.suspendLayout && layout && layout.isLayout && !layout.layoutBusy) { + layout.layout(); + } + + // Tell the ownerCt that it's child has changed and can be re-layed by ignoring the lastComponentSize cache. + if (ownerCt && ownerCt.componentLayout) { + ownerCtComponentLayout = ownerCt.componentLayout; + if (!owner.floating && ownerCtComponentLayout.monitorChildren && !ownerCtComponentLayout.layoutBusy) { + ownerCtComponentLayout.childrenChanged = true; + } + } + }, + + afterLayout : function(width, height, isSetSize, layoutOwner) { + this.doContainerLayout(); + this.owner.afterComponentLayout(width, height, isSetSize, layoutOwner); + } +}); + +/** + * @class Ext.state.Manager + * This is the global state manager. By default all components that are "state aware" check this class + * for state information if you don't pass them a custom state provider. In order for this class + * to be useful, it must be initialized with a provider when your application initializes. Example usage: +

+// in your initialization function
+init : function(){
+   Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
+   var win = new Window(...);
+   win.restoreState();
+}
+ 
+ * This class passes on calls from components to the underlying {@link Ext.state.Provider} so that + * there is a common interface that can be used without needing to refer to a specific provider instance + * in every component. + * @singleton + * @docauthor Evan Trimboli + */ +Ext.define('Ext.state.Manager', { + singleton: true, + requires: ['Ext.state.Provider'], + constructor: function() { + this.provider = Ext.create('Ext.state.Provider'); + }, + + + /** + * Configures the default state provider for your application + * @param {Provider} stateProvider The state provider to set + */ + setProvider : function(stateProvider){ + this.provider = stateProvider; + }, + + /** + * Returns the current value for a key + * @param {String} name The key name + * @param {Mixed} defaultValue The default value to return if the key lookup does not match + * @return {Mixed} The state data + */ + get : function(key, defaultValue){ + return this.provider.get(key, defaultValue); + }, + + /** + * Sets the value for a key + * @param {String} name The key name + * @param {Mixed} value The state data + */ + set : function(key, value){ + this.provider.set(key, value); + }, + + /** + * Clears a value from the state + * @param {String} name The key name + */ + clear : function(key){ + this.provider.clear(key); + }, + + /** + * Gets the currently configured state provider + * @return {Provider} The state provider + */ + getProvider : function(){ + return this.provider; + } +}); +/** + * @class Ext.state.Stateful + * A mixin for being able to save the state of an object to an underlying + * {@link Ext.state.Provider}. + */ +Ext.define('Ext.state.Stateful', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: ['Ext.state.Manager'], + + /* End Definitions */ + + /** + * @cfg {Boolean} stateful + *

A flag which causes the object to attempt to restore the state of + * internal properties from a saved state on startup. The object must have + * a {@link #stateId} for state to be managed. + * Auto-generated ids are not guaranteed to be stable across page loads and + * cannot be relied upon to save and restore the same state for a object.

+ *

For state saving to work, the state manager's provider must have been + * set to an implementation of {@link Ext.state.Provider} which overrides the + * {@link Ext.state.Provider#set set} and {@link Ext.state.Provider#get get} + * methods to save and recall name/value pairs. A built-in implementation, + * {@link Ext.state.CookieProvider} is available.

+ *

To set the state provider for the current page:

+ *

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

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

+ *

To save state, a stateful object first serializes its state by + * calling {@link #getState}. By default, this function does + * nothing. The developer must provide an implementation which returns an + * object hash which represents the restorable state of the object.

+ *

The value yielded by getState is passed to {@link Ext.state.Manager#set} + * which uses the configured {@link Ext.state.Provider} to save the object + * keyed by the {@link stateId}

. + *

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

+ *

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

+ *

You can perform extra processing on state save and restore by attaching + * handlers to the {@link #beforestaterestore}, {@link #staterestore}, + * {@link #beforestatesave} and {@link #statesave} events.

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

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

+ */ + + /** + * @cfg {Array} stateEvents + *

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

+ *

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

+ */ + + /** + * @cfg {Number} saveBuffer A buffer to be applied if many state events are fired within + * a short period. Defaults to 100. + */ + saveDelay: 100, + + autoGenIdRe: /^((\w+-)|(ext-comp-))\d{4,}$/i, + + constructor: function(config) { + var me = this; + + config = config || {}; + if (Ext.isDefined(config.stateful)) { + me.stateful = config.stateful; + } + if (Ext.isDefined(config.saveDelay)) { + me.saveDelay = config.saveDelay; + } + me.stateId = config.stateId; + + if (!me.stateEvents) { + me.stateEvents = []; + } + if (config.stateEvents) { + me.stateEvents.concat(config.stateEvents); + } + this.addEvents( + /** + * @event beforestaterestore + * Fires before the state of the object is restored. Return false from an event handler to stop the restore. + * @param {Ext.state.Stateful} this + * @param {Object} state The hash of state values returned from the StateProvider. If this + * event is not vetoed, then the state object is passed to applyState. By default, + * that simply copies property values into this object. The method maybe overriden to + * provide custom state restoration. + */ + 'beforestaterestore', + + /** + * @event staterestore + * Fires after the state of the object is restored. + * @param {Ext.state.Stateful} this + * @param {Object} state The hash of state values returned from the StateProvider. This is passed + * to applyState. By default, that simply copies property values into this + * object. The method maybe overriden to provide custom state restoration. + */ + 'staterestore', + + /** + * @event beforestatesave + * Fires before the state of the object is saved to the configured state provider. Return false to stop the save. + * @param {Ext.state.Stateful} this + * @param {Object} state The hash of state values. This is determined by calling + * getState() on the object. This method must be provided by the + * developer to return whetever representation of state is required, by default, Ext.state.Stateful + * has a null implementation. + */ + 'beforestatesave', + + /** + * @event statesave + * Fires after the state of the object is saved to the configured state provider. + * @param {Ext.state.Stateful} this + * @param {Object} state The hash of state values. This is determined by calling + * getState() on the object. This method must be provided by the + * developer to return whetever representation of state is required, by default, Ext.state.Stateful + * has a null implementation. + */ + 'statesave' + ); + me.mixins.observable.constructor.call(me); + if (me.stateful !== false) { + me.initStateEvents(); + me.initState(); + } + }, + + /** + * Initializes any state events for this object. + * @private + */ + initStateEvents: function() { + this.addStateEvents(this.stateEvents); + }, + + /** + * Add events that will trigger the state to be saved. + * @param {String/Array} events The event name or an array of event names. + */ + addStateEvents: function(events){ + if (!Ext.isArray(events)) { + events = [events]; + } + + var me = this, + i = 0, + len = events.length; + + for (; i < len; ++i) { + me.on(events[i], me.onStateChange, me); + } + }, + + /** + * This method is called when any of the {@link #stateEvents} are fired. + * @private + */ + onStateChange: function(){ + var me = this, + delay = me.saveDelay; + + if (delay > 0) { + if (!me.stateTask) { + me.stateTask = Ext.create('Ext.util.DelayedTask', me.saveState, me); + } + me.stateTask.delay(me.saveDelay); + } else { + me.saveState(); + } + }, + + /** + * Saves the state of the object to the persistence store. + * @private + */ + saveState: function() { + var me = this, + id, + state; + + if (me.stateful !== false) { + id = me.getStateId(); + if (id) { + state = me.getState(); + if (me.fireEvent('beforestatesave', me, state) !== false) { + Ext.state.Manager.set(id, state); + me.fireEvent('statesave', me, state); + } + } + } + }, + + /** + * Gets the current state of the object. By default this function returns null, + * it should be overridden in subclasses to implement methods for getting the state. + * @return {Object} The current state + */ + getState: function(){ + return null; + }, + + /** + * Applies the state to the object. This should be overridden in subclasses to do + * more complex state operations. By default it applies the state properties onto + * the current object. + * @param {Object} state The state + */ + applyState: function(state) { + if (state) { + Ext.apply(this, state); + } + }, + + /** + * Gets the state id for this object. + * @return {String} The state id, null if not found. + */ + getStateId: function() { + var me = this, + id = me.stateId; + + if (!id) { + id = me.autoGenIdRe.test(String(me.id)) ? null : me.id; + } + return id; + }, + + /** + * Initializes the state of the object upon construction. + * @private + */ + initState: function(){ + var me = this, + id = me.getStateId(), + state; + + if (me.stateful !== false) { + if (id) { + state = Ext.state.Manager.get(id); + if (state) { + state = Ext.apply({}, state); + if (me.fireEvent('beforestaterestore', me, state) !== false) { + me.applyState(state); + me.fireEvent('staterestore', me, state); + } + } + } + } + }, + + /** + * Destroys this stateful object. + */ + destroy: function(){ + var task = this.stateTask; + if (task) { + task.cancel(); + } + this.clearListeners(); + + } + +}); + +/** + * @class Ext.AbstractManager + * @extends Object + * @ignore + * Base Manager class + */ + +Ext.define('Ext.AbstractManager', { + + /* Begin Definitions */ + + requires: ['Ext.util.HashMap'], + + /* End Definitions */ + + typeName: 'type', + + constructor: function(config) { + Ext.apply(this, config || {}); + + /** + * Contains all of the items currently managed + * @property all + * @type Ext.util.MixedCollection + */ + 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 {Mixed} The item, undefined if not found. + */ + get : function(id) { + return this.all.get(id); + }, + + /** + * Registers an item to be managed + * @param {Mixed} item The item to register + */ + register: function(item) { + this.all.add(item); + }, + + /** + * Unregisters an item by removing it from this manager + * @param {Mixed} 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 {Constructor} 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 {Mixed} The instance of whatever this manager is managing + */ + create: function(config, defaultType) { + var type = config[this.typeName] || config.type || defaultType, + Constructor = this.types[type]; + + // + if (Constructor == undefined) { + Ext.Error.raise("The '" + type + "' type has not been registered with this manager"); + } + // + + 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. + * Returning false from the function will cease iteration. + * + * The paramaters passed to the function are: + *

    + *
  • key : String

    The key of the item

  • + *
  • value : Number

    The value of the item

  • + *
  • length : Number

    The total number of items in the collection

  • + *
+ * @param {Object} fn The function to execute. + * @param {Object} scope The scope to execute in. Defaults to this. + */ + each: function(fn, scope){ + this.all.each(fn, scope || this); + }, + + /** + * Gets the number of items in the collection. + * @return {Number} The number of items in the collection. + */ + getCount: function(){ + return this.all.getCount(); + } +}); + +/** + * @class Ext.PluginManager + * @extends Ext.AbstractManager + *

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

+ *

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

+ *

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

+ * @singleton + */ +Ext.define('Ext.PluginManager', { + extend: 'Ext.AbstractManager', + alternateClassName: 'Ext.PluginMgr', + singleton: true, + typeName: 'ptype', + + /** + * Creates a new Plugin from the specified config object using the + * config object's ptype to determine the class to instantiate. + * @param {Object} config A configuration object for the Plugin you wish to create. + * @param {Constructor} defaultType The constructor to provide the default Plugin type if + * the config object does not contain a ptype. (Optional if the config contains a ptype). + * @return {Ext.Component} The newly instantiated Plugin. + */ + //create: function(plugin, defaultType) { + // if (plugin instanceof this) { + // return plugin; + // } else { + // var type, config = {}; + // + // if (Ext.isString(plugin)) { + // type = plugin; + // } + // else { + // type = plugin[this.typeName] || defaultType; + // config = plugin; + // } + // + // return Ext.createByAlias('plugin.' + type, config); + // } + //}, + + create : function(config, defaultType){ + if (config.init) { + return config; + } else { + return Ext.createByAlias('plugin.' + (config.ptype || defaultType), config); + } + + // Prior system supported Singleton plugins. + //var PluginCls = this.types[config.ptype || defaultType]; + //if (PluginCls.init) { + // return PluginCls; + //} else { + // return new PluginCls(config); + //} + }, + + /** + * Returns all plugins registered with the given type. Here, 'type' refers to the type of plugin, not its ptype. + * @param {String} type The type to search for + * @param {Boolean} defaultsOnly True to only return plugins of this type where the plugin's isDefault property is truthy + * @return {Array} All matching plugins + */ + findByType: function(type, defaultsOnly) { + var matches = [], + types = this.types; + + for (var name in types) { + if (!types.hasOwnProperty(name)) { + continue; + } + var item = types[name]; + + if (item.type == type && (!defaultsOnly || (defaultsOnly === true && item.isDefault))) { + matches.push(item); + } + } + + return matches; + } +}, function() { + /** + * Shorthand for {@link Ext.PluginManager#registerType} + * @param {String} ptype The ptype mnemonic string by which the Plugin class + * may be looked up. + * @param {Constructor} cls The new Plugin class. + * @member Ext + * @method preg + */ + Ext.preg = function() { + return Ext.PluginManager.registerType.apply(Ext.PluginManager, arguments); + }; +}); + +/** + * @class Ext.ComponentManager + * @extends Ext.AbstractManager + *

Provides a registry of all Components (instances of {@link Ext.Component} or any subclass + * thereof) on a page so that they can be easily accessed by {@link Ext.Component component} + * {@link Ext.Component#id id} (see {@link #get}, or the convenience method {@link Ext#getCmp Ext.getCmp}).

+ *

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

+ *

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

+ *

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

+ * @singleton + */ +Ext.define('Ext.ComponentManager', { + extend: 'Ext.AbstractManager', + alternateClassName: 'Ext.ComponentMgr', + + singleton: true, + + typeName: 'xtype', + + /** + * Creates a new Component from the specified config object using the + * config object's xtype to determine the class to instantiate. + * @param {Object} config A configuration object for the Component you wish to create. + * @param {Constructor} defaultType The constructor to provide the default Component type if + * the config object does not contain a xtype. (Optional if the config contains a xtype). + * @return {Ext.Component} The newly instantiated Component. + */ + create: function(component, defaultType){ + if (component instanceof Ext.AbstractComponent) { + return component; + } + else if (Ext.isString(component)) { + return Ext.createByAlias('widget.' + component); + } + else { + var type = component.xtype || defaultType, + config = component; + + return Ext.createByAlias('widget.' + type, config); + } + }, + + registerType: function(type, cls) { + this.types[type] = cls; + cls[this.typeName] = type; + cls.prototype[this.typeName] = type; + } +}); +/** + * @class Ext.XTemplate + * @extends Ext.Template + *

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

+ * + * 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: 'Tommy Maintz',
    +title: 'Lead Developer',
    +company: 'Sencha Inc.',
    +email: 'tommy@sencha.com',
    +address: '5 Cups Drive',
    +city: 'Palo Alto',
    +state: 'CA',
    +zip: '44102',
    +drinks: ['Coffee', 'Soda', 'Water'],
    +kids: [{
    +        name: 'Joshua',
    +        age:3
    +    },{
    +        name: 'Matthew',
    +        age:2
    +    },{
    +        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).
    • + *
    + *

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

      + *
    • Double quotes must be encoded if used within the conditional
    • + *
    • There is no else operator — if needed, two opposite + * if statements should be used.
    • + *
    + *
    
    +<tpl if="age > 1 && age < 10">Child</tpl>
    +<tpl if="age >= 10 && age < 18">Teenager</tpl>
    +<tpl if="this.isGirl(name)">...</tpl>
    +<tpl if="id==\'download\'">...</tpl>
    +<tpl if="needsIcon"><img src="{icon}" class="{iconCls}"/></tpl>
    +// no good:
    +<tpl if="name == "Tommy"">Hello</tpl>
    +// encode " if it is part of the condition, e.g.
    +<tpl if="name == &quot;Tommy&quot;">Hello</tpl>
    + * 
    + * 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);
    + 
    + *
    + *
  • + * + * + *
  • 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. There are some special variables available in that code: + *

      + *
    • 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>'
    + );
    +tpl.overwrite(panel.body, data);
    + 
    + *
    + *
  • + * + *
  • 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>',
    +         // use opposite if statement to simulate 'else' processing:
    +        '<tpl if="this.isGirl(name) == false">',
    +            '<p>Boy: {name} - {age}</p>',
    +        '</tpl>',
    +        '<tpl if="this.isBaby(age)">',
    +            '<p>{name} is a baby!</p>',
    +        '</tpl>',
    +    '</tpl></p>',
    +    {
    +        // XTemplate configuration:
    +        compiled: true,
    +        // member functions:
    +        isGirl: function(name){
    +           return name == 'Sara Grace';
    +        },
    +        isBaby: function(age){
    +           return age < 1;
    +        }
    +    }
    +);
    +tpl.overwrite(panel.body, data);
    + 
    + *
    + *
  • + * + *
+ * + * @param {Mixed} config + */ + +Ext.define('Ext.XTemplate', { + + /* Begin Definitions */ + + extend: 'Ext.Template', + + statics: { + /** + * 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 + * @return {Ext.Template} The created template + * @static + */ + from: function(el, config) { + el = Ext.getDom(el); + return new this(el.value || el.innerHTML, config || {}); + } + }, + + /* End Definitions */ + + argsRe: /]*>((?:(?=([^<]+))\2|<(?!tpl\b[^>]*>))*?)<\/tpl>/, + nameRe: /^]*?for="(.*?)"/, + ifRe: /^]*?if="(.*?)"/, + execRe: /^]*?exec="(.*?)"/, + constructor: function() { + this.callParent(arguments); + + var me = this, + html = me.html, + argsRe = me.argsRe, + nameRe = me.nameRe, + ifRe = me.ifRe, + execRe = me.execRe, + id = 0, + tpls = [], + VALUES = 'values', + PARENT = 'parent', + XINDEX = 'xindex', + XCOUNT = 'xcount', + RETURN = 'return ', + WITHVALUES = 'with(values){ ', + m, matchName, matchIf, matchExec, exp, fn, exec, name, i; + + html = ['', html, ''].join(''); + + while ((m = html.match(argsRe))) { + exp = null; + fn = null; + exec = null; + matchName = m[0].match(nameRe); + matchIf = m[0].match(ifRe); + matchExec = m[0].match(execRe); + + exp = matchIf ? matchIf[1] : null; + if (exp) { + fn = Ext.functionFactory(VALUES, PARENT, XINDEX, XCOUNT, WITHVALUES + 'try{' + RETURN + Ext.String.htmlDecode(exp) + ';}catch(e){return;}}'); + } + + exp = matchExec ? matchExec[1] : null; + if (exp) { + exec = Ext.functionFactory(VALUES, PARENT, XINDEX, XCOUNT, WITHVALUES + Ext.String.htmlDecode(exp) + ';}'); + } + + name = matchName ? matchName[1] : null; + if (name) { + if (name === '.') { + name = VALUES; + } else if (name === '..') { + name = PARENT; + } + name = Ext.functionFactory(VALUES, PARENT, 'try{' + WITHVALUES + RETURN + name + ';}}catch(e){return;}'); + } + + tpls.push({ + id: id, + target: name, + exec: exec, + test: fn, + body: m[1] || '' + }); + + html = html.replace(m[0], '{xtpl' + id + '}'); + id = id + 1; + } + + for (i = tpls.length - 1; i >= 0; --i) { + me.compileTpl(tpls[i]); + } + me.master = tpls[tpls.length - 1]; + me.tpls = tpls; + }, + + // @private + applySubTemplate: function(id, values, parent, xindex, xcount) { + var me = this, t = me.tpls[id]; + return t.compiled.call(me, values, parent, xindex, xcount); + }, + /** + * @cfg {RegExp} codeRe The regular expression used to match code variables (default: matches {[expression]}). + */ + codeRe: /\{\[((?:\\\]|.|\n)*?)\]\}/g, + + re: /\{([\w-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?\}/g, + + // @private + compileTpl: function(tpl) { + var fm = Ext.util.Format, + me = this, + useFormat = me.disableFormats !== true, + body, bodyReturn, evaluatedFn; + + function fn(m, name, format, args, math) { + var v; + // name is what is inside the {} + // Name begins with xtpl, use a Sub Template + if (name.substr(0, 4) == 'xtpl') { + return "',this.applySubTemplate(" + name.substr(4) + ", values, parent, xindex, xcount),'"; + } + // name = "." - Just use the values object. + if (name == '.') { + // filter to not include arrays/objects/nulls + v = 'Ext.Array.indexOf(["string", "number", "boolean"], typeof values) > -1 || Ext.isDate(values) ? values : ""'; + } + + // name = "#" - Use the xindex + else if (name == '#') { + v = 'xindex'; + } + else if (name.substr(0, 7) == "parent.") { + v = name; + } + // name has a . in it - Use object literal notation, starting from values + else if (name.indexOf('.') != -1) { + v = "values." + name; + } + + // name is a property of values + else { + v = "values['" + name + "']"; + } + if (math) { + v = '(' + v + math + ')'; + } + if (format && useFormat) { + args = args ? ',' + args : ""; + if (format.substr(0, 5) != "this.") { + format = "fm." + format + '('; + } + else { + format = 'this.' + format.substr(5) + '('; + } + } + else { + args = ''; + format = "(" + v + " === undefined ? '' : "; + } + return "'," + format + v + args + "),'"; + } + + function codeFn(m, code) { + // Single quotes get escaped when the template is compiled, however we want to undo this when running code. + return "',(" + code.replace(me.compileARe, "'") + "),'"; + } + + bodyReturn = tpl.body.replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn).replace(me.codeRe, codeFn); + body = "evaluatedFn = function(values, parent, xindex, xcount){return ['" + bodyReturn + "'].join('');};"; + eval(body); + + tpl.compiled = function(values, parent, xindex, xcount) { + var vs, + length, + buffer, + i; + + if (tpl.test && !tpl.test.call(me, values, parent, xindex, xcount)) { + return ''; + } + + vs = tpl.target ? tpl.target.call(me, values, parent) : values; + if (!vs) { + return ''; + } + + parent = tpl.target ? values : parent; + if (tpl.target && Ext.isArray(vs)) { + buffer = []; + length = vs.length; + if (tpl.exec) { + for (i = 0; i < length; i++) { + buffer[buffer.length] = evaluatedFn.call(me, vs[i], parent, i + 1, length); + tpl.exec.call(me, vs[i], parent, i + 1, length); + } + } else { + for (i = 0; i < length; i++) { + buffer[buffer.length] = evaluatedFn.call(me, vs[i], parent, i + 1, length); + } + } + return buffer.join(''); + } + + if (tpl.exec) { + tpl.exec.call(me, vs, parent, xindex, xcount); + } + return evaluatedFn.call(me, vs, parent, xindex, xcount); + }; + + return this; + }, + + /** + * Returns an HTML fragment of this template with the specified values applied. + * @param {Object} values The template values. Can be an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}) + * @return {String} The HTML fragment + */ + applyTemplate: function(values) { + return this.master.compiled.call(this, values, {}, 1, 1); + }, + + /** + * Compile the template to a function for optimized performance. Recommended if the template will be used frequently. + * @return {Function} The compiled function + */ + compile: function() { + return this; + } +}, function() { + /** + * Alias for {@link #applyTemplate} + * 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 (i.e. {0}) or an object (i.e. {foo: 'bar'}) + * @return {String} The HTML fragment + * @member Ext.XTemplate + * @method apply + */ + this.createAlias('apply', 'applyTemplate'); +}); + +/** + * @class Ext.util.AbstractMixedCollection + */ +Ext.define('Ext.util.AbstractMixedCollection', { + requires: ['Ext.util.Filter'], + + mixins: { + observable: 'Ext.util.Observable' + }, + + constructor: function(allowFunctions, keyFn) { + var me = this; + + me.items = []; + me.map = {}; + me.keys = []; + me.length = 0; + + me.addEvents( + /** + * @event clear + * Fires when the collection is cleared. + */ + 'clear', + + /** + * @event add + * Fires when an item is added to the collection. + * @param {Number} index The index at which the item was added. + * @param {Object} o The item added. + * @param {String} key The key associated with the added item. + */ + 'add', + + /** + * @event replace + * Fires when an item is replaced in the collection. + * @param {String} key he key associated with the new added. + * @param {Object} old The item being replaced. + * @param {Object} new The new item. + */ + 'replace', + + /** + * @event remove + * Fires when an item is removed from the collection. + * @param {Object} o The item being removed. + * @param {String} key (optional) The key associated with the removed item. + */ + 'remove' + ); + + me.allowFunctions = allowFunctions === true; + + if (keyFn) { + me.getKey = keyFn; + } + + me.mixins.observable.constructor.call(me); + }, + + /** + * @cfg {Boolean} allowFunctions Specify true if the {@link #addAll} + * function should add function references to the collection. Defaults to + * false. + */ + allowFunctions : false, + + /** + * Adds an item to the collection. Fires the {@link #add} event when complete. + * @param {String} key

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

+ *

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

+ * @param {Object} o The item to add. + * @return {Object} The item added. + */ + add : function(key, obj){ + var me = this, + myObj = obj, + myKey = key, + old; + + if (arguments.length == 1) { + myObj = myKey; + myKey = me.getKey(myObj); + } + if (typeof myKey != 'undefined' && myKey !== null) { + old = me.map[myKey]; + if (typeof old != 'undefined') { + return me.replace(myKey, myObj); + } + me.map[myKey] = myObj; + } + me.length++; + me.items.push(myObj); + me.keys.push(myKey); + me.fireEvent('add', me.length - 1, myObj, myKey); + return myObj; + }, + + /** + * MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation + * simply returns item.id but you can provide your own implementation + * to return a different value as in the following examples:

+// normal way
+var mc = new Ext.util.MixedCollection();
+mc.add(someEl.dom.id, someEl);
+mc.add(otherEl.dom.id, otherEl);
+//and so on
+
+// using getKey
+var mc = new Ext.util.MixedCollection();
+mc.getKey = function(el){
+   return el.dom.id;
+};
+mc.add(someEl);
+mc.add(otherEl);
+
+// or via the constructor
+var mc = new Ext.util.MixedCollection(false, function(el){
+   return el.dom.id;
+});
+mc.add(someEl);
+mc.add(otherEl);
+     * 
+ * @param {Object} item The item for which to find the key. + * @return {Object} The key for the passed item. + */ + getKey : function(o){ + return o.id; + }, + + /** + * Replaces an item in the collection. Fires the {@link #replace} event when complete. + * @param {String} key

The key associated with the item to replace, or the replacement item.

+ *

If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key + * of your stored items is in a property called id, then the MixedCollection + * will be able to derive the key of the replacement item. If you want to replace an item + * with one having the same key value, then just pass the replacement item in this parameter.

+ * @param o {Object} o (optional) If the first parameter passed was a key, the item to associate + * with that key. + * @return {Object} The new item. + */ + replace : function(key, o){ + var me = this, + old, + index; + + if (arguments.length == 1) { + o = arguments[0]; + key = me.getKey(o); + } + old = me.map[key]; + if (typeof key == 'undefined' || key === null || typeof old == 'undefined') { + return me.add(key, o); + } + index = me.indexOfKey(key); + me.items[index] = o; + me.map[key] = o; + me.fireEvent('replace', key, old, o); + return o; + }, + + /** + * Adds all elements of an Array or an Object to the collection. + * @param {Object/Array} objs An Object containing properties which will be added + * to the collection, or an Array of values, each of which are added to the collection. + * Functions references will be added to the collection if {@link #allowFunctions} + * has been set to true. + */ + addAll : function(objs){ + var me = this, + i = 0, + args, + len, + key; + + if (arguments.length > 1 || Ext.isArray(objs)) { + args = arguments.length > 1 ? arguments : objs; + for (len = args.length; i < len; i++) { + me.add(args[i]); + } + } else { + for (key in objs) { + if (objs.hasOwnProperty(key)) { + if (me.allowFunctions || typeof objs[key] != 'function') { + me.add(key, objs[key]); + } + } + } + } + }, + + /** + * Executes the specified function once for every item in the collection, passing the following arguments: + *
    + *
  • item : Mixed

    The collection item

  • + *
  • index : Number

    The item's index

  • + *
  • length : Number

    The total number of items in the collection

  • + *
+ * The function should return a boolean value. Returning false from the function will stop the iteration. + * @param {Function} fn The function to execute for each item. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current item in the iteration. + */ + each : function(fn, scope){ + var items = [].concat(this.items), // each safe for removal + i = 0, + len = items.length, + item; + + for (; i < len; i++) { + item = items[i]; + if (fn.call(scope || item, item, i, len) === false) { + break; + } + } + }, + + /** + * Executes the specified function once for every key in the collection, passing each + * key, and its associated item as the first two parameters. + * @param {Function} fn The function to execute for each item. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the browser window. + */ + eachKey : function(fn, scope){ + var keys = this.keys, + items = this.items, + i = 0, + len = keys.length; + + for (; i < len; i++) { + fn.call(scope || window, keys[i], items[i], i, len); + } + }, + + /** + * Returns the first item in the collection which elicits a true return value from the + * passed selection function. + * @param {Function} fn The selection function to execute for each item. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the browser window. + * @return {Object} The first item in the collection which returned true from the selection function. + */ + findBy : function(fn, scope) { + var keys = this.keys, + items = this.items, + i = 0, + len = items.length; + + for (; i < len; i++) { + if (fn.call(scope || window, items[i], keys[i])) { + return items[i]; + } + } + return null; + }, + + // + find : function() { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. Use findBy instead.'); + } + return this.findBy.apply(this, arguments); + }, + // + + /** + * Inserts an item at the specified index in the collection. Fires the {@link #add} event when complete. + * @param {Number} index The index to insert the item at. + * @param {String} key The key to associate with the new item, or the item itself. + * @param {Object} o (optional) If the second parameter was a key, the new item. + * @return {Object} The item inserted. + */ + insert : function(index, key, obj){ + var me = this, + myKey = key, + myObj = obj; + + if (arguments.length == 2) { + myObj = myKey; + myKey = me.getKey(myObj); + } + if (me.containsKey(myKey)) { + me.suspendEvents(); + me.removeAtKey(myKey); + me.resumeEvents(); + } + if (index >= me.length) { + return me.add(myKey, myObj); + } + me.length++; + me.items.splice(index, 0, myObj); + if (typeof myKey != 'undefined' && myKey !== null) { + me.map[myKey] = myObj; + } + me.keys.splice(index, 0, myKey); + me.fireEvent('add', index, myObj, myKey); + return myObj; + }, + + /** + * Remove an item from the collection. + * @param {Object} o The item to remove. + * @return {Object} The item removed or false if no item was removed. + */ + remove : function(o){ + return this.removeAt(this.indexOf(o)); + }, + + /** + * Remove all items in the passed array from the collection. + * @param {Array} items An array of items to be removed. + * @return {Ext.util.MixedCollection} this object + */ + removeAll : function(items){ + Ext.each(items || [], function(item) { + this.remove(item); + }, this); + + return this; + }, + + /** + * Remove an item from a specified index in the collection. Fires the {@link #remove} event when complete. + * @param {Number} index The index within the collection of the item to remove. + * @return {Object} The item removed or false if no item was removed. + */ + removeAt : function(index){ + var me = this, + o, + key; + + if (index < me.length && index >= 0) { + me.length--; + o = me.items[index]; + me.items.splice(index, 1); + key = me.keys[index]; + if (typeof key != 'undefined') { + delete me.map[key]; + } + me.keys.splice(index, 1); + me.fireEvent('remove', o, key); + return o; + } + return false; + }, + + /** + * Removed an item associated with the passed key fom the collection. + * @param {String} key The key of the item to remove. + * @return {Object} The item removed or false if no item was removed. + */ + removeAtKey : function(key){ + return this.removeAt(this.indexOfKey(key)); + }, + + /** + * Returns the number of items in the collection. + * @return {Number} the number of items in the collection. + */ + getCount : function(){ + return this.length; + }, + + /** + * Returns index within the collection of the passed Object. + * @param {Object} o The item to find the index of. + * @return {Number} index of the item. Returns -1 if not found. + */ + indexOf : function(o){ + return Ext.Array.indexOf(this.items, o); + }, + + /** + * Returns index within the collection of the passed key. + * @param {String} key The key to find the index of. + * @return {Number} index of the key. + */ + indexOfKey : function(key){ + return Ext.Array.indexOf(this.keys, key); + }, + + /** + * Returns the item associated with the passed key OR index. + * Key has priority over index. This is the equivalent + * of calling {@link #key} first, then if nothing matched calling {@link #getAt}. + * @param {String/Number} key The key or index of the item. + * @return {Object} If the item is found, returns the item. If the item was not found, returns undefined. + * If an item was found, but is a Class, returns null. + */ + get : function(key) { + var me = this, + mk = me.map[key], + item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined; + return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype! + }, + + /** + * Returns the item at the specified index. + * @param {Number} index The index of the item. + * @return {Object} The item at the specified index. + */ + getAt : function(index) { + return this.items[index]; + }, + + /** + * Returns the item associated with the passed key. + * @param {String/Number} key The key of the item. + * @return {Object} The item associated with the passed key. + */ + getByKey : function(key) { + return this.map[key]; + }, + + /** + * Returns true if the collection contains the passed Object as an item. + * @param {Object} o The Object to look for in the collection. + * @return {Boolean} True if the collection contains the Object as an item. + */ + contains : function(o){ + return Ext.Array.contains(this.items, o); + }, + + /** + * Returns true if the collection contains the passed Object as a key. + * @param {String} key The key to look for in the collection. + * @return {Boolean} True if the collection contains the Object as a key. + */ + containsKey : function(key){ + return typeof this.map[key] != 'undefined'; + }, + + /** + * Removes all items from the collection. Fires the {@link #clear} event when complete. + */ + clear : function(){ + var me = this; + + me.length = 0; + me.items = []; + me.keys = []; + me.map = {}; + me.fireEvent('clear'); + }, + + /** + * Returns the first item in the collection. + * @return {Object} the first item in the collection.. + */ + first : function() { + return this.items[0]; + }, + + /** + * Returns the last item in the collection. + * @return {Object} the last item in the collection.. + */ + last : function() { + return this.items[this.length - 1]; + }, + + /** + * Collects all of the values of the given property and returns their sum + * @param {String} property The property to sum by + * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when + * summing fields in records, where the fields are all stored inside the 'data' object + * @param {Number} start (optional) The record index to start at (defaults to 0) + * @param {Number} end (optional) The record index to end at (defaults to -1) + * @return {Number} The total + */ + sum: function(property, root, start, end) { + var values = this.extractValues(property, root), + length = values.length, + sum = 0, + i; + + start = start || 0; + end = (end || end === 0) ? end : length - 1; + + for (i = start; i <= end; i++) { + sum += values[i]; + } + + return sum; + }, + + /** + * Collects unique values of a particular property in this MixedCollection + * @param {String} property The property to collect on + * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when + * summing fields in records, where the fields are all stored inside the 'data' object + * @param {Boolean} allowBlank (optional) Pass true to allow null, undefined or empty string values + * @return {Array} The unique values + */ + collect: function(property, root, allowNull) { + var values = this.extractValues(property, root), + length = values.length, + hits = {}, + unique = [], + value, strValue, i; + + for (i = 0; i < length; i++) { + value = values[i]; + strValue = String(value); + + if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) { + hits[strValue] = true; + unique.push(value); + } + } + + return unique; + }, + + /** + * @private + * Extracts all of the given property values from the items in the MC. Mainly used as a supporting method for + * functions like sum and collect. + * @param {String} property The property to extract + * @param {String} root Optional 'root' property to extract the first argument from. This is used mainly when + * extracting field data from Model instances, where the fields are stored inside the 'data' object + * @return {Array} The extracted values + */ + extractValues: function(property, root) { + var values = this.items; + + if (root) { + values = Ext.Array.pluck(values, root); + } + + return Ext.Array.pluck(values, property); + }, + + /** + * Returns a range of items in this collection + * @param {Number} startIndex (optional) The starting index. Defaults to 0. + * @param {Number} endIndex (optional) The ending index. Defaults to the last item. + * @return {Array} An array of items + */ + getRange : function(start, end){ + var me = this, + items = me.items, + range = [], + i; + + if (items.length < 1) { + return range; + } + + start = start || 0; + end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1); + if (start <= end) { + for (i = start; i <= end; i++) { + range[range.length] = items[i]; + } + } else { + for (i = start; i >= end; i--) { + range[range.length] = items[i]; + } + } + return range; + }, + + /** + *

Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single + * property/value pair with optional parameters for substring matching and case sensitivity. See + * {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively, + * MixedCollection can be easily filtered by property like this:

+

+//create a simple store with a few people defined
+var people = new Ext.util.MixedCollection();
+people.addAll([
+    {id: 1, age: 25, name: 'Ed'},
+    {id: 2, age: 24, name: 'Tommy'},
+    {id: 3, age: 24, name: 'Arne'},
+    {id: 4, age: 26, name: 'Aaron'}
+]);
+
+//a new MixedCollection containing only the items where age == 24
+var middleAged = people.filter('age', 24);
+
+ * + * + * @param {Array/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects + * @param {String/RegExp} value Either string that the property values + * should start with or a RegExp to test against the property + * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning + * @param {Boolean} caseSensitive (optional) True for case sensitive comparison (defaults to False). + * @return {MixedCollection} The new filtered collection + */ + filter : function(property, value, anyMatch, caseSensitive) { + var filters = [], + filterFn; + + //support for the simple case of filtering by property/value + if (Ext.isString(property)) { + filters.push(Ext.create('Ext.util.Filter', { + property : property, + value : value, + anyMatch : anyMatch, + caseSensitive: caseSensitive + })); + } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) { + filters = filters.concat(property); + } + + //at this point we have an array of zero or more Ext.util.Filter objects to filter with, + //so here we construct a function that combines these filters by ANDing them together + filterFn = function(record) { + var isMatch = true, + length = filters.length, + i; + + for (i = 0; i < length; i++) { + var filter = filters[i], + fn = filter.filterFn, + scope = filter.scope; + + isMatch = isMatch && fn.call(scope, record); + } + + return isMatch; + }; + + return this.filterBy(filterFn); + }, + + /** + * Filter by a function. Returns a new collection that has been filtered. + * The passed function will be called with each object in the collection. + * If the function returns true, the value is included otherwise it is filtered. + * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this MixedCollection. + * @return {MixedCollection} The new filtered collection + */ + filterBy : function(fn, scope) { + var me = this, + newMC = new this.self(), + keys = me.keys, + items = me.items, + length = items.length, + i; + + newMC.getKey = me.getKey; + + for (i = 0; i < length; i++) { + if (fn.call(scope || me, items[i], keys[i])) { + newMC.add(keys[i], items[i]); + } + } + + return newMC; + }, + + /** + * Finds the index of the first matching object in this collection by a specific property/value. + * @param {String} property The name of a property on your objects. + * @param {String/RegExp} value A string that the property values + * should start with or a RegExp to test against the property. + * @param {Number} start (optional) The index to start searching at (defaults to 0). + * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning. + * @param {Boolean} caseSensitive (optional) True for case sensitive comparison. + * @return {Number} The matched index or -1 + */ + findIndex : function(property, value, start, anyMatch, caseSensitive){ + if(Ext.isEmpty(value, false)){ + return -1; + } + value = this.createValueMatcher(value, anyMatch, caseSensitive); + return this.findIndexBy(function(o){ + return o && value.test(o[property]); + }, null, start); + }, + + /** + * Find the index of the first matching object in this collection by a function. + * If the function returns true it is considered a match. + * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key). + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this MixedCollection. + * @param {Number} start (optional) The index to start searching at (defaults to 0). + * @return {Number} The matched index or -1 + */ + findIndexBy : function(fn, scope, start){ + var me = this, + keys = me.keys, + items = me.items, + i = start || 0, + len = items.length; + + for (; i < len; i++) { + if (fn.call(scope || me, items[i], keys[i])) { + return i; + } + } + return -1; + }, + + /** + * Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering, + * and by Ext.data.Store#filter + * @private + * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe + * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors will be added. Defaults to false + * @param {Boolean} caseSensitive True to make the regex case sensitive (adds 'i' switch to regex). Defaults to false. + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true. + */ + createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) { + if (!value.exec) { // not a regex + var er = Ext.String.escapeRegex; + value = String(value); + + if (anyMatch === true) { + value = er(value); + } else { + value = '^' + er(value); + if (exactMatch === true) { + value += '$'; + } + } + value = new RegExp(value, caseSensitive ? '' : 'i'); + } + return value; + }, + + /** + * Creates a shallow copy of this collection + * @return {MixedCollection} + */ + clone : function() { + var me = this, + copy = new this.self(), + keys = me.keys, + items = me.items, + i = 0, + len = items.length; + + for(; i < len; i++){ + copy.add(keys[i], items[i]); + } + copy.getKey = me.getKey; + return copy; + } +}); + +/** + * @class Ext.util.Sortable + +A mixin which allows a data component to be sorted. This is used by e.g. {@link Ext.data.Store} and {@link Ext.data.TreeStore}. + +**NOTE**: This mixin is mainly for internal library use and most users should not need to use it directly. It +is more likely you will want to use one of the component classes that import this mixin, such as +{@link Ext.data.Store} or {@link Ext.data.TreeStore}. + * @markdown + * @docauthor Tommy Maintz + */ +Ext.define("Ext.util.Sortable", { + /** + * @property isSortable + * @type Boolean + * Flag denoting that this object is sortable. Always true. + */ + isSortable: true, + + /** + * The default sort direction to use if one is not specified (defaults to "ASC") + * @property defaultSortDirection + * @type String + */ + defaultSortDirection: "ASC", + + requires: [ + 'Ext.util.Sorter' + ], + + /** + * The property in each item that contains the data to sort. (defaults to null) + * @type String + */ + sortRoot: null, + + /** + * Performs initialization of this mixin. Component classes using this mixin should call this method + * during their own initialization. + */ + initSortable: function() { + var me = this, + sorters = me.sorters; + + /** + * The collection of {@link Ext.util.Sorter Sorters} currently applied to this Store + * @property sorters + * @type Ext.util.MixedCollection + */ + me.sorters = Ext.create('Ext.util.AbstractMixedCollection', false, function(item) { + return item.id || item.property; + }); + + if (sorters) { + me.sorters.addAll(me.decodeSorters(sorters)); + } + }, + + /** + *

Sorts the data in the Store by one or more of its properties. Example usage:

+

+//sort by a single field
+myStore.sort('myField', 'DESC');
+
+//sorting by multiple fields
+myStore.sort([
+    {
+        property : 'age',
+        direction: 'ASC'
+    },
+    {
+        property : 'name',
+        direction: 'DESC'
+    }
+]);
+
+ *

Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter} instances, and delegates the actual + * sorting to its internal {@link Ext.util.MixedCollection}.

+ *

When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field, so this code:

+

+store.sort('myField');
+store.sort('myField');
+     
+ *

Is equivalent to this code, because Store handles the toggling automatically:

+

+store.sort('myField', 'ASC');
+store.sort('myField', 'DESC');
+
+ * @param {String|Array} sorters Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model}, + * or an Array of sorter configurations. + * @param {String} direction The overall direction to sort the data by. Defaults to "ASC". + */ + sort: function(sorters, direction, where, doSort) { + var me = this, + sorter, sorterFn, + newSorters; + + if (Ext.isArray(sorters)) { + doSort = where; + where = direction; + newSorters = sorters; + } + else if (Ext.isObject(sorters)) { + doSort = where; + where = direction; + newSorters = [sorters]; + } + else if (Ext.isString(sorters)) { + sorter = me.sorters.get(sorters); + + if (!sorter) { + sorter = { + property : sorters, + direction: direction + }; + newSorters = [sorter]; + } + else if (direction === undefined) { + sorter.toggle(); + } + else { + sorter.setDirection(direction); + } + } + + if (newSorters && newSorters.length) { + newSorters = me.decodeSorters(newSorters); + if (Ext.isString(where)) { + if (where === 'prepend') { + sorters = me.sorters.clone().items; + + me.sorters.clear(); + me.sorters.addAll(newSorters); + me.sorters.addAll(sorters); + } + else { + me.sorters.addAll(newSorters); + } + } + else { + me.sorters.clear(); + me.sorters.addAll(newSorters); + } + + if (doSort !== false) { + me.onBeforeSort(newSorters); + } + } + + if (doSort !== false) { + sorters = me.sorters.items; + if (sorters.length) { + //construct an amalgamated sorter function which combines all of the Sorters passed + sorterFn = function(r1, r2) { + var result = sorters[0].sort(r1, r2), + length = sorters.length, + i; + + //if we have more than one sorter, OR any additional sorter functions together + for (i = 1; i < length; i++) { + result = result || sorters[i].sort.call(this, r1, r2); + } + + return result; + }; + + me.doSort(sorterFn); + } + } + + return sorters; + }, + + onBeforeSort: Ext.emptyFn, + + /** + * @private + * Normalizes an array of sorter objects, ensuring that they are all Ext.util.Sorter instances + * @param {Array} sorters The sorters array + * @return {Array} Array of Ext.util.Sorter objects + */ + decodeSorters: function(sorters) { + if (!Ext.isArray(sorters)) { + if (sorters === undefined) { + sorters = []; + } else { + sorters = [sorters]; + } + } + + var length = sorters.length, + Sorter = Ext.util.Sorter, + fields = this.model ? this.model.prototype.fields : null, + field, + config, i; + + for (i = 0; i < length; i++) { + config = sorters[i]; + + if (!(config instanceof Sorter)) { + if (Ext.isString(config)) { + config = { + property: config + }; + } + + Ext.applyIf(config, { + root : this.sortRoot, + direction: "ASC" + }); + + //support for 3.x style sorters where a function can be defined as 'fn' + if (config.fn) { + config.sorterFn = config.fn; + } + + //support a function to be passed as a sorter definition + if (typeof config == 'function') { + config = { + sorterFn: config + }; + } + + // ensure sortType gets pushed on if necessary + if (fields && !config.transform) { + field = fields.get(config.property); + config.transform = field ? field.sortType : undefined; + } + sorters[i] = Ext.create('Ext.util.Sorter', config); + } + } + + return sorters; + }, + + getSorters: function() { + return this.sorters.items; + }, + + /** + * Returns an object describing the current sort state of this Store. + * @return {Object} The sort state of the Store. An object with two properties:
    + *
  • field : String

    The name of the field by which the Records are sorted.

  • + *
  • direction : String

    The sort order, 'ASC' or 'DESC' (case-sensitive).

  • + *
+ * See {@link #sortInfo} for additional details. + */ + getSortState : function() { + return this.sortInfo; + } +}); +/** + * @class Ext.util.MixedCollection + *

+ * Represents a collection of a set of key and value pairs. Each key in the MixedCollection + * must be unique, the same key cannot exist twice. This collection is ordered, items in the + * collection can be accessed by index or via the key. Newly added items are added to + * the end of the collection. This class is similar to {@link Ext.util.HashMap} however it + * is heavier and provides more functionality. Sample usage: + *


+var coll = new Ext.util.MixedCollection();
+coll.add('key1', 'val1');
+coll.add('key2', 'val2');
+coll.add('key3', 'val3');
+
+console.log(coll.get('key1')); // prints 'val1'
+console.log(coll.indexOfKey('key3')); // prints 2
+ * 
+ * + *

+ * The MixedCollection also has support for sorting and filtering of the values in the collection. + *


+var coll = new Ext.util.MixedCollection();
+coll.add('key1', 100);
+coll.add('key2', -100);
+coll.add('key3', 17);
+coll.add('key4', 0);
+var biggerThanZero = coll.filterBy(function(value){
+    return value > 0;
+});
+console.log(biggerThanZero.getCount()); // prints 2
+ * 
+ *

+ * + * @constructor + * @param {Boolean} allowFunctions Specify true if the {@link #addAll} + * function should add function references to the collection. Defaults to + * false. + * @param {Function} keyFn A function that can accept an item of the type(s) stored in this MixedCollection + * and return the key value for that item. This is used when available to look up the key on items that + * were passed without an explicit key parameter to a MixedCollection method. Passing this parameter is + * equivalent to providing an implementation for the {@link #getKey} method. + */ +Ext.define('Ext.util.MixedCollection', { + extend: 'Ext.util.AbstractMixedCollection', + mixins: { + sortable: 'Ext.util.Sortable' + }, + + constructor: function() { + var me = this; + me.callParent(arguments); + me.addEvents('sort'); + me.mixins.sortable.initSortable.call(me); + }, + + doSort: function(sorterFn) { + this.sortBy(sorterFn); + }, + + /** + * @private + * Performs the actual sorting based on a direction and a sorting function. Internally, + * this creates a temporary array of all items in the MixedCollection, sorts it and then writes + * the sorted array data back into this.items and this.keys + * @param {String} property Property to sort by ('key', 'value', or 'index') + * @param {String} dir (optional) Direction to sort 'ASC' or 'DESC'. Defaults to 'ASC'. + * @param {Function} fn (optional) Comparison function that defines the sort order. + * Defaults to sorting by numeric value. + */ + _sort : function(property, dir, fn){ + var me = this, + i, len, + dsc = String(dir).toUpperCase() == 'DESC' ? -1 : 1, + + //this is a temporary array used to apply the sorting function + c = [], + keys = me.keys, + items = me.items; + + //default to a simple sorter function if one is not provided + fn = fn || function(a, b) { + return a - b; + }; + + //copy all the items into a temporary array, which we will sort + for(i = 0, len = items.length; i < len; i++){ + c[c.length] = { + key : keys[i], + value: items[i], + index: i + }; + } + + //sort the temporary array + Ext.Array.sort(c, function(a, b){ + var v = fn(a[property], b[property]) * dsc; + if(v === 0){ + v = (a.index < b.index ? -1 : 1); + } + return v; + }); + + //copy the temporary array back into the main this.items and this.keys objects + for(i = 0, len = c.length; i < len; i++){ + items[i] = c[i].value; + keys[i] = c[i].key; + } + + me.fireEvent('sort', me); + }, + + /** + * Sorts the collection by a single sorter function + * @param {Function} sorterFn The function to sort by + */ + sortBy: function(sorterFn) { + var me = this, + items = me.items, + keys = me.keys, + length = items.length, + temp = [], + i; + + //first we create a copy of the items array so that we can sort it + for (i = 0; i < length; i++) { + temp[i] = { + key : keys[i], + value: items[i], + index: i + }; + } + + Ext.Array.sort(temp, function(a, b) { + var v = sorterFn(a.value, b.value); + if (v === 0) { + v = (a.index < b.index ? -1 : 1); + } + + return v; + }); + + //copy the temporary array back into the main this.items and this.keys objects + for (i = 0; i < length; i++) { + items[i] = temp[i].value; + keys[i] = temp[i].key; + } + + me.fireEvent('sort', me, items, keys); + }, + + /** + * Reorders each of the items based on a mapping from old index to new index. Internally this + * just translates into a sort. The 'sort' event is fired whenever reordering has occured. + * @param {Object} mapping Mapping from old item index to new item index + */ + reorder: function(mapping) { + var me = this, + items = me.items, + index = 0, + length = items.length, + order = [], + remaining = [], + oldIndex; + + me.suspendEvents(); + + //object of {oldPosition: newPosition} reversed to {newPosition: oldPosition} + for (oldIndex in mapping) { + order[mapping[oldIndex]] = items[oldIndex]; + } + + for (index = 0; index < length; index++) { + if (mapping[index] == undefined) { + remaining.push(items[index]); + } + } + + for (index = 0; index < length; index++) { + if (order[index] == undefined) { + order[index] = remaining.shift(); + } + } + + me.clear(); + me.addAll(order); + + me.resumeEvents(); + me.fireEvent('sort', me); + }, + + /** + * Sorts this collection by keys. + * @param {String} direction (optional) 'ASC' or 'DESC'. Defaults to 'ASC'. + * @param {Function} fn (optional) Comparison function that defines the sort order. + * Defaults to sorting by case insensitive string. + */ + sortByKey : function(dir, fn){ + this._sort('key', dir, fn || function(a, b){ + var v1 = String(a).toUpperCase(), v2 = String(b).toUpperCase(); + return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0); + }); + } +}); + +/** + * @class Ext.data.StoreManager + * @extends Ext.util.MixedCollection + *

Contains a collection of all stores that are created that have an identifier. + * An identifier can be assigned by setting the {@link Ext.data.AbstractStore#storeId storeId} + * property. When a store is in the StoreManager, it can be referred to via it's identifier: + *


+Ext.create('Ext.data.Store', {
+    model: 'SomeModel',
+    storeId: 'myStore'
+});
+
+var store = Ext.data.StoreManager.lookup('myStore');
+ * 
+ * Also note that the {@link #lookup} method is aliased to {@link Ext#getStore} for convenience.

+ *

+ * If a store is registered with the StoreManager, you can also refer to the store by it's identifier when + * registering it with any Component that consumes data from a store: + *


+Ext.create('Ext.data.Store', {
+    model: 'SomeModel',
+    storeId: 'myStore'
+});
+
+Ext.create('Ext.view.View', {
+    store: 'myStore',
+    // other configuration here
+});
+ * 
+ *

+ * @singleton + * @docauthor Evan Trimboli + * TODO: Make this an AbstractMgr + */ +Ext.define('Ext.data.StoreManager', { + extend: 'Ext.util.MixedCollection', + alternateClassName: ['Ext.StoreMgr', 'Ext.data.StoreMgr', 'Ext.StoreManager'], + singleton: true, + uses: ['Ext.data.ArrayStore'], + + /** + * @cfg {Object} listeners @hide + */ + + /** + * Registers one or more Stores with the StoreManager. You do not normally need to register stores + * manually. Any store initialized with a {@link Ext.data.Store#storeId} will be auto-registered. + * @param {Ext.data.Store} store1 A Store instance + * @param {Ext.data.Store} store2 (optional) + * @param {Ext.data.Store} etc... (optional) + */ + register : function() { + for (var i = 0, s; (s = arguments[i]); i++) { + this.add(s); + } + }, + + /** + * Unregisters one or more Stores with the StoreManager + * @param {String/Object} id1 The id of the Store, or a Store instance + * @param {String/Object} id2 (optional) + * @param {String/Object} etc... (optional) + */ + unregister : function() { + for (var i = 0, s; (s = arguments[i]); i++) { + this.remove(this.lookup(s)); + } + }, + + /** + * Gets a registered Store by id + * @param {String/Object} id The id of the Store, or a Store instance, or a store configuration + * @return {Ext.data.Store} + */ + lookup : function(store) { + // handle the case when we are given an array or an array of arrays. + if (Ext.isArray(store)) { + var fields = ['field1'], + expand = !Ext.isArray(store[0]), + data = store, + i, + len; + + if(expand){ + data = []; + for (i = 0, len = store.length; i < len; ++i) { + data.push([store[i]]); + } + } else { + for(i = 2, len = store[0].length; i <= len; ++i){ + fields.push('field' + i); + } + } + return Ext.create('Ext.data.ArrayStore', { + data : data, + fields: fields, + autoDestroy: true, + autoCreated: true, + expanded: expand + }); + } + + if (Ext.isString(store)) { + // store id + return this.get(store); + } else { + // store instance or store config + return Ext.data.AbstractStore.create(store); + } + }, + + // getKey implementation for MixedCollection + getKey : function(o) { + return o.storeId; + } +}, function() { + /** + *

Creates a new store for the given id and config, then registers it with the {@link Ext.data.StoreManager Store Mananger}. + * Sample usage:

+

+    Ext.regStore('AllUsers', {
+        model: 'User'
+    });
+
+    //the store can now easily be used throughout the application
+    new Ext.List({
+        store: 'AllUsers',
+        ... other config
+    });
+    
+ * @param {String} id The id to set on the new store + * @param {Object} config The store config + * @param {Constructor} cls The new Component class. + * @member Ext + * @method regStore + */ + Ext.regStore = function(name, config) { + var store; + + if (Ext.isObject(name)) { + config = name; + } else { + config.storeId = name; + } + + if (config instanceof Ext.data.Store) { + store = config; + } else { + store = Ext.create('Ext.data.Store', config); + } + + return Ext.data.StoreManager.register(store); + }; + + /** + * Gets a registered Store by id (shortcut to {@link #lookup}) + * @param {String/Object} id The id of the Store, or a Store instance + * @return {Ext.data.Store} + * @member Ext + * @method getStore + */ + Ext.getStore = function(name) { + return Ext.data.StoreManager.lookup(name); + }; +}); + +/** + * @class Ext.LoadMask + * A simple utility class for generically masking elements while loading data. If the {@link #store} + * config option is specified, the masking will be automatically synchronized with the store's loading + * process and the mask element will be cached for reuse. + *

Example usage:

+ *

+// Basic mask:
+var myMask = new Ext.LoadMask(Ext.getBody(), {msg:"Please wait..."});
+myMask.show();
+
+ + * @constructor + * Create a new LoadMask + * @param {Mixed} el The element, element ID, or DOM node you wish to mask. Also, may be a Component who's element you wish to mask. + * @param {Object} config The config object + */ + +Ext.define('Ext.LoadMask', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: ['Ext.data.StoreManager'], + + /* End Definitions */ + + /** + * @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. + */ + + /** + * @cfg {String} msg + * The text to display in a centered loading message box (defaults to 'Loading...') + */ + msg : 'Loading...', + /** + * @cfg {String} msgCls + * The CSS class to apply to the loading message element (defaults to "x-mask-loading") + */ + msgCls : Ext.baseCSSPrefix + 'mask-loading', + + /** + * @cfg {Boolean} useMsg + * Whether or not to use a loading message class or simply mask the bound element. + */ + useMsg: true, + + /** + * Read-only. True if the mask is currently disabled so that it will not be displayed (defaults to false) + * @type Boolean + */ + disabled: false, + + constructor : function(el, config) { + var me = this; + + if (el.isComponent) { + me.bindComponent(el); + } else { + me.el = Ext.get(el); + } + Ext.apply(me, config); + + me.addEvents('beforeshow', 'show', 'hide'); + if (me.store) { + me.bindStore(me.store, true); + } + me.mixins.observable.constructor.call(me, config); + }, + + bindComponent: function(comp) { + var me = this, + listeners = { + resize: me.onComponentResize, + scope: me + }; + + if (comp.el) { + me.onComponentRender(comp); + } else { + listeners.render = { + fn: me.onComponentRender, + scope: me, + single: true + }; + } + me.mon(comp, listeners); + }, + + /** + * @private + * Called if we were configured with a Component, and that Component was not yet rendered. Collects the element to mask. + */ + onComponentRender: function(comp) { + this.el = comp.getContentTarget(); + }, + + /** + * @private + * Called when this LoadMask's Component is resized. The isMasked method also re-centers any displayed message. + */ + onComponentResize: function(comp, w, h) { + this.el.isMasked(); + }, + + /** + * Changes the data store bound to this LoadMask. + * @param {Store} store The store to bind to this LoadMask + */ + bindStore : function(store, initial) { + var me = this; + + if (!initial && me.store) { + me.mun(me.store, { + scope: me, + beforeload: me.onBeforeLoad, + load: me.onLoad, + exception: me.onLoad + }); + if(!store) { + me.store = null; + } + } + if (store) { + store = Ext.data.StoreManager.lookup(store); + me.mon(store, { + scope: me, + beforeload: me.onBeforeLoad, + load: me.onLoad, + exception: me.onLoad + }); + + } + me.store = store; + if (store && store.isLoading()) { + me.onBeforeLoad(); + } + }, + + /** + * Disables the mask to prevent it from being displayed + */ + disable : function() { + var me = this; + + me.disabled = true; + if (me.loading) { + me.onLoad(); + } + }, + + /** + * Enables the mask so that it can be displayed + */ + enable : function() { + this.disabled = false; + }, + + /** + * Method to determine whether this LoadMask is currently disabled. + * @return {Boolean} the disabled state of this LoadMask. + */ + isDisabled : function() { + return this.disabled; + }, + + // private + onLoad : function() { + var me = this; + + me.loading = false; + me.el.unmask(); + me.fireEvent('hide', me, me.el, me.store); + }, + + // private + onBeforeLoad : function() { + var me = this; + + if (!me.disabled && !me.loading && me.fireEvent('beforeshow', me, me.el, me.store) !== false) { + if (me.useMsg) { + me.el.mask(me.msg, me.msgCls, false); + } else { + me.el.mask(); + } + + me.fireEvent('show', me, me.el, me.store); + me.loading = true; + } + }, + + /** + * Show this LoadMask over the configured Element. + */ + show: function() { + this.onBeforeLoad(); + }, + + /** + * Hide this LoadMask. + */ + hide: function() { + this.onLoad(); + }, + + // private + destroy : function() { + this.hide(); + this.clearListeners(); + } +}); + +/** + * @class Ext.ComponentLoader + * @extends Ext.ElementLoader + * + * This class is used to load content via Ajax into a {@link Ext.Component}. In general + * this class will not be instanced directly, rather a loader configuration will be passed to the + * constructor of the {@link Ext.Component}. + * + * ## HTML Renderer + * By default, the content loaded will be processed as raw html. The response text + * from the request is taken and added to the component. This can be used in + * conjunction with the {@link #scripts} option to execute any inline scripts in + * the resulting content. Using this renderer has the same effect as passing the + * {@link Ext.Component#html} configuration option. + * + * ## Data Renderer + * This renderer allows content to be added by using JSON data and a {@link Ext.XTemplate}. + * The content received from the response is passed to the {@link Ext.Component#update} method. + * This content is run through the attached {@link Ext.Component#tpl} and the data is added to + * the Component. Using this renderer has the same effect as using the {@link Ext.Component#data} + * configuration in conjunction with a {@link Ext.Component#tpl}. + * + * ## Component Renderer + * This renderer can only be used with a {@link Ext.Container} and subclasses. It allows for + * Components to be loaded remotely into a Container. The response is expected to be a single/series of + * {@link Ext.Component} configuration objects. When the response is received, the data is decoded + * and then passed to {@link Ext.Container#add}. Using this renderer has the same effect as specifying + * the {@link Ext.Container#items} configuration on a Container. + * + * ## Custom Renderer + * A custom function can be passed to handle any other special case, see the {@link #renderer} option. + * + * ## Example Usage + * new Ext.Component({ + * tpl: '{firstName} - {lastName}', + * loader: { + * url: 'myPage.php', + * renderer: 'data', + * params: { + * userId: 1 + * } + * } + * }); + */ +Ext.define('Ext.ComponentLoader', { + + /* Begin Definitions */ + + extend: 'Ext.ElementLoader', + + statics: { + Renderer: { + Data: function(loader, response, active){ + var success = true; + try { + loader.getTarget().update(Ext.decode(response.responseText)); + } catch (e) { + success = false; + } + return success; + }, + + Component: function(loader, response, active){ + var success = true, + target = loader.getTarget(), + items = []; + + // + if (!target.isContainer) { + Ext.Error.raise({ + target: target, + msg: 'Components can only be loaded into a container' + }); + } + // + + try { + items = Ext.decode(response.responseText); + } catch (e) { + success = false; + } + + if (success) { + if (active.removeAll) { + target.removeAll(); + } + target.add(items); + } + return success; + } + } + }, + + /* End Definitions */ + + /** + * @cfg {Ext.Component/String} target The target {@link Ext.Component} for the loader. Defaults to null. + * If a string is passed it will be looked up via the id. + */ + target: null, + + /** + * @cfg {Mixed} loadMask True or a {@link Ext.LoadMask} configuration to enable masking during loading. Defaults to false. + */ + loadMask: false, + + /** + * @cfg {Boolean} scripts True to parse any inline script tags in the response. This only used when using the html + * {@link #renderer}. + */ + + /** + * @cfg {String/Function} renderer + +The type of content that is to be loaded into, which can be one of 3 types: + ++ **html** : Loads raw html content, see {@link Ext.Component#html} ++ **data** : Loads raw html content, see {@link Ext.Component#data} ++ **component** : Loads child {Ext.Component} instances. This option is only valid when used with a Container. + +Defaults to `html`. + +Alternatively, you can pass a function which is called with the following parameters. + ++ loader - Loader instance ++ response - The server response ++ active - The active request + +The function must return false is loading is not successful. Below is a sample of using a custom renderer: + + new Ext.Component({ + loader: { + url: 'myPage.php', + renderer: function(loader, response, active) { + var text = response.responseText; + loader.getTarget().update('The response is ' + text); + return true; + } + } + }); + * @markdown + */ + renderer: 'html', + + /** + * Set a {Ext.Component} as the target of this loader. Note that if the target is changed, + * any active requests will be aborted. + * @param {String/Ext.Component} target The component to be the target of this loader. If a string is passed + * it will be looked up via its id. + */ + setTarget: function(target){ + var me = this; + + if (Ext.isString(target)) { + target = Ext.getCmp(target); + } + + if (me.target && me.target != target) { + me.abort(); + } + me.target = target; + }, + + // inherit docs + removeMask: function(){ + this.target.setLoading(false); + }, + + /** + * Add the mask on the target + * @private + * @param {Mixed} mask The mask configuration + */ + addMask: function(mask){ + this.target.setLoading(mask); + }, + + /** + * Get the target of this loader. + * @return {Ext.Component} target The target, null if none exists. + */ + + setOptions: function(active, options){ + active.removeAll = Ext.isDefined(options.removeAll) ? options.removeAll : this.removeAll; + }, + + /** + * Gets the renderer to use + * @private + * @param {String/Function} renderer The renderer to use + * @return {Function} A rendering function to use. + */ + getRenderer: function(renderer){ + if (Ext.isFunction(renderer)) { + return renderer; + } + + var renderers = this.statics().Renderer; + switch (renderer) { + case 'component': + return renderers.Component; + case 'data': + return renderers.Data; + default: + return Ext.ElementLoader.Renderer.Html; + } + } +}); + +/** + * @class Ext.layout.component.Auto + * @extends Ext.layout.component.Component + * @private + * + *

The AutoLayout is the default layout manager delegated by {@link Ext.Component} to + * render any child Elements when no {@link Ext.Component#layout layout} is configured.

+ */ + +Ext.define('Ext.layout.component.Auto', { + + /* Begin Definitions */ + + alias: 'layout.autocomponent', + + extend: 'Ext.layout.component.Component', + + /* End Definitions */ + + type: 'autocomponent', + + onLayout : function(width, height) { + this.setTargetSize(width, height); + } +}); +/** + * @class Ext.AbstractComponent + *

An abstract base class which provides shared methods for Components across the Sencha product line.

+ *

Please refer to sub class's documentation

+ * @constructor + */ + +Ext.define('Ext.AbstractComponent', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable', + animate: 'Ext.util.Animate', + state: 'Ext.state.Stateful' + }, + + requires: [ + 'Ext.PluginManager', + 'Ext.ComponentManager', + 'Ext.core.Element', + 'Ext.core.DomHelper', + 'Ext.XTemplate', + 'Ext.ComponentQuery', + 'Ext.LoadMask', + 'Ext.ComponentLoader', + 'Ext.EventManager', + 'Ext.layout.Layout', + 'Ext.layout.component.Auto' + ], + + // Please remember to add dependencies whenever you use it + // I had to fix these many times already + uses: [ + 'Ext.ZIndexManager' + ], + + statics: { + AUTO_ID: 1000 + }, + + /* End Definitions */ + + isComponent: true, + + getAutoId: function() { + return ++Ext.AbstractComponent.AUTO_ID; + }, + + /** + * @cfg {String} id + *

The unique id of this component instance (defaults to an {@link #getId auto-assigned id}).

+ *

It should not be necessary to use this configuration except for singleton objects in your application. + * Components created with an id may be accessed globally using {@link Ext#getCmp Ext.getCmp}.

+ *

Instead of using assigned ids, use the {@link #itemId} config, and {@link Ext.ComponentQuery ComponentQuery} which + * provides selector-based searching for Sencha Components analogous to DOM querying. The {@link Ext.container.Container Container} + * class contains {@link Ext.container.Container#down shortcut methods} to query its descendant Components by selector.

+ *

Note that this id will also be used as the element id for the containing HTML element + * that is rendered to the page for this component. This allows you to write id-based CSS + * rules to style the specific instance of this component uniquely, and also to select + * sub-elements using this component's id as the parent.

+ *

Note: to avoid complications imposed by a unique id also see {@link #itemId}.

+ *

Note: to access the container of a Component see {@link #ownerCt}.

+ */ + + /** + * @cfg {String} itemId + *

An itemId can be used as an alternative way to get a reference to a component + * when no object reference is available. Instead of using an {@link #id} with + * {@link Ext}.{@link Ext#getCmp getCmp}, use itemId with + * {@link Ext.container.Container}.{@link Ext.container.Container#getComponent getComponent} which will retrieve + * itemId's or {@link #id}'s. Since itemId's are an index to the + * container's internal MixedCollection, the itemId is scoped locally to the container -- + * avoiding potential conflicts with {@link Ext.ComponentManager} which requires a unique + * {@link #id}.

+ *

+var c = new Ext.panel.Panel({ //
+    {@link Ext.Component#height height}: 300,
+    {@link #renderTo}: document.body,
+    {@link Ext.container.Container#layout layout}: 'auto',
+    {@link Ext.container.Container#items items}: [
+        {
+            itemId: 'p1',
+            {@link Ext.panel.Panel#title title}: 'Panel 1',
+            {@link Ext.Component#height height}: 150
+        },
+        {
+            itemId: 'p2',
+            {@link Ext.panel.Panel#title title}: 'Panel 2',
+            {@link Ext.Component#height height}: 150
+        }
+    ]
+})
+p1 = c.{@link Ext.container.Container#getComponent getComponent}('p1'); // not the same as {@link Ext#getCmp Ext.getCmp()}
+p2 = p1.{@link #ownerCt}.{@link Ext.container.Container#getComponent getComponent}('p2'); // reference via a sibling
+     * 
+ *

Also see {@link #id}, {@link #query}, {@link #down} and {@link #child}.

+ *

Note: to access the container of an item see {@link #ownerCt}.

+ */ + + /** + * This Component's owner {@link Ext.container.Container Container} (defaults to undefined, and is set automatically when + * this Component is added to a Container). Read-only. + *

Note: to access items within the Container see {@link #itemId}.

+ * @type Ext.Container + * @property ownerCt + */ + + /** + * @cfg {Mixed} autoEl + *

A tag name or {@link Ext.core.DomHelper DomHelper} spec used to create the {@link #getEl Element} which will + * encapsulate this Component.

+ *

You do not normally need to specify this. For the base classes {@link Ext.Component} and {@link Ext.container.Container}, + * this defaults to 'div'. The more complex Sencha classes use a more complex + * DOM structure specified by their own {@link #renderTpl}s.

+ *

This is intended to allow the developer to create application-specific utility Components encapsulated by + * different DOM elements. Example usage:


+{
+    xtype: 'component',
+    autoEl: {
+        tag: 'img',
+        src: 'http://www.example.com/example.jpg'
+    }
+}, {
+    xtype: 'component',
+    autoEl: {
+        tag: 'blockquote',
+        html: 'autoEl is cool!'
+    }
+}, {
+    xtype: 'container',
+    autoEl: 'ul',
+    cls: 'ux-unordered-list',
+    items: {
+        xtype: 'component',
+        autoEl: 'li',
+        html: 'First list item'
+    }
+}
+
+ */ + + /** + * @cfg {Mixed} renderTpl + *

An {@link Ext.XTemplate XTemplate} used to create the internal structure inside this Component's + * encapsulating {@link #getEl Element}.

+ *

You do not normally need to specify this. For the base classes {@link Ext.Component} + * and {@link Ext.container.Container}, this defaults to null which means that they will be initially rendered + * with no internal structure; they render their {@link #getEl Element} empty. The more specialized ExtJS and Touch classes + * which use a more complex DOM structure, provide their own template definitions.

+ *

This is intended to allow the developer to create application-specific utility Components with customized + * internal structure.

+ *

Upon rendering, any created child elements may be automatically imported into object properties using the + * {@link #renderSelectors} option.

+ */ + renderTpl: null, + + /** + * @cfg {Object} renderSelectors + +An object containing properties specifying {@link Ext.DomQuery DomQuery} selectors which identify child elements +created by the render process. + +After the Component's internal structure is rendered according to the {@link #renderTpl}, this object is iterated through, +and the found Elements are added as properties to the Component using the `renderSelector` property name. + +For example, a Component which rendered an image, and description into its element might use the following properties +coded into its prototype: + + renderTpl: '<img src="{imageUrl}" class="x-image-component-img"><div class="x-image-component-desc">{description}>/div<', + + renderSelectors: { + image: 'img.x-image-component-img', + descEl: 'div.x-image-component-desc' + } + +After rendering, the Component would have a property image referencing its child `img` Element, +and a property `descEl` referencing the `div` Element which contains the description. + + * @markdown + */ + + /** + * @cfg {Mixed} renderTo + *

Specify the id of the element, a DOM element or an existing Element that this component + * will be rendered into.

    + *
  • Notes :
      + *
      Do not use this option if the Component is to be a child item of + * a {@link Ext.container.Container Container}. It is the responsibility of the + * {@link Ext.container.Container Container}'s {@link Ext.container.Container#layout layout manager} + * to render and manage its child items.
      + *
      When using this config, a call to render() is not required.
      + *
  • + *
+ *

See {@link #render} also.

+ */ + + /** + * @cfg {Boolean} frame + *

Specify as true to have the Component inject framing elements within the Component at render time to + * provide a graphical rounded frame around the Component content.

+ *

This is only necessary when running on outdated, or non standard-compliant browsers such as Microsoft's Internet Explorer + * prior to version 9 which do not support rounded corners natively.

+ *

The extra space taken up by this framing is available from the read only property {@link #frameSize}.

+ */ + + /** + *

Read-only property indicating the width of any framing elements which were added within the encapsulating element + * to provide graphical, rounded borders. See the {@link #frame} config.

+ *

This is an object containing the frame width in pixels for all four sides of the Component containing + * the following properties:

    + *
  • top The width of the top framing element in pixels.
  • + *
  • right The width of the right framing element in pixels.
  • + *
  • bottom The width of the bottom framing element in pixels.
  • + *
  • left The width of the left framing element in pixels.
  • + *
+ * @property frameSize + * @type {Object} + */ + + /** + * @cfg {String/Object} componentLayout + *

The sizing and positioning of a Component's internal Elements is the responsibility of + * the Component's layout manager which sizes a Component's internal structure in response to the Component being sized.

+ *

Generally, developers will not use this configuration as all provided Components which need their internal + * elements sizing (Such as {@link Ext.form.field.Base input fields}) come with their own componentLayout managers.

+ *

The {@link Ext.layout.container.Auto default layout manager} will be used on instances of the base Ext.Component class + * which simply sizes the Component's encapsulating element to the height and width specified in the {@link #setSize} method.

+ */ + + /** + * @cfg {Mixed} tpl + * An {@link Ext.Template}, {@link Ext.XTemplate} + * or an array of strings to form an Ext.XTemplate. + * Used in conjunction with the {@link #data} and + * {@link #tplWriteMode} configurations. + */ + + /** + * @cfg {Mixed} data + * The initial set of data to apply to the {@link #tpl} to + * update the content area of the Component. + */ + + /** + * @cfg {String} tplWriteMode The Ext.(X)Template method to use when + * updating the content area of the Component. Defaults to 'overwrite' + * (see {@link Ext.XTemplate#overwrite}). + */ + tplWriteMode: 'overwrite', + + /** + * @cfg {String} baseCls + * The base CSS class to apply to this components's element. This will also be prepended to + * elements within this component like Panel's body will get a class x-panel-body. This means + * that if you create a subclass of Panel, and you want it to get all the Panels styling for the + * element and the body, you leave the baseCls x-panel and use componentCls to add specific styling for this + * component. + */ + baseCls: Ext.baseCSSPrefix + 'component', + + /** + * @cfg {String} componentCls + * CSS Class to be added to a components root level element to give distinction to it + * via styling. + */ + + /** + * @cfg {String} cls + * An optional extra CSS class that will be added to this component's Element (defaults to ''). This can be + * useful for adding customized styles to the component or any of its children using standard CSS rules. + */ + + /** + * @cfg {String} overCls + * An optional extra CSS class that will be added to this component's Element when the mouse moves + * over the Element, and removed when the mouse moves out. (defaults to ''). This can be + * useful for adding customized 'active' or 'hover' styles to the component or any of its children using standard CSS rules. + */ + + /** + * @cfg {String} disabledCls + * CSS class to add when the Component is disabled. Defaults to 'x-item-disabled'. + */ + disabledCls: Ext.baseCSSPrefix + 'item-disabled', + + /** + * @cfg {String/Array} ui + * A set style for a component. Can be a string or an Array of multiple strings (UIs) + */ + ui: 'default', + + /** + * @cfg {Array} uiCls + * An array of of classNames which are currently applied to this component + * @private + */ + uiCls: [], + + /** + * @cfg {String} style + * A custom style specification to be applied to this component's Element. Should be a valid argument to + * {@link Ext.core.Element#applyStyles}. + *

+        new Ext.panel.Panel({
+            title: 'Some Title',
+            renderTo: Ext.getBody(),
+            width: 400, height: 300,
+            layout: 'form',
+            items: [{
+                xtype: 'textarea',
+                style: {
+                    width: '95%',
+                    marginBottom: '10px'
+                }
+            },
+            new Ext.button.Button({
+                text: 'Send',
+                minWidth: '100',
+                style: {
+                    marginBottom: '10px'
+                }
+            })
+            ]
+        });
+     
+ */ + + /** + * @cfg {Number} width + * The width of this component in pixels. + */ + + /** + * @cfg {Number} height + * The height of this component in pixels. + */ + + /** + * @cfg {Number/String} border + * Specifies the border for this component. The border can be a single numeric value to apply to all sides or + * it can be a CSS style specification for each style, for example: '10 5 3 10'. + */ + + /** + * @cfg {Number/String} padding + * Specifies the padding for this component. The padding can be a single numeric value to apply to all sides or + * it can be a CSS style specification for each style, for example: '10 5 3 10'. + */ + + /** + * @cfg {Number/String} margin + * Specifies the margin for this component. The margin can be a single numeric value to apply to all sides or + * it can be a CSS style specification for each style, for example: '10 5 3 10'. + */ + + /** + * @cfg {Boolean} hidden + * Defaults to false. + */ + hidden: false, + + /** + * @cfg {Boolean} disabled + * Defaults to false. + */ + disabled: false, + + /** + * @cfg {Boolean} draggable + * Allows the component to be dragged. + */ + + /** + * Read-only property indicating whether or not the component can be dragged + * @property draggable + * @type {Boolean} + */ + draggable: false, + + /** + * @cfg {Boolean} floating + * Create the Component as a floating and use absolute positioning. + * Defaults to false. + */ + floating: false, + + /** + * @cfg {String} hideMode + * A String which specifies how this Component's encapsulating DOM element will be hidden. + * Values may be
    + *
  • 'display' : The Component will be hidden using the display: none style.
  • + *
  • 'visibility' : The Component will be hidden using the visibility: hidden style.
  • + *
  • 'offsets' : The Component will be hidden by absolutely positioning it out of the visible area of the document. This + * is useful when a hidden Component must maintain measurable dimensions. Hiding using display results + * in a Component having zero dimensions.
+ * Defaults to 'display'. + */ + hideMode: 'display', + + /** + * @cfg {String} contentEl + *

Optional. Specify an existing HTML element, or the id of an existing HTML element to use as the content + * for this component.

+ *
    + *
  • Description : + *
    This config option is used to take an existing HTML element and place it in the layout element + * of a new component (it simply moves the specified DOM element after the Component is rendered to use as the content.
  • + *
  • Notes : + *
    The specified HTML element is appended to the layout element of the component after any configured + * {@link #html HTML} has been inserted, and so the document will not contain this element at the time the {@link #render} event is fired.
    + *
    The specified HTML element used will not participate in any {@link Ext.container.Container#layout layout} + * scheme that the Component may use. It is just HTML. Layouts operate on child {@link Ext.container.Container#items items}.
    + *
    Add either the x-hidden or the x-hide-display CSS class to + * prevent a brief flicker of the content before it is rendered to the panel.
  • + *
+ */ + + /** + * @cfg {String/Object} html + * An HTML fragment, or a {@link Ext.core.DomHelper DomHelper} specification to use as the layout element + * content (defaults to ''). The HTML content is added after the component is rendered, + * so the document will not contain this HTML at the time the {@link #render} event is fired. + * This content is inserted into the body before any configured {@link #contentEl} is appended. + */ + + /** + * @cfg {String} styleHtmlContent + * True to automatically style the html inside the content target of this component (body for panels). + * Defaults to false. + */ + styleHtmlContent: false, + + /** + * @cfg {String} styleHtmlCls + * The class that is added to the content target when you set styleHtmlContent to true. + * Defaults to 'x-html' + */ + styleHtmlCls: Ext.baseCSSPrefix + 'html', + + /** + * @cfg {Number} minHeight + *

The minimum value in pixels which this Component will set its height to.

+ *

Warning: This will override any size management applied by layout managers.

+ */ + /** + * @cfg {Number} minWidth + *

The minimum value in pixels which this Component will set its width to.

+ *

Warning: This will override any size management applied by layout managers.

+ */ + /** + * @cfg {Number} maxHeight + *

The maximum value in pixels which this Component will set its height to.

+ *

Warning: This will override any size management applied by layout managers.

+ */ + /** + * @cfg {Number} maxWidth + *

The maximum value in pixels which this Component will set its width to.

+ *

Warning: This will override any size management applied by layout managers.

+ */ + + /** + * @cfg {Ext.ComponentLoader/Object} loader + * A configuration object or an instance of a {@link Ext.ComponentLoader} to load remote + * content for this Component. + */ + + // @private + allowDomMove: true, + + /** + * @cfg {Boolean} autoShow True to automatically show the component upon creation. + * This config option may only be used for {@link #floating} components or components + * that use {@link #autoRender}. Defaults to false. + */ + autoShow: false, + + /** + * @cfg {Mixed} autoRender + *

This config is intended mainly for {@link #floating} Components which may or may not be shown. Instead + * of using {@link #renderTo} in the configuration, and rendering upon construction, this allows a Component + * to render itself upon first {@link #show}.

+ *

Specify as true to have this Component render to the document body upon first show.

+ *

Specify as an element, or the ID of an element to have this Component render to a specific element upon first show.

+ *

This defaults to true for the {@link Ext.window.Window Window} class.

+ */ + autoRender: false, + + needsLayout: false, + + /** + * @cfg {Object/Array} plugins + * An object or array of objects that will provide custom functionality for this component. The only + * requirement for a valid plugin is that it contain an init method that accepts a reference of type Ext.Component. + * When a component is created, if any plugins are available, the component will call the init method on each + * plugin, passing a reference to itself. Each plugin can then call methods or respond to events on the + * component as needed to provide its functionality. + */ + + /** + * Read-only property indicating whether or not the component has been rendered. + * @property rendered + * @type {Boolean} + */ + rendered: false, + + weight: 0, + + trimRe: /^\s+|\s+$/g, + spacesRe: /\s+/, + + + /** + * This is an internal flag that you use when creating custom components. + * By default this is set to true which means that every component gets a mask when its disabled. + * Components like FieldContainer, FieldSet, Field, Button, Tab override this property to false + * since they want to implement custom disable logic. + * @property maskOnDisable + * @type {Boolean} + */ + maskOnDisable: true, + + constructor : function(config) { + var me = this, + i, len; + + config = config || {}; + me.initialConfig = config; + Ext.apply(me, config); + + me.addEvents( + /** + * @event beforeactivate + * Fires before a Component has been visually activated. + * Returning false from an event listener can prevent the activate + * from occurring. + * @param {Ext.Component} this + */ + 'beforeactivate', + /** + * @event activate + * Fires after a Component has been visually activated. + * @param {Ext.Component} this + */ + 'activate', + /** + * @event beforedeactivate + * Fires before a Component has been visually deactivated. + * Returning false from an event listener can prevent the deactivate + * from occurring. + * @param {Ext.Component} this + */ + 'beforedeactivate', + /** + * @event deactivate + * Fires after a Component has been visually deactivated. + * @param {Ext.Component} this + */ + 'deactivate', + /** + * @event added + * Fires after a Component had been added to a Container. + * @param {Ext.Component} this + * @param {Ext.container.Container} container Parent Container + * @param {Number} pos position of Component + */ + 'added', + /** + * @event disable + * Fires after the component is disabled. + * @param {Ext.Component} this + */ + 'disable', + /** + * @event enable + * Fires after the component is enabled. + * @param {Ext.Component} this + */ + 'enable', + /** + * @event beforeshow + * Fires before the component is shown when calling the {@link #show} method. + * Return false from an event handler to stop the show. + * @param {Ext.Component} this + */ + 'beforeshow', + /** + * @event show + * Fires after the component is shown when calling the {@link #show} method. + * @param {Ext.Component} this + */ + 'show', + /** + * @event beforehide + * Fires before the component is hidden when calling the {@link #hide} method. + * Return false from an event handler to stop the hide. + * @param {Ext.Component} this + */ + 'beforehide', + /** + * @event hide + * Fires after the component is hidden. + * Fires after the component is hidden when calling the {@link #hide} method. + * @param {Ext.Component} this + */ + 'hide', + /** + * @event removed + * Fires when a component is removed from an Ext.container.Container + * @param {Ext.Component} this + * @param {Ext.container.Container} ownerCt Container which holds the component + */ + 'removed', + /** + * @event beforerender + * Fires before the component is {@link #rendered}. Return false from an + * event handler to stop the {@link #render}. + * @param {Ext.Component} this + */ + 'beforerender', + /** + * @event render + * Fires after the component markup is {@link #rendered}. + * @param {Ext.Component} this + */ + 'render', + /** + * @event afterrender + *

Fires after the component rendering is finished.

+ *

The afterrender event is fired after this Component has been {@link #rendered}, been postprocesed + * by any afterRender method defined for the Component.

+ * @param {Ext.Component} this + */ + 'afterrender', + /** + * @event beforedestroy + * Fires before the component is {@link #destroy}ed. Return false from an event handler to stop the {@link #destroy}. + * @param {Ext.Component} this + */ + 'beforedestroy', + /** + * @event destroy + * Fires after the component is {@link #destroy}ed. + * @param {Ext.Component} this + */ + 'destroy', + /** + * @event resize + * Fires after the component is resized. + * @param {Ext.Component} this + * @param {Number} adjWidth The box-adjusted width that was set + * @param {Number} adjHeight The box-adjusted height that was set + */ + 'resize', + /** + * @event move + * Fires after the component is moved. + * @param {Ext.Component} this + * @param {Number} x The new x position + * @param {Number} y The new y position + */ + 'move' + ); + + me.getId(); + + me.mons = []; + me.additionalCls = []; + me.renderData = me.renderData || {}; + me.renderSelectors = me.renderSelectors || {}; + + if (me.plugins) { + me.plugins = [].concat(me.plugins); + for (i = 0, len = me.plugins.length; i < len; i++) { + me.plugins[i] = me.constructPlugin(me.plugins[i]); + } + } + + me.initComponent(); + + // ititComponent gets a chance to change the id property before registering + Ext.ComponentManager.register(me); + + // Dont pass the config so that it is not applied to 'this' again + me.mixins.observable.constructor.call(me); + me.mixins.state.constructor.call(me, config); + + // Move this into Observable? + if (me.plugins) { + me.plugins = [].concat(me.plugins); + for (i = 0, len = me.plugins.length; i < len; i++) { + me.plugins[i] = me.initPlugin(me.plugins[i]); + } + } + + me.loader = me.getLoader(); + + if (me.renderTo) { + me.render(me.renderTo); + } + + if (me.autoShow) { + me.show(); + } + + // + if (Ext.isDefined(me.disabledClass)) { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.Component: disabledClass has been deprecated. Please use disabledCls.'); + } + me.disabledCls = me.disabledClass; + delete me.disabledClass; + } + // + }, + + initComponent: Ext.emptyFn, + + show: Ext.emptyFn, + + animate: function(animObj) { + var me = this, + to; + + animObj = animObj || {}; + to = animObj.to || {}; + + if (Ext.fx.Manager.hasFxBlock(me.id)) { + return me; + } + // Special processing for animating Component dimensions. + if (!animObj.dynamic && (to.height || to.width)) { + var curWidth = me.getWidth(), + w = curWidth, + curHeight = me.getHeight(), + h = curHeight, + needsResize = false; + + if (to.height && to.height > curHeight) { + h = to.height; + needsResize = true; + } + if (to.width && to.width > curWidth) { + w = to.width; + needsResize = true; + } + + // If any dimensions are being increased, we must resize the internal structure + // of the Component, but then clip it by sizing its encapsulating element back to original dimensions. + // The animation will then progressively reveal the larger content. + if (needsResize) { + var clearWidth = !Ext.isNumber(me.width), + clearHeight = !Ext.isNumber(me.height); + + me.componentLayout.childrenChanged = true; + me.setSize(w, h, me.ownerCt); + me.el.setSize(curWidth, curHeight); + if (clearWidth) { + delete me.width; + } + if (clearHeight) { + delete me.height; + } + } + } + return me.mixins.animate.animate.apply(me, arguments); + }, + + /** + *

This method finds the topmost active layout who's processing will eventually determine the size and position of this + * Component.

+ *

This method is useful when dynamically adding Components into Containers, and some processing must take place after the + * final sizing and positioning of the Component has been performed.

+ * @returns + */ + findLayoutController: function() { + return this.findParentBy(function(c) { + // Return true if we are at the root of the Container tree + // or this Container's layout is busy but the next one up is not. + return !c.ownerCt || (c.layout.layoutBusy && !c.ownerCt.layout.layoutBusy); + }); + }, + + onShow : function() { + // Layout if needed + var needsLayout = this.needsLayout; + if (Ext.isObject(needsLayout)) { + this.doComponentLayout(needsLayout.width, needsLayout.height, needsLayout.isSetSize, needsLayout.ownerCt); + } + }, + + constructPlugin: function(plugin) { + if (plugin.ptype && typeof plugin.init != 'function') { + plugin.cmp = this; + plugin = Ext.PluginManager.create(plugin); + } + else if (typeof plugin == 'string') { + plugin = Ext.PluginManager.create({ + ptype: plugin, + cmp: this + }); + } + return plugin; + }, + + + // @private + initPlugin : function(plugin) { + plugin.init(this); + + return plugin; + }, + + /** + * Handles autoRender. + * Floating Components may have an ownerCt. If they are asking to be constrained, constrain them within that + * ownerCt, and have their z-index managed locally. Floating Components are always rendered to document.body + */ + doAutoRender: function() { + var me = this; + if (me.floating) { + me.render(document.body); + } else { + me.render(Ext.isBoolean(me.autoRender) ? Ext.getBody() : me.autoRender); + } + }, + + // @private + render : function(container, position) { + var me = this; + + if (!me.rendered && me.fireEvent('beforerender', me) !== false) { + // If this.el is defined, we want to make sure we are dealing with + // an Ext Element. + if (me.el) { + me.el = Ext.get(me.el); + } + + // Perform render-time processing for floating Components + if (me.floating) { + me.onFloatRender(); + } + + container = me.initContainer(container); + + me.onRender(container, position); + + // Tell the encapsulating element to hide itself in the way the Component is configured to hide + // This means DISPLAY, VISIBILITY or OFFSETS. + me.el.setVisibilityMode(Ext.core.Element[me.hideMode.toUpperCase()]); + + if (me.overCls) { + me.el.hover(me.addOverCls, me.removeOverCls, me); + } + + me.fireEvent('render', me); + + me.initContent(); + + me.afterRender(container); + me.fireEvent('afterrender', me); + + me.initEvents(); + + if (me.hidden) { + // Hiding during the render process should not perform any ancillary + // actions that the full hide process does; It is not hiding, it begins in a hidden state.' + // So just make the element hidden according to the configured hideMode + me.el.hide(); + } + + if (me.disabled) { + // pass silent so the event doesn't fire the first time. + me.disable(true); + } + } + return me; + }, + + // @private + onRender : function(container, position) { + var me = this, + el = me.el, + cls = me.initCls(), + styles = me.initStyles(), + renderTpl, renderData, i; + + position = me.getInsertPosition(position); + + if (!el) { + if (position) { + el = Ext.core.DomHelper.insertBefore(position, me.getElConfig(), true); + } + else { + el = Ext.core.DomHelper.append(container, me.getElConfig(), true); + } + } + else if (me.allowDomMove !== false) { + if (position) { + container.dom.insertBefore(el.dom, position); + } else { + container.dom.appendChild(el.dom); + } + } + + if (Ext.scopeResetCSS && !me.ownerCt) { + // If this component's el is the body element, we add the reset class to the html tag + if (el.dom == Ext.getBody().dom) { + el.parent().addCls(Ext.baseCSSPrefix + 'reset'); + } + else { + // Else we wrap this element in an element that adds the reset class. + me.resetEl = el.wrap({ + cls: Ext.baseCSSPrefix + 'reset' + }); + } + } + + el.addCls(cls); + el.setStyle(styles); + + // Here we check if the component has a height set through style or css. + // If it does then we set the this.height to that value and it won't be + // considered an auto height component + // if (this.height === undefined) { + // var height = el.getHeight(); + // // This hopefully means that the panel has an explicit height set in style or css + // if (height - el.getPadding('tb') - el.getBorderWidth('tb') > 0) { + // this.height = height; + // } + // } + + me.el = el; + + me.rendered = true; + me.addUIToElement(true); + //loop through all exisiting uiCls and update the ui in them + for (i = 0; i < me.uiCls.length; i++) { + me.addUIClsToElement(me.uiCls[i], true); + } + me.rendered = false; + me.initFrame(); + + renderTpl = me.initRenderTpl(); + if (renderTpl) { + renderData = me.initRenderData(); + renderTpl.append(me.getTargetEl(), renderData); + } + + me.applyRenderSelectors(); + + me.rendered = true; + + me.setUI(me.ui); + }, + + // @private + afterRender : function() { + var me = this, + pos, + xy; + + me.getComponentLayout(); + + // Set the size if a size is configured, or if this is the outermost Container + if (!me.ownerCt || (me.height || me.width)) { + me.setSize(me.width, me.height); + } + + // For floaters, calculate x and y if they aren't defined by aligning + // the sized element to the center of either the the container or the ownerCt + if (me.floating && (me.x === undefined || me.y === undefined)) { + if (me.floatParent) { + xy = me.el.getAlignToXY(me.floatParent.getTargetEl(), 'c-c'); + pos = me.floatParent.getTargetEl().translatePoints(xy[0], xy[1]); + } else { + xy = me.el.getAlignToXY(me.container, 'c-c'); + pos = me.container.translatePoints(xy[0], xy[1]); + } + me.x = me.x === undefined ? pos.left: me.x; + me.y = me.y === undefined ? pos.top: me.y; + } + + if (Ext.isDefined(me.x) || Ext.isDefined(me.y)) { + me.setPosition(me.x, me.y); + } + + if (me.styleHtmlContent) { + me.getTargetEl().addCls(me.styleHtmlCls); + } + }, + + frameCls: Ext.baseCSSPrefix + 'frame', + + frameTpl: [ + '', + '
{parent.baseCls}-{parent.ui}-{.}-tl" style="background-position: {tl}; padding-left: {frameWidth}px" role="presentation">', + '
{parent.baseCls}-{parent.ui}-{.}-tr" style="background-position: {tr}; padding-right: {frameWidth}px" role="presentation">', + '
{parent.baseCls}-{parent.ui}-{.}-tc" style="background-position: {tc}; height: {frameWidth}px" role="presentation">
', + '
', + '
', + '
', + '
{parent.baseCls}-{parent.ui}-{.}-ml" style="background-position: {ml}; padding-left: {frameWidth}px" role="presentation">', + '
{parent.baseCls}-{parent.ui}-{.}-mr" style="background-position: {mr}; padding-right: {frameWidth}px" role="presentation">', + '
{parent.baseCls}-{parent.ui}-{.}-mc" role="presentation">
', + '
', + '
', + '', + '
{parent.baseCls}-{parent.ui}-{.}-bl" style="background-position: {bl}; padding-left: {frameWidth}px" role="presentation">', + '
{parent.baseCls}-{parent.ui}-{.}-br" style="background-position: {br}; padding-right: {frameWidth}px" role="presentation">', + '
{parent.baseCls}-{parent.ui}-{.}-bc" style="background-position: {bc}; height: {frameWidth}px" role="presentation">
', + '
', + '
', + '
' + ], + + frameTableTpl: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
{parent.baseCls}-{parent.ui}-{.}-tl" style="background-position: {tl}; padding-left:{frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-tc" style="background-position: {tc}; height: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-tr" style="background-position: {tr}; padding-left: {frameWidth}px" role="presentation">
{parent.baseCls}-{parent.ui}-{.}-ml" style="background-position: {ml}; padding-left: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-mc" style="background-position: 0 0;" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-mr" style="background-position: {mr}; padding-left: {frameWidth}px" role="presentation">
{parent.baseCls}-{parent.ui}-{.}-bl" style="background-position: {bl}; padding-left: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-bc" style="background-position: {bc}; height: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-br" style="background-position: {br}; padding-left: {frameWidth}px" role="presentation">
' + ], + + /** + * @private + */ + initFrame : function() { + if (Ext.supports.CSS3BorderRadius) { + return false; + } + + var me = this, + frameInfo = me.getFrameInfo(), + frameWidth = frameInfo.width, + frameTpl = me.getFrameTpl(frameInfo.table); + + if (me.frame) { + // Here we render the frameTpl to this component. This inserts the 9point div or the table framing. + frameTpl.insertFirst(me.el, Ext.apply({}, { + ui: me.ui, + uiCls: me.uiCls, + frameCls: me.frameCls, + baseCls: me.baseCls, + frameWidth: frameWidth, + top: !!frameInfo.top, + left: !!frameInfo.left, + right: !!frameInfo.right, + bottom: !!frameInfo.bottom + }, me.getFramePositions(frameInfo))); + + // The frameBody is returned in getTargetEl, so that layouts render items to the correct target.= + me.frameBody = me.el.down('.' + me.frameCls + '-mc'); + + // Add the render selectors for each of the frame elements + Ext.apply(me.renderSelectors, { + frameTL: '.' + me.baseCls + '-tl', + frameTC: '.' + me.baseCls + '-tc', + frameTR: '.' + me.baseCls + '-tr', + frameML: '.' + me.baseCls + '-ml', + frameMC: '.' + me.baseCls + '-mc', + frameMR: '.' + me.baseCls + '-mr', + frameBL: '.' + me.baseCls + '-bl', + frameBC: '.' + me.baseCls + '-bc', + frameBR: '.' + me.baseCls + '-br' + }); + } + }, + + updateFrame: function() { + if (Ext.supports.CSS3BorderRadius) { + return false; + } + + var me = this, + wasTable = this.frameSize && this.frameSize.table, + oldFrameTL = this.frameTL, + oldFrameBL = this.frameBL, + oldFrameML = this.frameML, + oldFrameMC = this.frameMC, + newMCClassName; + + this.initFrame(); + + if (oldFrameMC) { + if (me.frame) { + // Reapply render selectors + delete me.frameTL; + delete me.frameTC; + delete me.frameTR; + delete me.frameML; + delete me.frameMC; + delete me.frameMR; + delete me.frameBL; + delete me.frameBC; + delete me.frameBR; + this.applyRenderSelectors(); + + // Store the class names set on the new mc + newMCClassName = this.frameMC.dom.className; + + // Replace the new mc with the old mc + oldFrameMC.insertAfter(this.frameMC); + this.frameMC.remove(); + + // Restore the reference to the old frame mc as the framebody + this.frameBody = this.frameMC = oldFrameMC; + + // Apply the new mc classes to the old mc element + oldFrameMC.dom.className = newMCClassName; + + // Remove the old framing + if (wasTable) { + me.el.query('> table')[1].remove(); + } + else { + if (oldFrameTL) { + oldFrameTL.remove(); + } + if (oldFrameBL) { + oldFrameBL.remove(); + } + oldFrameML.remove(); + } + } + else { + // We were framed but not anymore. Move all content from the old frame to the body + + } + } + else if (me.frame) { + this.applyRenderSelectors(); + } + }, + + getFrameInfo: function() { + if (Ext.supports.CSS3BorderRadius) { + return false; + } + + var me = this, + left = me.el.getStyle('background-position-x'), + top = me.el.getStyle('background-position-y'), + info, frameInfo = false, max; + + // Some browsers dont support background-position-x and y, so for those + // browsers let's split background-position into two parts. + if (!left && !top) { + info = me.el.getStyle('background-position').split(' '); + left = info[0]; + top = info[1]; + } + + // We actually pass a string in the form of '[type][tl][tr]px [type][br][bl]px' as + // the background position of this.el from the css to indicate to IE that this component needs + // framing. We parse it here and change the markup accordingly. + if (parseInt(left, 10) >= 1000000 && parseInt(top, 10) >= 1000000) { + max = Math.max; + + frameInfo = { + // Table markup starts with 110, div markup with 100. + table: left.substr(0, 3) == '110', + + // Determine if we are dealing with a horizontal or vertical component + vertical: top.substr(0, 3) == '110', + + // Get and parse the different border radius sizes + top: max(left.substr(3, 2), left.substr(5, 2)), + right: max(left.substr(5, 2), top.substr(3, 2)), + bottom: max(top.substr(3, 2), top.substr(5, 2)), + left: max(top.substr(5, 2), left.substr(3, 2)) + }; + + frameInfo.width = max(frameInfo.top, frameInfo.right, frameInfo.bottom, frameInfo.left); + + // Just to be sure we set the background image of the el to none. + me.el.setStyle('background-image', 'none'); + } + + // This happens when you set frame: true explicitly without using the x-frame mixin in sass. + // This way IE can't figure out what sizes to use and thus framing can't work. + if (me.frame === true && !frameInfo) { + // + Ext.Error.raise("You have set frame: true explicity on this component while it doesn't have any " + + "framing defined in the CSS template. In this case IE can't figure out what sizes " + + "to use and thus framing on this component will be disabled."); + // + } + + me.frame = me.frame || !!frameInfo; + me.frameSize = frameInfo || false; + + return frameInfo; + }, + + getFramePositions: function(frameInfo) { + var me = this, + frameWidth = frameInfo.width, + dock = me.dock, + positions, tc, bc, ml, mr; + + if (frameInfo.vertical) { + tc = '0 -' + (frameWidth * 0) + 'px'; + bc = '0 -' + (frameWidth * 1) + 'px'; + + if (dock && dock == "right") { + tc = 'right -' + (frameWidth * 0) + 'px'; + bc = 'right -' + (frameWidth * 1) + 'px'; + } + + positions = { + tl: '0 -' + (frameWidth * 0) + 'px', + tr: '0 -' + (frameWidth * 1) + 'px', + bl: '0 -' + (frameWidth * 2) + 'px', + br: '0 -' + (frameWidth * 3) + 'px', + + ml: '-' + (frameWidth * 1) + 'px 0', + mr: 'right 0', + + tc: tc, + bc: bc + }; + } else { + ml = '-' + (frameWidth * 0) + 'px 0'; + mr = 'right 0'; + + if (dock && dock == "bottom") { + ml = 'left bottom'; + mr = 'right bottom'; + } + + positions = { + tl: '0 -' + (frameWidth * 2) + 'px', + tr: 'right -' + (frameWidth * 3) + 'px', + bl: '0 -' + (frameWidth * 4) + 'px', + br: 'right -' + (frameWidth * 5) + 'px', + + ml: ml, + mr: mr, + + tc: '0 -' + (frameWidth * 0) + 'px', + bc: '0 -' + (frameWidth * 1) + 'px' + }; + } + + return positions; + }, + + /** + * @private + */ + getFrameTpl : function(table) { + return table ? this.getTpl('frameTableTpl') : this.getTpl('frameTpl'); + }, + + /** + *

Creates an array of class names from the configurations to add to this Component's el on render.

+ *

Private, but (possibly) used by ComponentQuery for selection by class name if Component is not rendered.

+ * @return {Array} An array of class names with which the Component's element will be rendered. + * @private + */ + initCls: function() { + var me = this, + cls = []; + + cls.push(me.baseCls); + + // + if (Ext.isDefined(me.cmpCls)) { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.Component: cmpCls has been deprecated. Please use componentCls.'); + } + me.componentCls = me.cmpCls; + delete me.cmpCls; + } + // + + if (me.componentCls) { + cls.push(me.componentCls); + } else { + me.componentCls = me.baseCls; + } + if (me.cls) { + cls.push(me.cls); + delete me.cls; + } + + return cls.concat(me.additionalCls); + }, + + /** + * Sets the UI for the component. This will remove any existing UIs on the component. It will also + * loop through any uiCls set on the component and rename them so they include the new UI + * @param {String} ui The new UI for the component + */ + setUI: function(ui) { + var me = this, + oldUICls = Ext.Array.clone(me.uiCls), + newUICls = [], + cls, + i; + + //loop through all exisiting uiCls and update the ui in them + for (i = 0; i < oldUICls.length; i++) { + cls = oldUICls[i]; + + me.removeClsWithUI(cls); + newUICls.push(cls); + } + + //remove the UI from the element + me.removeUIFromElement(); + + //set the UI + me.ui = ui; + + //add the new UI to the elemend + me.addUIToElement(); + + //loop through all exisiting uiCls and update the ui in them + for (i = 0; i < newUICls.length; i++) { + cls = newUICls[i]; + + me.addClsWithUI(cls); + } + }, + + /** + * Adds a cls to the uiCls array, which will also call {@link #addUIClsToElement} and adds + * to all elements of this component. + * @param {String/Array} cls A string or an array of strings to add to the uiCls + */ + addClsWithUI: function(cls) { + var me = this, + i; + + if (!Ext.isArray(cls)) { + cls = [cls]; + } + + for (i = 0; i < cls.length; i++) { + if (cls[i] && !me.hasUICls(cls[i])) { + me.uiCls = Ext.Array.clone(me.uiCls); + me.uiCls.push(cls[i]); + me.addUIClsToElement(cls[i]); + } + } + }, + + /** + * Removes a cls to the uiCls array, which will also call {@link #removeUIClsToElement} and removes + * it from all elements of this component. + * @param {String/Array} cls A string or an array of strings to remove to the uiCls + */ + removeClsWithUI: function(cls) { + var me = this, + i; + + if (!Ext.isArray(cls)) { + cls = [cls]; + } + + for (i = 0; i < cls.length; i++) { + if (cls[i] && me.hasUICls(cls[i])) { + me.uiCls = Ext.Array.remove(me.uiCls, cls[i]); + me.removeUIClsFromElement(cls[i]); + } + } + }, + + /** + * Checks if there is currently a specified uiCls + * @param {String} cls The cls to check + */ + hasUICls: function(cls) { + var me = this, + uiCls = me.uiCls || []; + + return Ext.Array.contains(uiCls, cls); + }, + + /** + * Method which adds a specified UI + uiCls to the components element. + * Can be overridden to remove the UI from more than just the components element. + * @param {String} ui The UI to remove from the element + * @private + */ + addUIClsToElement: function(cls, force) { + var me = this; + + me.addCls(Ext.baseCSSPrefix + cls); + me.addCls(me.baseCls + '-' + cls); + me.addCls(me.baseCls + '-' + me.ui + '-' + cls); + + if (!force && me.rendered && me.frame && !Ext.supports.CSS3BorderRadius) { + // define each element of the frame + var els = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'], + i, el; + + // loop through each of them, and if they are defined add the ui + for (i = 0; i < els.length; i++) { + el = me['frame' + els[i].toUpperCase()]; + + if (el && el.dom) { + el.addCls(me.baseCls + '-' + me.ui + '-' + els[i]); + el.addCls(me.baseCls + '-' + me.ui + '-' + cls + '-' + els[i]); + } + } + } + }, + + /** + * Method which removes a specified UI + uiCls from the components element. + * The cls which is added to the element will be: `this.baseCls + '-' + ui` + * @param {String} ui The UI to add to the element + * @private + */ + removeUIClsFromElement: function(cls, force) { + var me = this; + + me.removeCls(Ext.baseCSSPrefix + cls); + me.removeCls(me.baseCls + '-' + cls); + me.removeCls(me.baseCls + '-' + me.ui + '-' + cls); + + if (!force &&me.rendered && me.frame && !Ext.supports.CSS3BorderRadius) { + // define each element of the frame + var els = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'], + i, el; + + // loop through each of them, and if they are defined add the ui + for (i = 0; i < els.length; i++) { + el = me['frame' + els[i].toUpperCase()]; + if (el && el.dom) { + el.removeCls(me.baseCls + '-' + me.ui + '-' + cls + '-' + els[i]); + } + } + } + }, + + /** + * Method which adds a specified UI to the components element. + * @private + */ + addUIToElement: function(force) { + var me = this; + + me.addCls(me.baseCls + '-' + me.ui); + + if (me.rendered && me.frame && !Ext.supports.CSS3BorderRadius) { + // define each element of the frame + var els = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'], + i, el; + + // loop through each of them, and if they are defined add the ui + for (i = 0; i < els.length; i++) { + el = me['frame' + els[i].toUpperCase()]; + + if (el) { + el.addCls(me.baseCls + '-' + me.ui + '-' + els[i]); + } + } + } + }, + + /** + * Method which removes a specified UI from the components element. + * @private + */ + removeUIFromElement: function() { + var me = this; + + me.removeCls(me.baseCls + '-' + me.ui); + + if (me.rendered && me.frame && !Ext.supports.CSS3BorderRadius) { + // define each element of the frame + var els = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'], + i, el; + + // loop through each of them, and if they are defined add the ui + for (i = 0; i < els.length; i++) { + el = me['frame' + els[i].toUpperCase()]; + if (el) { + el.removeCls(me.baseCls + '-' + me.ui + '-' + els[i]); + } + } + } + }, + + getElConfig : function() { + var result = this.autoEl || {tag: 'div'}; + result.id = this.id; + return result; + }, + + /** + * This function takes the position argument passed to onRender and returns a + * DOM element that you can use in the insertBefore. + * @param {String/Number/Element/HTMLElement} position Index, element id or element you want + * to put this component before. + * @return {HTMLElement} DOM element that you can use in the insertBefore + */ + getInsertPosition: function(position) { + // Convert the position to an element to insert before + if (position !== undefined) { + if (Ext.isNumber(position)) { + position = this.container.dom.childNodes[position]; + } + else { + position = Ext.getDom(position); + } + } + + return position; + }, + + /** + * Adds ctCls to container. + * @return {Ext.core.Element} The initialized container + * @private + */ + initContainer: function(container) { + var me = this; + + // If you render a component specifying the el, we get the container + // of the el, and make sure we dont move the el around in the dom + // during the render + if (!container && me.el) { + container = me.el.dom.parentNode; + me.allowDomMove = false; + } + + me.container = Ext.get(container); + + if (me.ctCls) { + me.container.addCls(me.ctCls); + } + + return me.container; + }, + + /** + * Initialized the renderData to be used when rendering the renderTpl. + * @return {Object} Object with keys and values that are going to be applied to the renderTpl + * @private + */ + initRenderData: function() { + var me = this; + + return Ext.applyIf(me.renderData, { + ui: me.ui, + uiCls: me.uiCls, + baseCls: me.baseCls, + componentCls: me.componentCls, + frame: me.frame + }); + }, + + /** + * @private + */ + getTpl: function(name) { + var prototype = this.self.prototype, + ownerPrototype; + + if (this.hasOwnProperty(name)) { + if (!(this[name] instanceof Ext.XTemplate)) { + this[name] = Ext.ClassManager.dynInstantiate('Ext.XTemplate', this[name]); + } + + return this[name]; + } + + if (!(prototype[name] instanceof Ext.XTemplate)) { + ownerPrototype = prototype; + + do { + if (ownerPrototype.hasOwnProperty(name)) { + ownerPrototype[name] = Ext.ClassManager.dynInstantiate('Ext.XTemplate', ownerPrototype[name]); + break; + } + + ownerPrototype = ownerPrototype.superclass; + } while (ownerPrototype); + } + + return prototype[name]; + }, + + /** + * Initializes the renderTpl. + * @return {Ext.XTemplate} The renderTpl XTemplate instance. + * @private + */ + initRenderTpl: function() { + return this.getTpl('renderTpl'); + }, + + /** + * Function description + * @return {String} A CSS style string with style, padding, margin and border. + * @private + */ + initStyles: function() { + var style = {}, + me = this, + Element = Ext.core.Element; + + if (Ext.isString(me.style)) { + style = Element.parseStyles(me.style); + } else { + style = Ext.apply({}, me.style); + } + + // Convert the padding, margin and border properties from a space seperated string + // into a proper style string + if (me.padding !== undefined) { + style.padding = Element.unitizeBox((me.padding === true) ? 5 : me.padding); + } + + if (me.margin !== undefined) { + style.margin = Element.unitizeBox((me.margin === true) ? 5 : me.margin); + } + + delete me.style; + return style; + }, + + /** + * Initializes this components contents. It checks for the properties + * html, contentEl and tpl/data. + * @private + */ + initContent: function() { + var me = this, + target = me.getTargetEl(), + contentEl, + pre; + + if (me.html) { + target.update(Ext.core.DomHelper.markup(me.html)); + delete me.html; + } + + if (me.contentEl) { + contentEl = Ext.get(me.contentEl); + pre = Ext.baseCSSPrefix; + contentEl.removeCls([pre + 'hidden', pre + 'hide-display', pre + 'hide-offsets', pre + 'hide-nosize']); + target.appendChild(contentEl.dom); + } + + if (me.tpl) { + // Make sure this.tpl is an instantiated XTemplate + if (!me.tpl.isTemplate) { + me.tpl = Ext.create('Ext.XTemplate', me.tpl); + } + + if (me.data) { + me.tpl[me.tplWriteMode](target, me.data); + delete me.data; + } + } + }, + + // @private + initEvents : function() { + var me = this, + afterRenderEvents = me.afterRenderEvents, + property, listeners; + if (afterRenderEvents) { + for (property in afterRenderEvents) { + if (afterRenderEvents.hasOwnProperty(property)) { + listeners = afterRenderEvents[property]; + if (me[property] && me[property].on) { + me.mon(me[property], listeners); + } + } + } + } + }, + + /** + * Sets references to elements inside the component. E.g body -> x-panel-body + * @private + */ + applyRenderSelectors: function() { + var selectors = this.renderSelectors || {}, + el = this.el.dom, + selector; + + for (selector in selectors) { + if (selectors.hasOwnProperty(selector) && selectors[selector]) { + this[selector] = Ext.get(Ext.DomQuery.selectNode(selectors[selector], el)); + } + } + }, + + /** + * Tests whether this Component matches the selector string. + * @param {String} selector The selector string to test against. + * @return {Boolean} True if this Component matches the selector. + */ + is: function(selector) { + return Ext.ComponentQuery.is(this, selector); + }, + + /** + *

Walks up the ownerCt axis looking for an ancestor Container which matches + * the passed simple selector.

+ *

Example:


+var owningTabPanel = grid.up('tabpanel');
+
+ * @param {String} selector Optional. The simple selector to test. + * @return {Container} The matching ancestor Container (or undefined if no match was found). + */ + up: function(selector) { + var result = this.ownerCt; + if (selector) { + for (; result; result = result.ownerCt) { + if (Ext.ComponentQuery.is(result, selector)) { + return result; + } + } + } + return result; + }, + + /** + *

Returns the next sibling of this Component.

+ *

Optionally selects the next sibling which matches the passed {@link Ext.ComponentQuery ComponentQuery} selector.

+ *

May also be refered to as next()

+ *

Note that this is limited to siblings, and if no siblings of the item match, null is returned. Contrast with {@link #nextNode}

+ * @param {String} selector Optional A {@link Ext.ComponentQuery ComponentQuery} selector to filter the following items. + * @returns The next sibling (or the next sibling which matches the selector). Returns null if there is no matching sibling. + */ + nextSibling: function(selector) { + var o = this.ownerCt, it, last, idx, c; + if (o) { + it = o.items; + idx = it.indexOf(this) + 1; + if (idx) { + if (selector) { + for (last = it.getCount(); idx < last; idx++) { + if ((c = it.getAt(idx)).is(selector)) { + return c; + } + } + } else { + if (idx < it.getCount()) { + return it.getAt(idx); + } + } + } + } + return null; + }, + + /** + *

Returns the previous sibling of this Component.

+ *

Optionally selects the previous sibling which matches the passed {@link Ext.ComponentQuery ComponentQuery} selector.

+ *

May also be refered to as prev()

+ *

Note that this is limited to siblings, and if no siblings of the item match, null is returned. Contrast with {@link #previousNode}

+ * @param {String} selector Optional. A {@link Ext.ComponentQuery ComponentQuery} selector to filter the preceding items. + * @returns The previous sibling (or the previous sibling which matches the selector). Returns null if there is no matching sibling. + */ + previousSibling: function(selector) { + var o = this.ownerCt, it, idx, c; + if (o) { + it = o.items; + idx = it.indexOf(this); + if (idx != -1) { + if (selector) { + for (--idx; idx >= 0; idx--) { + if ((c = it.getAt(idx)).is(selector)) { + return c; + } + } + } else { + if (idx) { + return it.getAt(--idx); + } + } + } + } + return null; + }, + + /** + *

Returns the previous node in the Component tree in tree traversal order.

+ *

Note that this is not limited to siblings, and if invoked upon a node with no matching siblings, will + * walk the tree in reverse order to attempt to find a match. Contrast with {@link #previousSibling}.

+ * @param {String} selector Optional. A {@link Ext.ComponentQuery ComponentQuery} selector to filter the preceding nodes. + * @returns The previous node (or the previous node which matches the selector). Returns null if there is no matching node. + */ + previousNode: function(selector, includeSelf) { + var node = this, + result, + it, len, i; + + // If asked to include self, test me + if (includeSelf && node.is(selector)) { + return node; + } + + result = this.prev(selector); + if (result) { + return result; + } + + if (node.ownerCt) { + for (it = node.ownerCt.items.items, i = Ext.Array.indexOf(it, node) - 1; i > -1; i--) { + if (it[i].query) { + result = it[i].query(selector); + result = result[result.length - 1]; + if (result) { + return result; + } + } + } + return node.ownerCt.previousNode(selector, true); + } + }, + + /** + *

Returns the next node in the Component tree in tree traversal order.

+ *

Note that this is not limited to siblings, and if invoked upon a node with no matching siblings, will + * walk the tree to attempt to find a match. Contrast with {@link #pnextSibling}.

+ * @param {String} selector Optional A {@link Ext.ComponentQuery ComponentQuery} selector to filter the following nodes. + * @returns The next node (or the next node which matches the selector). Returns null if there is no matching node. + */ + nextNode: function(selector, includeSelf) { + var node = this, + result, + it, len, i; + + // If asked to include self, test me + if (includeSelf && node.is(selector)) { + return node; + } + + result = this.next(selector); + if (result) { + return result; + } + + if (node.ownerCt) { + for (it = node.ownerCt.items, i = it.indexOf(node) + 1, it = it.items, len = it.length; i < len; i++) { + if (it[i].down) { + result = it[i].down(selector); + if (result) { + return result; + } + } + } + return node.ownerCt.nextNode(selector); + } + }, + + /** + * Retrieves the id of this component. + * Will autogenerate an id if one has not already been set. + */ + getId : function() { + return this.id || (this.id = 'ext-comp-' + (this.getAutoId())); + }, + + getItemId : function() { + return this.itemId || this.id; + }, + + /** + * Retrieves the top level element representing this component. + */ + getEl : function() { + return this.el; + }, + + /** + * This is used to determine where to insert the 'html', 'contentEl' and 'items' in this component. + * @private + */ + getTargetEl: function() { + return this.frameBody || this.el; + }, + + /** + *

Tests whether or not this Component is of a specific xtype. This can test whether this Component is descended + * from the xtype (default) or whether it is directly of the xtype specified (shallow = true).

+ *

If using your own subclasses, be aware that a Component must register its own xtype + * to participate in determination of inherited xtypes.

+ *

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

+ *

Example usage:

+ *

+var t = new Ext.form.field.Text();
+var isText = t.isXType('textfield');        // true
+var isBoxSubclass = t.isXType('field');       // true, descended from Ext.form.field.Base
+var isBoxInstance = t.isXType('field', true); // false, not a direct Ext.form.field.Base instance
+
+ * @param {String} xtype The xtype to check for this Component + * @param {Boolean} shallow (optional) False to check whether this Component is descended from the xtype (this is + * the default), or true to check whether this Component is directly of the specified xtype. + * @return {Boolean} True if this component descends from the specified xtype, false otherwise. + */ + isXType: function(xtype, shallow) { + //assume a string by default + if (Ext.isFunction(xtype)) { + xtype = xtype.xtype; + //handle being passed the class, e.g. Ext.Component + } else if (Ext.isObject(xtype)) { + xtype = xtype.statics().xtype; + //handle being passed an instance + } + + return !shallow ? ('/' + this.getXTypes() + '/').indexOf('/' + xtype + '/') != -1: this.self.xtype == xtype; + }, + + /** + *

Returns this Component's xtype hierarchy as a slash-delimited string. For a list of all + * available xtypes, see the {@link Ext.Component} header.

+ *

If using your own subclasses, be aware that a Component must register its own xtype + * to participate in determination of inherited xtypes.

+ *

Example usage:

+ *

+var t = new Ext.form.field.Text();
+alert(t.getXTypes());  // alerts 'component/field/textfield'
+
+ * @return {String} The xtype hierarchy string + */ + getXTypes: function() { + var self = this.self, + xtypes = [], + parentPrototype = this, + xtype; + + if (!self.xtypes) { + while (parentPrototype && Ext.getClass(parentPrototype)) { + xtype = Ext.getClass(parentPrototype).xtype; + + if (xtype !== undefined) { + xtypes.unshift(xtype); + } + + parentPrototype = parentPrototype.superclass; + } + + self.xtypeChain = xtypes; + self.xtypes = xtypes.join('/'); + } + + return self.xtypes; + }, + + /** + * Update the content area of a component. + * @param {Mixed} htmlOrData + * If this component has been configured with a template via the tpl config + * then it will use this argument as data to populate the template. + * If this component was not configured with a template, the components + * content area will be updated via Ext.core.Element update + * @param {Boolean} loadScripts + * (optional) Only legitimate when using the html configuration. Defaults to false + * @param {Function} callback + * (optional) Only legitimate when using the html configuration. Callback to execute when scripts have finished loading + */ + update : function(htmlOrData, loadScripts, cb) { + var me = this; + + if (me.tpl && !Ext.isString(htmlOrData)) { + me.data = htmlOrData; + if (me.rendered) { + me.tpl[me.tplWriteMode](me.getTargetEl(), htmlOrData || {}); + } + } else { + me.html = Ext.isObject(htmlOrData) ? Ext.core.DomHelper.markup(htmlOrData) : htmlOrData; + if (me.rendered) { + me.getTargetEl().update(me.html, loadScripts, cb); + } + } + + if (me.rendered) { + me.doComponentLayout(); + } + }, + + /** + * Convenience function to hide or show this component by boolean. + * @param {Boolean} visible True to show, false to hide + * @return {Ext.Component} this + */ + setVisible : function(visible) { + return this[visible ? 'show': 'hide'](); + }, + + /** + * Returns true if this component is visible. + * @param {Boolean} deep.

Optional. Pass true to interrogate the visibility status of all + * parent Containers to determine whether this Component is truly visible to the user.

+ *

Generally, to determine whether a Component is hidden, the no argument form is needed. For example + * when creating dynamically laid out UIs in a hidden Container before showing them.

+ * @return {Boolean} True if this component is visible, false otherwise. + */ + isVisible: function(deep) { + var me = this, + child = me, + visible = !me.hidden, + ancestor = me.ownerCt; + + // Clear hiddenOwnerCt property + me.hiddenAncestor = false; + if (me.destroyed) { + return false; + } + + if (deep && visible && me.rendered && ancestor) { + while (ancestor) { + // If any ancestor is hidden, then this is hidden. + // If an ancestor Panel (only Panels have a collapse method) is collapsed, + // then its layoutTarget (body) is hidden, so this is hidden unless its within a + // docked item; they are still visible when collapsed (Unless they themseves are hidden) + if (ancestor.hidden || (ancestor.collapsed && + !(ancestor.getDockedItems && Ext.Array.contains(ancestor.getDockedItems(), child)))) { + // Store hiddenOwnerCt property if needed + me.hiddenAncestor = ancestor; + visible = false; + break; + } + child = ancestor; + ancestor = ancestor.ownerCt; + } + } + return visible; + }, + + /** + * Enable the component + * @param {Boolean} silent + * Passing false will supress the 'enable' event from being fired. + */ + enable: function(silent) { + var me = this; + + if (me.rendered) { + me.el.removeCls(me.disabledCls); + me.el.dom.disabled = false; + me.onEnable(); + } + + me.disabled = false; + + if (silent !== true) { + me.fireEvent('enable', me); + } + + return me; + }, + + /** + * Disable the component. + * @param {Boolean} silent + * Passing true, will supress the 'disable' event from being fired. + */ + disable: function(silent) { + var me = this; + + if (me.rendered) { + me.el.addCls(me.disabledCls); + me.el.dom.disabled = true; + me.onDisable(); + } + + me.disabled = true; + + if (silent !== true) { + me.fireEvent('disable', me); + } + + return me; + }, + + // @private + onEnable: function() { + if (this.maskOnDisable) { + this.el.unmask(); + } + }, + + // @private + onDisable : function() { + if (this.maskOnDisable) { + this.el.mask(); + } + }, + + /** + * Method to determine whether this Component is currently disabled. + * @return {Boolean} the disabled state of this Component. + */ + isDisabled : function() { + return this.disabled; + }, + + /** + * Enable or disable the component. + * @param {Boolean} disabled + */ + setDisabled : function(disabled) { + return this[disabled ? 'disable': 'enable'](); + }, + + /** + * Method to determine whether this Component is currently set to hidden. + * @return {Boolean} the hidden state of this Component. + */ + isHidden : function() { + return this.hidden; + }, + + /** + * Adds a CSS class to the top level element representing this component. + * @param {String} cls The CSS class name to add + * @return {Ext.Component} Returns the Component to allow method chaining. + */ + addCls : function(className) { + var me = this; + if (!className) { + return me; + } + if (!Ext.isArray(className)){ + className = className.replace(me.trimRe, '').split(me.spacesRe); + } + if (me.rendered) { + me.el.addCls(className); + } + else { + me.additionalCls = Ext.Array.unique(me.additionalCls.concat(className)); + } + return me; + }, + + /** + * @deprecated 4.0 Replaced by {link:#addCls} + * Adds a CSS class to the top level element representing this component. + * @param {String} cls The CSS class name to add + * @return {Ext.Component} Returns the Component to allow method chaining. + */ + addClass : function() { + return this.addCls.apply(this, arguments); + }, + + /** + * Removes a CSS class from the top level element representing this component. + * @returns {Ext.Component} Returns the Component to allow method chaining. + */ + removeCls : function(className) { + var me = this; + + if (!className) { + return me; + } + if (!Ext.isArray(className)){ + className = className.replace(me.trimRe, '').split(me.spacesRe); + } + if (me.rendered) { + me.el.removeCls(className); + } + else if (me.additionalCls.length) { + Ext.each(className, function(cls) { + Ext.Array.remove(me.additionalCls, cls); + }); + } + return me; + }, + + // + removeClass : function() { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.Component: removeClass has been deprecated. Please use removeCls.'); + } + return this.removeCls.apply(this, arguments); + }, + // + + addOverCls: function() { + var me = this; + if (!me.disabled) { + me.el.addCls(me.overCls); + } + }, + + removeOverCls: function() { + this.el.removeCls(this.overCls); + }, + + addListener : function(element, listeners, scope, options) { + var me = this, + fn, + option; + + if (Ext.isString(element) && (Ext.isObject(listeners) || options && options.element)) { + if (options.element) { + fn = listeners; + + listeners = {}; + listeners[element] = fn; + element = options.element; + if (scope) { + listeners.scope = scope; + } + + for (option in options) { + if (options.hasOwnProperty(option)) { + if (me.eventOptionsRe.test(option)) { + listeners[option] = options[option]; + } + } + } + } + + // At this point we have a variable called element, + // and a listeners object that can be passed to on + if (me[element] && me[element].on) { + me.mon(me[element], listeners); + } else { + me.afterRenderEvents = me.afterRenderEvents || {}; + me.afterRenderEvents[element] = listeners; + } + } + + return me.mixins.observable.addListener.apply(me, arguments); + }, + + // @TODO: implement removelistener to support the dom event stuff + + /** + * Provides the link for Observable's fireEvent method to bubble up the ownership hierarchy. + * @return {Ext.container.Container} the Container which owns this Component. + */ + getBubbleTarget : function() { + return this.ownerCt; + }, + + /** + * Method to determine whether this Component is floating. + * @return {Boolean} the floating state of this component. + */ + isFloating : function() { + return this.floating; + }, + + /** + * Method to determine whether this Component is draggable. + * @return {Boolean} the draggable state of this component. + */ + isDraggable : function() { + return !!this.draggable; + }, + + /** + * Method to determine whether this Component is droppable. + * @return {Boolean} the droppable state of this component. + */ + isDroppable : function() { + return !!this.droppable; + }, + + /** + * @private + * Method to manage awareness of when components are added to their + * respective Container, firing an added event. + * References are established at add time rather than at render time. + * @param {Ext.container.Container} container Container which holds the component + * @param {number} pos Position at which the component was added + */ + onAdded : function(container, pos) { + this.ownerCt = container; + this.fireEvent('added', this, container, pos); + }, + + /** + * @private + * Method to manage awareness of when components are removed from their + * respective Container, firing an removed event. References are properly + * cleaned up after removing a component from its owning container. + */ + onRemoved : function() { + var me = this; + + me.fireEvent('removed', me, me.ownerCt); + delete me.ownerCt; + }, + + // @private + beforeDestroy : Ext.emptyFn, + // @private + // @private + onResize : Ext.emptyFn, + + /** + * Sets the width and height of this Component. This method fires the {@link #resize} event. This method can accept + * either width and height as separate arguments, or you can pass a size object like {width:10, height:20}. + * @param {Mixed} width The new width to set. This may be one of:
    + *
  • A Number specifying the new width in the {@link #getEl Element}'s {@link Ext.core.Element#defaultUnit}s (by default, pixels).
  • + *
  • A String used to set the CSS width style.
  • + *
  • A size object in the format {width: widthValue, height: heightValue}.
  • + *
  • undefined to leave the width unchanged.
  • + *
+ * @param {Mixed} height The new height to set (not required if a size object is passed as the first arg). + * This may be one of:
    + *
  • A Number specifying the new height in the {@link #getEl Element}'s {@link Ext.core.Element#defaultUnit}s (by default, pixels).
  • + *
  • A String used to set the CSS height style. Animation may not be used.
  • + *
  • undefined to leave the height unchanged.
  • + *
+ * @return {Ext.Component} this + */ + setSize : function(width, height) { + var me = this, + layoutCollection; + + // support for standard size objects + if (Ext.isObject(width)) { + height = width.height; + width = width.width; + } + + // Constrain within configured maxima + if (Ext.isNumber(width)) { + width = Ext.Number.constrain(width, me.minWidth, me.maxWidth); + } + if (Ext.isNumber(height)) { + height = Ext.Number.constrain(height, me.minHeight, me.maxHeight); + } + + if (!me.rendered || !me.isVisible()) { + // If an ownerCt is hidden, add my reference onto the layoutOnShow stack. Set the needsLayout flag. + if (me.hiddenAncestor) { + layoutCollection = me.hiddenAncestor.layoutOnShow; + layoutCollection.remove(me); + layoutCollection.add(me); + } + me.needsLayout = { + width: width, + height: height, + isSetSize: true + }; + if (!me.rendered) { + me.width = (width !== undefined) ? width : me.width; + me.height = (height !== undefined) ? height : me.height; + } + return me; + } + me.doComponentLayout(width, height, true); + + return me; + }, + + setCalculatedSize : function(width, height, ownerCt) { + var me = this, + layoutCollection; + + // support for standard size objects + if (Ext.isObject(width)) { + ownerCt = width.ownerCt; + height = width.height; + width = width.width; + } + + // Constrain within configured maxima + if (Ext.isNumber(width)) { + width = Ext.Number.constrain(width, me.minWidth, me.maxWidth); + } + if (Ext.isNumber(height)) { + height = Ext.Number.constrain(height, me.minHeight, me.maxHeight); + } + + if (!me.rendered || !me.isVisible()) { + // If an ownerCt is hidden, add my reference onto the layoutOnShow stack. Set the needsLayout flag. + if (me.hiddenAncestor) { + layoutCollection = me.hiddenAncestor.layoutOnShow; + layoutCollection.remove(me); + layoutCollection.add(me); + } + me.needsLayout = { + width: width, + height: height, + isSetSize: false, + ownerCt: ownerCt + }; + return me; + } + me.doComponentLayout(width, height, false, ownerCt); + + return me; + }, + + /** + * This method needs to be called whenever you change something on this component that requires the Component's + * layout to be recalculated. + * @return {Ext.container.Container} this + */ + doComponentLayout : function(width, height, isSetSize, ownerCt) { + var me = this, + componentLayout = me.getComponentLayout(); + + // collapsed state is not relevant here, so no testing done. + // Only Panels have a collapse method, and that just sets the width/height such that only + // a single docked Header parallel to the collapseTo side are visible, and the Panel body is hidden. + if (me.rendered && componentLayout) { + width = (width !== undefined) ? width : me.width; + height = (height !== undefined) ? height : me.height; + if (isSetSize) { + me.width = width; + me.height = height; + } + + componentLayout.layout(width, height, isSetSize, ownerCt); + } + return me; + }, + + // @private + setComponentLayout : function(layout) { + var currentLayout = this.componentLayout; + if (currentLayout && currentLayout.isLayout && currentLayout != layout) { + currentLayout.setOwner(null); + } + this.componentLayout = layout; + layout.setOwner(this); + }, + + getComponentLayout : function() { + var me = this; + + if (!me.componentLayout || !me.componentLayout.isLayout) { + me.setComponentLayout(Ext.layout.Layout.create(me.componentLayout, 'autocomponent')); + } + return me.componentLayout; + }, + + /** + * @param {Number} adjWidth The box-adjusted width that was set + * @param {Number} adjHeight The box-adjusted height that was set + * @param {Boolean} isSetSize Whether or not the height/width are stored on the component permanently + * @param {Ext.Component} layoutOwner Component which sent the layout. Only used when isSetSize is false. + */ + afterComponentLayout: function(width, height, isSetSize, layoutOwner) { + this.fireEvent('resize', this, width, height); + }, + + /** + * Occurs before componentLayout is run. Returning false from this method will prevent the componentLayout + * from being executed. + * @param {Number} adjWidth The box-adjusted width that was set + * @param {Number} adjHeight The box-adjusted height that was set + * @param {Boolean} isSetSize Whether or not the height/width are stored on the component permanently + * @param {Ext.Component} layoutOwner Component which sent the layout. Only used when isSetSize is false. + */ + beforeComponentLayout: function(width, height, isSetSize, layoutOwner) { + return true; + }, + + /** + * Sets the left and top of the component. To set the page XY position instead, use {@link #setPagePosition}. + * This method fires the {@link #move} event. + * @param {Number} left The new left + * @param {Number} top The new top + * @return {Ext.Component} this + */ + setPosition : function(x, y) { + var me = this; + + if (Ext.isObject(x)) { + y = x.y; + x = x.x; + } + + if (!me.rendered) { + return me; + } + + if (x !== undefined || y !== undefined) { + me.el.setBox(x, y); + me.onPosition(x, y); + me.fireEvent('move', me, x, y); + } + return me; + }, + + /* @private + * Called after the component is moved, this method is empty by default but can be implemented by any + * subclass that needs to perform custom logic after a move occurs. + * @param {Number} x The new x position + * @param {Number} y The new y position + */ + onPosition: Ext.emptyFn, + + /** + * Sets the width of the component. This method fires the {@link #resize} event. + * @param {Number} width The new width to setThis may be one of:
    + *
  • A Number specifying the new width in the {@link #getEl Element}'s {@link Ext.core.Element#defaultUnit}s (by default, pixels).
  • + *
  • A String used to set the CSS width style.
  • + *
+ * @return {Ext.Component} this + */ + setWidth : function(width) { + return this.setSize(width); + }, + + /** + * Sets the height of the component. This method fires the {@link #resize} event. + * @param {Number} height The new height to set. This may be one of:
    + *
  • A Number specifying the new height in the {@link #getEl Element}'s {@link Ext.core.Element#defaultUnit}s (by default, pixels).
  • + *
  • A String used to set the CSS height style.
  • + *
  • undefined to leave the height unchanged.
  • + *
+ * @return {Ext.Component} this + */ + setHeight : function(height) { + return this.setSize(undefined, height); + }, + + /** + * Gets the current size of the component's underlying element. + * @return {Object} An object containing the element's size {width: (element width), height: (element height)} + */ + getSize : function() { + return this.el.getSize(); + }, + + /** + * Gets the current width of the component's underlying element. + * @return {Number} + */ + getWidth : function() { + return this.el.getWidth(); + }, + + /** + * Gets the current height of the component's underlying element. + * @return {Number} + */ + getHeight : function() { + return this.el.getHeight(); + }, + + /** + * Gets the {@link Ext.ComponentLoader} for this Component. + * @return {Ext.ComponentLoader} The loader instance, null if it doesn't exist. + */ + getLoader: function(){ + var me = this, + autoLoad = me.autoLoad ? (Ext.isObject(me.autoLoad) ? me.autoLoad : {url: me.autoLoad}) : null, + loader = me.loader || autoLoad; + + if (loader) { + if (!loader.isLoader) { + me.loader = Ext.create('Ext.ComponentLoader', Ext.apply({ + target: me, + autoLoad: autoLoad + }, loader)); + } else { + loader.setTarget(me); + } + return me.loader; + + } + return null; + }, + + /** + * This method allows you to show or hide a LoadMask on top of this component. + * @param {Boolean/Object/String} load True to show the default LoadMask, a config object + * that will be passed to the LoadMask constructor, or a message String to show. False to + * hide the current LoadMask. + * @param {Boolean} targetEl True to mask the targetEl of this Component instead of the this.el. + * For example, setting this to true on a Panel will cause only the body to be masked. (defaults to false) + * @return {Ext.LoadMask} The LoadMask instance that has just been shown. + */ + setLoading : function(load, targetEl) { + var me = this, + config; + + if (me.rendered) { + if (load !== false && !me.collapsed) { + if (Ext.isObject(load)) { + config = load; + } + else if (Ext.isString(load)) { + config = {msg: load}; + } + else { + config = {}; + } + me.loadMask = me.loadMask || Ext.create('Ext.LoadMask', targetEl ? me.getTargetEl() : me.el, config); + me.loadMask.show(); + } else if (me.loadMask) { + Ext.destroy(me.loadMask); + me.loadMask = null; + } + } + + return me.loadMask; + }, + + /** + * Sets the dock position of this component in its parent panel. Note that + * this only has effect if this item is part of the dockedItems collection + * of a parent that has a DockLayout (note that any Panel has a DockLayout + * by default) + * @return {Component} this + */ + setDocked : function(dock, layoutParent) { + var me = this; + + me.dock = dock; + if (layoutParent && me.ownerCt && me.rendered) { + me.ownerCt.doComponentLayout(); + } + return me; + }, + + onDestroy : function() { + var me = this; + + if (me.monitorResize && Ext.EventManager.resizeEvent) { + Ext.EventManager.resizeEvent.removeListener(me.setSize, me); + } + Ext.destroy(me.componentLayout, me.loadMask); + }, + + /** + * Destroys the Component. + */ + destroy : function() { + var me = this; + + if (!me.isDestroyed) { + if (me.fireEvent('beforedestroy', me) !== false) { + me.destroying = true; + me.beforeDestroy(); + + if (me.floating) { + delete me.floatParent; + // A zIndexManager is stamped into a *floating* Component when it is added to a Container. + // If it has no zIndexManager at render time, it is assigned to the global Ext.WindowManager instance. + if (me.zIndexManager) { + me.zIndexManager.unregister(me); + } + } else if (me.ownerCt && me.ownerCt.remove) { + me.ownerCt.remove(me, false); + } + + if (me.rendered) { + me.el.remove(); + } + + me.onDestroy(); + + // Attempt to destroy all plugins + Ext.destroy(me.plugins); + + Ext.ComponentManager.unregister(me); + me.fireEvent('destroy', me); + + me.mixins.state.destroy.call(me); + + me.clearListeners(); + me.destroying = false; + me.isDestroyed = true; + } + } + }, + + /** + * Retrieves a plugin by its pluginId which has been bound to this + * component. + * @returns {Ext.AbstractPlugin} pluginInstance + */ + getPlugin: function(pluginId) { + var i = 0, + plugins = this.plugins, + ln = plugins.length; + for (; i < ln; i++) { + if (plugins[i].pluginId === pluginId) { + return plugins[i]; + } + } + }, + + /** + * Determines whether this component is the descendant of a particular container. + * @param {Ext.Container} container + * @returns {Boolean} isDescendant + */ + isDescendantOf: function(container) { + return !!this.findParentBy(function(p){ + return p === container; + }); + } +}, function() { + this.createAlias({ + on: 'addListener', + prev: 'previousSibling', + next: 'nextSibling' + }); +}); + +/** + * @class Ext.AbstractPlugin + * @extends Object + * + * Plugins are injected + */ +Ext.define('Ext.AbstractPlugin', { + disabled: false, + + constructor: function(config) { + // + if (!config.cmp && Ext.global.console) { + Ext.global.console.warn("Attempted to attach a plugin "); + } + // + Ext.apply(this, config); + }, + + getCmp: function() { + return this.cmp; + }, + + /** + * The init method is invoked after initComponent has been run for the + * component which we are injecting the plugin into. + */ + init: Ext.emptyFn, + + /** + * The destroy method is invoked by the owning Component at the time the Component is being destroyed. + * Use this method to clean up an resources. + */ + destroy: Ext.emptyFn, + + /** + * Enable the plugin and set the disabled flag to false. + */ + enable: function() { + this.disabled = false; + }, + + /** + * Disable the plugin and set the disabled flag to true. + */ + disable: function() { + this.disabled = true; + } +}); + +/** + * @class Ext.data.Connection + * The Connection class encapsulates a connection to the page's originating domain, allowing requests to be made either + * to a configured URL, or to a URL specified at request time. + * + * Requests made by this class are asynchronous, and will return immediately. No data from the server will be available + * to the statement immediately following the {@link #request} call. To process returned data, use a success callback + * in the request options object, or an {@link #requestcomplete event listener}. + * + *

File Uploads

+ * + * File uploads are not performed using normal "Ajax" techniques, that is they are not performed using XMLHttpRequests. + * Instead the form is submitted in the standard manner with the DOM <form> element temporarily modified to have its + * target set to refer to a dynamically generated, hidden <iframe> which is inserted into the document but removed + * after the return data has been gathered. + * + * The server response is parsed by the browser to create the document for the IFRAME. If the server is using JSON to + * send the return object, then the Content-Type header must be set to "text/html" in order to tell the browser to + * insert the text unchanged into the document body. + * + * Characters which are significant to an HTML parser must be sent as HTML entities, so encode "<" as "&lt;", "&" as + * "&amp;" etc. + * + * The response text is retrieved from the document, and a fake XMLHttpRequest object is created containing a + * responseText property in order to conform to the requirements of event handlers and callbacks. + * + * Be aware that file upload packets are sent with the content type multipart/form and some server technologies + * (notably JEE) may require some custom processing in order to retrieve parameter names and parameter values from the + * packet content. + * + * Also note that it's not possible to check the response code of the hidden iframe, so the success handler will ALWAYS fire. + */ +Ext.define('Ext.data.Connection', { + mixins: { + observable: 'Ext.util.Observable' + }, + + statics: { + requestId: 0 + }, + + url: null, + async: true, + method: null, + username: '', + password: '', + + /** + * @cfg {Boolean} disableCaching (Optional) True to add a unique cache-buster param to GET requests. (defaults to true) + * @type Boolean + */ + disableCaching: true, + + /** + * @cfg {String} disableCachingParam (Optional) Change the parameter which is sent went disabling caching + * through a cache buster. Defaults to '_dc' + * @type String + */ + disableCachingParam: '_dc', + + /** + * @cfg {Number} timeout (Optional) The timeout in milliseconds to be used for requests. (defaults to 30000) + */ + timeout : 30000, + + /** + * @param {Object} extraParams (Optional) Any parameters to be appended to the request. + */ + + useDefaultHeader : true, + defaultPostHeader : 'application/x-www-form-urlencoded; charset=UTF-8', + useDefaultXhrHeader : true, + defaultXhrHeader : 'XMLHttpRequest', + + constructor : function(config) { + config = config || {}; + Ext.apply(this, config); + + this.addEvents( + /** + * @event beforerequest + * Fires before a network request is made to retrieve a data object. + * @param {Connection} conn This Connection object. + * @param {Object} options The options config object passed to the {@link #request} method. + */ + 'beforerequest', + /** + * @event requestcomplete + * Fires if the request was successfully completed. + * @param {Connection} conn This Connection object. + * @param {Object} response The XHR object containing the response data. + * See The XMLHttpRequest Object + * for details. + * @param {Object} options The options config object passed to the {@link #request} method. + */ + 'requestcomplete', + /** + * @event requestexception + * Fires if an error HTTP status was returned from the server. + * See HTTP Status Code Definitions + * for details of HTTP status codes. + * @param {Connection} conn This Connection object. + * @param {Object} response The XHR object containing the response data. + * See The XMLHttpRequest Object + * for details. + * @param {Object} options The options config object passed to the {@link #request} method. + */ + 'requestexception' + ); + this.requests = {}; + this.mixins.observable.constructor.call(this); + }, + + /** + *

Sends an HTTP request to a remote server.

+ *

Important: Ajax server requests are asynchronous, and this call will + * return before the response has been received. Process any returned data + * in a callback function.

+ *

+Ext.Ajax.request({
+url: 'ajax_demo/sample.json',
+success: function(response, opts) {
+  var obj = Ext.decode(response.responseText);
+  console.dir(obj);
+},
+failure: function(response, opts) {
+  console.log('server-side failure with status code ' + response.status);
+}
+});
+     * 
+ *

To execute a callback function in the correct scope, use the scope option.

+ * @param {Object} options An object which may contain the following properties:
    + *
  • url : String/Function (Optional)
    The URL to + * which to send the request, or a function to call which returns a URL string. The scope of the + * function is specified by the scope option. Defaults to the configured + * {@link #url}.
  • + *
  • params : Object/String/Function (Optional)
    + * An object containing properties which are used as parameters to the + * request, a url encoded string or a function to call to get either. The scope of the function + * is specified by the scope option.
  • + *
  • method : String (Optional)
    The HTTP method to use + * for the request. Defaults to the configured method, or if no method was configured, + * "GET" if no parameters are being sent, and "POST" if parameters are being sent. Note that + * the method name is case-sensitive and should be all caps.
  • + *
  • callback : Function (Optional)
    The + * function to be called upon receipt of the HTTP response. The callback is + * called regardless of success or failure and is passed the following + * parameters:
      + *
    • options : Object
      The parameter to the request call.
    • + *
    • success : Boolean
      True if the request succeeded.
    • + *
    • response : Object
      The XMLHttpRequest object containing the response data. + * See http://www.w3.org/TR/XMLHttpRequest/ for details about + * accessing elements of the response.
    • + *
  • + *
  • success : Function (Optional)
    The function + * to be called upon success of the request. The callback is passed the following + * parameters:
      + *
    • response : Object
      The XMLHttpRequest object containing the response data.
    • + *
    • options : Object
      The parameter to the request call.
    • + *
  • + *
  • failure : Function (Optional)
    The function + * to be called upon failure of the request. The callback is passed the + * following parameters:
      + *
    • response : Object
      The XMLHttpRequest object containing the response data.
    • + *
    • options : Object
      The parameter to the request call.
    • + *
  • + *
  • scope : Object (Optional)
    The scope in + * which to execute the callbacks: The "this" object for the callback function. If the url, or params options were + * specified as functions from which to draw values, then this also serves as the scope for those function calls. + * Defaults to the browser window.
  • + *
  • timeout : Number (Optional)
    The timeout in milliseconds to be used for this request. Defaults to 30 seconds.
  • + *
  • form : Element/HTMLElement/String (Optional)
    The <form> + * Element or the id of the <form> to pull parameters from.
  • + *
  • isUpload : Boolean (Optional)
    Only meaningful when used + * with the form option. + *

    True if the form object is a file upload (will be set automatically if the form was + * configured with enctype "multipart/form-data").

    + *

    File uploads are not performed using normal "Ajax" techniques, that is they are not + * performed using XMLHttpRequests. Instead the form is submitted in the standard manner with the + * DOM <form> element temporarily modified to have its + * target set to refer + * to a dynamically generated, hidden <iframe> which is inserted into the document + * but removed after the return data has been gathered.

    + *

    The server response is parsed by the browser to create the document for the IFRAME. If the + * server is using JSON to send the return object, then the + * Content-Type header + * must be set to "text/html" in order to tell the browser to insert the text unchanged into the document body.

    + *

    The response text is retrieved from the document, and a fake XMLHttpRequest object + * is created containing a responseText property in order to conform to the + * requirements of event handlers and callbacks.

    + *

    Be aware that file upload packets are sent with the content type multipart/form + * and some server technologies (notably JEE) may require some custom processing in order to + * retrieve parameter names and parameter values from the packet content.

    + *
  • + *
  • headers : Object (Optional)
    Request + * headers to set for the request.
  • + *
  • xmlData : Object (Optional)
    XML document + * to use for the post. Note: This will be used instead of params for the post + * data. Any params will be appended to the URL.
  • + *
  • jsonData : Object/String (Optional)
    JSON + * data to use as the post. Note: This will be used instead of params for the post + * data. Any params will be appended to the URL.
  • + *
  • disableCaching : Boolean (Optional)
    True + * to add a unique cache-buster param to GET requests.
  • + *

+ *

The options object may also contain any other property which might be needed to perform + * postprocessing in a callback because it is passed to callback functions.

+ * @return {Object} request The request object. This may be used + * to cancel the request. + */ + request : function(options) { + options = options || {}; + var me = this, + scope = options.scope || window, + username = options.username || me.username, + password = options.password || me.password || '', + async, + requestOptions, + request, + headers, + xhr; + + if (me.fireEvent('beforerequest', me, options) !== false) { + + requestOptions = me.setOptions(options, scope); + + if (this.isFormUpload(options) === true) { + this.upload(options.form, requestOptions.url, requestOptions.data, options); + return null; + } + + // if autoabort is set, cancel the current transactions + if (options.autoAbort === true || me.autoAbort) { + me.abort(); + } + + // create a connection object + xhr = this.getXhrInstance(); + + async = options.async !== false ? (options.async || me.async) : false; + + // open the request + if (username) { + xhr.open(requestOptions.method, requestOptions.url, async, username, password); + } else { + xhr.open(requestOptions.method, requestOptions.url, async); + } + + headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params); + + // create the transaction object + request = { + id: ++Ext.data.Connection.requestId, + xhr: xhr, + headers: headers, + options: options, + async: async, + timeout: setTimeout(function() { + request.timedout = true; + me.abort(request); + }, options.timeout || me.timeout) + }; + me.requests[request.id] = request; + + // bind our statechange listener + if (async) { + xhr.onreadystatechange = Ext.Function.bind(me.onStateChange, me, [request]); + } + + // start the request! + xhr.send(requestOptions.data); + if (!async) { + return this.onComplete(request); + } + return request; + } else { + Ext.callback(options.callback, options.scope, [options, undefined, undefined]); + return null; + } + }, + + /** + * Upload a form using a hidden iframe. + * @param {Mixed} form The form to upload + * @param {String} url The url to post to + * @param {String} params Any extra parameters to pass + * @param {Object} options The initial options + */ + upload: function(form, url, params, options){ + form = Ext.getDom(form); + options = options || {}; + + var id = Ext.id(), + frame = document.createElement('iframe'), + hiddens = [], + encoding = 'multipart/form-data', + buf = { + target: form.target, + method: form.method, + encoding: form.encoding, + enctype: form.enctype, + action: form.action + }, hiddenItem; + + /* + * Originally this behaviour was modified for Opera 10 to apply the secure URL after + * the frame had been added to the document. It seems this has since been corrected in + * Opera so the behaviour has been reverted, the URL will be set before being added. + */ + Ext.fly(frame).set({ + id: id, + name: id, + cls: Ext.baseCSSPrefix + 'hide-display', + src: Ext.SSL_SECURE_URL + }); + + document.body.appendChild(frame); + + // This is required so that IE doesn't pop the response up in a new window. + if (document.frames) { + document.frames[id].name = id; + } + + Ext.fly(form).set({ + target: id, + method: 'POST', + enctype: encoding, + encoding: encoding, + action: url || buf.action + }); + + // add dynamic params + if (params) { + Ext.iterate(Ext.Object.fromQueryString(params), function(name, value){ + hiddenItem = document.createElement('input'); + Ext.fly(hiddenItem).set({ + type: 'hidden', + value: value, + name: name + }); + form.appendChild(hiddenItem); + hiddens.push(hiddenItem); + }); + } + + Ext.fly(frame).on('load', Ext.Function.bind(this.onUploadComplete, this, [frame, options]), null, {single: true}); + form.submit(); + + Ext.fly(form).set(buf); + Ext.each(hiddens, function(h) { + Ext.removeNode(h); + }); + }, + + onUploadComplete: function(frame, options){ + var me = this, + // bogus response object + response = { + responseText: '', + responseXML: null + }, doc, firstChild; + + try { + doc = frame.contentWindow.document || frame.contentDocument || window.frames[id].document; + if (doc) { + if (doc.body) { + if (/textarea/i.test((firstChild = doc.body.firstChild || {}).tagName)) { // json response wrapped in textarea + response.responseText = firstChild.value; + } else { + response.responseText = doc.body.innerHTML; + } + } + //in IE the document may still have a body even if returns XML. + response.responseXML = doc.XMLDocument || doc; + } + } catch (e) { + } + + me.fireEvent('requestcomplete', me, response, options); + + Ext.callback(options.success, options.scope, [response, options]); + Ext.callback(options.callback, options.scope, [options, true, response]); + + setTimeout(function(){ + Ext.removeNode(frame); + }, 100); + }, + + /** + * Detect whether the form is intended to be used for an upload. + * @private + */ + isFormUpload: function(options){ + var form = this.getForm(options); + if (form) { + return (options.isUpload || (/multipart\/form-data/i).test(form.getAttribute('enctype'))); + } + return false; + }, + + /** + * Get the form object from options. + * @private + * @param {Object} options The request options + * @return {HTMLElement} The form, null if not passed + */ + getForm: function(options){ + return Ext.getDom(options.form) || null; + }, + + /** + * Set various options such as the url, params for the request + * @param {Object} options The initial options + * @param {Object} scope The scope to execute in + * @return {Object} The params for the request + */ + setOptions: function(options, scope){ + var me = this, + params = options.params || {}, + extraParams = me.extraParams, + urlParams = options.urlParams, + url = options.url || me.url, + jsonData = options.jsonData, + method, + disableCache, + data; + + + // allow params to be a method that returns the params object + if (Ext.isFunction(params)) { + params = params.call(scope, options); + } + + // allow url to be a method that returns the actual url + if (Ext.isFunction(url)) { + url = url.call(scope, options); + } + + url = this.setupUrl(options, url); + + // + if (!url) { + Ext.Error.raise({ + options: options, + msg: 'No URL specified' + }); + } + // + + // check for xml or json data, and make sure json data is encoded + data = options.rawData || options.xmlData || jsonData || null; + if (jsonData && !Ext.isPrimitive(jsonData)) { + data = Ext.encode(data); + } + + // make sure params are a url encoded string and include any extraParams if specified + if (Ext.isObject(params)) { + params = Ext.Object.toQueryString(params); + } + + if (Ext.isObject(extraParams)) { + extraParams = Ext.Object.toQueryString(extraParams); + } + + params = params + ((extraParams) ? ((params) ? '&' : '') + extraParams : ''); + + urlParams = Ext.isObject(urlParams) ? Ext.Object.toQueryString(urlParams) : urlParams; + + params = this.setupParams(options, params); + + // decide the proper method for this request + method = (options.method || me.method || ((params || data) ? 'POST' : 'GET')).toUpperCase(); + this.setupMethod(options, method); + + + disableCache = options.disableCaching !== false ? (options.disableCaching || me.disableCaching) : false; + // if the method is get append date to prevent caching + if (method === 'GET' && disableCache) { + url = Ext.urlAppend(url, (options.disableCachingParam || me.disableCachingParam) + '=' + (new Date().getTime())); + } + + // if the method is get or there is json/xml data append the params to the url + if ((method == 'GET' || data) && params) { + url = Ext.urlAppend(url, params); + params = null; + } + + // allow params to be forced into the url + if (urlParams) { + url = Ext.urlAppend(url, urlParams); + } + + return { + url: url, + method: method, + data: data || params || null + }; + }, + + /** + * Template method for overriding url + * @private + * @param {Object} options + * @param {String} url + * @return {String} The modified url + */ + setupUrl: function(options, url){ + var form = this.getForm(options); + if (form) { + url = url || form.action; + } + return url; + }, + + + /** + * Template method for overriding params + * @private + * @param {Object} options + * @param {String} params + * @return {String} The modified params + */ + setupParams: function(options, params) { + var form = this.getForm(options), + serializedForm; + if (form && !this.isFormUpload(options)) { + serializedForm = Ext.core.Element.serializeForm(form); + params = params ? (params + '&' + serializedForm) : serializedForm; + } + return params; + }, + + /** + * Template method for overriding method + * @private + * @param {Object} options + * @param {String} method + * @return {String} The modified method + */ + setupMethod: function(options, method){ + if (this.isFormUpload(options)) { + return 'POST'; + } + return method; + }, + + /** + * Setup all the headers for the request + * @private + * @param {Object} xhr The xhr object + * @param {Object} options The options for the request + * @param {Object} data The data for the request + * @param {Object} params The params for the request + */ + setupHeaders: function(xhr, options, data, params){ + var me = this, + headers = Ext.apply({}, options.headers || {}, me.defaultHeaders || {}), + contentType = me.defaultPostHeader, + jsonData = options.jsonData, + xmlData = options.xmlData, + key, + header; + + if (!headers['Content-Type'] && (data || params)) { + if (data) { + if (options.rawData) { + contentType = 'text/plain'; + } else { + if (xmlData && Ext.isDefined(xmlData)) { + contentType = 'text/xml'; + } else if (jsonData && Ext.isDefined(jsonData)) { + contentType = 'application/json'; + } + } + } + headers['Content-Type'] = contentType; + } + + if (me.useDefaultXhrHeader && !headers['X-Requested-With']) { + headers['X-Requested-With'] = me.defaultXhrHeader; + } + // set up all the request headers on the xhr object + try{ + for (key in headers) { + if (headers.hasOwnProperty(key)) { + header = headers[key]; + xhr.setRequestHeader(key, header); + } + + } + } catch(e) { + me.fireEvent('exception', key, header); + } + return headers; + }, + + /** + * Creates the appropriate XHR transport for the browser. + * @private + */ + getXhrInstance: (function(){ + var options = [function(){ + return new XMLHttpRequest(); + }, function(){ + return new ActiveXObject('MSXML2.XMLHTTP.3.0'); + }, function(){ + return new ActiveXObject('MSXML2.XMLHTTP'); + }, function(){ + return new ActiveXObject('Microsoft.XMLHTTP'); + }], i = 0, + len = options.length, + xhr; + + for(; i < len; ++i) { + try{ + xhr = options[i]; + xhr(); + break; + }catch(e){} + } + return xhr; + })(), + + /** + * Determine whether this object has a request outstanding. + * @param {Object} request (Optional) defaults to the last transaction + * @return {Boolean} True if there is an outstanding request. + */ + isLoading : function(request) { + if (!(request && request.xhr)) { + return false; + } + // if there is a connection and readyState is not 0 or 4 + var state = request.xhr.readyState; + return !(state === 0 || state == 4); + }, + + /** + * Aborts any outstanding request. + * @param {Object} request (Optional) defaults to the last request + */ + abort : function(request) { + var me = this, + requests = me.requests, + id; + + if (request && me.isLoading(request)) { + /** + * Clear out the onreadystatechange here, this allows us + * greater control, the browser may/may not fire the function + * depending on a series of conditions. + */ + request.xhr.onreadystatechange = null; + request.xhr.abort(); + me.clearTimeout(request); + if (!request.timedout) { + request.aborted = true; + } + me.onComplete(request); + me.cleanup(request); + } else if (!request) { + for(id in requests) { + if (requests.hasOwnProperty(id)) { + me.abort(requests[id]); + } + } + } + }, + + /** + * Fires when the state of the xhr changes + * @private + * @param {Object} request The request + */ + onStateChange : function(request) { + if (request.xhr.readyState == 4) { + this.clearTimeout(request); + this.onComplete(request); + this.cleanup(request); + } + }, + + /** + * Clear the timeout on the request + * @private + * @param {Object} The request + */ + clearTimeout: function(request){ + clearTimeout(request.timeout); + delete request.timeout; + }, + + /** + * Clean up any left over information from the request + * @private + * @param {Object} The request + */ + cleanup: function(request){ + request.xhr = null; + delete request.xhr; + }, + + /** + * To be called when the request has come back from the server + * @private + * @param {Object} request + * @return {Object} The response + */ + onComplete : function(request) { + var me = this, + options = request.options, + result = me.parseStatus(request.xhr.status), + success = result.success, + response; + + if (success) { + response = me.createResponse(request); + me.fireEvent('requestcomplete', me, response, options); + Ext.callback(options.success, options.scope, [response, options]); + } else { + if (result.isException || request.aborted || request.timedout) { + response = me.createException(request); + } else { + response = me.createResponse(request); + } + me.fireEvent('requestexception', me, response, options); + Ext.callback(options.failure, options.scope, [response, options]); + } + Ext.callback(options.callback, options.scope, [options, success, response]); + delete me.requests[request.id]; + return response; + }, + + /** + * Check if the response status was successful + * @param {Number} status The status code + * @return {Object} An object containing success/status state + */ + parseStatus: function(status) { + // see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223 + status = status == 1223 ? 204 : status; + + var success = (status >= 200 && status < 300) || status == 304, + isException = false; + + if (!success) { + switch (status) { + case 12002: + case 12029: + case 12030: + case 12031: + case 12152: + case 13030: + isException = true; + break; + } + } + return { + success: success, + isException: isException + }; + }, + + /** + * Create the response object + * @private + * @param {Object} request + */ + createResponse : function(request) { + var xhr = request.xhr, + headers = {}, + lines = xhr.getAllResponseHeaders().replace(/\r\n/g, '\n').split('\n'), + count = lines.length, + line, index, key, value, response; + + while (count--) { + line = lines[count]; + index = line.indexOf(':'); + if(index >= 0) { + key = line.substr(0, index).toLowerCase(); + if (line.charAt(index + 1) == ' ') { + ++index; + } + headers[key] = line.substr(index + 1); + } + } + + request.xhr = null; + delete request.xhr; + + response = { + request: request, + requestId : request.id, + status : xhr.status, + statusText : xhr.statusText, + getResponseHeader : function(header){ return headers[header.toLowerCase()]; }, + getAllResponseHeaders : function(){ return headers; }, + responseText : xhr.responseText, + responseXML : xhr.responseXML + }; + + // If we don't explicitly tear down the xhr reference, IE6/IE7 will hold this in the closure of the + // functions created with getResponseHeader/getAllResponseHeaders + xhr = null; + return response; + }, + + /** + * Create the exception object + * @private + * @param {Object} request + */ + createException : function(request) { + return { + request : request, + requestId : request.id, + status : request.aborted ? -1 : 0, + statusText : request.aborted ? 'transaction aborted' : 'communication failure', + aborted: request.aborted, + timedout: request.timedout + }; + } +}); + +/** + * @class Ext.Ajax + * @singleton + * @markdown + * @extends Ext.data.Connection + +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 be changing a property on the Ext.Ajax class: + + Ext.Ajax.timeout = 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.timeout = 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, + + /** + * @cfg {String} url @hide + */ + /** + * @cfg {Object} extraParams @hide + */ + /** + * @cfg {Object} defaultHeaders @hide + */ + /** + * @cfg {String} method (Optional) @hide + */ + /** + * @cfg {Number} timeout (Optional) @hide + */ + /** + * @cfg {Boolean} autoAbort (Optional) @hide + */ + + /** + * @cfg {Boolean} disableCaching (Optional) @hide + */ + + /** + * @property disableCaching + * True to add a unique cache-buster param to GET requests. (defaults to true) + * @type Boolean + */ + /** + * @property url + * The default URL to be used for requests to the server. (defaults to undefined) + * If the server receives all requests through one URL, setting this once is easier than + * entering it on every request. + * @type String + */ + /** + * @property extraParams + * An object containing properties which are used as extra parameters to each request made + * by this object (defaults to undefined). Session information and other data that you need + * to pass with each request are commonly put here. + * @type Object + */ + /** + * @property defaultHeaders + * An object containing request headers which are added to each request made by this object + * (defaults to undefined). + * @type Object + */ + /** + * @property method + * The default HTTP method to be used for requests. Note that this is case-sensitive and + * should be all caps (defaults to undefined; if not set but params are present will use + * "POST", otherwise will use "GET".) + * @type String + */ + /** + * @property timeout + * The timeout in milliseconds to be used for requests. (defaults to 30000) + * @type Number + */ + + /** + * @property autoAbort + * Whether a new request should abort any pending requests. (defaults to false) + * @type Boolean + */ + autoAbort : false +}); +/** + * @author Ed Spencer + * @class Ext.data.Association + * @extends Object + * + *

Associations enable you to express relationships between different {@link Ext.data.Model Models}. Let's say we're + * writing an ecommerce system where Users can make Orders - there's a relationship between these Models that we can + * express like this:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name', 'email'],
+
+    hasMany: {model: 'Order', name: 'orders'}
+});
+
+Ext.define('Order', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'user_id', 'status', 'price'],
+
+    belongsTo: 'User'
+});
+
+ * + *

We've set up two models - User and Order - and told them about each other. You can set up as many associations on + * each Model as you need using the two default types - {@link Ext.data.HasManyAssociation hasMany} and + * {@link Ext.data.BelongsToAssociation belongsTo}. There's much more detail on the usage of each of those inside their + * documentation pages. If you're not familiar with Models already, {@link Ext.data.Model there is plenty on those too}.

+ * + *

Further Reading

+ * + *
    + *
  • {@link Ext.data.HasManyAssociation hasMany associations} + *
  • {@link Ext.data.BelongsToAssociation belongsTo associations} + *
  • {@link Ext.data.Model using Models} + *
+ * + * Self association models + *

We can also have models that create parent/child associations between the same type. Below is an example, where + * groups can be nested inside other groups:

+ *

+
+// Server Data
+{
+    "groups": {
+        "id": 10,
+        "parent_id": 100,
+        "name": "Main Group",
+        "parent_group": {
+            "id": 100,
+            "parent_id": null,
+            "name": "Parent Group"
+        },
+        "child_groups": [{
+            "id": 2,
+            "parent_id": 10,
+            "name": "Child Group 1"
+        },{
+            "id": 3,
+            "parent_id": 10,
+            "name": "Child Group 2"
+        },{
+            "id": 4,
+            "parent_id": 10,
+            "name": "Child Group 3"
+        }]
+    }
+}
+
+// Client code
+Ext.define('Group', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'parent_id', 'name'],
+    proxy: {
+        type: 'ajax',
+        url: 'data.json',
+        reader: {
+            type: 'json',
+            root: 'groups'
+        }
+    },
+    associations: [{
+        type: 'hasMany',
+        model: 'Group',
+        primaryKey: 'id',
+        foreignKey: 'parent_id',
+        autoLoad: true,
+        associationKey: 'child_groups' // read child data from child_groups
+    }, {
+        type: 'belongsTo',
+        model: 'Group',
+        primaryKey: 'id',
+        foreignKey: 'parent_id',
+        autoLoad: true,
+        associationKey: 'parent_group' // read parent data from parent_group
+    }]
+});
+
+
+Ext.onReady(function(){
+    
+    Group.load(10, {
+        success: function(group){
+            console.log(group.getGroup().get('name'));
+            
+            group.groups().each(function(rec){
+                console.log(rec.get('name'));
+            });
+        }
+    });
+    
+});
+ * 
+ * + * @constructor + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.Association', { + /** + * @cfg {String} ownerModel The string name of the model that owns the association. Required + */ + + /** + * @cfg {String} associatedModel The string name of the model that is being associated with. Required + */ + + /** + * @cfg {String} primaryKey The name of the primary key on the associated model. Defaults to 'id'. + * In general this will be the {@link Ext.data.Model#idProperty} of the Model. + */ + primaryKey: 'id', + + /** + * @cfg {Ext.data.reader.Reader} reader A special reader to read associated data + */ + + /** + * @cfg {String} associationKey The name of the property in the data to read the association from. + * Defaults to the name of the associated model. + */ + + defaultReaderType: 'json', + + statics: { + create: function(association){ + if (!association.isAssociation) { + if (Ext.isString(association)) { + association = { + type: association + }; + } + + switch (association.type) { + case 'belongsTo': + return Ext.create('Ext.data.BelongsToAssociation', association); + case 'hasMany': + return Ext.create('Ext.data.HasManyAssociation', association); + //TODO Add this back when it's fixed +// case 'polymorphic': +// return Ext.create('Ext.data.PolymorphicAssociation', association); + default: + // + Ext.Error.raise('Unknown Association type: "' + association.type + '"'); + // + } + } + return association; + } + }, + + constructor: function(config) { + Ext.apply(this, config); + + var types = Ext.ModelManager.types, + ownerName = config.ownerModel, + associatedName = config.associatedModel, + ownerModel = types[ownerName], + associatedModel = types[associatedName], + ownerProto; + + // + if (ownerModel === undefined) { + Ext.Error.raise("The configured ownerModel was not valid (you tried " + ownerName + ")"); + } + if (associatedModel === undefined) { + Ext.Error.raise("The configured associatedModel was not valid (you tried " + associatedName + ")"); + } + // + + this.ownerModel = ownerModel; + this.associatedModel = associatedModel; + + /** + * The name of the model that 'owns' the association + * @property ownerName + * @type String + */ + + /** + * The name of the model is on the other end of the association (e.g. if a User model hasMany Orders, this is 'Order') + * @property associatedName + * @type String + */ + + Ext.applyIf(this, { + ownerName : ownerName, + associatedName: associatedName + }); + }, + + /** + * Get a specialized reader for reading associated data + * @return {Ext.data.reader.Reader} The reader, null if not supplied + */ + getReader: function(){ + var me = this, + reader = me.reader, + model = me.associatedModel; + + if (reader) { + if (Ext.isString(reader)) { + reader = { + type: reader + }; + } + if (reader.isReader) { + reader.setModel(model); + } else { + Ext.applyIf(reader, { + model: model, + type : me.defaultReaderType + }); + } + me.reader = Ext.createByAlias('reader.' + reader.type, reader); + } + return me.reader || null; + } +}); + +/** + * @author Ed Spencer + * @class Ext.ModelManager + * @extends Ext.AbstractManager + +The ModelManager keeps track of all {@link Ext.data.Model} types defined in your application. + +__Creating Model Instances__ +Model instances can be created by using the {@link #create} function. It is also possible to do +this by using the Model type directly. The following snippets are equivalent: + + Ext.define('User', { + extend: 'Ext.data.Model', + fields: ['first', 'last'] + }); + + // method 1, create through the manager + Ext.ModelManager.create({ + first: 'Ed', + last: 'Spencer' + }, 'User'); + + // method 2, create on the type directly + new User({ + first: 'Ed', + last: 'Spencer' + }); + +__Accessing Model Types__ +A reference to a Model type can be obtained by using the {@link #getModel} function. Since models types +are normal classes, you can access the type directly. The following snippets are equivalent: + + Ext.define('User', { + extend: 'Ext.data.Model', + fields: ['first', 'last'] + }); + + // method 1, access model type through the manager + var UserType = Ext.ModelManager.getModel('User'); + + // method 2, reference the type directly + var UserType = User; + + * @markdown + * @singleton + */ +Ext.define('Ext.ModelManager', { + extend: 'Ext.AbstractManager', + alternateClassName: 'Ext.ModelMgr', + requires: ['Ext.data.Association'], + + singleton: true, + + typeName: 'mtype', + + /** + * Private stack of associations that must be created once their associated model has been defined + * @property associationStack + * @type Array + */ + associationStack: [], + + /** + * Registers a model definition. All model plugins marked with isDefault: true are bootstrapped + * immediately, as are any addition plugins defined in the model config. + * @private + */ + registerType: function(name, config) { + var proto = config.prototype, + model; + if (proto && proto.isModel) { + // registering an already defined model + model = config; + } else { + // passing in a configuration + if (!config.extend) { + config.extend = 'Ext.data.Model'; + } + model = Ext.define(name, config); + } + this.types[name] = model; + return model; + }, + + /** + * @private + * Private callback called whenever a model has just been defined. This sets up any associations + * that were waiting for the given model to be defined + * @param {Function} model The model that was just created + */ + onModelDefined: function(model) { + var stack = this.associationStack, + length = stack.length, + create = [], + association, i, created; + + for (i = 0; i < length; i++) { + association = stack[i]; + + if (association.associatedModel == model.modelName) { + create.push(association); + } + } + + for (i = 0, length = create.length; i < length; i++) { + created = create[i]; + this.types[created.ownerModel].prototype.associations.add(Ext.data.Association.create(created)); + Ext.Array.remove(stack, created); + } + }, + + /** + * Registers an association where one of the models defined doesn't exist yet. + * The ModelManager will check when new models are registered if it can link them + * together + * @private + * @param {Ext.data.Association} association The association + */ + registerDeferredAssociation: function(association){ + this.associationStack.push(association); + }, + + /** + * Returns the {@link Ext.data.Model} for a given model name + * @param {String/Object} id The id of the model or the model instance. + */ + getModel: function(id) { + var model = id; + if (typeof model == 'string') { + model = this.types[model]; + } + return model; + }, + + /** + * Creates a new instance of a Model using the given data. + * @param {Object} data Data to initialize the Model's fields with + * @param {String} name The name of the model to create + * @param {Number} id Optional unique id of the Model instance (see {@link Ext.data.Model}) + */ + create: function(config, name, id) { + var con = typeof name == 'function' ? name : this.types[name || config.name]; + + return new con(config, id); + } +}, function() { + + /** + * Creates a new Model class from the specified config object. See {@link Ext.data.Model} for full examples. + * + * @param {Object} config A configuration object for the Model you wish to create. + * @return {Ext.data.Model} The newly registered Model + * @member Ext + * @method regModel + */ + Ext.regModel = function() { + // + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.regModel has been deprecated. Models can now be created by extending Ext.data.Model: Ext.define("MyModel", {extend: "Ext.data.Model", fields: []});.'); + } + // + return this.ModelManager.registerType.apply(this.ModelManager, arguments); + }; +}); + +/** + * @class Ext.app.Controller + * @constructor + * + * Controllers are the glue that binds an application together. All they really do is listen for events (usually from + * views) and take some action. Here's how we might create a Controller to manage Users: + * + * Ext.define('MyApp.controller.Users', { + * extend: 'Ext.app.Controller', + * + * init: function() { + * console.log('Initialized Users! This happens before the Application launch function is called'); + * } + * }); + * + * The init function is a special method that is called when your application boots. It is called before the + * {@link Ext.app.Application Application}'s launch function is executed so gives a hook point to run any code before + * your Viewport is created. + * + * The init function is a great place to set up how your controller interacts with the view, and is usually used in + * conjunction with another Controller function - {@link Ext.app.Controller#control control}. The control function + * makes it easy to listen to events on your view classes and take some action with a handler function. Let's update + * our Users controller to tell us when the panel is rendered: + * + * Ext.define('MyApp.controller.Users', { + * extend: 'Ext.app.Controller', + * + * init: function() { + * this.control({ + * 'viewport > panel': { + * render: this.onPanelRendered + * } + * }); + * }, + * + * onPanelRendered: function() { + * console.log('The panel was rendered'); + * } + * }); + * + * We've updated the init function to use this.control to set up listeners on views in our application. The control + * function uses the new ComponentQuery engine to quickly and easily get references to components on the page. If you + * are not familiar with ComponentQuery yet, be sure to check out THIS GUIDE for a full explanation. In brief though, + * it allows us to pass a CSS-like selector that will find every matching component on the page. + * + * In our init function above we supplied 'viewport > panel', which translates to "find me every Panel that is a direct + * child of a Viewport". We then supplied an object that maps event names (just 'render' in this case) to handler + * functions. The overall effect is that whenever any component that matches our selector fires a 'render' event, our + * onPanelRendered function is called. + * + * Using refs + * + * One of the most useful parts of Controllers is the new ref system. These use the new {@link Ext.ComponentQuery} to + * make it really easy to get references to Views on your page. Let's look at an example of this now: + * + * Ext.define('MyApp.controller.Users', { + extend: 'Ext.app.Controller', + + refs: [ + { + ref: 'list', + selector: 'grid' + } + ], + + init: function() { + this.control({ + 'button': { + click: this.refreshGrid + } + }); + }, + + refreshGrid: function() { + this.getList().store.load(); + } + }); + * + * This example assumes the existence of a {@link Ext.grid.Panel Grid} on the page, which contains a single button to + * refresh the Grid when clicked. In our refs array, we set up a reference to the grid. There are two parts to this - + * the 'selector', which is a {@link Ext.ComponentQuery ComponentQuery} selector which finds any grid on the page and + * assigns it to the reference 'list'. + * + * By giving the reference a name, we get a number of things for free. The first is the getList function that we use in + * the refreshGrid method above. This is generated automatically by the Controller based on the name of our ref, which + * was capitalized and prepended with get to go from 'list' to 'getList'. + * + * The way this works is that the first time getList is called by your code, the ComponentQuery selector is run and the + * first component that matches the selector ('grid' in this case) will be returned. All future calls to getList will + * use a cached reference to that grid. Usually it is advised to use a specific ComponentQuery selector that will only + * match a single View in your application (in the case above our selector will match any grid on the page). + * + * Bringing it all together, our init function is called when the application boots, at which time we call this.control + * to listen to any click on a {@link Ext.button.Button button} and call our refreshGrid function (again, this will + * match any button on the page so we advise a more specific selector than just 'button', but have left it this way for + * simplicity). When the button is clicked we use out getList function to refresh the grid. + * + * You can create any number of refs and control any number of components this way, simply adding more functions to + * your Controller as you go. For an example of real-world usage of Controllers see the Feed Viewer example in the + * examples/app/feed-viewer folder in the SDK download. + * + * Generated getter methods + * + * Refs aren't the only thing that generate convenient getter methods. Controllers often have to deal with Models and + * Stores so the framework offers a couple of easy ways to get access to those too. Let's look at another example: + * + * Ext.define('MyApp.controller.Users', { + extend: 'Ext.app.Controller', + + models: ['User'], + stores: ['AllUsers', 'AdminUsers'], + + init: function() { + var User = this.getUserModel(), + allUsers = this.getAllUsersStore(); + + var ed = new User({name: 'Ed'}); + allUsers.add(ed); + } + }); + * + * By specifying Models and Stores that the Controller cares about, it again dynamically loads them from the appropriate + * locations (app/model/User.js, app/store/AllUsers.js and app/store/AdminUsers.js in this case) and creates getter + * functions for them all. The example above will create a new User model instance and add it to the AllUsers Store. + * Of course, you could do anything in this function but in this case we just did something simple to demonstrate the + * functionality. + * + * Further Reading + * + * For more information about writing Ext JS 4 applications, please see the + * application architecture guide. Also see the {@link Ext.app.Application} documentation. + * + * @markdown + * @docauthor Ed Spencer + */ +Ext.define('Ext.app.Controller', { + /** + * @cfg {Object} id The id of this controller. You can use this id when dispatching. + */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + onClassExtended: function(cls, data) { + var className = Ext.getClassName(cls), + match = className.match(/^(.*)\.controller\./); + + if (match !== null) { + var namespace = Ext.Loader.getPrefix(className) || match[1], + onBeforeClassCreated = data.onBeforeClassCreated, + requires = [], + modules = ['model', 'view', 'store'], + prefix; + + data.onBeforeClassCreated = function(cls, data) { + var i, ln, module, + items, j, subLn, item; + + for (i = 0,ln = modules.length; i < ln; i++) { + module = modules[i]; + + items = Ext.Array.from(data[module + 's']); + + for (j = 0,subLn = items.length; j < subLn; j++) { + item = items[j]; + + prefix = Ext.Loader.getPrefix(item); + + if (prefix === '' || prefix === item) { + requires.push(namespace + '.' + module + '.' + item); + } + else { + requires.push(item); + } + } + } + + Ext.require(requires, Ext.Function.pass(onBeforeClassCreated, arguments, this)); + }; + } + }, + + constructor: function(config) { + this.mixins.observable.constructor.call(this, config); + + Ext.apply(this, config || {}); + + this.createGetters('model', this.models); + this.createGetters('store', this.stores); + this.createGetters('view', this.views); + + if (this.refs) { + this.ref(this.refs); + } + }, + + // Template method + init: function(application) {}, + // Template method + onLaunch: function(application) {}, + + createGetters: function(type, refs) { + type = Ext.String.capitalize(type); + Ext.Array.each(refs, function(ref) { + var fn = 'get', + parts = ref.split('.'); + + // Handle namespaced class names. E.g. feed.Add becomes getFeedAddView etc. + Ext.Array.each(parts, function(part) { + fn += Ext.String.capitalize(part); + }); + fn += type; + + if (!this[fn]) { + this[fn] = Ext.Function.pass(this['get' + type], [ref], this); + } + // Execute it right away + this[fn](ref); + }, + this); + }, + + ref: function(refs) { + var me = this; + refs = Ext.Array.from(refs); + Ext.Array.each(refs, function(info) { + var ref = info.ref, + fn = 'get' + Ext.String.capitalize(ref); + if (!me[fn]) { + me[fn] = Ext.Function.pass(me.getRef, [ref, info], me); + } + }); + }, + + 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, + selector = info.selector, + 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('beforedestroy', function() { + me.refCache[ref] = null; + }); + } + } + + return cached; + }, + + control: function(selectors, listeners) { + this.application.control(selectors, listeners, this); + }, + + getController: function(name) { + return this.application.getController(name); + }, + + getStore: function(name) { + return this.application.getStore(name); + }, + + getModel: function(model) { + return this.application.getModel(model); + }, + + getView: function(view) { + return this.application.getView(view); + } +}); + +/** + * @class Ext.data.SortTypes + * This class defines a series of static methods that are used on a + * {@link Ext.data.Field} for performing sorting. The methods cast the + * underlying values into a data type that is appropriate for sorting on + * that particular field. If a {@link Ext.data.Field#type} is specified, + * the sortType will be set to a sane default if the sortType is not + * explicitly defined on the field. The sortType will make any necessary + * modifications to the value and return it. + *
    + *
  • asText - Removes any tags and converts the value to a string
  • + *
  • asUCText - Removes any tags and converts the value to an uppercase string
  • + *
  • asUCText - Converts the value to an uppercase string
  • + *
  • asDate - Converts the value into Unix epoch time
  • + *
  • asFloat - Converts the value to a floating point number
  • + *
  • asInt - Converts the value to an integer number
  • + *
+ *

+ * It is also possible to create a custom sortType that can be used throughout + * an application. + *


+Ext.apply(Ext.data.SortTypes, {
+    asPerson: function(person){
+        // expects an object with a first and last name property
+        return person.lastName.toUpperCase() + person.firstName.toLowerCase();
+    }    
+});
+
+Ext.define('Employee', {
+    extend: 'Ext.data.Model',
+    fields: [{
+        name: 'person',
+        sortType: 'asPerson'
+    }, {
+        name: 'salary',
+        type: 'float' // sortType set to asFloat
+    }]
+});
+ * 
+ *

+ * @singleton + * @docauthor Evan Trimboli + */ +Ext.define('Ext.data.SortTypes', { + + singleton: true, + + /** + * Default sort that does nothing + * @param {Mixed} s The value being converted + * @return {Mixed} The comparison value + */ + none : function(s) { + return s; + }, + + /** + * The regular expression used to strip tags + * @type {RegExp} + * @property + */ + stripTagsRE : /<\/?[^>]+>/gi, + + /** + * Strips all HTML tags to sort on text only + * @param {Mixed} s The value being converted + * @return {String} The comparison value + */ + asText : function(s) { + return String(s).replace(this.stripTagsRE, ""); + }, + + /** + * Strips all HTML tags to sort on text only - Case insensitive + * @param {Mixed} s The value being converted + * @return {String} The comparison value + */ + asUCText : function(s) { + return String(s).toUpperCase().replace(this.stripTagsRE, ""); + }, + + /** + * Case insensitive string + * @param {Mixed} s The value being converted + * @return {String} The comparison value + */ + asUCString : function(s) { + return String(s).toUpperCase(); + }, + + /** + * Date sorting + * @param {Mixed} s The value being converted + * @return {Number} The comparison value + */ + asDate : function(s) { + if(!s){ + return 0; + } + if(Ext.isDate(s)){ + return s.getTime(); + } + return Date.parse(String(s)); + }, + + /** + * Float sorting + * @param {Mixed} s The value being converted + * @return {Float} The comparison value + */ + asFloat : function(s) { + var val = parseFloat(String(s).replace(/,/g, "")); + return isNaN(val) ? 0 : val; + }, + + /** + * Integer sorting + * @param {Mixed} s The value being converted + * @return {Number} The comparison value + */ + asInt : function(s) { + var val = parseInt(String(s).replace(/,/g, ""), 10); + return isNaN(val) ? 0 : val; + } +}); +/** + * @author Ed Spencer + * @class Ext.data.Errors + * @extends Ext.util.MixedCollection + * + *

Wraps a collection of validation error responses and provides convenient functions for + * accessing and errors for specific fields.

+ * + *

Usually this class does not need to be instantiated directly - instances are instead created + * automatically when {@link Ext.data.Model#validate validate} on a model instance:

+ * +

+//validate some existing model instance - in this case it returned 2 failures messages
+var errors = myModel.validate();
+
+errors.isValid(); //false
+
+errors.length; //2
+errors.getByField('name');  // [{field: 'name',  error: 'must be present'}]
+errors.getByField('title'); // [{field: 'title', error: 'is too short'}]
+
+ */ +Ext.define('Ext.data.Errors', { + extend: 'Ext.util.MixedCollection', + + /** + * Returns true if there are no errors in the collection + * @return {Boolean} + */ + isValid: function() { + return this.length === 0; + }, + + /** + * Returns all of the errors for the given field + * @param {String} fieldName The field to get errors for + * @return {Array} All errors for the given field + */ + getByField: function(fieldName) { + var errors = [], + error, field, i; + + for (i = 0; i < this.length; i++) { + error = this.items[i]; + + if (error.field == fieldName) { + errors.push(error); + } + } + + return errors; + } +}); + +/** + * @author Ed Spencer + * @class Ext.data.Operation + * @extends Object + * + *

Represents a single read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}. + * Operation objects are used to enable communication between Stores and Proxies. Application + * developers should rarely need to interact with Operation objects directly.

+ * + *

Several Operations can be batched together in a {@link Ext.data.Batch batch}.

+ * + * @constructor + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.Operation', { + /** + * @cfg {Boolean} synchronous True if this Operation is to be executed synchronously (defaults to true). This + * property is inspected by a {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in + * parallel or not. + */ + synchronous: true, + + /** + * @cfg {String} action The action being performed by this Operation. Should be one of 'create', 'read', 'update' or 'destroy' + */ + action: undefined, + + /** + * @cfg {Array} filters Optional array of filter objects. Only applies to 'read' actions. + */ + filters: undefined, + + /** + * @cfg {Array} sorters Optional array of sorter objects. Only applies to 'read' actions. + */ + sorters: undefined, + + /** + * @cfg {Object} group Optional grouping configuration. Only applies to 'read' actions where grouping is desired. + */ + group: undefined, + + /** + * @cfg {Number} start The start index (offset), used in paging when running a 'read' action. + */ + start: undefined, + + /** + * @cfg {Number} limit The number of records to load. Used on 'read' actions when paging is being used. + */ + limit: undefined, + + /** + * @cfg {Ext.data.Batch} batch The batch that this Operation is a part of (optional) + */ + batch: undefined, + + /** + * Read-only property tracking the start status of this Operation. Use {@link #isStarted}. + * @property started + * @type Boolean + * @private + */ + started: false, + + /** + * Read-only property tracking the run status of this Operation. Use {@link #isRunning}. + * @property running + * @type Boolean + * @private + */ + running: false, + + /** + * Read-only property tracking the completion status of this Operation. Use {@link #isComplete}. + * @property complete + * @type Boolean + * @private + */ + complete: false, + + /** + * Read-only property tracking whether the Operation was successful or not. This starts as undefined and is set to true + * or false by the Proxy that is executing the Operation. It is also set to false by {@link #setException}. Use + * {@link #wasSuccessful} to query success status. + * @property success + * @type Boolean + * @private + */ + success: undefined, + + /** + * Read-only property tracking the exception status of this Operation. Use {@link #hasException} and see {@link #getError}. + * @property exception + * @type Boolean + * @private + */ + exception: false, + + /** + * The error object passed when {@link #setException} was called. This could be any object or primitive. + * @property error + * @type Mixed + * @private + */ + error: undefined, + + constructor: function(config) { + Ext.apply(this, config || {}); + }, + + /** + * Marks the Operation as started + */ + setStarted: function() { + this.started = true; + this.running = true; + }, + + /** + * Marks the Operation as completed + */ + setCompleted: function() { + this.complete = true; + this.running = false; + }, + + /** + * Marks the Operation as successful + */ + setSuccessful: function() { + this.success = true; + }, + + /** + * Marks the Operation as having experienced an exception. Can be supplied with an option error message/object. + * @param {Mixed} error Optional error string/object + */ + setException: function(error) { + this.exception = true; + this.success = false; + this.running = false; + this.error = error; + }, + + /** + * Returns true if this Operation encountered an exception (see also {@link #getError}) + * @return {Boolean} True if there was an exception + */ + hasException: function() { + return this.exception === true; + }, + + /** + * Returns the error string or object that was set using {@link #setException} + * @return {Mixed} The error object + */ + getError: function() { + return this.error; + }, + + /** + * Returns an array of Ext.data.Model instances as set by the Proxy. + * @return {Array} Any loaded Records + */ + getRecords: function() { + var resultSet = this.getResultSet(); + + return (resultSet === undefined ? this.records : resultSet.records); + }, + + /** + * Returns the ResultSet object (if set by the Proxy). This object will contain the {@link Ext.data.Model model} instances + * as well as meta data such as number of instances fetched, number available etc + * @return {Ext.data.ResultSet} The ResultSet object + */ + getResultSet: function() { + return this.resultSet; + }, + + /** + * Returns true if the Operation has been started. Note that the Operation may have started AND completed, + * see {@link #isRunning} to test if the Operation is currently running. + * @return {Boolean} True if the Operation has started + */ + isStarted: function() { + return this.started === true; + }, + + /** + * Returns true if the Operation has been started but has not yet completed. + * @return {Boolean} True if the Operation is currently running + */ + isRunning: function() { + return this.running === true; + }, + + /** + * Returns true if the Operation has been completed + * @return {Boolean} True if the Operation is complete + */ + isComplete: function() { + return this.complete === true; + }, + + /** + * Returns true if the Operation has completed and was successful + * @return {Boolean} True if successful + */ + wasSuccessful: function() { + return this.isComplete() && this.success === true; + }, + + /** + * @private + * Associates this Operation with a Batch + * @param {Ext.data.Batch} batch The batch + */ + setBatch: function(batch) { + this.batch = batch; + }, + + /** + * Checks whether this operation should cause writing to occur. + * @return {Boolean} Whether the operation should cause a write to occur. + */ + allowWrite: function() { + return this.action != 'read'; + } +}); +/** + * @author Ed Spencer + * @class Ext.data.validations + * @extends Object + * + *

This singleton contains a set of validation functions that can be used to validate any type + * of data. They are most often used in {@link Ext.data.Model Models}, where they are automatically + * set up and executed.

+ */ +Ext.define('Ext.data.validations', { + singleton: true, + + /** + * The default error message used when a presence validation fails + * @property presenceMessage + * @type String + */ + presenceMessage: 'must be present', + + /** + * The default error message used when a length validation fails + * @property lengthMessage + * @type String + */ + lengthMessage: 'is the wrong length', + + /** + * The default error message used when a format validation fails + * @property formatMessage + * @type Boolean + */ + formatMessage: 'is the wrong format', + + /** + * The default error message used when an inclusion validation fails + * @property inclusionMessage + * @type String + */ + inclusionMessage: 'is not included in the list of acceptable values', + + /** + * The default error message used when an exclusion validation fails + * @property exclusionMessage + * @type String + */ + exclusionMessage: 'is not an acceptable value', + + /** + * Validates that the given value is present + * @param {Object} config Optional config object + * @param {Mixed} value The value to validate + * @return {Boolean} True if validation passed + */ + presence: function(config, value) { + if (value === undefined) { + value = config; + } + + return !!value; + }, + + /** + * Returns true if the given value is between the configured min and max values + * @param {Object} config Optional config object + * @param {String} value The value to validate + * @return {Boolean} True if the value passes validation + */ + length: function(config, value) { + if (value === undefined) { + return false; + } + + var length = value.length, + min = config.min, + max = config.max; + + if ((min && length < min) || (max && length > max)) { + return false; + } else { + return true; + } + }, + + /** + * Returns true if the given value passes validation against the configured {@link #matcher} regex + * @param {Object} config Optional config object + * @param {String} value The value to validate + * @return {Boolean} True if the value passes the format validation + */ + format: function(config, value) { + return !!(config.matcher && config.matcher.test(value)); + }, + + /** + * Validates that the given value is present in the configured {@link #list} + * @param {String} value The value to validate + * @return {Boolean} True if the value is present in the list + */ + inclusion: function(config, value) { + return config.list && Ext.Array.indexOf(config.list,value) != -1; + }, + + /** + * Validates that the given value is present in the configured {@link #list} + * @param {Object} config Optional config object + * @param {String} value The value to validate + * @return {Boolean} True if the value is not present in the list + */ + exclusion: function(config, value) { + return config.list && Ext.Array.indexOf(config.list,value) == -1; + } +}); +/** + * @author Ed Spencer + * @class Ext.data.ResultSet + * @extends Object + * + *

Simple wrapper class that represents a set of records returned by a Proxy.

+ * + * @constructor + * Creates the new ResultSet + */ +Ext.define('Ext.data.ResultSet', { + /** + * @cfg {Boolean} loaded + * True if the records have already been loaded. This is only meaningful when dealing with + * SQL-backed proxies + */ + loaded: true, + + /** + * @cfg {Number} count + * The number of records in this ResultSet. Note that total may differ from this number + */ + count: 0, + + /** + * @cfg {Number} total + * The total number of records reported by the data source. This ResultSet may form a subset of + * those records (see count) + */ + total: 0, + + /** + * @cfg {Boolean} success + * True if the ResultSet loaded successfully, false if any errors were encountered + */ + success: false, + + /** + * @cfg {Array} records The array of record instances. Required + */ + + constructor: function(config) { + Ext.apply(this, config); + + /** + * DEPRECATED - will be removed in Ext JS 5.0. This is just a copy of this.total - use that instead + * @property totalRecords + * @type Mixed + */ + this.totalRecords = this.total; + + if (config.count === undefined) { + this.count = this.records.length; + } + } +}); +/** + * @author Ed Spencer + * @class Ext.data.writer.Writer + * @extends Object + * + *

Base Writer class used by most subclasses of {@link Ext.data.proxy.Server}. This class is + * responsible for taking a set of {@link Ext.data.Operation} objects and a {@link Ext.data.Request} + * object and modifying that request based on the Operations.

+ * + *

For example a Ext.data.writer.Json would format the Operations and their {@link Ext.data.Model} + * instances based on the config options passed to the JsonWriter's constructor.

+ * + *

Writers are not needed for any kind of local storage - whether via a + * {@link Ext.data.proxy.WebStorage Web Storage proxy} (see {@link Ext.data.proxy.LocalStorage localStorage} + * and {@link Ext.data.proxy.SessionStorage sessionStorage}) or just in memory via a + * {@link Ext.data.proxy.Memory MemoryProxy}.

+ * + * @constructor + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.writer.Writer', { + alias: 'writer.base', + alternateClassName: ['Ext.data.DataWriter', 'Ext.data.Writer'], + + /** + * @cfg {Boolean} writeAllFields True to write all fields from the record to the server. If set to false it + * will only send the fields that were modified. Defaults to true. Note that any fields that have + * {@link Ext.data.Field#persist} set to false will still be ignored. + */ + writeAllFields: true, + + /** + * @cfg {String} nameProperty This property is used to read the key for each value that will be sent to the server. + * For example: + *

+Ext.define('Person', {
+    extend: 'Ext.data.Model',
+    fields: [{
+        name: 'first',
+        mapping: 'firstName'
+    }, {
+        name: 'last',
+        mapping: 'lastName'
+    }, {
+        name: 'age'
+    }]
+});
+new Ext.data.writer.Writer({
+    writeAllFields: true,
+    nameProperty: 'mapping'
+});
+
+// This will be sent to the server
+{
+    firstName: 'first name value',
+    lastName: 'last name value',
+    age: 1
+}
+
+     * 
+ * Defaults to name. If the value is not present, the field name will always be used. + */ + nameProperty: 'name', + + constructor: function(config) { + Ext.apply(this, config); + }, + + /** + * Prepares a Proxy's Ext.data.Request object + * @param {Ext.data.Request} request The request object + * @return {Ext.data.Request} The modified request object + */ + write: function(request) { + var operation = request.operation, + records = operation.records || [], + len = records.length, + i = 0, + data = []; + + for (; i < len; i++) { + data.push(this.getRecordData(records[i])); + } + return this.writeRecords(request, data); + }, + + /** + * Formats the data for each record before sending it to the server. This + * method should be overridden to format the data in a way that differs from the default. + * @param {Object} record The record that we are writing to the server. + * @return {Object} An object literal of name/value keys to be written to the server. + * By default this method returns the data property on the record. + */ + getRecordData: function(record) { + var isPhantom = record.phantom === true, + writeAll = this.writeAllFields || isPhantom, + nameProperty = this.nameProperty, + fields = record.fields, + data = {}, + changes, + name, + field, + key; + + if (writeAll) { + fields.each(function(field){ + if (field.persist) { + name = field[nameProperty] || field.name; + data[name] = record.get(field.name); + } + }); + } else { + // Only write the changes + changes = record.getChanges(); + for (key in changes) { + if (changes.hasOwnProperty(key)) { + field = fields.get(key); + name = field[nameProperty] || field.name; + data[name] = changes[key]; + } + } + if (!isPhantom) { + // always include the id for non phantoms + data[record.idProperty] = record.getId(); + } + } + return data; + } +}); + +/** + * @class Ext.util.Floating + * A mixin to add floating capability to a Component + */ +Ext.define('Ext.util.Floating', { + + uses: ['Ext.Layer', 'Ext.window.Window'], + + /** + * @cfg {Boolean} focusOnToFront + * Specifies whether the floated component should be automatically {@link #focus focused} when it is + * {@link #toFront brought to the front}. Defaults to true. + */ + focusOnToFront: true, + + /** + * @cfg {String/Boolean} shadow Specifies whether the floating component should be given a shadow. Set to + * true to automatically create an {@link Ext.Shadow}, or a string indicating the + * shadow's display {@link Ext.Shadow#mode}. Set to false to disable the shadow. + * (Defaults to 'sides'.) + */ + shadow: 'sides', + + constructor: function(config) { + this.floating = true; + this.el = Ext.create('Ext.Layer', Ext.apply({}, config, { + hideMode: this.hideMode, + hidden: this.hidden, + shadow: Ext.isDefined(this.shadow) ? this.shadow : 'sides', + shadowOffset: this.shadowOffset, + constrain: false, + shim: this.shim === false ? false : undefined + }), this.el); + }, + + onFloatRender: function() { + var me = this; + me.zIndexParent = me.getZIndexParent(); + me.setFloatParent(me.ownerCt); + delete me.ownerCt; + + if (me.zIndexParent) { + me.zIndexParent.registerFloatingItem(me); + } else { + Ext.WindowManager.register(me); + } + }, + + setFloatParent: function(floatParent) { + var me = this; + + // Remove listeners from previous floatParent + if (me.floatParent) { + me.mun(me.floatParent, { + hide: me.onFloatParentHide, + show: me.onFloatParentShow, + scope: me + }); + } + + me.floatParent = floatParent; + + // Floating Components as children of Containers must hide when their parent hides. + if (floatParent) { + me.mon(me.floatParent, { + hide: me.onFloatParentHide, + show: me.onFloatParentShow, + scope: me + }); + } + + // If a floating Component is configured to be constrained, but has no configured + // constrainTo setting, set its constrainTo to be it's ownerCt before rendering. + if ((me.constrain || me.constrainHeader) && !me.constrainTo) { + me.constrainTo = floatParent ? floatParent.getTargetEl() : me.container; + } + }, + + onFloatParentHide: function() { + this.showOnParentShow = this.isVisible(); + this.hide(); + }, + + onFloatParentShow: function() { + if (this.showOnParentShow) { + delete this.showOnParentShow; + this.show(); + } + }, + + /** + * @private + *

Finds the ancestor Container responsible for allocating zIndexes for the passed Component.

+ *

That will be the outermost floating Container (a Container which has no ownerCt and has floating:true).

+ *

If we have no ancestors, or we walk all the way up to the document body, there's no zIndexParent, + * and the global Ext.WindowManager will be used.

+ */ + getZIndexParent: function() { + var p = this.ownerCt, + c; + + if (p) { + while (p) { + c = p; + p = p.ownerCt; + } + if (c.floating) { + return c; + } + } + }, + + // private + // z-index is managed by the zIndexManager and may be overwritten at any time. + // Returns the next z-index to be used. + // If this is a Container, then it will have rebased any managed floating Components, + // and so the next available z-index will be approximately 10000 above that. + setZIndex: function(index) { + var me = this; + this.el.setZIndex(index); + + // Next item goes 10 above; + index += 10; + + // When a Container with floating items has its z-index set, it rebases any floating items it is managing. + // The returned value is a round number approximately 10000 above the last z-index used. + if (me.floatingItems) { + index = Math.floor(me.floatingItems.setBase(index) / 100) * 100 + 10000; + } + return index; + }, + + /** + *

Moves this floating Component into a constrain region.

+ *

By default, this Component is constrained to be within the container it was added to, or the element + * it was rendered to.

+ *

An alternative constraint may be passed.

+ * @param {Mixed} constrainTo Optional. The Element or {@link Ext.util.Region Region} into which this Component is to be constrained. + */ + doConstrain: function(constrainTo) { + var me = this, + constrainEl, + vector, + xy; + + if (me.constrain || me.constrainHeader) { + if (me.constrainHeader) { + constrainEl = me.header.el; + } else { + constrainEl = me.el; + } + vector = constrainEl.getConstrainVector(constrainTo || (me.floatParent && me.floatParent.getTargetEl()) || me.container); + if (vector) { + xy = me.getPosition(); + xy[0] += vector[0]; + xy[1] += vector[1]; + me.setPosition(xy); + } + } + }, + + /** + * Aligns this floating Component to the specified element + * @param {Mixed} element The element or {@link Ext.Component} to align to. If passing a component, it must + * be a omponent instance. If a string id is passed, it will be used as an element id. + * @param {String} position (optional, defaults to "tl-bl?") The position to align to (see {@link Ext.core.Element#alignTo} for more details). + * @param {Array} offsets (optional) Offset the positioning by [x, y] + * @return {Component} this + */ + alignTo: function(element, position, offsets) { + if (element.isComponent) { + element = element.getEl(); + } + var xy = this.el.getAlignToXY(element, position, offsets); + this.setPagePosition(xy); + return this; + }, + + /** + *

Brings this floating Component to the front of any other visible, floating Components managed by the same {@link Ext.ZIndexManager ZIndexManager}

+ *

If this Component is modal, inserts the modal mask just below this Component in the z-index stack.

+ * @param {Boolean} preventFocus (optional) Specify true to prevent the Component from being focused. + * @return {Component} this + */ + toFront: function(preventFocus) { + var me = this; + + // Find the floating Component which provides the base for this Component's zIndexing. + // That must move to front to then be able to rebase its zIndex stack and move this to the front + if (me.zIndexParent) { + me.zIndexParent.toFront(true); + } + if (me.zIndexManager.bringToFront(me)) { + if (!Ext.isDefined(preventFocus)) { + preventFocus = !me.focusOnToFront; + } + if (!preventFocus) { + // Kick off a delayed focus request. + // If another floating Component is toFronted before the delay expires + // this will not receive focus. + me.focus(false, true); + } + } + return me; + }, + + /** + *

This method is called internally by {@link Ext.ZIndexManager} to signal that a floating + * Component has either been moved to the top of its zIndex stack, or pushed from the top of its zIndex stack.

+ *

If a Window is superceded by another Window, deactivating it hides its shadow.

+ *

This method also fires the {@link #activate} or {@link #deactivate} event depending on which action occurred.

+ * @param {Boolean} active True to activate the Component, false to deactivate it (defaults to false) + * @param {Component} newActive The newly active Component which is taking over topmost zIndex position. + */ + setActive: function(active, newActive) { + if (active) { + if ((this instanceof Ext.window.Window) && !this.maximized) { + this.el.enableShadow(true); + } + this.fireEvent('activate', this); + } else { + // Only the *Windows* in a zIndex stack share a shadow. All other types of floaters + // can keep their shadows all the time + if ((this instanceof Ext.window.Window) && (newActive instanceof Ext.window.Window)) { + this.el.disableShadow(); + } + this.fireEvent('deactivate', this); + } + }, + + /** + * Sends this Component to the back of (lower z-index than) any other visible windows + * @return {Component} this + */ + toBack: function() { + this.zIndexManager.sendToBack(this); + return this; + }, + + /** + * Center this Component in its container. + * @return {Component} this + */ + center: function() { + var xy = this.el.getAlignToXY(this.container, 'c-c'); + this.setPagePosition(xy); + return this; + }, + + // private + syncShadow : function(){ + if (this.floating) { + this.el.sync(true); + } + }, + + // private + fitContainer: function() { + var parent = this.floatParent, + container = parent ? parent.getTargetEl() : this.container, + size = container.getViewSize(false); + + this.setSize(size); + } +}); +/** + * @class Ext.layout.container.AbstractContainer + * @extends Ext.layout.Layout + * Please refer to sub classes documentation + */ + +Ext.define('Ext.layout.container.AbstractContainer', { + + /* Begin Definitions */ + + extend: 'Ext.layout.Layout', + + /* End Definitions */ + + type: 'container', + + fixedLayout: true, + + // @private + managedHeight: true, + // @private + managedWidth: true, + + /** + * @cfg {Boolean} bindToOwnerCtComponent + * Flag to notify the ownerCt Component on afterLayout of a change + */ + bindToOwnerCtComponent: false, + + /** + * @cfg {Boolean} bindToOwnerCtContainer + * Flag to notify the ownerCt Container on afterLayout of a change + */ + bindToOwnerCtContainer: false, + + /** + * @cfg {String} itemCls + *

An optional extra CSS class that will be added to the container. This can be useful for adding + * customized styles to the container or any of its children using standard CSS rules. See + * {@link Ext.Component}.{@link Ext.Component#ctCls ctCls} also.

+ *

+ */ + + isManaged: function(dimension) { + dimension = Ext.String.capitalize(dimension); + var me = this, + child = me, + managed = me['managed' + dimension], + ancestor = me.owner.ownerCt; + + if (ancestor && ancestor.layout) { + while (ancestor && ancestor.layout) { + if (managed === false || ancestor.layout['managed' + dimension] === false) { + managed = false; + break; + } + ancestor = ancestor.ownerCt; + } + } + return managed; + }, + + layout: function() { + var me = this, + owner = me.owner; + if (Ext.isNumber(owner.height) || owner.isViewport) { + me.managedHeight = false; + } + if (Ext.isNumber(owner.width) || owner.isViewport) { + me.managedWidth = false; + } + me.callParent(arguments); + }, + + /** + * Set the size of an item within the Container. We should always use setCalculatedSize. + * @private + */ + setItemSize: function(item, width, height) { + if (Ext.isObject(width)) { + height = width.height; + width = width.width; + } + item.setCalculatedSize(width, height, this.owner); + }, + + /** + *

Returns an array of child components either for a render phase (Performed in the beforeLayout method of the layout's + * base class), or the layout phase (onLayout).

+ * @return {Array} of child components + */ + getLayoutItems: function() { + return this.owner && this.owner.items && this.owner.items.items || []; + }, + + afterLayout: function() { + this.owner.afterLayout(this); + }, + /** + * Returns the owner component's resize element. + * @return {Ext.core.Element} + */ + getTarget: function() { + return this.owner.getTargetEl(); + }, + /** + *

Returns the element into which rendering must take place. Defaults to the owner Container's {@link Ext.AbstractComponent#targetEl}.

+ * May be overridden in layout managers which implement an inner element. + * @return {Ext.core.Element} + */ + getRenderTarget: function() { + return this.owner.getTargetEl(); + } +}); + +/** + * @class Ext.ZIndexManager + *

A class that manages a group of {@link Ext.Component#floating} Components and provides z-order management, + * and Component activation behavior, including masking below the active (topmost) Component.

+ *

{@link Ext.Component#floating Floating} Components which are rendered directly into the document (Such as {@link Ext.window.Window Window}s which are + * {@link Ext.Component#show show}n are managed by a {@link Ext.WindowManager global instance}.

+ *

{@link Ext.Component#floating Floating} Components which are descendants of {@link Ext.Component#floating floating} Containers + * (For example a {Ext.view.BoundList BoundList} within an {@link Ext.window.Window Window}, or a {@link Ext.menu.Menu Menu}), + * are managed by a ZIndexManager owned by that floating Container. So ComboBox dropdowns within Windows will have managed z-indices + * guaranteed to be correct, relative to the Window.

+ * @constructor + */ +Ext.define('Ext.ZIndexManager', { + + alternateClassName: 'Ext.WindowGroup', + + statics: { + zBase : 9000 + }, + + constructor: function(container) { + var me = this; + + me.list = {}; + me.zIndexStack = []; + me.front = null; + + if (container) { + + // This is the ZIndexManager for an Ext.container.Container, base its zseed on the zIndex of the Container's element + if (container.isContainer) { + container.on('resize', me._onContainerResize, me); + me.zseed = Ext.Number.from(container.getEl().getStyle('zIndex'), me.getNextZSeed()); + // The containing element we will be dealing with (eg masking) is the content target + me.targetEl = container.getTargetEl(); + me.container = container; + } + // This is the ZIndexManager for a DOM element + else { + Ext.EventManager.onWindowResize(me._onContainerResize, me); + me.zseed = me.getNextZSeed(); + me.targetEl = Ext.get(container); + } + } + // No container passed means we are the global WindowManager. Our target is the doc body. + // DOM must be ready to collect that ref. + else { + Ext.EventManager.onWindowResize(me._onContainerResize, me); + me.zseed = me.getNextZSeed(); + Ext.onDocumentReady(function() { + me.targetEl = Ext.getBody(); + }); + } + }, + + getNextZSeed: function() { + return (Ext.ZIndexManager.zBase += 10000); + }, + + setBase: function(baseZIndex) { + this.zseed = baseZIndex; + return this.assignZIndices(); + }, + + // private + assignZIndices: function() { + var a = this.zIndexStack, + len = a.length, + i = 0, + zIndex = this.zseed, + comp; + + for (; i < len; i++) { + comp = a[i]; + if (comp && !comp.hidden) { + + // Setting the zIndex of a Component returns the topmost zIndex consumed by + // that Component. + // If it's just a plain floating Component such as a BoundList, then the + // return value is the passed value plus 10, ready for the next item. + // If a floating *Container* has its zIndex set, it re-orders its managed + // floating children, starting from that new base, and returns a value 10000 above + // the highest zIndex which it allocates. + zIndex = comp.setZIndex(zIndex); + } + } + this._activateLast(); + return zIndex; + }, + + // private + _setActiveChild: function(comp) { + if (comp != this.front) { + + if (this.front) { + this.front.setActive(false, comp); + } + this.front = comp; + if (comp) { + comp.setActive(true); + if (comp.modal) { + this._showModalMask(comp.el.getStyle('zIndex') - 4); + } + } + } + }, + + // private + _activateLast: function(justHidden) { + var comp, + lastActivated = false, + i; + + // Go down through the z-index stack. + // Activate the next visible one down. + // Keep going down to find the next visible modal one to shift the modal mask down under + for (i = this.zIndexStack.length-1; i >= 0; --i) { + comp = this.zIndexStack[i]; + if (!comp.hidden) { + if (!lastActivated) { + this._setActiveChild(comp); + lastActivated = true; + } + + // Move any modal mask down to just under the next modal floater down the stack + if (comp.modal) { + this._showModalMask(comp.el.getStyle('zIndex') - 4); + return; + } + } + } + + // none to activate, so there must be no modal mask. + // And clear the currently active property + this._hideModalMask(); + if (!lastActivated) { + this._setActiveChild(null); + } + }, + + _showModalMask: function(zIndex) { + if (!this.mask) { + this.mask = this.targetEl.createChild({ + cls: Ext.baseCSSPrefix + 'mask' + }); + this.mask.setVisibilityMode(Ext.core.Element.DISPLAY); + this.mask.on('click', this._onMaskClick, this); + } + Ext.getBody().addCls(Ext.baseCSSPrefix + 'body-masked'); + this.mask.setSize(this.targetEl.getViewSize(true)); + this.mask.setStyle('zIndex', zIndex); + this.mask.show(); + }, + + _hideModalMask: function() { + if (this.mask) { + Ext.getBody().removeCls(Ext.baseCSSPrefix + 'body-masked'); + this.mask.hide(); + } + }, + + _onMaskClick: function() { + if (this.front) { + this.front.focus(); + } + }, + + _onContainerResize: function() { + if (this.mask && this.mask.isVisible()) { + this.mask.setSize(this.targetEl.getViewSize(true)); + } + }, + + /** + *

Registers a floating {@link Ext.Component} with this ZIndexManager. This should not + * need to be called under normal circumstances. Floating Components (such as Windows, BoundLists and Menus) are automatically registered + * with a {@link Ext.Component#zIndexManager zIndexManager} at render time.

+ *

Where this may be useful is moving Windows between two ZIndexManagers. For example, + * to bring the Ext.MessageBox dialog under the same manager as the Desktop's + * ZIndexManager in the desktop sample app:

+MyDesktop.getDesktop().getManager().register(Ext.MessageBox);
+
+ * @param {Component} comp The Component to register. + */ + register : function(comp) { + if (comp.zIndexManager) { + comp.zIndexManager.unregister(comp); + } + comp.zIndexManager = this; + + this.list[comp.id] = comp; + this.zIndexStack.push(comp); + comp.on('hide', this._activateLast, this); + }, + + /** + *

Unregisters a {@link Ext.Component} from this ZIndexManager. This should not + * need to be called. Components are automatically unregistered upon destruction. + * See {@link #register}.

+ * @param {Component} comp The Component to unregister. + */ + unregister : function(comp) { + delete comp.zIndexManager; + if (this.list && this.list[comp.id]) { + delete this.list[comp.id]; + comp.un('hide', this._activateLast); + Ext.Array.remove(this.zIndexStack, comp); + + // Destruction requires that the topmost visible floater be activated. Same as hiding. + this._activateLast(comp); + } + }, + + /** + * Gets a registered Component by id. + * @param {String/Object} id The id of the Component or a {@link Ext.Component} instance + * @return {Ext.Component} + */ + get : function(id) { + return typeof id == "object" ? id : this.list[id]; + }, + + /** + * Brings the specified Component to the front of any other active Components in this ZIndexManager. + * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance + * @return {Boolean} True if the dialog was brought to the front, else false + * if it was already in front + */ + bringToFront : function(comp) { + comp = this.get(comp); + if (comp != this.front) { + Ext.Array.remove(this.zIndexStack, comp); + this.zIndexStack.push(comp); + this.assignZIndices(); + return true; + } + if (comp.modal) { + Ext.getBody().addCls(Ext.baseCSSPrefix + 'body-masked'); + this.mask.setSize(Ext.core.Element.getViewWidth(true), Ext.core.Element.getViewHeight(true)); + this.mask.show(); + } + return false; + }, + + /** + * Sends the specified Component to the back of other active Components in this ZIndexManager. + * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance + * @return {Ext.Component} The Component + */ + sendToBack : function(comp) { + comp = this.get(comp); + Ext.Array.remove(this.zIndexStack, comp); + this.zIndexStack.unshift(comp); + this.assignZIndices(); + return comp; + }, + + /** + * Hides all Components managed by this ZIndexManager. + */ + hideAll : function() { + for (var id in this.list) { + if (this.list[id].isComponent && this.list[id].isVisible()) { + this.list[id].hide(); + } + } + }, + + /** + * @private + * Temporarily hides all currently visible managed Components. This is for when + * dragging a Window which may manage a set of floating descendants in its ZIndexManager; + * they should all be hidden just for the duration of the drag. + */ + hide: function() { + var i = 0, + ln = this.zIndexStack.length, + comp; + + this.tempHidden = []; + for (; i < ln; i++) { + comp = this.zIndexStack[i]; + if (comp.isVisible()) { + this.tempHidden.push(comp); + comp.hide(); + } + } + }, + + /** + * @private + * Restores temporarily hidden managed Components to visibility. + */ + show: function() { + var i = 0, + ln = this.tempHidden.length, + comp, + x, + y; + + for (; i < ln; i++) { + comp = this.tempHidden[i]; + x = comp.x; + y = comp.y; + comp.show(); + comp.setPosition(x, y); + } + delete this.tempHidden; + }, + + /** + * Gets the currently-active Component in this ZIndexManager. + * @return {Ext.Component} The active Component + */ + getActive : function() { + return this.front; + }, + + /** + * Returns zero or more Components in this ZIndexManager using the custom search function passed to this method. + * The function should accept a single {@link Ext.Component} reference as its only argument and should + * return true if the Component matches the search criteria, otherwise it should return false. + * @param {Function} fn The search function + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the Component being tested. + * that gets passed to the function if not specified) + * @return {Array} An array of zero or more matching windows + */ + getBy : function(fn, scope) { + var r = [], + i = 0, + len = this.zIndexStack.length, + comp; + + for (; i < len; i++) { + comp = this.zIndexStack[i]; + if (fn.call(scope||comp, comp) !== false) { + r.push(comp); + } + } + return r; + }, + + /** + * Executes the specified function once for every Component in this ZIndexManager, passing each + * Component as the only parameter. Returning false from the function will stop the iteration. + * @param {Function} fn The function to execute for each item + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Component in the iteration. + */ + each : function(fn, scope) { + var comp; + for (var id in this.list) { + comp = this.list[id]; + if (comp.isComponent && fn.call(scope || comp, comp) === false) { + return; + } + } + }, + + /** + * Executes the specified function once for every Component in this ZIndexManager, passing each + * Component as the only parameter. Returning false from the function will stop the iteration. + * The components are passed to the function starting at the bottom and proceeding to the top. + * @param {Function} fn The function to execute for each item + * @param {Object} scope (optional) The scope (this reference) in which the function + * is executed. Defaults to the current Component in the iteration. + */ + eachBottomUp: function (fn, scope) { + var comp, + stack = this.zIndexStack, + i, n; + + for (i = 0, n = stack.length ; i < n; i++) { + comp = stack[i]; + if (comp.isComponent && fn.call(scope || comp, comp) === false) { + return; + } + } + }, + + /** + * Executes the specified function once for every Component in this ZIndexManager, passing each + * Component as the only parameter. Returning false from the function will stop the iteration. + * The components are passed to the function starting at the top and proceeding to the bottom. + * @param {Function} fn The function to execute for each item + * @param {Object} scope (optional) The scope (this reference) in which the function + * is executed. Defaults to the current Component in the iteration. + */ + eachTopDown: function (fn, scope) { + var comp, + stack = this.zIndexStack, + i; + + for (i = stack.length ; i-- > 0; ) { + comp = stack[i]; + if (comp.isComponent && fn.call(scope || comp, comp) === false) { + return; + } + } + }, + + destroy: function() { + delete this.zIndexStack; + delete this.list; + delete this.container; + delete this.targetEl; + } +}, function() { + /** + * @class Ext.WindowManager + * @extends Ext.ZIndexManager + *

The default global floating Component group that is available automatically.

+ *

This manages instances of floating Components which were rendered programatically without + * being added to a {@link Ext.container.Container Container}, and for floating Components which were added into non-floating Containers.

+ *

Floating Containers create their own instance of ZIndexManager, and floating Components added at any depth below + * there are managed by that ZIndexManager.

+ * @singleton + */ + Ext.WindowManager = Ext.WindowMgr = new this(); +}); + +/** + * @class Ext.layout.container.boxOverflow.None + * @extends Object + * @private + * Base class for Box Layout overflow handlers. These specialized classes are invoked when a Box Layout + * (either an HBox or a VBox) has child items that are either too wide (for HBox) or too tall (for VBox) + * for its container. + */ +Ext.define('Ext.layout.container.boxOverflow.None', { + + alternateClassName: 'Ext.layout.boxOverflow.None', + + constructor: function(layout, config) { + this.layout = layout; + Ext.apply(this, config || {}); + }, + + handleOverflow: Ext.emptyFn, + + clearOverflow: Ext.emptyFn, + + /** + * @private + * Normalizes an item reference, string id or numerical index into a reference to the item + * @param {Ext.Component|String|Number} item The item reference, id or index + * @return {Ext.Component} The item + */ + getItem: function(item) { + return this.layout.owner.getComponent(item); + } +}); +/** + * @class Ext.util.KeyMap + * Handles mapping keys to actions for an element. One key map can be used for multiple actions. + * The constructor accepts the same config object as defined by {@link #addBinding}. + * If you bind a callback function to a KeyMap, anytime the KeyMap handles an expected key + * combination it will call the function with this signature (if the match is a multi-key + * combination the callback will still be called only once): (String key, Ext.EventObject e) + * A KeyMap can also handle a string representation of keys.
+ * Usage: +

+// map one key by key code
+var map = new Ext.util.KeyMap("my-element", {
+    key: 13, // or Ext.EventObject.ENTER
+    fn: myHandler,
+    scope: myObject
+});
+
+// map multiple keys to one action by string
+var map = new Ext.util.KeyMap("my-element", {
+    key: "a\r\n\t",
+    fn: myHandler,
+    scope: myObject
+});
+
+// map multiple keys to multiple actions by strings and array of codes
+var map = new Ext.util.KeyMap("my-element", [
+    {
+        key: [10,13],
+        fn: function(){ alert("Return was pressed"); }
+    }, {
+        key: "abc",
+        fn: function(){ alert('a, b or c was pressed'); }
+    }, {
+        key: "\t",
+        ctrl:true,
+        shift:true,
+        fn: function(){ alert('Control + shift + tab was pressed.'); }
+    }
+]);
+
+ * Note: A KeyMap starts enabled + * @constructor + * @param {Mixed} el The element to bind to + * @param {Object} binding The binding (see {@link #addBinding}) + * @param {String} eventName (optional) The event to bind to (defaults to "keydown") + */ +Ext.define('Ext.util.KeyMap', { + alternateClassName: 'Ext.KeyMap', + + constructor: function(el, binding, eventName){ + var me = this; + + Ext.apply(me, { + el: Ext.get(el), + eventName: eventName || me.eventName, + bindings: [] + }); + if (binding) { + me.addBinding(binding); + } + me.enable(); + }, + + eventName: 'keydown', + + /** + * Add a new binding to this KeyMap. The following config object properties are supported: + *
+Property            Type             Description
+----------          ---------------  ----------------------------------------------------------------------
+key                 String/Array     A single keycode or an array of keycodes to handle
+shift               Boolean          True to handle key only when shift is pressed, False to handle the key only when shift is not pressed (defaults to undefined)
+ctrl                Boolean          True to handle key only when ctrl is pressed, False to handle the key only when ctrl is not pressed (defaults to undefined)
+alt                 Boolean          True to handle key only when alt is pressed, False to handle the key only when alt is not pressed (defaults to undefined)
+handler             Function         The function to call when KeyMap finds the expected key combination
+fn                  Function         Alias of handler (for backwards-compatibility)
+scope               Object           The scope of the callback function
+defaultEventAction  String           A default action to apply to the event. Possible values are: stopEvent, stopPropagation, preventDefault. If no value is set no action is performed. 
+
+ * + * Usage: + *

+// Create a KeyMap
+var map = new Ext.util.KeyMap(document, {
+    key: Ext.EventObject.ENTER,
+    fn: handleKey,
+    scope: this
+});
+
+//Add a new binding to the existing KeyMap later
+map.addBinding({
+    key: 'abc',
+    shift: true,
+    fn: handleKey,
+    scope: this
+});
+
+ * @param {Object/Array} binding A single KeyMap config or an array of configs + */ + addBinding : function(binding){ + if (Ext.isArray(binding)) { + Ext.each(binding, this.addBinding, this); + return; + } + + var keyCode = binding.key, + processed = false, + key, + keys, + keyString, + i, + len; + + if (Ext.isString(keyCode)) { + keys = []; + keyString = keyCode.toLowerCase(); + + for (i = 0, len = keyString.length; i < len; ++i){ + keys.push(keyString.charCodeAt(i)); + } + keyCode = keys; + processed = true; + } + + if (!Ext.isArray(keyCode)) { + keyCode = [keyCode]; + } + + if (!processed) { + for (i = 0, len = keyCode.length; i < len; ++i) { + key = keyCode[i]; + if (Ext.isString(key)) { + keyCode[i] = key.toLowerCase().charCodeAt(0); + } + } + } + + this.bindings.push(Ext.apply({ + keyCode: keyCode + }, binding)); + }, + + /** + * Process any keydown events on the element + * @private + * @param {Ext.EventObject} event + */ + handleKeyDown: function(event) { + if (this.enabled) { //just in case + var bindings = this.bindings, + i = 0, + len = bindings.length; + + event = this.processEvent(event); + for(; i < len; ++i){ + this.processBinding(bindings[i], event); + } + } + }, + + /** + * Ugly hack to allow this class to be tested. Currently WebKit gives + * no way to raise a key event properly with both + * a) A keycode + * b) The alt/ctrl/shift modifiers + * So we have to simulate them here. Yuk! + * This is a stub method intended to be overridden by tests. + * More info: https://bugs.webkit.org/show_bug.cgi?id=16735 + * @private + */ + processEvent: function(event){ + return event; + }, + + /** + * Process a particular binding and fire the handler if necessary. + * @private + * @param {Object} binding The binding information + * @param {Ext.EventObject} event + */ + processBinding: function(binding, event){ + if (this.checkModifiers(binding, event)) { + var key = event.getKey(), + handler = binding.fn || binding.handler, + scope = binding.scope || this, + keyCode = binding.keyCode, + defaultEventAction = binding.defaultEventAction, + i, + len, + keydownEvent = new Ext.EventObjectImpl(event); + + + for (i = 0, len = keyCode.length; i < len; ++i) { + if (key === keyCode[i]) { + if (handler.call(scope, key, event) !== true && defaultEventAction) { + keydownEvent[defaultEventAction](); + } + break; + } + } + } + }, + + /** + * Check if the modifiers on the event match those on the binding + * @private + * @param {Object} binding + * @param {Ext.EventObject} event + * @return {Boolean} True if the event matches the binding + */ + checkModifiers: function(binding, e){ + var keys = ['shift', 'ctrl', 'alt'], + i = 0, + len = keys.length, + val, key; + + for (; i < len; ++i){ + key = keys[i]; + val = binding[key]; + if (!(val === undefined || (val === e[key + 'Key']))) { + return false; + } + } + return true; + }, + + /** + * Shorthand for adding a single key listener + * @param {Number/Array/Object} key Either the numeric key code, array of key codes or an object with the + * following options: + * {key: (number or array), shift: (true/false), ctrl: (true/false), alt: (true/false)} + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the browser window. + */ + on: function(key, fn, scope) { + var keyCode, shift, ctrl, alt; + if (Ext.isObject(key) && !Ext.isArray(key)) { + keyCode = key.key; + shift = key.shift; + ctrl = key.ctrl; + alt = key.alt; + } else { + keyCode = key; + } + this.addBinding({ + key: keyCode, + shift: shift, + ctrl: ctrl, + alt: alt, + fn: fn, + scope: scope + }); + }, + + /** + * Returns true if this KeyMap is enabled + * @return {Boolean} + */ + isEnabled : function(){ + return this.enabled; + }, + + /** + * Enables this KeyMap + */ + enable: function(){ + if(!this.enabled){ + this.el.on(this.eventName, this.handleKeyDown, this); + this.enabled = true; + } + }, + + /** + * Disable this KeyMap + */ + disable: function(){ + if(this.enabled){ + this.el.removeListener(this.eventName, this.handleKeyDown, this); + this.enabled = false; + } + }, + + /** + * Convenience function for setting disabled/enabled by boolean. + * @param {Boolean} disabled + */ + setDisabled : function(disabled){ + if (disabled) { + this.disable(); + } else { + this.enable(); + } + }, + + /** + * Destroys the KeyMap instance and removes all handlers. + * @param {Boolean} removeEl True to also remove the attached element + */ + destroy: function(removeEl){ + var me = this; + + me.bindings = []; + me.disable(); + if (removeEl === true) { + me.el.remove(); + } + delete me.el; + } +}); +/** + * @class Ext.util.ClickRepeater + * @extends Ext.util.Observable + * + * A wrapper class which can be applied to any element. Fires a "click" event while the + * mouse is pressed. The interval between firings may be specified in the config but + * defaults to 20 milliseconds. + * + * Optionally, a CSS class may be applied to the element during the time it is pressed. + * + * @constructor + * @param {Mixed} el The element to listen on + * @param {Object} config + */ + +Ext.define('Ext.util.ClickRepeater', { + extend: 'Ext.util.Observable', + + constructor : function(el, config){ + this.el = Ext.get(el); + this.el.unselectable(); + + Ext.apply(this, config); + + this.addEvents( + /** + * @event mousedown + * Fires when the mouse button is depressed. + * @param {Ext.util.ClickRepeater} this + * @param {Ext.EventObject} e + */ + "mousedown", + /** + * @event click + * Fires on a specified interval during the time the element is pressed. + * @param {Ext.util.ClickRepeater} this + * @param {Ext.EventObject} e + */ + "click", + /** + * @event mouseup + * Fires when the mouse key is released. + * @param {Ext.util.ClickRepeater} this + * @param {Ext.EventObject} e + */ + "mouseup" + ); + + if(!this.disabled){ + this.disabled = true; + this.enable(); + } + + // allow inline handler + if(this.handler){ + this.on("click", this.handler, this.scope || this); + } + + this.callParent(); + }, + + /** + * @cfg {Mixed} el The element to act as a button. + */ + + /** + * @cfg {String} pressedCls A CSS class name to be applied to the element while pressed. + */ + + /** + * @cfg {Boolean} accelerate True if autorepeating should start slowly and accelerate. + * "interval" and "delay" are ignored. + */ + + /** + * @cfg {Number} interval The interval between firings of the "click" event. Default 20 ms. + */ + interval : 20, + + /** + * @cfg {Number} delay The initial delay before the repeating event begins firing. + * Similar to an autorepeat key delay. + */ + delay: 250, + + /** + * @cfg {Boolean} preventDefault True to prevent the default click event + */ + preventDefault : true, + /** + * @cfg {Boolean} stopDefault True to stop the default click event + */ + stopDefault : false, + + timer : 0, + + /** + * Enables the repeater and allows events to fire. + */ + enable: function(){ + if(this.disabled){ + this.el.on('mousedown', this.handleMouseDown, this); + if (Ext.isIE){ + this.el.on('dblclick', this.handleDblClick, this); + } + if(this.preventDefault || this.stopDefault){ + this.el.on('click', this.eventOptions, this); + } + } + this.disabled = false; + }, + + /** + * Disables the repeater and stops events from firing. + */ + disable: function(/* private */ force){ + if(force || !this.disabled){ + clearTimeout(this.timer); + if(this.pressedCls){ + this.el.removeCls(this.pressedCls); + } + Ext.getDoc().un('mouseup', this.handleMouseUp, this); + this.el.removeAllListeners(); + } + this.disabled = true; + }, + + /** + * Convenience function for setting disabled/enabled by boolean. + * @param {Boolean} disabled + */ + setDisabled: function(disabled){ + this[disabled ? 'disable' : 'enable'](); + }, + + eventOptions: function(e){ + if(this.preventDefault){ + e.preventDefault(); + } + if(this.stopDefault){ + e.stopEvent(); + } + }, + + // private + destroy : function() { + this.disable(true); + Ext.destroy(this.el); + this.clearListeners(); + }, + + handleDblClick : function(e){ + clearTimeout(this.timer); + this.el.blur(); + + this.fireEvent("mousedown", this, e); + this.fireEvent("click", this, e); + }, + + // private + handleMouseDown : function(e){ + clearTimeout(this.timer); + this.el.blur(); + if(this.pressedCls){ + this.el.addCls(this.pressedCls); + } + this.mousedownTime = new Date(); + + Ext.getDoc().on("mouseup", this.handleMouseUp, this); + this.el.on("mouseout", this.handleMouseOut, this); + + this.fireEvent("mousedown", this, e); + this.fireEvent("click", this, e); + + // Do not honor delay or interval if acceleration wanted. + if (this.accelerate) { + this.delay = 400; + } + + // Re-wrap the event object in a non-shared object, so it doesn't lose its context if + // the global shared EventObject gets a new Event put into it before the timer fires. + e = new Ext.EventObjectImpl(e); + + this.timer = Ext.defer(this.click, this.delay || this.interval, this, [e]); + }, + + // private + click : function(e){ + this.fireEvent("click", this, e); + this.timer = Ext.defer(this.click, this.accelerate ? + this.easeOutExpo(Ext.Date.getElapsed(this.mousedownTime), + 400, + -390, + 12000) : + this.interval, this, [e]); + }, + + easeOutExpo : function (t, b, c, d) { + return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; + }, + + // private + handleMouseOut : function(){ + clearTimeout(this.timer); + if(this.pressedCls){ + this.el.removeCls(this.pressedCls); + } + this.el.on("mouseover", this.handleMouseReturn, this); + }, + + // private + handleMouseReturn : function(){ + this.el.un("mouseover", this.handleMouseReturn, this); + if(this.pressedCls){ + this.el.addCls(this.pressedCls); + } + this.click(); + }, + + // private + handleMouseUp : function(e){ + clearTimeout(this.timer); + this.el.un("mouseover", this.handleMouseReturn, this); + this.el.un("mouseout", this.handleMouseOut, this); + Ext.getDoc().un("mouseup", this.handleMouseUp, this); + if(this.pressedCls){ + this.el.removeCls(this.pressedCls); + } + this.fireEvent("mouseup", this, e); + } +}); + +/** + * Component layout for buttons + * @class Ext.layout.component.Button + * @extends Ext.layout.component.Component + * @private + */ +Ext.define('Ext.layout.component.Button', { + + /* Begin Definitions */ + + alias: ['layout.button'], + + extend: 'Ext.layout.component.Component', + + /* End Definitions */ + + type: 'button', + + cellClsRE: /-btn-(tl|br)\b/, + htmlRE: /<.*>/, + + beforeLayout: function() { + return this.callParent(arguments) || this.lastText !== this.owner.text; + }, + + /** + * Set the dimensions of the inner <button> element to match the + * component dimensions. + */ + onLayout: function(width, height) { + var me = this, + isNum = Ext.isNumber, + owner = me.owner, + ownerEl = owner.el, + btnEl = owner.btnEl, + btnInnerEl = owner.btnInnerEl, + minWidth = owner.minWidth, + maxWidth = owner.maxWidth, + ownerWidth, btnFrameWidth, metrics; + + me.getTargetInfo(); + me.callParent(arguments); + + btnInnerEl.unclip(); + me.setTargetSize(width, height); + + if (!isNum(width)) { + // In IE7 strict mode button elements with width:auto get strange extra side margins within + // the wrapping table cell, but they go away if the width is explicitly set. So we measure + // the size of the text and set the width to match. + if (owner.text && Ext.isIE7 && Ext.isStrict && btnEl && btnEl.getWidth() > 20) { + btnFrameWidth = me.btnFrameWidth; + metrics = Ext.util.TextMetrics.measure(btnInnerEl, owner.text); + ownerEl.setWidth(metrics.width + btnFrameWidth + me.adjWidth); + btnEl.setWidth(metrics.width + btnFrameWidth); + btnInnerEl.setWidth(metrics.width + btnFrameWidth); + } else { + // Remove any previous fixed widths + ownerEl.setWidth(null); + btnEl.setWidth(null); + btnInnerEl.setWidth(null); + } + + // Handle maxWidth/minWidth config + if (minWidth || maxWidth) { + ownerWidth = ownerEl.getWidth(); + if (minWidth && (ownerWidth < minWidth)) { + me.setTargetSize(minWidth, height); + } + else if (maxWidth && (ownerWidth > maxWidth)) { + btnInnerEl.clip(); + me.setTargetSize(maxWidth, height); + } + } + } + + this.lastText = owner.text; + }, + + setTargetSize: function(width, height) { + var me = this, + owner = me.owner, + isNum = Ext.isNumber, + btnInnerEl = owner.btnInnerEl, + btnWidth = (isNum(width) ? width - me.adjWidth : width), + btnHeight = (isNum(height) ? height - me.adjHeight : height), + btnFrameHeight = me.btnFrameHeight, + text = owner.getText(), + textHeight; + + me.callParent(arguments); + me.setElementSize(owner.btnEl, btnWidth, btnHeight); + me.setElementSize(btnInnerEl, btnWidth, btnHeight); + if (isNum(btnHeight)) { + btnInnerEl.setStyle('line-height', btnHeight - btnFrameHeight + 'px'); + } + + // Button text may contain markup that would force it to wrap to more than one line (e.g. 'Button
Label'). + // When this happens, we cannot use the line-height set above for vertical centering; we instead reset the + // line-height to normal, measure the rendered text height, and add padding-top to center the text block + // vertically within the button's height. This is more expensive than the basic line-height approach so + // we only do it if the text contains markup. + if (text && this.htmlRE.test(text)) { + btnInnerEl.setStyle('line-height', 'normal'); + textHeight = Ext.util.TextMetrics.measure(btnInnerEl, text).height; + btnInnerEl.setStyle('padding-top', me.btnFrameTop + Math.max(btnInnerEl.getHeight() - btnFrameHeight - textHeight, 0) / 2 + 'px'); + me.setElementSize(btnInnerEl, btnWidth, btnHeight); + } + }, + + getTargetInfo: function() { + var me = this, + owner = me.owner, + ownerEl = owner.el, + frameSize = me.frameSize, + frameBody = owner.frameBody, + btnWrap = owner.btnWrap, + innerEl = owner.btnInnerEl; + + if (!('adjWidth' in me)) { + Ext.apply(me, { + // Width adjustment must take into account the arrow area. The btnWrap is the which has padding to accommodate the arrow. + adjWidth: frameSize.left + frameSize.right + ownerEl.getBorderWidth('lr') + ownerEl.getPadding('lr') + + btnWrap.getPadding('lr') + (frameBody ? frameBody.getFrameWidth('lr') : 0), + adjHeight: frameSize.top + frameSize.bottom + ownerEl.getBorderWidth('tb') + ownerEl.getPadding('tb') + + btnWrap.getPadding('tb') + (frameBody ? frameBody.getFrameWidth('tb') : 0), + btnFrameWidth: innerEl.getFrameWidth('lr'), + btnFrameHeight: innerEl.getFrameWidth('tb'), + btnFrameTop: innerEl.getFrameWidth('t') + }); + } + + return me.callParent(); + } +}); +/** + * @class Ext.util.TextMetrics + *

+ * Provides precise pixel measurements for blocks of text so that you can determine exactly how high and + * wide, in pixels, a given block of text will be. Note that when measuring text, it should be plain text and + * should not contain any HTML, otherwise it may not be measured correctly.

+ *

The measurement works by copying the relevant CSS styles that can affect the font related display, + * then checking the size of an element that is auto-sized. Note that if the text is multi-lined, you must + * provide a fixed width when doing the measurement.

+ * + *

+ * If multiple measurements are being done on the same element, you create a new instance to initialize + * to avoid the overhead of copying the styles to the element repeatedly. + *

+ */ +Ext.define('Ext.util.TextMetrics', { + statics: { + shared: null, + /** + * Measures the size of the specified text + * @param {String/HTMLElement} el The element, dom node or id from which to copy existing CSS styles + * that can affect the size of the rendered text + * @param {String} text The text to measure + * @param {Number} fixedWidth (optional) If the text will be multiline, you have to set a fixed width + * in order to accurately measure the text height + * @return {Object} An object containing the text's size {width: (width), height: (height)} + */ + measure: function(el, text, fixedWidth){ + var me = this, + shared = me.shared; + + if(!shared){ + shared = me.shared = new me(el, fixedWidth); + } + shared.bind(el); + shared.setFixedWidth(fixedWidth || 'auto'); + return shared.getSize(text); + }, + + /** + * Destroy the TextMetrics instance created by {@link #measure}. + */ + destroy: function(){ + var me = this; + Ext.destroy(me.shared); + me.shared = null; + } + }, + + /** + * @constructor + * @param {Mixed} bindTo The element to bind to. + * @param {Number} fixedWidth A fixed width to apply to the measuring element. + */ + constructor: function(bindTo, fixedWidth){ + var measure = this.measure = Ext.getBody().createChild({ + cls: 'x-textmetrics' + }); + this.el = Ext.get(bindTo); + + measure.position('absolute'); + measure.setLeftTop(-1000, -1000); + measure.hide(); + + if (fixedWidth) { + measure.setWidth(fixedWidth); + } + }, + + /** + *

Only available on the instance returned from {@link #createInstance}, not on the singleton.

+ * Returns the size of the specified text based on the internal element's style and width properties + * @param {String} text The text to measure + * @return {Object} An object containing the text's size {width: (width), height: (height)} + */ + getSize: function(text){ + var measure = this.measure, + size; + + measure.update(text); + size = measure.getSize(); + measure.update(''); + return size; + }, + + /** + * Binds this TextMetrics instance to a new element + * @param {Mixed} el The element + */ + bind: function(el){ + var me = this; + + me.el = Ext.get(el); + me.measure.setStyle( + me.el.getStyles('font-size','font-style', 'font-weight', 'font-family','line-height', 'text-transform', 'letter-spacing') + ); + }, + + /** + * Sets a fixed width on the internal measurement element. If the text will be multiline, you have + * to set a fixed width in order to accurately measure the text height. + * @param {Number} width The width to set on the element + */ + setFixedWidth : function(width){ + this.measure.setWidth(width); + }, + + /** + * Returns the measured width of the specified text + * @param {String} text The text to measure + * @return {Number} width The width in pixels + */ + getWidth : function(text){ + this.measure.dom.style.width = 'auto'; + return this.getSize(text).width; + }, + + /** + * Returns the measured height of the specified text + * @param {String} text The text to measure + * @return {Number} height The height in pixels + */ + getHeight : function(text){ + return this.getSize(text).height; + }, + + /** + * Destroy this instance + */ + destroy: function(){ + var me = this; + me.measure.remove(); + delete me.el; + delete me.measure; + } +}, function(){ + Ext.core.Element.addMethods({ + /** + * Returns the width in pixels of the passed text, or the width of the text in this Element. + * @param {String} text The text to measure. Defaults to the innerHTML of the element. + * @param {Number} min (Optional) The minumum value to return. + * @param {Number} max (Optional) The maximum value to return. + * @return {Number} The text width in pixels. + * @member Ext.core.Element getTextWidth + */ + getTextWidth : function(text, min, max){ + return Ext.Number.constrain(Ext.util.TextMetrics.measure(this.dom, Ext.value(text, this.dom.innerHTML, true)).width, min || 0, max || 1000000); + } + }); +}); + +/** + * @class Ext.layout.container.boxOverflow.Scroller + * @extends Ext.layout.container.boxOverflow.None + * @private + */ +Ext.define('Ext.layout.container.boxOverflow.Scroller', { + + /* Begin Definitions */ + + extend: 'Ext.layout.container.boxOverflow.None', + requires: ['Ext.util.ClickRepeater', 'Ext.core.Element'], + alternateClassName: 'Ext.layout.boxOverflow.Scroller', + mixins: { + observable: 'Ext.util.Observable' + }, + + /* End Definitions */ + + /** + * @cfg {Boolean} animateScroll + * True to animate the scrolling of items within the layout (defaults to true, ignored if enableScroll is false) + */ + animateScroll: false, + + /** + * @cfg {Number} scrollIncrement + * The number of pixels to scroll by on scroller click (defaults to 24) + */ + scrollIncrement: 20, + + /** + * @cfg {Number} wheelIncrement + * The number of pixels to increment on mouse wheel scrolling (defaults to 3). + */ + wheelIncrement: 10, + + /** + * @cfg {Number} scrollRepeatInterval + * Number of milliseconds between each scroll while a scroller button is held down (defaults to 20) + */ + scrollRepeatInterval: 60, + + /** + * @cfg {Number} scrollDuration + * Number of milliseconds that each scroll animation lasts (defaults to 400) + */ + scrollDuration: 400, + + /** + * @cfg {String} beforeCtCls + * CSS class added to the beforeCt element. This is the element that holds any special items such as scrollers, + * which must always be present at the leftmost edge of the Container + */ + + /** + * @cfg {String} afterCtCls + * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers, + * which must always be present at the rightmost edge of the Container + */ + + /** + * @cfg {String} scrollerCls + * CSS class added to both scroller elements if enableScroll is used + */ + scrollerCls: Ext.baseCSSPrefix + 'box-scroller', + + /** + * @cfg {String} beforeScrollerCls + * CSS class added to the left scroller element if enableScroll is used + */ + + /** + * @cfg {String} afterScrollerCls + * CSS class added to the right scroller element if enableScroll is used + */ + + constructor: function(layout, config) { + this.layout = layout; + Ext.apply(this, config || {}); + + this.addEvents( + /** + * @event scroll + * @param {Ext.layout.container.boxOverflow.Scroller} scroller The layout scroller + * @param {Number} newPosition The new position of the scroller + * @param {Boolean/Object} animate If animating or not. If true, it will be a animation configuration, else it will be false + */ + 'scroll' + ); + }, + + initCSSClasses: function() { + var me = this, + layout = me.layout; + + if (!me.CSSinitialized) { + me.beforeCtCls = me.beforeCtCls || Ext.baseCSSPrefix + 'box-scroller-' + layout.parallelBefore; + me.afterCtCls = me.afterCtCls || Ext.baseCSSPrefix + 'box-scroller-' + layout.parallelAfter; + me.beforeScrollerCls = me.beforeScrollerCls || Ext.baseCSSPrefix + layout.owner.getXType() + '-scroll-' + layout.parallelBefore; + me.afterScrollerCls = me.afterScrollerCls || Ext.baseCSSPrefix + layout.owner.getXType() + '-scroll-' + layout.parallelAfter; + me.CSSinitializes = true; + } + }, + + handleOverflow: function(calculations, targetSize) { + var me = this, + layout = me.layout, + methodName = 'get' + layout.parallelPrefixCap, + newSize = {}; + + me.initCSSClasses(); + me.callParent(arguments); + this.createInnerElements(); + this.showScrollers(); + newSize[layout.perpendicularPrefix] = targetSize[layout.perpendicularPrefix]; + newSize[layout.parallelPrefix] = targetSize[layout.parallelPrefix] - (me.beforeCt[methodName]() + me.afterCt[methodName]()); + return { targetSize: newSize }; + }, + + /** + * @private + * Creates the beforeCt and afterCt elements if they have not already been created + */ + createInnerElements: function() { + var me = this, + target = me.layout.getRenderTarget(); + + //normal items will be rendered to the innerCt. beforeCt and afterCt allow for fixed positioning of + //special items such as scrollers or dropdown menu triggers + if (!me.beforeCt) { + target.addCls(Ext.baseCSSPrefix + me.layout.direction + '-box-overflow-body'); + me.beforeCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + me.beforeCtCls}, 'before'); + me.afterCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + me.afterCtCls}, 'after'); + me.createWheelListener(); + } + }, + + /** + * @private + * Sets up an listener to scroll on the layout's innerCt mousewheel event + */ + createWheelListener: function() { + this.layout.innerCt.on({ + scope : this, + mousewheel: function(e) { + e.stopEvent(); + + this.scrollBy(e.getWheelDelta() * this.wheelIncrement * -1, false); + } + }); + }, + + /** + * @private + */ + clearOverflow: function() { + this.hideScrollers(); + }, + + /** + * @private + * Shows the scroller elements in the beforeCt and afterCt. Creates the scrollers first if they are not already + * present. + */ + showScrollers: function() { + this.createScrollers(); + this.beforeScroller.show(); + this.afterScroller.show(); + this.updateScrollButtons(); + + this.layout.owner.addClsWithUI('scroller'); + }, + + /** + * @private + * Hides the scroller elements in the beforeCt and afterCt + */ + hideScrollers: function() { + if (this.beforeScroller != undefined) { + this.beforeScroller.hide(); + this.afterScroller.hide(); + + this.layout.owner.removeClsWithUI('scroller'); + } + }, + + /** + * @private + * Creates the clickable scroller elements and places them into the beforeCt and afterCt + */ + createScrollers: function() { + if (!this.beforeScroller && !this.afterScroller) { + var before = this.beforeCt.createChild({ + cls: Ext.String.format("{0} {1} ", this.scrollerCls, this.beforeScrollerCls) + }); + + var after = this.afterCt.createChild({ + cls: Ext.String.format("{0} {1}", this.scrollerCls, this.afterScrollerCls) + }); + + before.addClsOnOver(this.beforeScrollerCls + '-hover'); + after.addClsOnOver(this.afterScrollerCls + '-hover'); + + before.setVisibilityMode(Ext.core.Element.DISPLAY); + after.setVisibilityMode(Ext.core.Element.DISPLAY); + + this.beforeRepeater = Ext.create('Ext.util.ClickRepeater', before, { + interval: this.scrollRepeatInterval, + handler : this.scrollLeft, + scope : this + }); + + this.afterRepeater = Ext.create('Ext.util.ClickRepeater', after, { + interval: this.scrollRepeatInterval, + handler : this.scrollRight, + scope : this + }); + + /** + * @property beforeScroller + * @type Ext.core.Element + * The left scroller element. Only created when needed. + */ + this.beforeScroller = before; + + /** + * @property afterScroller + * @type Ext.core.Element + * The left scroller element. Only created when needed. + */ + this.afterScroller = after; + } + }, + + /** + * @private + */ + destroy: function() { + Ext.destroy(this.beforeRepeater, this.afterRepeater, this.beforeScroller, this.afterScroller, this.beforeCt, this.afterCt); + }, + + /** + * @private + * Scrolls left or right by the number of pixels specified + * @param {Number} delta Number of pixels to scroll to the right by. Use a negative number to scroll left + */ + scrollBy: function(delta, animate) { + this.scrollTo(this.getScrollPosition() + delta, animate); + }, + + /** + * @private + * @return {Object} Object passed to scrollTo when scrolling + */ + getScrollAnim: function() { + return { + duration: this.scrollDuration, + callback: this.updateScrollButtons, + scope : this + }; + }, + + /** + * @private + * Enables or disables each scroller button based on the current scroll position + */ + updateScrollButtons: function() { + if (this.beforeScroller == undefined || this.afterScroller == undefined) { + return; + } + + var beforeMeth = this.atExtremeBefore() ? 'addCls' : 'removeCls', + afterMeth = this.atExtremeAfter() ? 'addCls' : 'removeCls', + beforeCls = this.beforeScrollerCls + '-disabled', + afterCls = this.afterScrollerCls + '-disabled'; + + this.beforeScroller[beforeMeth](beforeCls); + this.afterScroller[afterMeth](afterCls); + this.scrolling = false; + }, + + /** + * @private + * Returns true if the innerCt scroll is already at its left-most point + * @return {Boolean} True if already at furthest left point + */ + atExtremeBefore: function() { + return this.getScrollPosition() === 0; + }, + + /** + * @private + * Scrolls to the left by the configured amount + */ + scrollLeft: function() { + this.scrollBy(-this.scrollIncrement, false); + }, + + /** + * @private + * Scrolls to the right by the configured amount + */ + scrollRight: function() { + this.scrollBy(this.scrollIncrement, false); + }, + + /** + * Returns the current scroll position of the innerCt element + * @return {Number} The current scroll position + */ + getScrollPosition: function(){ + var layout = this.layout; + return parseInt(layout.innerCt.dom['scroll' + layout.parallelBeforeCap], 10) || 0; + }, + + /** + * @private + * Returns the maximum value we can scrollTo + * @return {Number} The max scroll value + */ + getMaxScrollPosition: function() { + var layout = this.layout; + return layout.innerCt.dom['scroll' + layout.parallelPrefixCap] - this.layout.innerCt['get' + layout.parallelPrefixCap](); + }, + + /** + * @private + * Returns true if the innerCt scroll is already at its right-most point + * @return {Boolean} True if already at furthest right point + */ + atExtremeAfter: function() { + return this.getScrollPosition() >= this.getMaxScrollPosition(); + }, + + /** + * @private + * Scrolls to the given position. Performs bounds checking. + * @param {Number} position The position to scroll to. This is constrained. + * @param {Boolean} animate True to animate. If undefined, falls back to value of this.animateScroll + */ + scrollTo: function(position, animate) { + var me = this, + layout = me.layout, + oldPosition = me.getScrollPosition(), + newPosition = Ext.Number.constrain(position, 0, me.getMaxScrollPosition()); + + if (newPosition != oldPosition && !me.scrolling) { + if (animate == undefined) { + animate = me.animateScroll; + } + + layout.innerCt.scrollTo(layout.parallelBefore, newPosition, animate ? me.getScrollAnim() : false); + if (animate) { + me.scrolling = true; + } else { + me.scrolling = false; + me.updateScrollButtons(); + } + + me.fireEvent('scroll', me, newPosition, animate ? me.getScrollAnim() : false); + } + }, + + /** + * Scrolls to the given component. + * @param {String|Number|Ext.Component} item The item to scroll to. Can be a numerical index, component id + * or a reference to the component itself. + * @param {Boolean} animate True to animate the scrolling + */ + scrollToItem: function(item, animate) { + var me = this, + layout = me.layout, + visibility, + box, + newPos; + + item = me.getItem(item); + if (item != undefined) { + visibility = this.getItemVisibility(item); + if (!visibility.fullyVisible) { + box = item.getBox(true, true); + newPos = box[layout.parallelPosition]; + if (visibility.hiddenEnd) { + newPos -= (this.layout.innerCt['get' + layout.parallelPrefixCap]() - box[layout.parallelPrefix]); + } + this.scrollTo(newPos, animate); + } + } + }, + + /** + * @private + * For a given item in the container, return an object with information on whether the item is visible + * with the current innerCt scroll value. + * @param {Ext.Component} item The item + * @return {Object} Values for fullyVisible, hiddenStart and hiddenEnd + */ + getItemVisibility: function(item) { + var me = this, + box = me.getItem(item).getBox(true, true), + layout = me.layout, + itemStart = box[layout.parallelPosition], + itemEnd = itemStart + box[layout.parallelPrefix], + scrollStart = me.getScrollPosition(), + scrollEnd = scrollStart + layout.innerCt['get' + layout.parallelPrefixCap](); + + return { + hiddenStart : itemStart < scrollStart, + hiddenEnd : itemEnd > scrollEnd, + fullyVisible: itemStart > scrollStart && itemEnd < scrollEnd + }; + } +}); +/** + * @class Ext.util.Offset + * @ignore + */ +Ext.define('Ext.util.Offset', { + + /* Begin Definitions */ + + statics: { + fromObject: function(obj) { + return new this(obj.x, obj.y); + } + }, + + /* End Definitions */ + + constructor: function(x, y) { + this.x = (x != null && !isNaN(x)) ? x : 0; + this.y = (y != null && !isNaN(y)) ? y : 0; + + return this; + }, + + copy: function() { + return new Ext.util.Offset(this.x, this.y); + }, + + copyFrom: function(p) { + this.x = p.x; + this.y = p.y; + }, + + toString: function() { + return "Offset[" + this.x + "," + this.y + "]"; + }, + + equals: function(offset) { + // + if(!(offset instanceof this.statics())) { + Ext.Error.raise('Offset must be an instance of Ext.util.Offset'); + } + // + + return (this.x == offset.x && this.y == offset.y); + }, + + round: function(to) { + if (!isNaN(to)) { + var factor = Math.pow(10, to); + this.x = Math.round(this.x * factor) / factor; + this.y = Math.round(this.y * factor) / factor; + } else { + this.x = Math.round(this.x); + this.y = Math.round(this.y); + } + }, + + isZero: function() { + return this.x == 0 && this.y == 0; + } +}); + +/** + * @class Ext.util.KeyNav + *

Provides a convenient wrapper for normalized keyboard navigation. KeyNav allows you to bind + * navigation keys to function calls that will get called when the keys are pressed, providing an easy + * way to implement custom navigation schemes for any UI component.

+ *

The following are all of the possible keys that can be implemented: enter, space, left, right, up, down, tab, esc, + * pageUp, pageDown, del, backspace, home, end. Usage:

+

+var nav = new Ext.util.KeyNav("my-element", {
+    "left" : function(e){
+        this.moveLeft(e.ctrlKey);
+    },
+    "right" : function(e){
+        this.moveRight(e.ctrlKey);
+    },
+    "enter" : function(e){
+        this.save();
+    },
+    scope : this
+});
+
+ * @constructor + * @param {Mixed} el The element to bind to + * @param {Object} config The config + */ +Ext.define('Ext.util.KeyNav', { + + alternateClassName: 'Ext.KeyNav', + + requires: ['Ext.util.KeyMap'], + + statics: { + keyOptions: { + left: 37, + right: 39, + up: 38, + down: 40, + space: 32, + pageUp: 33, + pageDown: 34, + del: 46, + backspace: 8, + home: 36, + end: 35, + enter: 13, + esc: 27, + tab: 9 + } + }, + + constructor: function(el, config){ + this.setConfig(el, config || {}); + }, + + /** + * Sets up a configuration for the KeyNav. + * @private + * @param {Mixed} el The element to bind to + * @param {Object}A configuration object as specified in the constructor. + */ + setConfig: function(el, config) { + if (this.map) { + this.map.destroy(); + } + + var map = Ext.create('Ext.util.KeyMap', el, null, this.getKeyEvent('forceKeyDown' in config ? config.forceKeyDown : this.forceKeyDown)), + keys = Ext.util.KeyNav.keyOptions, + scope = config.scope || this, + key; + + this.map = map; + for (key in keys) { + if (keys.hasOwnProperty(key)) { + if (config[key]) { + map.addBinding({ + scope: scope, + key: keys[key], + handler: Ext.Function.bind(this.handleEvent, scope, [config[key]], true), + defaultEventAction: config.defaultEventAction || this.defaultEventAction + }); + } + } + } + + map.disable(); + if (!config.disabled) { + map.enable(); + } + }, + + /** + * Method for filtering out the map argument + * @private + * @param {Ext.util.KeyMap} map + * @param {Ext.EventObject} event + * @param {Object} options Contains the handler to call + */ + handleEvent: function(map, event, handler){ + return handler.call(this, event); + }, + + /** + * @cfg {Boolean} disabled + * True to disable this KeyNav instance (defaults to false) + */ + disabled: false, + + /** + * @cfg {String} defaultEventAction + * The method to call on the {@link Ext.EventObject} after this KeyNav intercepts a key. Valid values are + * {@link Ext.EventObject#stopEvent}, {@link Ext.EventObject#preventDefault} and + * {@link Ext.EventObject#stopPropagation} (defaults to 'stopEvent') + */ + defaultEventAction: "stopEvent", + + /** + * @cfg {Boolean} forceKeyDown + * Handle the keydown event instead of keypress (defaults to false). KeyNav automatically does this for IE since + * IE does not propagate special keys on keypress, but setting this to true will force other browsers to also + * handle keydown instead of keypress. + */ + forceKeyDown: false, + + /** + * Destroy this KeyNav (this is the same as calling disable). + * @param {Boolean} removeEl True to remove the element associated with this KeyNav. + */ + destroy: function(removeEl){ + this.map.destroy(removeEl); + delete this.map; + }, + + /** + * Enable this KeyNav + */ + enable: function() { + this.map.enable(); + this.disabled = false; + }, + + /** + * Disable this KeyNav + */ + disable: function() { + this.map.disable(); + this.disabled = true; + }, + + /** + * Convenience function for setting disabled/enabled by boolean. + * @param {Boolean} disabled + */ + setDisabled : function(disabled){ + this.map.setDisabled(disabled); + this.disabled = disabled; + }, + + /** + * Determines the event to bind to listen for keys. Depends on the {@link #forceKeyDown} setting, + * as well as the useKeyDown option on the EventManager. + * @return {String} The type of event to listen for. + */ + getKeyEvent: function(forceKeyDown){ + return (forceKeyDown || Ext.EventManager.useKeyDown) ? 'keydown' : 'keypress'; + } +}); + +/** + * @class Ext.fx.Queue + * Animation Queue mixin to handle chaining and queueing by target. + * @private + */ + +Ext.define('Ext.fx.Queue', { + + requires: ['Ext.util.HashMap'], + + constructor: function() { + this.targets = Ext.create('Ext.util.HashMap'); + this.fxQueue = {}; + }, + + // @private + getFxDefaults: function(targetId) { + var target = this.targets.get(targetId); + if (target) { + return target.fxDefaults; + } + return {}; + }, + + // @private + setFxDefaults: function(targetId, obj) { + var target = this.targets.get(targetId); + if (target) { + target.fxDefaults = Ext.apply(target.fxDefaults || {}, obj); + } + }, + + // @private + stopAnimation: function(targetId) { + var me = this, + queue = me.getFxQueue(targetId), + ln = queue.length; + while (ln) { + queue[ln - 1].end(); + ln--; + } + }, + + /** + * @private + * Returns current animation object if the element has any effects actively running or queued, else returns false. + */ + getActiveAnimation: function(targetId) { + var queue = this.getFxQueue(targetId); + return (queue && !!queue.length) ? queue[0] : false; + }, + + // @private + hasFxBlock: function(targetId) { + var queue = this.getFxQueue(targetId); + return queue && queue[0] && queue[0].block; + }, + + // @private get fx queue for passed target, create if needed. + getFxQueue: function(targetId) { + if (!targetId) { + return false; + } + var me = this, + queue = me.fxQueue[targetId], + target = me.targets.get(targetId); + + if (!target) { + return false; + } + + if (!queue) { + me.fxQueue[targetId] = []; + // GarbageCollector will need to clean up Elements since they aren't currently observable + if (target.type != 'element') { + target.target.on('destroy', function() { + me.fxQueue[targetId] = []; + }); + } + } + return me.fxQueue[targetId]; + }, + + // @private + queueFx: function(anim) { + var me = this, + target = anim.target, + queue, ln; + + if (!target) { + return; + } + + queue = me.getFxQueue(target.getId()); + ln = queue.length; + + if (ln) { + if (anim.concurrent) { + anim.paused = false; + } + else { + queue[ln - 1].on('afteranimate', function() { + anim.paused = false; + }); + } + } + else { + anim.paused = false; + } + anim.on('afteranimate', function() { + Ext.Array.remove(queue, anim); + if (anim.remove) { + if (target.type == 'element') { + var el = Ext.get(target.id); + if (el) { + el.remove(); + } + } + } + }, this); + queue.push(anim); + } +}); +/** + * @class Ext.fx.target.Target + +This class specifies a generic target for an animation. It provides a wrapper around a +series of different types of objects to allow for a generic animation API. +A target can be a single object or a Composite object containing other objects that are +to be animated. This class and it's subclasses are generally not created directly, the +underlying animation will create the appropriate Ext.fx.target.Target object by passing +the instance to be animated. + +The following types of objects can be animated: +- {@link #Ext.fx.target.Component Components} +- {@link #Ext.fx.target.Element Elements} +- {@link #Ext.fx.target.Sprite Sprites} + + * @markdown + * @abstract + * @constructor + * @param {Mixed} target The object to be animated + */ + +Ext.define('Ext.fx.target.Target', { + + isAnimTarget: true, + + constructor: function(target) { + this.target = target; + this.id = this.getId(); + }, + + getId: function() { + return this.target.id; + } +}); + +/** + * @class Ext.fx.target.Sprite + * @extends Ext.fx.target.Target + +This class represents a animation target for a {@link Ext.draw.Sprite}. In general this class will not be +created directly, the {@link Ext.draw.Sprite} will be passed to the animation and +and the appropriate target will be created. + + * @markdown + */ + +Ext.define('Ext.fx.target.Sprite', { + + /* Begin Definitions */ + + extend: 'Ext.fx.target.Target', + + /* End Definitions */ + + type: 'draw', + + getFromPrim: function(sprite, attr) { + var o; + if (attr == 'translate') { + o = { + x: sprite.attr.translation.x || 0, + y: sprite.attr.translation.y || 0 + }; + } + else if (attr == 'rotate') { + o = { + degrees: sprite.attr.rotation.degrees || 0, + x: sprite.attr.rotation.x, + y: sprite.attr.rotation.y + }; + } + else { + o = sprite.attr[attr]; + } + return o; + }, + + getAttr: function(attr, val) { + return [[this.target, val != undefined ? val : this.getFromPrim(this.target, attr)]]; + }, + + setAttr: function(targetData) { + var ln = targetData.length, + spriteArr = [], + attrs, attr, attrArr, attPtr, spritePtr, idx, value, i, j, x, y, ln2; + for (i = 0; i < ln; i++) { + attrs = targetData[i].attrs; + for (attr in attrs) { + attrArr = attrs[attr]; + ln2 = attrArr.length; + for (j = 0; j < ln2; j++) { + spritePtr = attrArr[j][0]; + attPtr = attrArr[j][1]; + if (attr === 'translate') { + value = { + x: attPtr.x, + y: attPtr.y + }; + } + else if (attr === 'rotate') { + x = attPtr.x; + if (isNaN(x)) { + x = null; + } + y = attPtr.y; + if (isNaN(y)) { + y = null; + } + value = { + degrees: attPtr.degrees, + x: x, + y: y + }; + } + else if (attr === 'width' || attr === 'height' || attr === 'x' || attr === 'y') { + value = parseFloat(attPtr); + } + else { + value = attPtr; + } + idx = Ext.Array.indexOf(spriteArr, spritePtr); + if (idx == -1) { + spriteArr.push([spritePtr, {}]); + idx = spriteArr.length - 1; + } + spriteArr[idx][1][attr] = value; + } + } + } + ln = spriteArr.length; + for (i = 0; i < ln; i++) { + spritePtr = spriteArr[i]; + spritePtr[0].setAttributes(spritePtr[1]); + } + this.target.redraw(); + } +}); + +/** + * @class Ext.fx.target.CompositeSprite + * @extends Ext.fx.target.Sprite + +This class represents a animation target for a {@link Ext.draw.CompositeSprite}. It allows +each {@link Ext.draw.Sprite} in the group to be animated as a whole. In general this class will not be +created directly, the {@link Ext.draw.CompositeSprite} will be passed to the animation and +and the appropriate target will be created. + + * @markdown + */ + +Ext.define('Ext.fx.target.CompositeSprite', { + + /* Begin Definitions */ + + extend: 'Ext.fx.target.Sprite', + + /* End Definitions */ + + getAttr: function(attr, val) { + var out = [], + target = this.target; + target.each(function(sprite) { + out.push([sprite, val != undefined ? val : this.getFromPrim(sprite, attr)]); + }, this); + return out; + } +}); + +/** + * @class Ext.fx.target.Component + * @extends Ext.fx.target.Target + * + * This class represents a animation target for a {@link Ext.Component}. In general this class will not be + * created directly, the {@link Ext.Component} will be passed to the animation and + * and the appropriate target will be created. + */ +Ext.define('Ext.fx.target.Component', { + + /* Begin Definitions */ + + extend: 'Ext.fx.target.Target', + + /* End Definitions */ + + type: 'component', + + // Methods to call to retrieve unspecified "from" values from a target Component + getPropMethod: { + top: function() { + return this.getPosition(true)[1]; + }, + left: function() { + return this.getPosition(true)[0]; + }, + x: function() { + return this.getPosition()[0]; + }, + y: function() { + return this.getPosition()[1]; + }, + height: function() { + return this.getHeight(); + }, + width: function() { + return this.getWidth(); + }, + opacity: function() { + return this.el.getStyle('opacity'); + } + }, + + compMethod: { + top: 'setPosition', + left: 'setPosition', + x: 'setPagePosition', + y: 'setPagePosition', + height: 'setSize', + width: 'setSize', + opacity: 'setOpacity' + }, + + // Read the named attribute from the target Component. Use the defined getter for the attribute + getAttr: function(attr, val) { + return [[this.target, val !== undefined ? val : this.getPropMethod[attr].call(this.target)]]; + }, + + setAttr: function(targetData, isFirstFrame, isLastFrame) { + var me = this, + target = me.target, + ln = targetData.length, + attrs, attr, o, i, j, meth, targets, left, top, w, h; + for (i = 0; i < ln; i++) { + attrs = targetData[i].attrs; + for (attr in attrs) { + targets = attrs[attr].length; + meth = { + setPosition: {}, + setPagePosition: {}, + setSize: {}, + setOpacity: {} + }; + for (j = 0; j < targets; j++) { + o = attrs[attr][j]; + // We REALLY want a single function call, so push these down to merge them: eg + // meth.setPagePosition.target = + // meth.setPagePosition['x'] = 100 + // meth.setPagePosition['y'] = 100 + meth[me.compMethod[attr]].target = o[0]; + meth[me.compMethod[attr]][attr] = o[1]; + } + if (meth.setPosition.target) { + o = meth.setPosition; + left = (o.left === undefined) ? undefined : parseInt(o.left, 10); + top = (o.top === undefined) ? undefined : parseInt(o.top, 10); + o.target.setPosition(left, top); + } + if (meth.setPagePosition.target) { + o = meth.setPagePosition; + o.target.setPagePosition(o.x, o.y); + } + if (meth.setSize.target) { + o = meth.setSize; + // Dimensions not being animated MUST NOT be autosized. They must remain at current value. + w = (o.width === undefined) ? o.target.getWidth() : parseInt(o.width, 10); + h = (o.height === undefined) ? o.target.getHeight() : parseInt(o.height, 10); + + // Only set the size of the Component on the last frame, or if the animation was + // configured with dynamic: true. + // In other cases, we just set the target element size. + // This will result in either clipping if animating a reduction in size, or the revealing of + // the inner elements of the Component if animating an increase in size. + // Component's animate function initially resizes to the larger size before resizing the + // outer element to clip the contents. + if (isLastFrame || me.dynamic) { + o.target.componentLayout.childrenChanged = true; + + // Flag if we are being called by an animating layout: use setCalculatedSize + if (me.layoutAnimation) { + o.target.setCalculatedSize(w, h); + } else { + o.target.setSize(w, h); + } + } + else { + o.target.el.setSize(w, h); + } + } + if (meth.setOpacity.target) { + o = meth.setOpacity; + o.target.el.setStyle('opacity', o.opacity); + } + } + } + } +}); + +/** + * @class Ext.fx.CubicBezier + * @ignore + */ +Ext.define('Ext.fx.CubicBezier', { + + /* Begin Definitions */ + + singleton: true, + + /* End Definitions */ + + cubicBezierAtTime: function(t, p1x, p1y, p2x, p2y, duration) { + var cx = 3 * p1x, + bx = 3 * (p2x - p1x) - cx, + ax = 1 - cx - bx, + cy = 3 * p1y, + by = 3 * (p2y - p1y) - cy, + ay = 1 - cy - by; + function sampleCurveX(t) { + return ((ax * t + bx) * t + cx) * t; + } + function solve(x, epsilon) { + var t = solveCurveX(x, epsilon); + return ((ay * t + by) * t + cy) * t; + } + function solveCurveX(x, epsilon) { + var t0, t1, t2, x2, d2, i; + for (t2 = x, i = 0; i < 8; i++) { + x2 = sampleCurveX(t2) - x; + if (Math.abs(x2) < epsilon) { + return t2; + } + d2 = (3 * ax * t2 + 2 * bx) * t2 + cx; + if (Math.abs(d2) < 1e-6) { + break; + } + t2 = t2 - x2 / d2; + } + t0 = 0; + t1 = 1; + t2 = x; + if (t2 < t0) { + return t0; + } + if (t2 > t1) { + return t1; + } + while (t0 < t1) { + x2 = sampleCurveX(t2); + if (Math.abs(x2 - x) < epsilon) { + return t2; + } + if (x > x2) { + t0 = t2; + } else { + t1 = t2; + } + t2 = (t1 - t0) / 2 + t0; + } + return t2; + } + return solve(t, 1 / (200 * duration)); + }, + + cubicBezier: function(x1, y1, x2, y2) { + var fn = function(pos) { + return Ext.fx.CubicBezier.cubicBezierAtTime(pos, x1, y1, x2, y2, 1); + }; + fn.toCSS3 = function() { + return 'cubic-bezier(' + [x1, y1, x2, y2].join(',') + ')'; + }; + fn.reverse = function() { + return Ext.fx.CubicBezier.cubicBezier(1 - x2, 1 - y2, 1 - x1, 1 - y1); + }; + return fn; + } +}); +/** + * @class Ext.draw.Color + * @extends Object + * + * Represents an RGB color and provides helper functions get + * color components in HSL color space. + */ +Ext.define('Ext.draw.Color', { + + /* Begin Definitions */ + + /* End Definitions */ + + colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + rgbRe: /\s*rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)\s*/, + hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/, + + /** + * @cfg {Number} lightnessFactor + * + * The default factor to compute the lighter or darker color. Defaults to 0.2. + */ + lightnessFactor: 0.2, + + /** + * @constructor + * @param {Number} red Red component (0..255) + * @param {Number} green Green component (0..255) + * @param {Number} blue Blue component (0..255) + */ + constructor : function(red, green, blue) { + var me = this, + clamp = Ext.Number.constrain; + me.r = clamp(red, 0, 255); + me.g = clamp(green, 0, 255); + me.b = clamp(blue, 0, 255); + }, + + /** + * Get the red component of the color, in the range 0..255. + * @return {Number} + */ + getRed: function() { + return this.r; + }, + + /** + * Get the green component of the color, in the range 0..255. + * @return {Number} + */ + getGreen: function() { + return this.g; + }, + + /** + * Get the blue component of the color, in the range 0..255. + * @return {Number} + */ + getBlue: function() { + return this.b; + }, + + /** + * Get the RGB values. + * @return {Array} + */ + getRGB: function() { + var me = this; + return [me.r, me.g, me.b]; + }, + + /** + * Get the equivalent HSL components of the color. + * @return {Array} + */ + getHSL: function() { + var me = this, + r = me.r / 255, + g = me.g / 255, + b = me.b / 255, + max = Math.max(r, g, b), + min = Math.min(r, g, b), + delta = max - min, + h, + s = 0, + l = 0.5 * (max + min); + + // min==max means achromatic (hue is undefined) + if (min != max) { + s = (l < 0.5) ? delta / (max + min) : delta / (2 - max - min); + if (r == max) { + h = 60 * (g - b) / delta; + } else if (g == max) { + h = 120 + 60 * (b - r) / delta; + } else { + h = 240 + 60 * (r - g) / delta; + } + if (h < 0) { + h += 360; + } + if (h >= 360) { + h -= 360; + } + } + return [h, s, l]; + }, + + /** + * Return a new color that is lighter than this color. + * @param {Number} factor Lighter factor (0..1), default to 0.2 + * @return Ext.draw.Color + */ + getLighter: function(factor) { + var hsl = this.getHSL(); + factor = factor || this.lightnessFactor; + hsl[2] = Ext.Number.constrain(hsl[2] + factor, 0, 1); + return this.fromHSL(hsl[0], hsl[1], hsl[2]); + }, + + /** + * Return a new color that is darker than this color. + * @param {Number} factor Darker factor (0..1), default to 0.2 + * @return Ext.draw.Color + */ + getDarker: function(factor) { + factor = factor || this.lightnessFactor; + return this.getLighter(-factor); + }, + + /** + * Return the color in the hex format, i.e. '#rrggbb'. + * @return {String} + */ + toString: function() { + var me = this, + round = Math.round, + r = round(me.r).toString(16), + g = round(me.g).toString(16), + b = round(me.b).toString(16); + r = (r.length == 1) ? '0' + r : r; + g = (g.length == 1) ? '0' + g : g; + b = (b.length == 1) ? '0' + b : b; + return ['#', r, g, b].join(''); + }, + + /** + * Convert a color to hexadecimal format. + * + * @param {String|Array} color The color value (i.e 'rgb(255, 255, 255)', 'color: #ffffff'). + * Can also be an Array, in this case the function handles the first member. + * @returns {String} The color in hexadecimal format. + */ + toHex: function(color) { + if (Ext.isArray(color)) { + color = color[0]; + } + if (!Ext.isString(color)) { + return ''; + } + if (color.substr(0, 1) === '#') { + return color; + } + var digits = this.colorToHexRe.exec(color); + + if (Ext.isArray(digits)) { + var red = parseInt(digits[2], 10), + green = parseInt(digits[3], 10), + blue = parseInt(digits[4], 10), + rgb = blue | (green << 8) | (red << 16); + return digits[1] + '#' + ("000000" + rgb.toString(16)).slice(-6); + } + else { + return ''; + } + }, + + /** + * Parse the string and create a new color. + * + * Supported formats: '#rrggbb', '#rgb', and 'rgb(r,g,b)'. + * + * If the string is not recognized, an undefined will be returned instead. + * + * @param {String} str Color in string. + * @returns Ext.draw.Color + */ + fromString: function(str) { + var values, r, g, b, + parse = parseInt; + + if ((str.length == 4 || str.length == 7) && str.substr(0, 1) === '#') { + values = str.match(this.hexRe); + if (values) { + r = parse(values[1], 16) >> 0; + g = parse(values[2], 16) >> 0; + b = parse(values[3], 16) >> 0; + if (str.length == 4) { + r += (r * 16); + g += (g * 16); + b += (b * 16); + } + } + } + else { + values = str.match(this.rgbRe); + if (values) { + r = values[1]; + g = values[2]; + b = values[3]; + } + } + + return (typeof r == 'undefined') ? undefined : Ext.create('Ext.draw.Color', r, g, b); + }, + + /** + * Returns the gray value (0 to 255) of the color. + * + * The gray value is calculated using the formula r*0.3 + g*0.59 + b*0.11. + * + * @returns {Number} + */ + getGrayscale: function() { + // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale + return this.r * 0.3 + this.g * 0.59 + this.b * 0.11; + }, + + /** + * Create a new color based on the specified HSL values. + * + * @param {Number} h Hue component (0..359) + * @param {Number} s Saturation component (0..1) + * @param {Number} l Lightness component (0..1) + * @returns Ext.draw.Color + */ + fromHSL: function(h, s, l) { + var C, X, m, i, rgb = [], + abs = Math.abs, + floor = Math.floor; + + if (s == 0 || h == null) { + // achromatic + rgb = [l, l, l]; + } + else { + // http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL + // C is the chroma + // X is the second largest component + // m is the lightness adjustment + h /= 60; + C = s * (1 - abs(2 * l - 1)); + X = C * (1 - abs(h - 2 * floor(h / 2) - 1)); + m = l - C / 2; + switch (floor(h)) { + case 0: + rgb = [C, X, 0]; + break; + case 1: + rgb = [X, C, 0]; + break; + case 2: + rgb = [0, C, X]; + break; + case 3: + rgb = [0, X, C]; + break; + case 4: + rgb = [X, 0, C]; + break; + case 5: + rgb = [C, 0, X]; + break; + } + rgb = [rgb[0] + m, rgb[1] + m, rgb[2] + m]; + } + return Ext.create('Ext.draw.Color', rgb[0] * 255, rgb[1] * 255, rgb[2] * 255); + } +}, function() { + var prototype = this.prototype; + + //These functions are both static and instance. TODO: find a more elegant way of copying them + this.addStatics({ + fromHSL: function() { + return prototype.fromHSL.apply(prototype, arguments); + }, + fromString: function() { + return prototype.fromString.apply(prototype, arguments); + }, + toHex: function() { + return prototype.toHex.apply(prototype, arguments); + } + }); +}); + +/** + * @class Ext.dd.StatusProxy + * A specialized drag proxy that supports a drop status icon, {@link Ext.Layer} styles and auto-repair. This is the + * default drag proxy used by all Ext.dd components. + * @constructor + * @param {Object} config + */ +Ext.define('Ext.dd.StatusProxy', { + animRepair: false, + + constructor: function(config){ + Ext.apply(this, config); + this.id = this.id || Ext.id(); + this.proxy = Ext.createWidget('component', { + floating: true, + id: this.id, + html: '
' + + '
', + cls: Ext.baseCSSPrefix + 'dd-drag-proxy ' + this.dropNotAllowed, + shadow: !config || config.shadow !== false, + renderTo: document.body + }); + + this.el = this.proxy.el; + this.el.show(); + this.el.setVisibilityMode(Ext.core.Element.VISIBILITY); + this.el.hide(); + + this.ghost = Ext.get(this.el.dom.childNodes[1]); + this.dropStatus = this.dropNotAllowed; + }, + /** + * @cfg {String} dropAllowed + * The CSS class to apply to the status element when drop is allowed (defaults to "x-dd-drop-ok"). + */ + dropAllowed : Ext.baseCSSPrefix + 'dd-drop-ok', + /** + * @cfg {String} dropNotAllowed + * The CSS class to apply to the status element when drop is not allowed (defaults to "x-dd-drop-nodrop"). + */ + dropNotAllowed : Ext.baseCSSPrefix + 'dd-drop-nodrop', + + /** + * Updates the proxy's visual element to indicate the status of whether or not drop is allowed + * over the current target element. + * @param {String} cssClass The css class for the new drop status indicator image + */ + setStatus : function(cssClass){ + cssClass = cssClass || this.dropNotAllowed; + if(this.dropStatus != cssClass){ + this.el.replaceCls(this.dropStatus, cssClass); + this.dropStatus = cssClass; + } + }, + + /** + * Resets the status indicator to the default dropNotAllowed value + * @param {Boolean} clearGhost True to also remove all content from the ghost, false to preserve it + */ + reset : function(clearGhost){ + this.el.dom.className = Ext.baseCSSPrefix + 'dd-drag-proxy ' + this.dropNotAllowed; + this.dropStatus = this.dropNotAllowed; + if(clearGhost){ + this.ghost.update(""); + } + }, + + /** + * Updates the contents of the ghost element + * @param {String/HTMLElement} html The html that will replace the current innerHTML of the ghost element, or a + * DOM node to append as the child of the ghost element (in which case the innerHTML will be cleared first). + */ + update : function(html){ + if(typeof html == "string"){ + this.ghost.update(html); + }else{ + this.ghost.update(""); + html.style.margin = "0"; + this.ghost.dom.appendChild(html); + } + var el = this.ghost.dom.firstChild; + if(el){ + Ext.fly(el).setStyle('float', 'none'); + } + }, + + /** + * Returns the underlying proxy {@link Ext.Layer} + * @return {Ext.Layer} el + */ + getEl : function(){ + return this.el; + }, + + /** + * Returns the ghost element + * @return {Ext.core.Element} el + */ + getGhost : function(){ + return this.ghost; + }, + + /** + * Hides the proxy + * @param {Boolean} clear True to reset the status and clear the ghost contents, false to preserve them + */ + hide : function(clear) { + this.proxy.hide(); + if (clear) { + this.reset(true); + } + }, + + /** + * Stops the repair animation if it's currently running + */ + stop : function(){ + if(this.anim && this.anim.isAnimated && this.anim.isAnimated()){ + this.anim.stop(); + } + }, + + /** + * Displays this proxy + */ + show : function() { + this.proxy.show(); + this.proxy.toFront(); + }, + + /** + * Force the Layer to sync its shadow and shim positions to the element + */ + sync : function(){ + this.proxy.el.sync(); + }, + + /** + * Causes the proxy to return to its position of origin via an animation. Should be called after an + * invalid drop operation by the item being dragged. + * @param {Array} xy The XY position of the element ([x, y]) + * @param {Function} callback The function to call after the repair is complete. + * @param {Object} scope The scope (this reference) in which the callback function is executed. Defaults to the browser window. + */ + repair : function(xy, callback, scope){ + this.callback = callback; + this.scope = scope; + if (xy && this.animRepair !== false) { + this.el.addCls(Ext.baseCSSPrefix + 'dd-drag-repair'); + this.el.hideUnders(true); + this.anim = this.el.animate({ + duration: this.repairDuration || 500, + easing: 'ease-out', + to: { + x: xy[0], + y: xy[1] + }, + stopAnimation: true, + callback: this.afterRepair, + scope: this + }); + } else { + this.afterRepair(); + } + }, + + // private + afterRepair : function(){ + this.hide(true); + if(typeof this.callback == "function"){ + this.callback.call(this.scope || this); + } + this.callback = null; + this.scope = null; + }, + + destroy: function(){ + Ext.destroy(this.ghost, this.proxy, this.el); + } +}); +/** + * @class Ext.panel.Proxy + * @extends Object + * A custom drag proxy implementation specific to {@link Ext.panel.Panel}s. This class + * is primarily used internally for the Panel's drag drop implementation, and + * should never need to be created directly. + * @constructor + * @param panel The {@link Ext.panel.Panel} to proxy for + * @param config Configuration options + */ +Ext.define('Ext.panel.Proxy', { + + alternateClassName: 'Ext.dd.PanelProxy', + + constructor: function(panel, config){ + /** + * @property panel + * @type Ext.panel.Panel + */ + this.panel = panel; + this.id = this.panel.id +'-ddproxy'; + Ext.apply(this, config); + }, + + /** + * @cfg {Boolean} insertProxy True to insert a placeholder proxy element + * while dragging the panel, false to drag with no proxy (defaults to true). + * Most Panels are not absolute positioned and therefore we need to reserve + * this space. + */ + insertProxy: true, + + // private overrides + setStatus: Ext.emptyFn, + reset: Ext.emptyFn, + update: Ext.emptyFn, + stop: Ext.emptyFn, + sync: Ext.emptyFn, + + /** + * Gets the proxy's element + * @return {Element} The proxy's element + */ + getEl: function(){ + return this.ghost.el; + }, + + /** + * Gets the proxy's ghost Panel + * @return {Panel} The proxy's ghost Panel + */ + getGhost: function(){ + return this.ghost; + }, + + /** + * Gets the proxy element. This is the element that represents where the + * Panel was before we started the drag operation. + * @return {Element} The proxy's element + */ + getProxy: function(){ + return this.proxy; + }, + + /** + * Hides the proxy + */ + hide : function(){ + if (this.ghost) { + if (this.proxy) { + this.proxy.remove(); + delete this.proxy; + } + + // Unghost the Panel, do not move the Panel to where the ghost was + this.panel.unghost(null, false); + delete this.ghost; + } + }, + + /** + * Shows the proxy + */ + show: function(){ + if (!this.ghost) { + var panelSize = this.panel.getSize(); + this.panel.el.setVisibilityMode(Ext.core.Element.DISPLAY); + this.ghost = this.panel.ghost(); + if (this.insertProxy) { + // bc Panels aren't absolute positioned we need to take up the space + // of where the panel previously was + this.proxy = this.panel.el.insertSibling({cls: Ext.baseCSSPrefix + 'panel-dd-spacer'}); + this.proxy.setSize(panelSize); + } + } + }, + + // private + repair: function(xy, callback, scope) { + this.hide(); + if (typeof callback == "function") { + callback.call(scope || this); + } + }, + + /** + * Moves the proxy to a different position in the DOM. This is typically + * called while dragging the Panel to keep the proxy sync'd to the Panel's + * location. + * @param {HTMLElement} parentNode The proxy's parent DOM node + * @param {HTMLElement} before (optional) The sibling node before which the + * proxy should be inserted (defaults to the parent's last child if not + * specified) + */ + moveProxy : function(parentNode, before){ + if (this.proxy) { + parentNode.insertBefore(this.proxy.dom, before); + } + } +}); +/** + * @class Ext.layout.component.AbstractDock + * @extends Ext.layout.component.Component + * @private + * This ComponentLayout handles docking for Panels. It takes care of panels that are + * part of a ContainerLayout that sets this Panel's size and Panels that are part of + * an AutoContainerLayout in which this panel get his height based of the CSS or + * or its content. + */ + +Ext.define('Ext.layout.component.AbstractDock', { + + /* Begin Definitions */ + + extend: 'Ext.layout.component.Component', + + /* End Definitions */ + + type: 'dock', + + /** + * @private + * @property autoSizing + * @type boolean + * This flag is set to indicate this layout may have an autoHeight/autoWidth. + */ + autoSizing: true, + + beforeLayout: function() { + var returnValue = this.callParent(arguments); + if (returnValue !== false && (!this.initializedBorders || this.childrenChanged) && (!this.owner.border || this.owner.manageBodyBorders)) { + this.handleItemBorders(); + this.initializedBorders = true; + } + return returnValue; + }, + + handleItemBorders: function() { + var owner = this.owner, + body = owner.body, + docked = this.getLayoutItems(), + borders = { + top: [], + right: [], + bottom: [], + left: [] + }, + oldBorders = this.borders, + opposites = { + top: 'bottom', + right: 'left', + bottom: 'top', + left: 'right' + }, + i, ln, item, dock, side; + + for (i = 0, ln = docked.length; i < ln; i++) { + item = docked[i]; + dock = item.dock; + + if (item.ignoreBorderManagement) { + continue; + } + + if (!borders[dock].satisfied) { + borders[dock].push(item); + borders[dock].satisfied = true; + } + + if (!borders.top.satisfied && opposites[dock] !== 'top') { + borders.top.push(item); + } + if (!borders.right.satisfied && opposites[dock] !== 'right') { + borders.right.push(item); + } + if (!borders.bottom.satisfied && opposites[dock] !== 'bottom') { + borders.bottom.push(item); + } + if (!borders.left.satisfied && opposites[dock] !== 'left') { + borders.left.push(item); + } + } + + if (oldBorders) { + for (side in oldBorders) { + if (oldBorders.hasOwnProperty(side)) { + ln = oldBorders[side].length; + if (!owner.manageBodyBorders) { + for (i = 0; i < ln; i++) { + oldBorders[side][i].removeCls(Ext.baseCSSPrefix + 'docked-noborder-' + side); + } + if (!oldBorders[side].satisfied && !owner.bodyBorder) { + body.removeCls(Ext.baseCSSPrefix + 'docked-noborder-' + side); + } + } + else if (oldBorders[side].satisfied) { + body.setStyle('border-' + side + '-width', ''); + } + } + } + } + + for (side in borders) { + if (borders.hasOwnProperty(side)) { + ln = borders[side].length; + if (!owner.manageBodyBorders) { + for (i = 0; i < ln; i++) { + borders[side][i].addCls(Ext.baseCSSPrefix + 'docked-noborder-' + side); + } + if ((!borders[side].satisfied && !owner.bodyBorder) || owner.bodyBorder === false) { + body.addCls(Ext.baseCSSPrefix + 'docked-noborder-' + side); + } + } + else if (borders[side].satisfied) { + body.setStyle('border-' + side + '-width', '1px'); + } + } + } + + this.borders = borders; + }, + + /** + * @protected + * @param {Ext.Component} owner The Panel that owns this DockLayout + * @param {Ext.core.Element} target The target in which we are going to render the docked items + * @param {Array} args The arguments passed to the ComponentLayout.layout method + */ + onLayout: function(width, height) { + var me = this, + owner = me.owner, + body = owner.body, + layout = owner.layout, + target = me.getTarget(), + autoWidth = false, + autoHeight = false, + padding, border, frameSize; + + // We start of by resetting all the layouts info + var info = me.info = { + boxes: [], + size: { + width: width, + height: height + }, + bodyBox: {} + }; + + Ext.applyIf(info, me.getTargetInfo()); + + // We need to bind to the ownerCt whenever we do not have a user set height or width. + if (owner && owner.ownerCt && owner.ownerCt.layout && owner.ownerCt.layout.isLayout) { + if (!Ext.isNumber(owner.height) || !Ext.isNumber(owner.width)) { + owner.ownerCt.layout.bindToOwnerCtComponent = true; + } + else { + owner.ownerCt.layout.bindToOwnerCtComponent = false; + } + } + + // Determine if we have an autoHeight or autoWidth. + if (height === undefined || height === null || width === undefined || width === null) { + padding = info.padding; + border = info.border; + frameSize = me.frameSize; + + // Auto-everything, clear out any style height/width and read from css + if ((height === undefined || height === null) && (width === undefined || width === null)) { + autoHeight = true; + autoWidth = true; + me.setTargetSize(null); + me.setBodyBox({width: null, height: null}); + } + // Auto-height + else if (height === undefined || height === null) { + autoHeight = true; + // Clear any sizing that we already set in a previous layout + me.setTargetSize(width); + me.setBodyBox({width: width - padding.left - border.left - padding.right - border.right - frameSize.left - frameSize.right, height: null}); + // Auto-width + } + else { + autoWidth = true; + // Clear any sizing that we already set in a previous layout + me.setTargetSize(null, height); + me.setBodyBox({width: null, height: height - padding.top - padding.bottom - border.top - border.bottom - frameSize.top - frameSize.bottom}); + } + + // Run the container + if (layout && layout.isLayout) { + // Auto-Sized so have the container layout notify the component layout. + layout.bindToOwnerCtComponent = true; + layout.layout(); + + // If this is an autosized container layout, then we must compensate for a + // body that is being autosized. We do not want to adjust the body's size + // to accommodate the dock items, but rather we will want to adjust the + // target's size. + // + // This is necessary because, particularly in a Box layout, all child items + // are set with absolute dimensions that are not flexible to the size of its + // innerCt/target. So once they are laid out, they are sized for good. By + // shrinking the body box to accommodate dock items, we're merely cutting off + // parts of the body. Not good. Instead, the target's size should expand + // to fit the dock items in. This is valid because the target container is + // suppose to be autosized to fit everything accordingly. + info.autoSizedCtLayout = layout.autoSize === true; + } + + // The dockItems method will add all the top and bottom docked items height + // to the info.panelSize height. That's why we have to call setSize after + // we dock all the items to actually set the panel's width and height. + // We have to do this because the panel body and docked items will be position + // absolute which doesn't stretch the panel. + me.dockItems(autoWidth, autoHeight); + me.setTargetSize(info.size.width, info.size.height); + } + else { + me.setTargetSize(width, height); + me.dockItems(); + } + me.callParent(arguments); + }, + + /** + * @protected + * This method will first update all the information about the docked items, + * body dimensions and position, the panel's total size. It will then + * set all these values on the docked items and panel body. + * @param {Array} items Array containing all the docked items + * @param {Boolean} autoBoxes Set this to true if the Panel is part of an + * AutoContainerLayout + */ + dockItems : function(autoWidth, autoHeight) { + this.calculateDockBoxes(autoWidth, autoHeight); + + // Both calculateAutoBoxes and calculateSizedBoxes are changing the + // information about the body, panel size, and boxes for docked items + // inside a property called info. + var info = this.info, + boxes = info.boxes, + ln = boxes.length, + dock, i; + + // We are going to loop over all the boxes that were calculated + // and set the position of each item the box belongs to. + for (i = 0; i < ln; i++) { + dock = boxes[i]; + dock.item.setPosition(dock.x, dock.y); + if ((autoWidth || autoHeight) && dock.layout && dock.layout.isLayout) { + // Auto-Sized so have the container layout notify the component layout. + dock.layout.bindToOwnerCtComponent = true; + } + } + + // Don't adjust body width/height if the target is using an auto container layout. + // But, we do want to adjust the body size if the container layout is auto sized. + if (!info.autoSizedCtLayout) { + if (autoWidth) { + info.bodyBox.width = null; + } + if (autoHeight) { + info.bodyBox.height = null; + } + } + + // If the bodyBox has been adjusted because of the docked items + // we will update the dimensions and position of the panel's body. + this.setBodyBox(info.bodyBox); + }, + + /** + * @protected + * This method will set up some initial information about the panel size and bodybox + * and then loop over all the items you pass it to take care of stretching, aligning, + * dock position and all calculations involved with adjusting the body box. + * @param {Array} items Array containing all the docked items we have to layout + */ + calculateDockBoxes : function(autoWidth, autoHeight) { + // We want to use the Panel's el width, and the Panel's body height as the initial + // size we are going to use in calculateDockBoxes. We also want to account for + // the border of the panel. + var me = this, + target = me.getTarget(), + items = me.getLayoutItems(), + owner = me.owner, + bodyEl = owner.body, + info = me.info, + size = info.size, + ln = items.length, + padding = info.padding, + border = info.border, + frameSize = me.frameSize, + item, i, box, rect; + + // If this Panel is inside an AutoContainerLayout, we will base all the calculations + // around the height of the body and the width of the panel. + if (autoHeight) { + size.height = bodyEl.getHeight() + padding.top + border.top + padding.bottom + border.bottom + frameSize.top + frameSize.bottom; + } + else { + size.height = target.getHeight(); + } + if (autoWidth) { + size.width = bodyEl.getWidth() + padding.left + border.left + padding.right + border.right + frameSize.left + frameSize.right; + } + else { + size.width = target.getWidth(); + } + + info.bodyBox = { + x: padding.left + frameSize.left, + y: padding.top + frameSize.top, + width: size.width - padding.left - border.left - padding.right - border.right - frameSize.left - frameSize.right, + height: size.height - border.top - padding.top - border.bottom - padding.bottom - frameSize.top - frameSize.bottom + }; + + // Loop over all the docked items + for (i = 0; i < ln; i++) { + item = items[i]; + // The initBox method will take care of stretching and alignment + // In some cases it will also layout the dock items to be able to + // get a width or height measurement + box = me.initBox(item); + + if (autoHeight === true) { + box = me.adjustAutoBox(box, i); + } + else { + box = me.adjustSizedBox(box, i); + } + + // Save our box. This allows us to loop over all docked items and do all + // calculations first. Then in one loop we will actually size and position + // all the docked items that have changed. + info.boxes.push(box); + } + }, + + /** + * @protected + * This method will adjust the position of the docked item and adjust the body box + * accordingly. + * @param {Object} box The box containing information about the width and height + * of this docked item + * @param {Number} index The index position of this docked item + * @return {Object} The adjusted box + */ + adjustSizedBox : function(box, index) { + var bodyBox = this.info.bodyBox, + frameSize = this.frameSize, + info = this.info, + padding = info.padding, + pos = box.type, + border = info.border; + + switch (pos) { + case 'top': + box.y = bodyBox.y; + break; + + case 'left': + box.x = bodyBox.x; + break; + + case 'bottom': + box.y = (bodyBox.y + bodyBox.height) - box.height; + break; + + case 'right': + box.x = (bodyBox.x + bodyBox.width) - box.width; + break; + } + + if (box.ignoreFrame) { + if (pos == 'bottom') { + box.y += (frameSize.bottom + padding.bottom + border.bottom); + } + else { + box.y -= (frameSize.top + padding.top + border.top); + } + if (pos == 'right') { + box.x += (frameSize.right + padding.right + border.right); + } + else { + box.x -= (frameSize.left + padding.left + border.left); + } + } + + // If this is not an overlaying docked item, we have to adjust the body box + if (!box.overlay) { + switch (pos) { + case 'top': + bodyBox.y += box.height; + bodyBox.height -= box.height; + break; + + case 'left': + bodyBox.x += box.width; + bodyBox.width -= box.width; + break; + + case 'bottom': + bodyBox.height -= box.height; + break; + + case 'right': + bodyBox.width -= box.width; + break; + } + } + return box; + }, + + /** + * @protected + * This method will adjust the position of the docked item inside an AutoContainerLayout + * and adjust the body box accordingly. + * @param {Object} box The box containing information about the width and height + * of this docked item + * @param {Number} index The index position of this docked item + * @return {Object} The adjusted box + */ + adjustAutoBox : function (box, index) { + var info = this.info, + bodyBox = info.bodyBox, + size = info.size, + boxes = info.boxes, + boxesLn = boxes.length, + pos = box.type, + frameSize = this.frameSize, + padding = info.padding, + border = info.border, + autoSizedCtLayout = info.autoSizedCtLayout, + ln = (boxesLn < index) ? boxesLn : index, + i, adjustBox; + + if (pos == 'top' || pos == 'bottom') { + // This can affect the previously set left and right and bottom docked items + for (i = 0; i < ln; i++) { + adjustBox = boxes[i]; + if (adjustBox.stretched && adjustBox.type == 'left' || adjustBox.type == 'right') { + adjustBox.height += box.height; + } + else if (adjustBox.type == 'bottom') { + adjustBox.y += box.height; + } + } + } + + switch (pos) { + case 'top': + box.y = bodyBox.y; + if (!box.overlay) { + bodyBox.y += box.height; + } + size.height += box.height; + break; + + case 'bottom': + box.y = (bodyBox.y + bodyBox.height); + size.height += box.height; + break; + + case 'left': + box.x = bodyBox.x; + if (!box.overlay) { + bodyBox.x += box.width; + if (autoSizedCtLayout) { + size.width += box.width; + } else { + bodyBox.width -= box.width; + } + } + break; + + case 'right': + if (!box.overlay) { + if (autoSizedCtLayout) { + size.width += box.width; + } else { + bodyBox.width -= box.width; + } + } + box.x = (bodyBox.x + bodyBox.width); + break; + } + + if (box.ignoreFrame) { + if (pos == 'bottom') { + box.y += (frameSize.bottom + padding.bottom + border.bottom); + } + else { + box.y -= (frameSize.top + padding.top + border.top); + } + if (pos == 'right') { + box.x += (frameSize.right + padding.right + border.right); + } + else { + box.x -= (frameSize.left + padding.left + border.left); + } + } + return box; + }, + + /** + * @protected + * This method will create a box object, with a reference to the item, the type of dock + * (top, left, bottom, right). It will also take care of stretching and aligning of the + * docked items. + * @param {Ext.Component} item The docked item we want to initialize the box for + * @return {Object} The initial box containing width and height and other useful information + */ + initBox : function(item) { + var me = this, + bodyBox = me.info.bodyBox, + horizontal = (item.dock == 'top' || item.dock == 'bottom'), + owner = me.owner, + frameSize = me.frameSize, + info = me.info, + padding = info.padding, + border = info.border, + box = { + item: item, + overlay: item.overlay, + type: item.dock, + offsets: Ext.core.Element.parseBox(item.offsets || {}), + ignoreFrame: item.ignoreParentFrame + }; + // First we are going to take care of stretch and align properties for all four dock scenarios. + if (item.stretch !== false) { + box.stretched = true; + if (horizontal) { + box.x = bodyBox.x + box.offsets.left; + box.width = bodyBox.width - (box.offsets.left + box.offsets.right); + if (box.ignoreFrame) { + box.width += (frameSize.left + frameSize.right + border.left + border.right + padding.left + padding.right); + } + item.setCalculatedSize(box.width - item.el.getMargin('lr'), undefined, owner); + } + else { + box.y = bodyBox.y + box.offsets.top; + box.height = bodyBox.height - (box.offsets.bottom + box.offsets.top); + if (box.ignoreFrame) { + box.height += (frameSize.top + frameSize.bottom + border.top + border.bottom + padding.top + padding.bottom); + } + item.setCalculatedSize(undefined, box.height - item.el.getMargin('tb'), owner); + + // At this point IE will report the left/right-docked toolbar as having a width equal to the + // container's full width. Forcing a repaint kicks it into shape so it reports the correct width. + if (!Ext.supports.ComputedStyle) { + item.el.repaint(); + } + } + } + else { + item.doComponentLayout(); + box.width = item.getWidth() - (box.offsets.left + box.offsets.right); + box.height = item.getHeight() - (box.offsets.bottom + box.offsets.top); + box.y += box.offsets.top; + if (horizontal) { + box.x = (item.align == 'right') ? bodyBox.width - box.width : bodyBox.x; + box.x += box.offsets.left; + } + } + + // If we haven't calculated the width or height of the docked item yet + // do so, since we need this for our upcoming calculations + if (box.width == undefined) { + box.width = item.getWidth() + item.el.getMargin('lr'); + } + if (box.height == undefined) { + box.height = item.getHeight() + item.el.getMargin('tb'); + } + + return box; + }, + + /** + * @protected + * Returns an array containing all the visible docked items inside this layout's owner Panel + * @return {Array} An array containing all the visible docked items of the Panel + */ + getLayoutItems : function() { + var it = this.owner.getDockedItems(), + ln = it.length, + i = 0, + result = []; + for (; i < ln; i++) { + if (it[i].isVisible(true)) { + result.push(it[i]); + } + } + return result; + }, + + /** + * @protected + * Render the top and left docked items before any existing DOM nodes in our render target, + * and then render the right and bottom docked items after. This is important, for such things + * as tab stops and ARIA readers, that the DOM nodes are in a meaningful order. + * Our collection of docked items will already be ordered via Panel.getDockedItems(). + */ + renderItems: function(items, target) { + var cns = target.dom.childNodes, + cnsLn = cns.length, + ln = items.length, + domLn = 0, + i, j, cn, item; + + // Calculate the number of DOM nodes in our target that are not our docked items + for (i = 0; i < cnsLn; i++) { + cn = Ext.get(cns[i]); + for (j = 0; j < ln; j++) { + item = items[j]; + if (item.rendered && (cn.id == item.el.id || cn.down('#' + item.el.id))) { + break; + } + } + + if (j === ln) { + domLn++; + } + } + + // Now we go through our docked items and render/move them + for (i = 0, j = 0; i < ln; i++, j++) { + item = items[i]; + + // If we're now at the right/bottom docked item, we jump ahead in our + // DOM position, just past the existing DOM nodes. + // + // TODO: This is affected if users provide custom weight values to their + // docked items, which puts it out of (t,l,r,b) order. Avoiding a second + // sort operation here, for now, in the name of performance. getDockedItems() + // needs the sort operation not just for this layout-time rendering, but + // also for getRefItems() to return a logical ordering (FocusManager, CQ, et al). + if (i === j && (item.dock === 'right' || item.dock === 'bottom')) { + j += domLn; + } + + // Same logic as Layout.renderItems() + if (item && !item.rendered) { + this.renderItem(item, target, j); + } + else if (!this.isValidParent(item, target, j)) { + this.moveItem(item, target, j); + } + } + }, + + /** + * @protected + * This function will be called by the dockItems method. Since the body is positioned absolute, + * we need to give it dimensions and a position so that it is in the middle surrounded by + * docked items + * @param {Object} box An object containing new x, y, width and height values for the + * Panel's body + */ + setBodyBox : function(box) { + var me = this, + owner = me.owner, + body = owner.body, + info = me.info, + bodyMargin = info.bodyMargin, + padding = info.padding, + border = info.border, + frameSize = me.frameSize; + + // Panel collapse effectively hides the Panel's body, so this is a no-op. + if (owner.collapsed) { + return; + } + + if (Ext.isNumber(box.width)) { + box.width -= bodyMargin.left + bodyMargin.right; + } + + if (Ext.isNumber(box.height)) { + box.height -= bodyMargin.top + bodyMargin.bottom; + } + + me.setElementSize(body, box.width, box.height); + if (Ext.isNumber(box.x)) { + body.setLeft(box.x - padding.left - frameSize.left); + } + if (Ext.isNumber(box.y)) { + body.setTop(box.y - padding.top - frameSize.top); + } + }, + + /** + * @protected + * We are overriding the Ext.layout.Layout configureItem method to also add a class that + * indicates the position of the docked item. We use the itemCls (x-docked) as a prefix. + * An example of a class added to a dock: right item is x-docked-right + * @param {Ext.Component} item The item we are configuring + */ + configureItem : function(item, pos) { + this.callParent(arguments); + + item.addCls(Ext.baseCSSPrefix + 'docked'); + item.addClsWithUI('docked-' + item.dock); + }, + + afterRemove : function(item) { + this.callParent(arguments); + if (this.itemCls) { + item.el.removeCls(this.itemCls + '-' + item.dock); + } + var dom = item.el.dom; + + if (!item.destroying && dom) { + dom.parentNode.removeChild(dom); + } + this.childrenChanged = true; + } +}); +/** + * @class Ext.app.EventBus + * @private + * + * Class documentation for the MVC classes will be present before 4.0 final, in the mean time please refer to the MVC + * guide + */ +Ext.define('Ext.app.EventBus', { + requires: [ + 'Ext.util.Event' + ], + mixins: { + observable: 'Ext.util.Observable' + }, + + constructor: function() { + this.mixins.observable.constructor.call(this); + + this.bus = {}; + + var me = this; + Ext.override(Ext.Component, { + fireEvent: function(ev) { + if (Ext.util.Observable.prototype.fireEvent.apply(this, arguments) !== false) { + return me.dispatch.call(me, ev, this, arguments); + } + return false; + } + }); + }, + + dispatch: function(ev, target, args) { + var bus = this.bus, + selectors = bus[ev], + selector, controllers, id, events, event, i, ln; + + if (selectors) { + // Loop over all the selectors that are bound to this event + for (selector in selectors) { + // Check if the target matches the selector + if (target.is(selector)) { + // Loop over all the controllers that are bound to this selector + controllers = selectors[selector]; + for (id in controllers) { + // Loop over all the events that are bound to this selector on this controller + events = controllers[id]; + for (i = 0, ln = events.length; i < ln; i++) { + event = events[i]; + // Fire the event! + return event.fire.apply(event, Array.prototype.slice.call(args, 1)); + } + } + } + } + } + }, + + control: function(selectors, listeners, controller) { + var bus = this.bus, + selector, fn; + + if (Ext.isString(selectors)) { + selector = selectors; + selectors = {}; + selectors[selector] = listeners; + this.control(selectors, null, controller); + return; + } + + Ext.Object.each(selectors, function(selector, listeners) { + Ext.Object.each(listeners, function(ev, listener) { + var options = {}, + scope = controller, + event = Ext.create('Ext.util.Event', controller, ev); + + // Normalize the listener + if (Ext.isObject(listener)) { + options = listener; + listener = options.fn; + scope = options.scope || controller; + delete options.fn; + delete options.scope; + } + + event.addListener(listener, scope, options); + + // Create the bus tree if it is not there yet + bus[ev] = bus[ev] || {}; + bus[ev][selector] = bus[ev][selector] || {}; + bus[ev][selector][controller.id] = bus[ev][selector][controller.id] || []; + + // Push our listener in our bus + bus[ev][selector][controller.id].push(event); + }); + }); + } +}); +/** + * @class Ext.data.Types + *

This is s static class containing the system-supplied data types which may be given to a {@link Ext.data.Field Field}.

+ *

The properties in this class are used as type indicators in the {@link Ext.data.Field Field} class, so to + * test whether a Field is of a certain type, compare the {@link Ext.data.Field#type type} property against properties + * of this class.

+ *

Developers may add their own application-specific data types to this class. Definition names must be UPPERCASE. + * each type definition must contain three properties:

+ *
    + *
  • convert : Function
    A function to convert raw data values from a data block into the data + * to be stored in the Field. The function is passed the collowing parameters: + *
      + *
    • v : Mixed
      The data value as read by the Reader, if undefined will use + * the configured {@link Ext.data.Field#defaultValue defaultValue}.
    • + *
    • rec : Mixed
      The data object containing the row as read by the Reader. + * Depending on the Reader type, this could be an Array ({@link Ext.data.reader.Array ArrayReader}), an object + * ({@link Ext.data.reader.Json JsonReader}), or an XML element.
    • + *
  • + *
  • sortType : Function
    A function to convert the stored data into comparable form, as defined by {@link Ext.data.SortTypes}.
  • + *
  • type : String
    A textual data type name.
  • + *
+ *

For example, to create a VELatLong field (See the Microsoft Bing Mapping API) containing the latitude/longitude value of a datapoint on a map from a JsonReader data block + * which contained the properties lat and long, you would define a new data type like this:

+ *

+// Add a new Field data type which stores a VELatLong object in the Record.
+Ext.data.Types.VELATLONG = {
+    convert: function(v, data) {
+        return new VELatLong(data.lat, data.long);
+    },
+    sortType: function(v) {
+        return v.Latitude;  // When sorting, order by latitude
+    },
+    type: 'VELatLong'
+};
+
+ *

Then, when declaring a Model, use


+var types = Ext.data.Types; // allow shorthand type access
+Ext.define('Unit',
+    extend: 'Ext.data.Model', 
+    fields: [
+        { name: 'unitName', mapping: 'UnitName' },
+        { name: 'curSpeed', mapping: 'CurSpeed', type: types.INT },
+        { name: 'latitude', mapping: 'lat', type: types.FLOAT },
+        { name: 'latitude', mapping: 'lat', type: types.FLOAT },
+        { name: 'position', type: types.VELATLONG }
+    ]
+});
+
+ * @singleton + */ +Ext.define('Ext.data.Types', { + singleton: true, + requires: ['Ext.data.SortTypes'] +}, function() { + var st = Ext.data.SortTypes; + + Ext.apply(Ext.data.Types, { + /** + * @type Regexp + * @property stripRe + * A regular expression for stripping non-numeric characters from a numeric value. Defaults to /[\$,%]/g. + * This should be overridden for localization. + */ + stripRe: /[\$,%]/g, + + /** + * @type Object. + * @property AUTO + * This data type means that no conversion is applied to the raw data before it is placed into a Record. + */ + AUTO: { + convert: function(v) { + return v; + }, + sortType: st.none, + type: 'auto' + }, + + /** + * @type Object. + * @property STRING + * This data type means that the raw data is converted into a String before it is placed into a Record. + */ + STRING: { + convert: function(v) { + var defaultValue = this.useNull ? null : ''; + return (v === undefined || v === null) ? defaultValue : String(v); + }, + sortType: st.asUCString, + type: 'string' + }, + + /** + * @type Object. + * @property INT + * This data type means that the raw data is converted into an integer before it is placed into a Record. + *

The synonym INTEGER is equivalent.

+ */ + INT: { + convert: function(v) { + return v !== undefined && v !== null && v !== '' ? + parseInt(String(v).replace(Ext.data.Types.stripRe, ''), 10) : (this.useNull ? null : 0); + }, + sortType: st.none, + type: 'int' + }, + + /** + * @type Object. + * @property FLOAT + * This data type means that the raw data is converted into a number before it is placed into a Record. + *

The synonym NUMBER is equivalent.

+ */ + FLOAT: { + convert: function(v) { + return v !== undefined && v !== null && v !== '' ? + parseFloat(String(v).replace(Ext.data.Types.stripRe, ''), 10) : (this.useNull ? null : 0); + }, + sortType: st.none, + type: 'float' + }, + + /** + * @type Object. + * @property BOOL + *

This data type means that the raw data is converted into a boolean before it is placed into + * a Record. The string "true" and the number 1 are converted to boolean true.

+ *

The synonym BOOLEAN is equivalent.

+ */ + BOOL: { + convert: function(v) { + if (this.useNull && v === undefined || v === null || v === '') { + return null; + } + return v === true || v === 'true' || v == 1; + }, + sortType: st.none, + type: 'bool' + }, + + /** + * @type Object. + * @property DATE + * This data type means that the raw data is converted into a Date before it is placed into a Record. + * The date format is specified in the constructor of the {@link Ext.data.Field} to which this type is + * being applied. + */ + DATE: { + convert: function(v) { + var df = this.dateFormat; + if (!v) { + return null; + } + if (Ext.isDate(v)) { + return v; + } + if (df) { + if (df == 'timestamp') { + return new Date(v*1000); + } + if (df == 'time') { + return new Date(parseInt(v, 10)); + } + return Ext.Date.parse(v, df); + } + + var parsed = Date.parse(v); + return parsed ? new Date(parsed) : null; + }, + sortType: st.asDate, + type: 'date' + } + }); + + Ext.apply(Ext.data.Types, { + /** + * @type Object. + * @property BOOLEAN + *

This data type means that the raw data is converted into a boolean before it is placed into + * a Record. The string "true" and the number 1 are converted to boolean true.

+ *

The synonym BOOL is equivalent.

+ */ + BOOLEAN: this.BOOL, + + /** + * @type Object. + * @property INTEGER + * This data type means that the raw data is converted into an integer before it is placed into a Record. + *

The synonym INT is equivalent.

+ */ + INTEGER: this.INT, + + /** + * @type Object. + * @property NUMBER + * This data type means that the raw data is converted into a number before it is placed into a Record. + *

The synonym FLOAT is equivalent.

+ */ + NUMBER: this.FLOAT + }); +}); + +/** + * @author Ed Spencer + * @class Ext.data.Field + * @extends Object + * + *

Fields are used to define what a Model is. They aren't instantiated directly - instead, when we create a class + * that extends {@link Ext.data.Model}, it will automatically create a Field instance for each field configured in a + * {@link Ext.data.Model Model}. For example, we might set up a model like this:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        'name', 'email',
+        {name: 'age', type: 'int'},
+        {name: 'gender', type: 'string', defaultValue: 'Unknown'}
+    ]
+});
+
+ * + *

Four fields will have been created for the User Model - name, email, age and gender. Note that we specified a + * couple of different formats here; if we only pass in the string name of the field (as with name and email), the + * field is set up with the 'auto' type. It's as if we'd done this instead:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'name', type: 'auto'},
+        {name: 'email', type: 'auto'},
+        {name: 'age', type: 'int'},
+        {name: 'gender', type: 'string', defaultValue: 'Unknown'}
+    ]
+});
+
+ * + *

Types and conversion

+ * + *

The {@link #type} is important - it's used to automatically convert data passed to the field into the correct + * format. In our example above, the name and email fields used the 'auto' type and will just accept anything that is + * passed into them. The 'age' field had an 'int' type however, so if we passed 25.4 this would be rounded to 25.

+ * + *

Sometimes a simple type isn't enough, or we want to perform some processing when we load a Field's data. We can + * do this using a {@link #convert} function. Here, we're going to create a new field based on another:

+ * +
+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        'name', 'email',
+        {name: 'age', type: 'int'},
+        {name: 'gender', type: 'string', defaultValue: 'Unknown'},
+
+        {
+            name: 'firstName',
+            convert: function(value, record) {
+                var fullName  = record.get('name'),
+                    splits    = fullName.split(" "),
+                    firstName = splits[0];
+
+                return firstName;
+            }
+        }
+    ]
+});
+
+ * + *

Now when we create a new User, the firstName is populated automatically based on the name:

+ * +
+var ed = Ext.ModelManager.create({name: 'Ed Spencer'}, 'User');
+
+console.log(ed.get('firstName')); //logs 'Ed', based on our convert function
+
+ * + *

In fact, if we log out all of the data inside ed, we'll see this:

+ * +
+console.log(ed.data);
+
+//outputs this:
+{
+    age: 0,
+    email: "",
+    firstName: "Ed",
+    gender: "Unknown",
+    name: "Ed Spencer"
+}
+
+ * + *

The age field has been given a default of zero because we made it an int type. As an auto field, email has + * defaulted to an empty string. When we registered the User model we set gender's {@link #defaultValue} to 'Unknown' + * so we see that now. Let's correct that and satisfy ourselves that the types work as we expect:

+ * +
+ed.set('gender', 'Male');
+ed.get('gender'); //returns 'Male'
+
+ed.set('age', 25.4);
+ed.get('age'); //returns 25 - we wanted an int, not a float, so no decimal places allowed
+
+ * + */ +Ext.define('Ext.data.Field', { + requires: ['Ext.data.Types', 'Ext.data.SortTypes'], + alias: 'data.field', + + constructor : function(config) { + if (Ext.isString(config)) { + config = {name: config}; + } + Ext.apply(this, config); + + var types = Ext.data.Types, + st = this.sortType, + t; + + if (this.type) { + if (Ext.isString(this.type)) { + this.type = types[this.type.toUpperCase()] || types.AUTO; + } + } else { + this.type = types.AUTO; + } + + // named sortTypes are supported, here we look them up + if (Ext.isString(st)) { + this.sortType = Ext.data.SortTypes[st]; + } else if(Ext.isEmpty(st)) { + this.sortType = this.type.sortType; + } + + if (!this.convert) { + this.convert = this.type.convert; + } + }, + + /** + * @cfg {String} name + * The name by which the field is referenced within the Model. This is referenced by, for example, + * the dataIndex property in column definition objects passed to {@link Ext.grid.property.HeaderContainer}. + *

Note: In the simplest case, if no properties other than name are required, a field + * definition may consist of just a String for the field name.

+ */ + + /** + * @cfg {Mixed} type + * (Optional) The data type for automatic conversion from received data to the stored value if {@link Ext.data.Field#convert convert} + * has not been specified. This may be specified as a string value. Possible values are + *
    + *
  • auto (Default, implies no conversion)
  • + *
  • string
  • + *
  • int
  • + *
  • float
  • + *
  • boolean
  • + *
  • date
+ *

This may also be specified by referencing a member of the {@link Ext.data.Types} class.

+ *

Developers may create their own application-specific data types by defining new members of the + * {@link Ext.data.Types} class.

+ */ + + /** + * @cfg {Function} convert + * (Optional) A function which converts the value provided by the Reader into an object that will be stored + * in the Model. It is passed the following parameters:
    + *
  • v : Mixed
    The data value as read by the Reader, if undefined will use + * the configured {@link Ext.data.Field#defaultValue defaultValue}.
  • + *
  • rec : Ext.data.Model
    The data object containing the Model as read so far by the + * Reader. Note that the Model may not be fully populated at this point as the fields are read in the order that + * they are defined in your {@link #fields} array.
  • + *
+ *

+// example of convert function
+function fullName(v, record){
+    return record.name.last + ', ' + record.name.first;
+}
+
+function location(v, record){
+    return !record.city ? '' : (record.city + ', ' + record.state);
+}
+
+Ext.define('Dude', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'fullname',  convert: fullName},
+        {name: 'firstname', mapping: 'name.first'},
+        {name: 'lastname',  mapping: 'name.last'},
+        {name: 'city', defaultValue: 'homeless'},
+        'state',
+        {name: 'location',  convert: location}
+    ]
+});
+
+// create the data store
+var store = new Ext.data.Store({
+    reader: {
+        type: 'json',
+        model: 'Dude',
+        idProperty: 'key',
+        root: 'daRoot',
+        totalProperty: 'total'
+    }
+});
+
+var myData = [
+    { key: 1,
+      name: { first: 'Fat',    last:  'Albert' }
+      // notice no city, state provided in data object
+    },
+    { key: 2,
+      name: { first: 'Barney', last:  'Rubble' },
+      city: 'Bedrock', state: 'Stoneridge'
+    },
+    { key: 3,
+      name: { first: 'Cliff',  last:  'Claven' },
+      city: 'Boston',  state: 'MA'
+    }
+];
+     * 
+ */ + /** + * @cfg {String} dateFormat + *

(Optional) Used when converting received data into a Date when the {@link #type} is specified as "date".

+ *

A format string for the {@link Ext.Date#parse Ext.Date.parse} function, or "timestamp" if the + * value provided by the Reader is a UNIX timestamp, or "time" if the value provided by the Reader is a + * javascript millisecond timestamp. See {@link Date}

+ */ + dateFormat: null, + + /** + * @cfg {Boolean} useNull + *

(Optional) Use when converting received data into a Number type (either int or float). If the value cannot be parsed, + * null will be used if useNull is true, otherwise the value will be 0. Defaults to false + */ + useNull: false, + + /** + * @cfg {Mixed} defaultValue + * (Optional) The default value used when a Model is being created by a {@link Ext.data.reader.Reader Reader} + * when the item referenced by the {@link Ext.data.Field#mapping mapping} does not exist in the data + * object (i.e. undefined). (defaults to "") + */ + defaultValue: "", + /** + * @cfg {String/Number} mapping + *

(Optional) A path expression for use by the {@link Ext.data.reader.Reader} implementation + * that is creating the {@link Ext.data.Model Model} to extract the Field value from the data object. + * If the path expression is the same as the field name, the mapping may be omitted.

+ *

The form of the mapping expression depends on the Reader being used.

+ *
    + *
  • {@link Ext.data.reader.Json}
    The mapping is a string containing the javascript + * expression to reference the data from an element of the data item's {@link Ext.data.reader.Json#root root} Array. Defaults to the field name.
  • + *
  • {@link Ext.data.reader.Xml}
    The mapping is an {@link Ext.DomQuery} path to the data + * item relative to the DOM element that represents the {@link Ext.data.reader.Xml#record record}. Defaults to the field name.
  • + *
  • {@link Ext.data.reader.Array}
    The mapping is a number indicating the Array index + * of the field's value. Defaults to the field specification's Array position.
  • + *
+ *

If a more complex value extraction strategy is required, then configure the Field with a {@link #convert} + * function. This is passed the whole row object, and may interrogate it in whatever way is necessary in order to + * return the desired data.

+ */ + mapping: null, + /** + * @cfg {Function} sortType + * (Optional) A function which converts a Field's value to a comparable value in order to ensure + * correct sort ordering. Predefined functions are provided in {@link Ext.data.SortTypes}. A custom + * sort example:

+// current sort     after sort we want
+// +-+------+          +-+------+
+// |1|First |          |1|First |
+// |2|Last  |          |3|Second|
+// |3|Second|          |2|Last  |
+// +-+------+          +-+------+
+
+sortType: function(value) {
+   switch (value.toLowerCase()) // native toLowerCase():
+   {
+      case 'first': return 1;
+      case 'second': return 2;
+      default: return 3;
+   }
+}
+     * 
+ */ + sortType : null, + /** + * @cfg {String} sortDir + * (Optional) Initial direction to sort ("ASC" or "DESC"). Defaults to + * "ASC". + */ + sortDir : "ASC", + /** + * @cfg {Boolean} allowBlank + * @private + * (Optional) Used for validating a {@link Ext.data.Model model}, defaults to true. + * An empty value here will cause {@link Ext.data.Model}.{@link Ext.data.Model#isValid isValid} + * to evaluate to false. + */ + allowBlank : true, + + /** + * @cfg {Boolean} persist + * False to exclude this field from the {@link Ext.data.Model#modified} fields in a model. This + * will also exclude the field from being written using a {@link Ext.data.writer.Writer}. This option + * is useful when model fields are used to keep state on the client but do not need to be persisted + * to the server. Defaults to true. + */ + persist: true +}); + +/** + * @author Ed Spencer + * @class Ext.data.reader.Reader + * @extends Object + * + *

Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance or a {@link Ext.data.Store Store} + * - usually in response to an AJAX request. This is normally handled transparently by passing some configuration to either the + * {@link Ext.data.Model Model} or the {@link Ext.data.Store Store} in question - see their documentation for further details.

+ * + *

Loading Nested Data

+ * + *

Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.Association associations} + * configured on each Model. Below is an example demonstrating the flexibility of these associations in a fictional CRM system which + * manages a User, their Orders, OrderItems and Products. First we'll define the models: + * +


+Ext.define("User", {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'name'
+    ],
+
+    hasMany: {model: 'Order', name: 'orders'},
+
+    proxy: {
+        type: 'rest',
+        url : 'users.json',
+        reader: {
+            type: 'json',
+            root: 'users'
+        }
+    }
+});
+
+Ext.define("Order", {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'total'
+    ],
+
+    hasMany  : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
+    belongsTo: 'User'
+});
+
+Ext.define("OrderItem", {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'price', 'quantity', 'order_id', 'product_id'
+    ],
+
+    belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
+});
+
+Ext.define("Product", {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'name'
+    ],
+
+    hasMany: 'OrderItem'
+});
+
+ * + *

This may be a lot to take in - basically a User has many Orders, each of which is composed of several OrderItems. Finally, + * each OrderItem has a single Product. This allows us to consume data like this:

+ * +

+{
+    "users": [
+        {
+            "id": 123,
+            "name": "Ed",
+            "orders": [
+                {
+                    "id": 50,
+                    "total": 100,
+                    "order_items": [
+                        {
+                            "id"      : 20,
+                            "price"   : 40,
+                            "quantity": 2,
+                            "product" : {
+                                "id": 1000,
+                                "name": "MacBook Pro"
+                            }
+                        },
+                        {
+                            "id"      : 21,
+                            "price"   : 20,
+                            "quantity": 3,
+                            "product" : {
+                                "id": 1001,
+                                "name": "iPhone"
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    ]
+}
+
+ * + *

The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's sake), all of the Orders + * for each User (again just 1 in this case), all of the OrderItems for each Order (2 order items in this case), and finally + * the Product associated with each OrderItem. Now we can read the data and use it as follows: + * +


+var store = new Ext.data.Store({
+    model: "User"
+});
+
+store.load({
+    callback: function() {
+        //the user that was loaded
+        var user = store.first();
+
+        console.log("Orders for " + user.get('name') + ":")
+
+        //iterate over the Orders for each User
+        user.orders().each(function(order) {
+            console.log("Order ID: " + order.getId() + ", which contains items:");
+
+            //iterate over the OrderItems for each Order
+            order.orderItems().each(function(orderItem) {
+                //we know that the Product data is already loaded, so we can use the synchronous getProduct
+                //usually, we would use the asynchronous version (see {@link Ext.data.BelongsToAssociation})
+                var product = orderItem.getProduct();
+
+                console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
+            });
+        });
+    }
+});
+
+ * + *

Running the code above results in the following:

+ * +

+Orders for Ed:
+Order ID: 50, which contains items:
+2 orders of MacBook Pro
+3 orders of iPhone
+
+ * + * @constructor + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.reader.Reader', { + requires: ['Ext.data.ResultSet'], + alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'], + + /** + * @cfg {String} idProperty Name of the property within a row object + * that contains a record identifier value. Defaults to The id of the model. + * If an idProperty is explicitly specified it will override that of the one specified + * on the model + */ + + /** + * @cfg {String} totalProperty Name of the property from which to + * retrieve the total number of records in the dataset. This is only needed + * if the whole dataset is not passed in one go, but is being paged from + * the remote server. Defaults to total. + */ + totalProperty: 'total', + + /** + * @cfg {String} successProperty Name of the property from which to + * retrieve the success attribute. Defaults to success. See + * {@link Ext.data.proxy.Proxy}.{@link Ext.data.proxy.Proxy#exception exception} + * for additional information. + */ + successProperty: 'success', + + /** + * @cfg {String} root Required. The name of the property + * which contains the Array of row objects. Defaults to undefined. + * An exception will be thrown if the root property is undefined. The data + * packet value for this property should be an empty array to clear the data + * or show no data. + */ + root: '', + + /** + * @cfg {String} messageProperty The name of the property which contains a response message. + * This property is optional. + */ + + /** + * @cfg {Boolean} implicitIncludes True to automatically parse models nested within other models in a response + * object. See the Ext.data.reader.Reader intro docs for full explanation. Defaults to true. + */ + implicitIncludes: true, + + isReader: true, + + constructor: function(config) { + var me = this; + + Ext.apply(me, config || {}); + me.fieldCount = 0; + me.model = Ext.ModelManager.getModel(config.model); + if (me.model) { + me.buildExtractors(); + } + }, + + /** + * Sets a new model for the reader. + * @private + * @param {Object} model The model to set. + * @param {Boolean} setOnProxy True to also set on the Proxy, if one is configured + */ + setModel: function(model, setOnProxy) { + var me = this; + + me.model = Ext.ModelManager.getModel(model); + me.buildExtractors(true); + + if (setOnProxy && me.proxy) { + me.proxy.setModel(me.model, true); + } + }, + + /** + * Reads the given response object. This method normalizes the different types of response object that may be passed + * to it, before handing off the reading of records to the {@link #readRecords} function. + * @param {Object} response The response object. This may be either an XMLHttpRequest object or a plain JS object + * @return {Ext.data.ResultSet} The parsed ResultSet object + */ + read: function(response) { + var data = response; + + if (response && response.responseText) { + data = this.getResponseData(response); + } + + if (data) { + return this.readRecords(data); + } else { + return this.nullResultSet; + } + }, + + /** + * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to call + * this function before running its own logic and returning the Ext.data.ResultSet instance. For most + * Readers additional processing should not be needed. + * @param {Mixed} data The raw data object + * @return {Ext.data.ResultSet} A ResultSet object + */ + readRecords: function(data) { + var me = this; + + /* + * We check here whether the number of fields has changed since the last read. + * This works around an issue when a Model is used for both a Tree and another + * source, because the tree decorates the model with extra fields and it causes + * issues because the readers aren't notified. + */ + if (me.fieldCount !== me.getFields().length) { + me.buildExtractors(true); + } + + /** + * The raw data object that was last passed to readRecords. Stored for further processing if needed + * @property rawData + * @type Mixed + */ + me.rawData = data; + + data = me.getData(data); + + // If we pass an array as the data, we dont use getRoot on the data. + // Instead the root equals to the data. + var root = Ext.isArray(data) ? data : me.getRoot(data), + success = true, + recordCount = 0, + total, value, records, message; + + if (root) { + total = root.length; + } + + if (me.totalProperty) { + value = parseInt(me.getTotal(data), 10); + if (!isNaN(value)) { + total = value; + } + } + + if (me.successProperty) { + value = me.getSuccess(data); + if (value === false || value === 'false') { + success = false; + } + } + + if (me.messageProperty) { + message = me.getMessage(data); + } + + if (root) { + records = me.extractData(root); + recordCount = records.length; + } else { + recordCount = 0; + records = []; + } + + return Ext.create('Ext.data.ResultSet', { + total : total || recordCount, + count : recordCount, + records: records, + success: success, + message: message + }); + }, + + /** + * Returns extracted, type-cast rows of data. Iterates to call #extractValues for each row + * @param {Object[]/Object} data-root from server response + * @private + */ + extractData : function(root) { + var me = this, + values = [], + records = [], + Model = me.model, + i = 0, + length = root.length, + idProp = me.getIdProperty(), + node, id, record; + + if (!root.length && Ext.isObject(root)) { + root = [root]; + length = 1; + } + + for (; i < length; i++) { + node = root[i]; + values = me.extractValues(node); + id = me.getId(node); + + + record = new Model(values, id); + record.raw = node; + records.push(record); + + if (me.implicitIncludes) { + me.readAssociated(record, node); + } + } + + return records; + }, + + /** + * @private + * Loads a record's associations from the data object. This prepopulates hasMany and belongsTo associations + * on the record provided. + * @param {Ext.data.Model} record The record to load associations for + * @param {Mixed} data The data object + * @return {String} Return value description + */ + readAssociated: function(record, data) { + var associations = record.associations.items, + i = 0, + length = associations.length, + association, associationData, proxy, reader; + + for (; i < length; i++) { + association = associations[i]; + associationData = this.getAssociatedDataRoot(data, association.associationKey || association.name); + + if (associationData) { + reader = association.getReader(); + if (!reader) { + proxy = association.associatedModel.proxy; + // if the associated model has a Reader already, use that, otherwise attempt to create a sensible one + if (proxy) { + reader = proxy.getReader(); + } else { + reader = new this.constructor({ + model: association.associatedName + }); + } + } + association.read(record, reader, associationData); + } + } + }, + + /** + * @private + * Used internally by {@link #readAssociated}. Given a data object (which could be json, xml etc) for a specific + * record, this should return the relevant part of that data for the given association name. This is only really + * needed to support the XML Reader, which has to do a query to get the associated data object + * @param {Mixed} data The raw data object + * @param {String} associationName The name of the association to get data for (uses associationKey if present) + * @return {Mixed} The root + */ + getAssociatedDataRoot: function(data, associationName) { + return data[associationName]; + }, + + getFields: function() { + return this.model.prototype.fields.items; + }, + + /** + * @private + * Given an object representing a single model instance's data, iterates over the model's fields and + * builds an object with the value for each field. + * @param {Object} data The data object to convert + * @return {Object} Data object suitable for use with a model constructor + */ + extractValues: function(data) { + var fields = this.getFields(), + i = 0, + length = fields.length, + output = {}, + field, value; + + for (; i < length; i++) { + field = fields[i]; + value = this.extractorFunctions[i](data); + + output[field.name] = value; + } + + return output; + }, + + /** + * @private + * By default this function just returns what is passed to it. It can be overridden in a subclass + * to return something else. See XmlReader for an example. + * @param {Object} data The data object + * @return {Object} The normalized data object + */ + getData: function(data) { + return data; + }, + + /** + * @private + * This will usually need to be implemented in a subclass. Given a generic data object (the type depends on the type + * of data we are reading), this function should return the object as configured by the Reader's 'root' meta data config. + * See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned. + * @param {Mixed} data The data object + * @return {Mixed} The same data object + */ + getRoot: function(data) { + return data; + }, + + /** + * Takes a raw response object (as passed to this.read) and returns the useful data segment of it. This must be implemented by each subclass + * @param {Object} response The responce object + * @return {Object} The useful data from the response + */ + getResponseData: function(response) { + // + Ext.Error.raise("getResponseData must be implemented in the Ext.data.reader.Reader subclass"); + // + }, + + /** + * @private + * Reconfigures the meta data tied to this Reader + */ + onMetaChange : function(meta) { + var fields = meta.fields, + newModel; + + Ext.apply(this, meta); + + if (fields) { + newModel = Ext.define("Ext.data.reader.Json-Model" + Ext.id(), { + extend: 'Ext.data.Model', + fields: fields + }); + this.setModel(newModel, true); + } else { + this.buildExtractors(true); + } + }, + + /** + * Get the idProperty to use for extracting data + * @private + * @return {String} The id property + */ + getIdProperty: function(){ + var prop = this.idProperty; + if (Ext.isEmpty(prop)) { + prop = this.model.prototype.idProperty; + } + return prop; + }, + + /** + * @private + * This builds optimized functions for retrieving record data and meta data from an object. + * Subclasses may need to implement their own getRoot function. + * @param {Boolean} force True to automatically remove existing extractor functions first (defaults to false) + */ + buildExtractors: function(force) { + var me = this, + idProp = me.getIdProperty(), + totalProp = me.totalProperty, + successProp = me.successProperty, + messageProp = me.messageProperty, + accessor; + + if (force === true) { + delete me.extractorFunctions; + } + + if (me.extractorFunctions) { + return; + } + + //build the extractors for all the meta data + if (totalProp) { + me.getTotal = me.createAccessor(totalProp); + } + + if (successProp) { + me.getSuccess = me.createAccessor(successProp); + } + + if (messageProp) { + me.getMessage = me.createAccessor(messageProp); + } + + if (idProp) { + accessor = me.createAccessor(idProp); + + me.getId = function(record) { + var id = accessor.call(me, record); + return (id === undefined || id === '') ? null : id; + }; + } else { + me.getId = function() { + return null; + }; + } + me.buildFieldExtractors(); + }, + + /** + * @private + */ + buildFieldExtractors: function() { + //now build the extractors for all the fields + var me = this, + fields = me.getFields(), + ln = fields.length, + i = 0, + extractorFunctions = [], + field, map; + + for (; i < ln; i++) { + field = fields[i]; + map = (field.mapping !== undefined && field.mapping !== null) ? field.mapping : field.name; + + extractorFunctions.push(me.createAccessor(map)); + } + me.fieldCount = ln; + + me.extractorFunctions = extractorFunctions; + } +}, function() { + Ext.apply(this, { + // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string) + nullResultSet: Ext.create('Ext.data.ResultSet', { + total : 0, + count : 0, + records: [], + success: true + }) + }); +}); +/** + * @author Ed Spencer + * @class Ext.data.reader.Json + * @extends Ext.data.reader.Reader + * + *

The JSON Reader is used by a Proxy to read a server response that is sent back in JSON format. This usually + * happens as a result of loading a Store - for example we might create something like this:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name', 'email']
+});
+
+var store = new Ext.data.Store({
+    model: 'User',
+    proxy: {
+        type: 'ajax',
+        url : 'users.json',
+        reader: {
+            type: 'json'
+        }
+    }
+});
+
+ * + *

The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're + * not already familiar with them.

+ * + *

We created the simplest type of JSON Reader possible by simply telling our {@link Ext.data.Store Store}'s + * {@link Ext.data.proxy.Proxy Proxy} that we want a JSON Reader. The Store automatically passes the configured model to the + * Store, so it is as if we passed this instead: + * +


+reader: {
+    type : 'json',
+    model: 'User'
+}
+
+ * + *

The reader we set up is ready to read data from our server - at the moment it will accept a response like this:

+ * +

+[
+    {
+        "id": 1,
+        "name": "Ed Spencer",
+        "email": "ed@sencha.com"
+    },
+    {
+        "id": 2,
+        "name": "Abe Elias",
+        "email": "abe@sencha.com"
+    }
+]
+
+ * + *

Reading other JSON formats

+ * + *

If you already have your JSON format defined and it doesn't look quite like what we have above, you can usually + * pass JsonReader a couple of configuration options to make it parse your format. For example, we can use the + * {@link #root} configuration to parse data that comes back like this:

+ * +

+{
+    "users": [
+       {
+           "id": 1,
+           "name": "Ed Spencer",
+           "email": "ed@sencha.com"
+       },
+       {
+           "id": 2,
+           "name": "Abe Elias",
+           "email": "abe@sencha.com"
+       }
+    ]
+}
+
+ * + *

To parse this we just pass in a {@link #root} configuration that matches the 'users' above:

+ * +

+reader: {
+    type: 'json',
+    root: 'users'
+}
+
+ * + *

Sometimes the JSON structure is even more complicated. Document databases like CouchDB often provide metadata + * around each record inside a nested structure like this:

+ * +

+{
+    "total": 122,
+    "offset": 0,
+    "users": [
+        {
+            "id": "ed-spencer-1",
+            "value": 1,
+            "user": {
+                "id": 1,
+                "name": "Ed Spencer",
+                "email": "ed@sencha.com"
+            }
+        }
+    ]
+}
+
+ * + *

In the case above the record data is nested an additional level inside the "users" array as each "user" item has + * additional metadata surrounding it ('id' and 'value' in this case). To parse data out of each "user" item in the + * JSON above we need to specify the {@link #record} configuration like this:

+ * +

+reader: {
+    type  : 'json',
+    root  : 'users',
+    record: 'user'
+}
+
+ * + *

Response metadata

+ * + *

The server can return additional data in its response, such as the {@link #totalProperty total number of records} + * and the {@link #successProperty success status of the response}. These are typically included in the JSON response + * like this:

+ * +

+{
+    "total": 100,
+    "success": true,
+    "users": [
+        {
+            "id": 1,
+            "name": "Ed Spencer",
+            "email": "ed@sencha.com"
+        }
+    ]
+}
+
+ * + *

If these properties are present in the JSON response they can be parsed out by the JsonReader and used by the + * Store that loaded it. We can set up the names of these properties by specifying a final pair of configuration + * options:

+ * +

+reader: {
+    type : 'json',
+    root : 'users',
+    totalProperty  : 'total',
+    successProperty: 'success'
+}
+
+ * + *

These final options are not necessary to make the Reader work, but can be useful when the server needs to report + * an error or if it needs to indicate that there is a lot of data available of which only a subset is currently being + * returned.

+ */ +Ext.define('Ext.data.reader.Json', { + extend: 'Ext.data.reader.Reader', + alternateClassName: 'Ext.data.JsonReader', + alias : 'reader.json', + + root: '', + + /** + * @cfg {String} record The optional location within the JSON response that the record data itself can be found at. + * See the JsonReader intro docs for more details. This is not often needed and defaults to undefined. + */ + + /** + * @cfg {Boolean} useSimpleAccessors True to ensure that field names/mappings are treated as literals when + * reading values. Defalts to false. + * For example, by default, using the mapping "foo.bar.baz" will try and read a property foo from the root, then a property bar + * from foo, then a property baz from bar. Setting the simple accessors to true will read the property with the name + * "foo.bar.baz" direct from the root object. + */ + useSimpleAccessors: false, + + /** + * Reads a JSON object and returns a ResultSet. Uses the internal getTotal and getSuccess extractors to + * retrieve meta data from the response, and extractData to turn the JSON data into model instances. + * @param {Object} data The raw JSON data + * @return {Ext.data.ResultSet} A ResultSet containing model instances and meta data about the results + */ + readRecords: function(data) { + //this has to be before the call to super because we use the meta data in the superclass readRecords + if (data.metaData) { + this.onMetaChange(data.metaData); + } + + /** + * DEPRECATED - will be removed in Ext JS 5.0. This is just a copy of this.rawData - use that instead + * @property jsonData + * @type Mixed + */ + this.jsonData = data; + return this.callParent([data]); + }, + + //inherit docs + getResponseData: function(response) { + try { + var data = Ext.decode(response.responseText); + } + catch (ex) { + Ext.Error.raise({ + response: response, + json: response.responseText, + parseError: ex, + msg: 'Unable to parse the JSON returned by the server: ' + ex.toString() + }); + } + // + if (!data) { + Ext.Error.raise('JSON object not found'); + } + // + + return data; + }, + + //inherit docs + buildExtractors : function() { + var me = this; + + me.callParent(arguments); + + if (me.root) { + me.getRoot = me.createAccessor(me.root); + } else { + me.getRoot = function(root) { + return root; + }; + } + }, + + /** + * @private + * We're just preparing the data for the superclass by pulling out the record objects we want. If a {@link #record} + * was specified we have to pull those out of the larger JSON object, which is most of what this function is doing + * @param {Object} root The JSON root node + * @return {Array} The records + */ + extractData: function(root) { + var recordName = this.record, + data = [], + length, i; + + if (recordName) { + length = root.length; + + for (i = 0; i < length; i++) { + data[i] = root[i][recordName]; + } + } else { + data = root; + } + return this.callParent([data]); + }, + + /** + * @private + * Returns an accessor function for the given property string. Gives support for properties such as the following: + * 'someProperty' + * 'some.property' + * 'some["property"]' + * This is used by buildExtractors to create optimized extractor functions when casting raw data into model instances. + */ + createAccessor: function() { + var re = /[\[\.]/; + + return function(expr) { + if (Ext.isEmpty(expr)) { + return Ext.emptyFn; + } + if (Ext.isFunction(expr)) { + return expr; + } + if (this.useSimpleAccessors !== true) { + var i = String(expr).search(re); + if (i >= 0) { + return Ext.functionFactory('obj', 'return obj' + (i > 0 ? '.' : '') + expr); + } + } + return function(obj) { + return obj[expr]; + }; + }; + }() +}); +/** + * @class Ext.data.writer.Json + * @extends Ext.data.writer.Writer + * @ignore + */ +Ext.define('Ext.data.writer.Json', { + extend: 'Ext.data.writer.Writer', + alternateClassName: 'Ext.data.JsonWriter', + alias: 'writer.json', + + /** + * @cfg {String} root The key under which the records in this Writer will be placed. Defaults to undefined. + * Example generated request, using root: 'records': +

+{'records': [{name: 'my record'}, {name: 'another record'}]}
+
+ */ + root: undefined, + + /** + * @cfg {Boolean} encode True to use Ext.encode() on the data before sending. Defaults to false. + * The encode option should only be set to true when a {@link #root} is defined, because the values will be + * sent as part of the request parameters as opposed to a raw post. The root will be the name of the parameter + * sent to the server. + */ + encode: false, + + /** + * @cfg {Boolean} allowSingle False to ensure that records are always wrapped in an array, even if there is only + * one record being sent. When there is more than one record, they will always be encoded into an array. + * Defaults to true. Example: + *

+// with allowSingle: true
+"root": {
+    "first": "Mark",
+    "last": "Corrigan"
+}
+
+// with allowSingle: false
+"root": [{
+    "first": "Mark",
+    "last": "Corrigan"
+}]
+     * 
+ */ + allowSingle: true, + + //inherit docs + writeRecords: function(request, data) { + var root = this.root; + + if (this.allowSingle && data.length == 1) { + // convert to single object format + data = data[0]; + } + + if (this.encode) { + if (root) { + // sending as a param, need to encode + request.params[root] = Ext.encode(data); + } else { + // + Ext.Error.raise('Must specify a root when using encode'); + // + } + } else { + // send as jsonData + request.jsonData = request.jsonData || {}; + if (root) { + request.jsonData[root] = data; + } else { + request.jsonData = data; + } + } + return request; + } +}); + +/** + * @author Ed Spencer + * @class Ext.data.proxy.Proxy + * + *

Proxies are used by {@link Ext.data.Store Stores} to handle the loading and saving of {@link Ext.data.Model Model} data. + * Usually developers will not need to create or interact with proxies directly.

+ *

Types of Proxy

+ * + *

There are two main types of Proxy - {@link Ext.data.proxy.Client Client} and {@link Ext.data.proxy.Server Server}. The Client proxies + * save their data locally and include the following subclasses:

+ * + *
    + *
  • {@link Ext.data.proxy.LocalStorage LocalStorageProxy} - saves its data to localStorage if the browser supports it
  • + *
  • {@link Ext.data.proxy.SessionStorage SessionStorageProxy} - saves its data to sessionStorage if the browsers supports it
  • + *
  • {@link Ext.data.proxy.Memory MemoryProxy} - holds data in memory only, any data is lost when the page is refreshed
  • + *
+ * + *

The Server proxies save their data by sending requests to some remote server. These proxies include:

+ * + *
    + *
  • {@link Ext.data.proxy.Ajax Ajax} - sends requests to a server on the same domain
  • + *
  • {@link Ext.data.proxy.JsonP JsonP} - uses JSON-P to send requests to a server on a different domain
  • + *
  • {@link Ext.data.proxy.Direct Direct} - uses {@link Ext.direct} to send requests
  • + *
+ * + *

Proxies operate on the principle that all operations performed are either Create, Read, Update or Delete. These four operations + * are mapped to the methods {@link #create}, {@link #read}, {@link #update} and {@link #destroy} respectively. Each Proxy subclass + * implements these functions.

+ * + *

The CRUD methods each expect an {@link Ext.data.Operation Operation} object as the sole argument. The Operation encapsulates + * information about the action the Store wishes to perform, the {@link Ext.data.Model model} instances that are to be modified, etc. + * See the {@link Ext.data.Operation Operation} documentation for more details. Each CRUD method also accepts a callback function to be + * called asynchronously on completion.

+ * + *

Proxies also support batching of Operations via a {@link Ext.data.Batch batch} object, invoked by the {@link #batch} method.

+ * + * @constructor + * Creates the Proxy + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.proxy.Proxy', { + alias: 'proxy.proxy', + alternateClassName: ['Ext.data.DataProxy', 'Ext.data.Proxy'], + requires: [ + 'Ext.data.reader.Json', + 'Ext.data.writer.Json' + ], + uses: [ + 'Ext.data.Batch', + 'Ext.data.Operation', + 'Ext.data.Model' + ], + mixins: { + observable: 'Ext.util.Observable' + }, + + /** + * @cfg {String} batchOrder + * Comma-separated ordering 'create', 'update' and 'destroy' actions when batching. Override this + * to set a different order for the batched CRUD actions to be executed in. Defaults to 'create,update,destroy' + */ + batchOrder: 'create,update,destroy', + + /** + * @cfg {Boolean} batchActions True to batch actions of a particular type when synchronizing the store. + * Defaults to true. + */ + batchActions: true, + + /** + * @cfg {String} defaultReaderType The default registered reader type. Defaults to 'json' + * @private + */ + defaultReaderType: 'json', + + /** + * @cfg {String} defaultWriterType The default registered writer type. Defaults to 'json' + * @private + */ + defaultWriterType: 'json', + + /** + * @cfg {String/Ext.data.Model} model The name of the Model to tie to this Proxy. Can be either the string name of + * the Model, or a reference to the Model constructor. Required. + */ + + isProxy: true, + + constructor: function(config) { + config = config || {}; + + if (config.model === undefined) { + delete config.model; + } + + this.mixins.observable.constructor.call(this, config); + + if (this.model !== undefined && !(this.model instanceof Ext.data.Model)) { + this.setModel(this.model); + } + }, + + /** + * Sets the model associated with this proxy. This will only usually be called by a Store + * @param {String|Ext.data.Model} model The new model. Can be either the model name string, + * or a reference to the model's constructor + * @param {Boolean} setOnStore Sets the new model on the associated Store, if one is present + */ + setModel: function(model, setOnStore) { + this.model = Ext.ModelManager.getModel(model); + + var reader = this.reader, + writer = this.writer; + + this.setReader(reader); + this.setWriter(writer); + + if (setOnStore && this.store) { + this.store.setModel(this.model); + } + }, + + /** + * Returns the model attached to this Proxy + * @return {Ext.data.Model} The model + */ + getModel: function() { + return this.model; + }, + + /** + * Sets the Proxy's Reader by string, config object or Reader instance + * @param {String|Object|Ext.data.reader.Reader} reader The new Reader, which can be either a type string, a configuration object + * or an Ext.data.reader.Reader instance + * @return {Ext.data.reader.Reader} The attached Reader object + */ + setReader: function(reader) { + var me = this; + + if (reader === undefined || typeof reader == 'string') { + reader = { + type: reader + }; + } + + if (reader.isReader) { + reader.setModel(me.model); + } else { + Ext.applyIf(reader, { + proxy: me, + model: me.model, + type : me.defaultReaderType + }); + + reader = Ext.createByAlias('reader.' + reader.type, reader); + } + + me.reader = reader; + return me.reader; + }, + + /** + * Returns the reader currently attached to this proxy instance + * @return {Ext.data.reader.Reader} The Reader instance + */ + getReader: function() { + return this.reader; + }, + + /** + * Sets the Proxy's Writer by string, config object or Writer instance + * @param {String|Object|Ext.data.writer.Writer} writer The new Writer, which can be either a type string, a configuration object + * or an Ext.data.writer.Writer instance + * @return {Ext.data.writer.Writer} The attached Writer object + */ + setWriter: function(writer) { + if (writer === undefined || typeof writer == 'string') { + writer = { + type: writer + }; + } + + if (!(writer instanceof Ext.data.writer.Writer)) { + Ext.applyIf(writer, { + model: this.model, + type : this.defaultWriterType + }); + + writer = Ext.createByAlias('writer.' + writer.type, writer); + } + + this.writer = writer; + + return this.writer; + }, + + /** + * Returns the writer currently attached to this proxy instance + * @return {Ext.data.writer.Writer} The Writer instance + */ + getWriter: function() { + return this.writer; + }, + + /** + * Performs the given create operation. + * @param {Ext.data.Operation} operation The Operation to perform + * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not) + * @param {Object} scope Scope to execute the callback function in + */ + create: Ext.emptyFn, + + /** + * Performs the given read operation. + * @param {Ext.data.Operation} operation The Operation to perform + * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not) + * @param {Object} scope Scope to execute the callback function in + */ + read: Ext.emptyFn, + + /** + * Performs the given update operation. + * @param {Ext.data.Operation} operation The Operation to perform + * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not) + * @param {Object} scope Scope to execute the callback function in + */ + update: Ext.emptyFn, + + /** + * Performs the given destroy operation. + * @param {Ext.data.Operation} operation The Operation to perform + * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not) + * @param {Object} scope Scope to execute the callback function in + */ + destroy: Ext.emptyFn, + + /** + * Performs a batch of {@link Ext.data.Operation Operations}, in the order specified by {@link #batchOrder}. Used internally by + * {@link Ext.data.Store}'s {@link Ext.data.Store#sync sync} method. Example usage: + *

+     * myProxy.batch({
+     *     create : [myModel1, myModel2],
+     *     update : [myModel3],
+     *     destroy: [myModel4, myModel5]
+     * });
+     * 
+ * Where the myModel* above are {@link Ext.data.Model Model} instances - in this case 1 and 2 are new instances and have not been + * saved before, 3 has been saved previously but needs to be updated, and 4 and 5 have already been saved but should now be destroyed. + * @param {Object} operations Object containing the Model instances to act upon, keyed by action name + * @param {Object} listeners Optional listeners object passed straight through to the Batch - see {@link Ext.data.Batch} + * @return {Ext.data.Batch} The newly created Ext.data.Batch object + */ + batch: function(operations, listeners) { + var me = this, + batch = Ext.create('Ext.data.Batch', { + proxy: me, + listeners: listeners || {} + }), + useBatch = me.batchActions, + records; + + Ext.each(me.batchOrder.split(','), function(action) { + records = operations[action]; + if (records) { + if (useBatch) { + batch.add(Ext.create('Ext.data.Operation', { + action: action, + records: records + })); + } else { + Ext.each(records, function(record){ + batch.add(Ext.create('Ext.data.Operation', { + action : action, + records: [record] + })); + }); + } + } + }, me); + + batch.start(); + return batch; + } +}, function() { + // Ext.data.proxy.ProxyMgr.registerType('proxy', this); + + //backwards compatibility + Ext.data.DataProxy = this; + // Ext.deprecate('platform', '2.0', function() { + // Ext.data.DataProxy = this; + // }, this); +}); + +/** + * @author Ed Spencer + * @class Ext.data.proxy.Server + * @extends Ext.data.proxy.Proxy + * + *

ServerProxy is a superclass of {@link Ext.data.proxy.JsonP JsonPProxy} and {@link Ext.data.proxy.Ajax AjaxProxy}, + * and would not usually be used directly.

+ * + *

ServerProxy should ideally be named HttpProxy as it is a superclass for all HTTP proxies - for Ext JS 4.x it has been + * called ServerProxy to enable any 3.x applications that reference the HttpProxy to continue to work (HttpProxy is now an + * alias of AjaxProxy).

+ */ +Ext.define('Ext.data.proxy.Server', { + extend: 'Ext.data.proxy.Proxy', + alias : 'proxy.server', + alternateClassName: 'Ext.data.ServerProxy', + uses : ['Ext.data.Request'], + + /** + * @cfg {String} url The URL from which to request the data object. + */ + + /** + * @cfg {Object/String/Ext.data.reader.Reader} reader The Ext.data.reader.Reader to use to decode the server's response. This can + * either be a Reader instance, a config object or just a valid Reader type name (e.g. 'json', 'xml'). + */ + + /** + * @cfg {Object/String/Ext.data.writer.Writer} writer The Ext.data.writer.Writer to use to encode any request sent to the server. + * This can either be a Writer instance, a config object or just a valid Writer type name (e.g. 'json', 'xml'). + */ + + /** + * @cfg {String} pageParam The name of the 'page' parameter to send in a request. Defaults to 'page'. Set this to + * undefined if you don't want to send a page parameter + */ + pageParam: 'page', + + /** + * @cfg {String} startParam The name of the 'start' parameter to send in a request. Defaults to 'start'. Set this + * to undefined if you don't want to send a start parameter + */ + startParam: 'start', + + /** + * @cfg {String} limitParam The name of the 'limit' parameter to send in a request. Defaults to 'limit'. Set this + * to undefined if you don't want to send a limit parameter + */ + limitParam: 'limit', + + /** + * @cfg {String} groupParam The name of the 'group' parameter to send in a request. Defaults to 'group'. Set this + * to undefined if you don't want to send a group parameter + */ + groupParam: 'group', + + /** + * @cfg {String} sortParam The name of the 'sort' parameter to send in a request. Defaults to 'sort'. Set this + * to undefined if you don't want to send a sort parameter + */ + sortParam: 'sort', + + /** + * @cfg {String} filterParam The name of the 'filter' parameter to send in a request. Defaults to 'filter'. Set + * this to undefined if you don't want to send a filter parameter + */ + filterParam: 'filter', + + /** + * @cfg {String} directionParam The name of the direction parameter to send in a request. This is only used when simpleSortMode is set to true. + * Defaults to 'dir'. + */ + directionParam: 'dir', + + /** + * @cfg {Boolean} simpleSortMode Enabling simpleSortMode in conjunction with remoteSort will only send one sort property and a direction when a remote sort is requested. + * The directionParam and sortParam will be sent with the property name and either 'ASC' or 'DESC' + */ + simpleSortMode: false, + + /** + * @cfg {Boolean} noCache (optional) Defaults to true. Disable caching by adding a unique parameter + * name to the request. + */ + noCache : true, + + /** + * @cfg {String} cacheString The name of the cache param added to the url when using noCache (defaults to "_dc") + */ + cacheString: "_dc", + + /** + * @cfg {Number} timeout (optional) The number of milliseconds to wait for a response. Defaults to 30 seconds. + */ + timeout : 30000, + + /** + * @cfg {Object} api + * Specific urls to call on CRUD action methods "read", "create", "update" and "destroy". + * Defaults to:

+api: {
+    read    : undefined,
+    create  : undefined,
+    update  : undefined,
+    destroy : undefined
+}
+     * 
+ *

The url is built based upon the action being executed [load|create|save|destroy] + * using the commensurate {@link #api} property, or if undefined default to the + * configured {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.


+ *

For example:

+ *

+api: {
+    load :    '/controller/load',
+    create :  '/controller/new',
+    save :    '/controller/update',
+    destroy : '/controller/destroy_action'
+}
+     * 
+ *

If the specific URL for a given CRUD action is undefined, the CRUD action request + * will be directed to the configured {@link Ext.data.proxy.Server#url url}.

+ */ + + /** + * @ignore + */ + constructor: function(config) { + var me = this; + + config = config || {}; + this.addEvents( + /** + * @event exception + * Fires when the server returns an exception + * @param {Ext.data.proxy.Proxy} this + * @param {Object} response The response from the AJAX request + * @param {Ext.data.Operation} operation The operation that triggered request + */ + 'exception' + ); + me.callParent([config]); + + /** + * @cfg {Object} extraParams Extra parameters that will be included on every request. Individual requests with params + * of the same name will override these params when they are in conflict. + */ + me.extraParams = config.extraParams || {}; + + me.api = config.api || {}; + + //backwards compatibility, will be deprecated in 5.0 + me.nocache = me.noCache; + }, + + //in a ServerProxy all four CRUD operations are executed in the same manner, so we delegate to doRequest in each case + create: function() { + return this.doRequest.apply(this, arguments); + }, + + read: function() { + return this.doRequest.apply(this, arguments); + }, + + update: function() { + return this.doRequest.apply(this, arguments); + }, + + destroy: function() { + return this.doRequest.apply(this, arguments); + }, + + /** + * Creates and returns an Ext.data.Request object based on the options passed by the {@link Ext.data.Store Store} + * that this Proxy is attached to. + * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute + * @return {Ext.data.Request} The request object + */ + buildRequest: function(operation) { + var params = Ext.applyIf(operation.params || {}, this.extraParams || {}), + request; + + //copy any sorters, filters etc into the params so they can be sent over the wire + params = Ext.applyIf(params, this.getParams(params, operation)); + + if (operation.id && !params.id) { + params.id = operation.id; + } + + request = Ext.create('Ext.data.Request', { + params : params, + action : operation.action, + records : operation.records, + operation: operation, + url : operation.url + }); + + request.url = this.buildUrl(request); + + /* + * Save the request on the Operation. Operations don't usually care about Request and Response data, but in the + * ServerProxy and any of its subclasses we add both request and response as they may be useful for further processing + */ + operation.request = request; + + return request; + }, + + /** + * + */ + processResponse: function(success, operation, request, response, callback, scope){ + var me = this, + reader, + result, + records, + length, + mc, + record, + i; + + if (success === true) { + reader = me.getReader(); + result = reader.read(me.extractResponseData(response)); + records = result.records; + length = records.length; + + if (result.success !== false) { + mc = Ext.create('Ext.util.MixedCollection', true, function(r) {return r.getId();}); + mc.addAll(operation.records); + for (i = 0; i < length; i++) { + record = mc.get(records[i].getId()); + + if (record) { + record.beginEdit(); + record.set(record.data); + record.endEdit(true); + } + } + + //see comment in buildRequest for why we include the response object here + Ext.apply(operation, { + response: response, + resultSet: result + }); + + operation.setCompleted(); + operation.setSuccessful(); + } else { + operation.setException(result.message); + me.fireEvent('exception', this, response, operation); + } + } else { + me.setException(operation, response); + me.fireEvent('exception', this, response, operation); + } + + //this callback is the one that was passed to the 'read' or 'write' function above + if (typeof callback == 'function') { + callback.call(scope || me, operation); + } + + me.afterRequest(request, success); + }, + + /** + * Sets up an exception on the operation + * @private + * @param {Ext.data.Operation} operation The operation + * @param {Object} response The response + */ + setException: function(operation, response){ + operation.setException({ + status: response.status, + statusText: response.statusText + }); + }, + + /** + * Template method to allow subclasses to specify how to get the response for the reader. + * @private + * @param {Object} response The server response + * @return {Mixed} The response data to be used by the reader + */ + extractResponseData: function(response){ + return response; + }, + + /** + * Encode any values being sent to the server. Can be overridden in subclasses. + * @private + * @param {Array} An array of sorters/filters. + * @return {Mixed} The encoded value + */ + applyEncoding: function(value){ + return Ext.encode(value); + }, + + /** + * Encodes the array of {@link Ext.util.Sorter} objects into a string to be sent in the request url. By default, + * this simply JSON-encodes the sorter data + * @param {Array} sorters The array of {@link Ext.util.Sorter Sorter} objects + * @return {String} The encoded sorters + */ + encodeSorters: function(sorters) { + var min = [], + length = sorters.length, + i = 0; + + for (; i < length; i++) { + min[i] = { + property : sorters[i].property, + direction: sorters[i].direction + }; + } + return this.applyEncoding(min); + + }, + + /** + * Encodes the array of {@link Ext.util.Filter} objects into a string to be sent in the request url. By default, + * this simply JSON-encodes the filter data + * @param {Array} sorters The array of {@link Ext.util.Filter Filter} objects + * @return {String} The encoded filters + */ + encodeFilters: function(filters) { + var min = [], + length = filters.length, + i = 0; + + for (; i < length; i++) { + min[i] = { + property: filters[i].property, + value : filters[i].value + }; + } + return this.applyEncoding(min); + }, + + /** + * @private + * Copy any sorters, filters etc into the params so they can be sent over the wire + */ + getParams: function(params, operation) { + params = params || {}; + + var me = this, + isDef = Ext.isDefined, + groupers = operation.groupers, + sorters = operation.sorters, + filters = operation.filters, + page = operation.page, + start = operation.start, + limit = operation.limit, + + simpleSortMode = me.simpleSortMode, + + pageParam = me.pageParam, + startParam = me.startParam, + limitParam = me.limitParam, + groupParam = me.groupParam, + sortParam = me.sortParam, + filterParam = me.filterParam, + directionParam = me.directionParam; + + if (pageParam && isDef(page)) { + params[pageParam] = page; + } + + if (startParam && isDef(start)) { + params[startParam] = start; + } + + if (limitParam && isDef(limit)) { + params[limitParam] = limit; + } + + if (groupParam && groupers && groupers.length > 0) { + // Grouper is a subclass of sorter, so we can just use the sorter method + params[groupParam] = me.encodeSorters(groupers); + } + + if (sortParam && sorters && sorters.length > 0) { + if (simpleSortMode) { + params[sortParam] = sorters[0].property; + params[directionParam] = sorters[0].direction; + } else { + params[sortParam] = me.encodeSorters(sorters); + } + + } + + if (filterParam && filters && filters.length > 0) { + params[filterParam] = me.encodeFilters(filters); + } + + return params; + }, + + /** + * Generates a url based on a given Ext.data.Request object. By default, ServerProxy's buildUrl will + * add the cache-buster param to the end of the url. Subclasses may need to perform additional modifications + * to the url. + * @param {Ext.data.Request} request The request object + * @return {String} The url + */ + buildUrl: function(request) { + var me = this, + url = me.getUrl(request); + + // + if (!url) { + Ext.Error.raise("You are using a ServerProxy but have not supplied it with a url."); + } + // + + if (me.noCache) { + url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.cacheString, Ext.Date.now())); + } + + return url; + }, + + /** + * Get the url for the request taking into account the order of priority, + * - The request + * - The api + * - The url + * @private + * @param {Ext.data.Request} request The request + * @return {String} The url + */ + getUrl: function(request){ + return request.url || this.api[request.action] || this.url; + }, + + /** + * In ServerProxy subclasses, the {@link #create}, {@link #read}, {@link #update} and {@link #destroy} methods all pass + * through to doRequest. Each ServerProxy subclass must implement the doRequest method - see {@link Ext.data.proxy.JsonP} + * and {@link Ext.data.proxy.Ajax} for examples. This method carries the same signature as each of the methods that delegate to it. + * @param {Ext.data.Operation} operation The Ext.data.Operation object + * @param {Function} callback The callback function to call when the Operation has completed + * @param {Object} scope The scope in which to execute the callback + */ + doRequest: function(operation, callback, scope) { + // + Ext.Error.raise("The doRequest function has not been implemented on your Ext.data.proxy.Server subclass. See src/data/ServerProxy.js for details"); + // + }, + + /** + * Optional callback function which can be used to clean up after a request has been completed. + * @param {Ext.data.Request} request The Request object + * @param {Boolean} success True if the request was successful + */ + afterRequest: Ext.emptyFn, + + onDestroy: function() { + Ext.destroy(this.reader, this.writer); + } +}); + +/** + * @author Ed Spencer + * @class Ext.data.proxy.Ajax + * @extends Ext.data.proxy.Server + * + *

AjaxProxy is one of the most widely-used ways of getting data into your application. It uses AJAX requests to + * load data from the server, usually to be placed into a {@link Ext.data.Store Store}. Let's take a look at a typical + * setup. Here we're going to set up a Store that has an AjaxProxy. To prepare, we'll also set up a + * {@link Ext.data.Model Model}:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name', 'email']
+});
+
+//The Store contains the AjaxProxy as an inline configuration
+var store = new Ext.data.Store({
+    model: 'User',
+    proxy: {
+        type: 'ajax',
+        url : 'users.json'
+    }
+});
+
+store.load();
+
+ * + *

Our example is going to load user data into a Store, so we start off by defining a {@link Ext.data.Model Model} + * with the fields that we expect the server to return. Next we set up the Store itself, along with a {@link #proxy} + * configuration. This configuration was automatically turned into an Ext.data.proxy.Ajax instance, with the url we + * specified being passed into AjaxProxy's constructor. It's as if we'd done this:

+ * +

+new Ext.data.proxy.Ajax({
+    url: 'users.json',
+    model: 'User',
+    reader: 'json'
+});
+
+ * + *

A couple of extra configurations appeared here - {@link #model} and {@link #reader}. These are set by default + * when we create the proxy via the Store - the Store already knows about the Model, and Proxy's default + * {@link Ext.data.reader.Reader Reader} is {@link Ext.data.reader.Json JsonReader}.

+ * + *

Now when we call store.load(), the AjaxProxy springs into action, making a request to the url we configured + * ('users.json' in this case). As we're performing a read, it sends a GET request to that url (see {@link #actionMethods} + * to customize this - by default any kind of read will be sent as a GET request and any kind of write will be sent as a + * POST request).

+ * + *

Limitations

+ * + *

AjaxProxy cannot be used to retrieve data from other domains. If your application is running on http://domainA.com + * it cannot load data from http://domainB.com because browsers have a built-in security policy that prohibits domains + * talking to each other via AJAX.

+ * + *

If you need to read data from another domain and can't set up a proxy server (some software that runs on your own + * domain's web server and transparently forwards requests to http://domainB.com, making it look like they actually came + * from http://domainA.com), you can use {@link Ext.data.proxy.JsonP} and a technique known as JSON-P (JSON with + * Padding), which can help you get around the problem so long as the server on http://domainB.com is set up to support + * JSON-P responses. See {@link Ext.data.proxy.JsonP JsonPProxy}'s introduction docs for more details.

+ * + *

Readers and Writers

+ * + *

AjaxProxy can be configured to use any type of {@link Ext.data.reader.Reader Reader} to decode the server's response. If + * no Reader is supplied, AjaxProxy will default to using a {@link Ext.data.reader.Json JsonReader}. Reader configuration + * can be passed in as a simple object, which the Proxy automatically turns into a {@link Ext.data.reader.Reader Reader} + * instance:

+ * +

+var proxy = new Ext.data.proxy.Ajax({
+    model: 'User',
+    reader: {
+        type: 'xml',
+        root: 'users'
+    }
+});
+
+proxy.getReader(); //returns an {@link Ext.data.reader.Xml XmlReader} instance based on the config we supplied
+
+ * + *

Url generation

+ * + *

AjaxProxy automatically inserts any sorting, filtering, paging and grouping options into the url it generates for + * each request. These are controlled with the following configuration options:

+ * + *
    + *
  • {@link #pageParam} - controls how the page number is sent to the server + * (see also {@link #startParam} and {@link #limitParam})
  • + *
  • {@link #sortParam} - controls how sort information is sent to the server
  • + *
  • {@link #groupParam} - controls how grouping information is sent to the server
  • + *
  • {@link #filterParam} - controls how filter information is sent to the server
  • + *
+ * + *

Each request sent by AjaxProxy is described by an {@link Ext.data.Operation Operation}. To see how we can + * customize the generated urls, let's say we're loading the Proxy with the following Operation:

+ * +

+var operation = new Ext.data.Operation({
+    action: 'read',
+    page  : 2
+});
+
+ * + *

Now we'll issue the request for this Operation by calling {@link #read}:

+ * +

+var proxy = new Ext.data.proxy.Ajax({
+    url: '/users'
+});
+
+proxy.read(operation); //GET /users?page=2
+
+ * + *

Easy enough - the Proxy just copied the page property from the Operation. We can customize how this page data is + * sent to the server:

+ * +

+var proxy = new Ext.data.proxy.Ajax({
+    url: '/users',
+    pagePage: 'pageNumber'
+});
+
+proxy.read(operation); //GET /users?pageNumber=2
+
+ * + *

Alternatively, our Operation could have been configured to send start and limit parameters instead of page:

+ * +

+var operation = new Ext.data.Operation({
+    action: 'read',
+    start : 50,
+    limit : 25
+});
+
+var proxy = new Ext.data.proxy.Ajax({
+    url: '/users'
+});
+
+proxy.read(operation); //GET /users?start=50&limit=25
+
+ * + *

Again we can customize this url:

+ * +

+var proxy = new Ext.data.proxy.Ajax({
+    url: '/users',
+    startParam: 'startIndex',
+    limitParam: 'limitIndex'
+});
+
+proxy.read(operation); //GET /users?startIndex=50&limitIndex=25
+
+ * + *

AjaxProxy will also send sort and filter information to the server. Let's take a look at how this looks with a + * more expressive Operation object:

+ * +

+var operation = new Ext.data.Operation({
+    action: 'read',
+    sorters: [
+        new Ext.util.Sorter({
+            property : 'name',
+            direction: 'ASC'
+        }),
+        new Ext.util.Sorter({
+            property : 'age',
+            direction: 'DESC'
+        })
+    ],
+    filters: [
+        new Ext.util.Filter({
+            property: 'eyeColor',
+            value   : 'brown'
+        })
+    ]
+});
+
+ * + *

This is the type of object that is generated internally when loading a {@link Ext.data.Store Store} with sorters + * and filters defined. By default the AjaxProxy will JSON encode the sorters and filters, resulting in something like + * this (note that the url is escaped before sending the request, but is left unescaped here for clarity):

+ * +

+var proxy = new Ext.data.proxy.Ajax({
+    url: '/users'
+});
+
+proxy.read(operation); //GET /users?sort=[{"property":"name","direction":"ASC"},{"property":"age","direction":"DESC"}]&filter=[{"property":"eyeColor","value":"brown"}]
+
+ * + *

We can again customize how this is created by supplying a few configuration options. Let's say our server is set + * up to receive sorting information is a format like "sortBy=name#ASC,age#DESC". We can configure AjaxProxy to provide + * that format like this:

+ * +

+ var proxy = new Ext.data.proxy.Ajax({
+     url: '/users',
+     sortParam: 'sortBy',
+     filterParam: 'filterBy',
+
+     //our custom implementation of sorter encoding - turns our sorters into "name#ASC,age#DESC"
+     encodeSorters: function(sorters) {
+         var length   = sorters.length,
+             sortStrs = [],
+             sorter, i;
+
+         for (i = 0; i < length; i++) {
+             sorter = sorters[i];
+
+             sortStrs[i] = sorter.property + '#' + sorter.direction
+         }
+
+         return sortStrs.join(",");
+     }
+ });
+
+ proxy.read(operation); //GET /users?sortBy=name#ASC,age#DESC&filterBy=[{"property":"eyeColor","value":"brown"}]
+ 
+ * + *

We can also provide a custom {@link #encodeFilters} function to encode our filters.

+ * + * @constructor + * + *

Note that if this HttpProxy is being used by a {@link Ext.data.Store Store}, then the + * Store's call to {@link #load} will override any specified callback and params + * options. In this case, use the Store's {@link Ext.data.Store#events events} to modify parameters, + * or react to loading events. The Store's {@link Ext.data.Store#baseParams baseParams} may also be + * used to pass parameters known at instantiation time.

+ * + *

If an options parameter is passed, the singleton {@link Ext.Ajax} object will be used to make + * the request.

+ */ +Ext.define('Ext.data.proxy.Ajax', { + requires: ['Ext.util.MixedCollection', 'Ext.Ajax'], + extend: 'Ext.data.proxy.Server', + alias: 'proxy.ajax', + alternateClassName: ['Ext.data.HttpProxy', 'Ext.data.AjaxProxy'], + + /** + * @property actionMethods + * Mapping of action name to HTTP request method. In the basic AjaxProxy these are set to 'GET' for 'read' actions and 'POST' + * for 'create', 'update' and 'destroy' actions. The {@link Ext.data.proxy.Rest} maps these to the correct RESTful methods. + */ + actionMethods: { + create : 'POST', + read : 'GET', + update : 'POST', + destroy: 'POST' + }, + + /** + * @cfg {Object} headers Any headers to add to the Ajax request. Defaults to undefined. + */ + + /** + * @ignore + */ + doRequest: function(operation, callback, scope) { + var writer = this.getWriter(), + request = this.buildRequest(operation, callback, scope); + + if (operation.allowWrite()) { + request = writer.write(request); + } + + Ext.apply(request, { + headers : this.headers, + timeout : this.timeout, + scope : this, + callback : this.createRequestCallback(request, operation, callback, scope), + method : this.getMethod(request), + disableCaching: false // explicitly set it to false, ServerProxy handles caching + }); + + Ext.Ajax.request(request); + + return request; + }, + + /** + * Returns the HTTP method name for a given request. By default this returns based on a lookup on {@link #actionMethods}. + * @param {Ext.data.Request} request The request object + * @return {String} The HTTP method to use (should be one of 'GET', 'POST', 'PUT' or 'DELETE') + */ + getMethod: function(request) { + return this.actionMethods[request.action]; + }, + + /** + * @private + * TODO: This is currently identical to the JsonPProxy version except for the return function's signature. There is a lot + * of code duplication inside the returned function so we need to find a way to DRY this up. + * @param {Ext.data.Request} request The Request object + * @param {Ext.data.Operation} operation The Operation being executed + * @param {Function} callback The callback function to be called when the request completes. This is usually the callback + * passed to doRequest + * @param {Object} scope The scope in which to execute the callback function + * @return {Function} The callback function + */ + createRequestCallback: function(request, operation, callback, scope) { + var me = this; + + return function(options, success, response) { + me.processResponse(success, operation, request, response, callback, scope); + }; + } +}, function() { + //backwards compatibility, remove in Ext JS 5.0 + Ext.data.HttpProxy = this; +}); + +/** + * @author Ed Spencer + * @class Ext.data.Model + * + *

A Model represents some object that your application manages. For example, one might define a Model for Users, Products, + * Cars, or any other real-world object that we want to model in the system. Models are registered via the {@link Ext.ModelManager model manager}, + * and are used by {@link Ext.data.Store stores}, which are in turn used by many of the data-bound components in Ext.

+ * + *

Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'name',  type: 'string'},
+        {name: 'age',   type: 'int'},
+        {name: 'phone', type: 'string'},
+        {name: 'alive', type: 'boolean', defaultValue: true}
+    ],
+
+    changeName: function() {
+        var oldName = this.get('name'),
+            newName = oldName + " The Barbarian";
+
+        this.set('name', newName);
+    }
+});
+
+* +*

The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link Ext.ModelManager ModelManager}, and all +* other functions and properties are copied to the new Model's prototype.

+* +*

Now we can create instances of our User model and call any model logic we defined:

+* +

+var user = Ext.ModelManager.create({
+    name : 'Conan',
+    age  : 24,
+    phone: '555-555-5555'
+}, 'User');
+
+user.changeName();
+user.get('name'); //returns "Conan The Barbarian"
+
+ * + *

Validations

+ * + *

Models have built-in support for validations, which are executed against the validator functions in + * {@link Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to models:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'name',     type: 'string'},
+        {name: 'age',      type: 'int'},
+        {name: 'phone',    type: 'string'},
+        {name: 'gender',   type: 'string'},
+        {name: 'username', type: 'string'},
+        {name: 'alive',    type: 'boolean', defaultValue: true}
+    ],
+
+    validations: [
+        {type: 'presence',  field: 'age'},
+        {type: 'length',    field: 'name',     min: 2},
+        {type: 'inclusion', field: 'gender',   list: ['Male', 'Female']},
+        {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']},
+        {type: 'format',    field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
+    ]
+});
+
+ * + *

The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors} + * object:

+ * +

+var instance = Ext.ModelManager.create({
+    name: 'Ed',
+    gender: 'Male',
+    username: 'edspencer'
+}, 'User');
+
+var errors = instance.validate();
+
+ * + *

Associations

+ * + *

Models can have associations with other Models via {@link Ext.data.BelongsToAssociation belongsTo} and + * {@link Ext.data.HasManyAssociation hasMany} associations. For example, let's say we're writing a blog administration + * application which deals with Users, Posts and Comments. We can express the relationships between these models like this:

+ * +

+Ext.define('Post', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'user_id'],
+
+    belongsTo: 'User',
+    hasMany  : {model: 'Comment', name: 'comments'}
+});
+
+Ext.define('Comment', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'user_id', 'post_id'],
+
+    belongsTo: 'Post'
+});
+
+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: ['id'],
+
+    hasMany: [
+        'Post',
+        {model: 'Comment', name: 'comments'}
+    ]
+});
+
+ * + *

See the docs for {@link Ext.data.BelongsToAssociation} and {@link Ext.data.HasManyAssociation} for details on the usage + * and configuration of associations. Note that associations can also be specified like this:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: ['id'],
+
+    associations: [
+        {type: 'hasMany', model: 'Post',    name: 'posts'},
+        {type: 'hasMany', model: 'Comment', name: 'comments'}
+    ]
+});
+
+ * + *

Using a Proxy

+ * + *

Models are great for representing types of data and relationships, but sooner or later we're going to want to + * load or save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, + * which can be set directly on the Model:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name', 'email'],
+
+    proxy: {
+        type: 'rest',
+        url : '/users'
+    }
+});
+
+ * + *

Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save data to and from a + * RESTful backend. Let's see how this works:

+ * +

+var user = Ext.ModelManager.create({name: 'Ed Spencer', email: 'ed@sencha.com'}, 'User');
+
+user.save(); //POST /users
+
+ * + *

Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this + * Model's data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't + * have an id, and performs the appropriate action - in this case issuing a POST request to the url we configured + * (/users). We configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full + * list.

+ * + *

Loading data via the Proxy is equally easy:

+ * +

+//get a reference to the User model class
+var User = Ext.ModelManager.getModel('User');
+
+//Uses the configured RestProxy to make a GET request to /users/123
+User.load(123, {
+    success: function(user) {
+        console.log(user.getId()); //logs 123
+    }
+});
+
+ * + *

Models can also be updated and destroyed easily:

+ * +

+//the user Model we loaded in the last snippet:
+user.set('name', 'Edward Spencer');
+
+//tells the Proxy to save the Model. In this case it will perform a PUT request to /users/123 as this Model already has an id
+user.save({
+    success: function() {
+        console.log('The User was updated');
+    }
+});
+
+//tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
+user.destroy({
+    success: function() {
+        console.log('The User was destroyed!');
+    }
+});
+
+ * + *

Usage in Stores

+ * + *

It is very common to want to load a set of Model instances to be displayed and manipulated in the UI. We do this + * by creating a {@link Ext.data.Store Store}:

+ * +

+var store = new Ext.data.Store({
+    model: 'User'
+});
+
+//uses the Proxy we set up on Model to load the Store data
+store.load();
+
+ * + *

A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain + * a set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the + * {@link Ext.data.Store Store docs} for more information on Stores.

+ * + * @constructor + * @param {Object} data An object containing keys corresponding to this model's fields, and their associated values + * @param {Number} id Optional unique ID to assign to this model instance + */ +Ext.define('Ext.data.Model', { + alternateClassName: 'Ext.data.Record', + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: [ + 'Ext.ModelManager', + 'Ext.data.Field', + 'Ext.data.Errors', + 'Ext.data.Operation', + 'Ext.data.validations', + 'Ext.data.proxy.Ajax', + 'Ext.util.MixedCollection' + ], + + onClassExtended: function(cls, data) { + var onBeforeClassCreated = data.onBeforeClassCreated; + + data.onBeforeClassCreated = function(cls, data) { + var me = this, + name = Ext.getClassName(cls), + prototype = cls.prototype, + superCls = cls.prototype.superclass, + + validations = data.validations || [], + fields = data.fields || [], + associations = data.associations || [], + belongsTo = data.belongsTo, + hasMany = data.hasMany, + + fieldsMixedCollection = new Ext.util.MixedCollection(false, function(field) { + return field.name; + }), + + associationsMixedCollection = new Ext.util.MixedCollection(false, function(association) { + return association.name; + }), + + superValidations = superCls.validations, + superFields = superCls.fields, + superAssociations = superCls.associations, + + association, i, ln, + dependencies = []; + + // Save modelName on class and its prototype + cls.modelName = name; + prototype.modelName = name; + + // Merge the validations of the superclass and the new subclass + if (superValidations) { + validations = superValidations.concat(validations); + } + + data.validations = validations; + + // Merge the fields of the superclass and the new subclass + if (superFields) { + fields = superFields.items.concat(fields); + } + + for (i = 0, ln = fields.length; i < ln; ++i) { + fieldsMixedCollection.add(new Ext.data.Field(fields[i])); + } + + data.fields = fieldsMixedCollection; + + //associations can be specified in the more convenient format (e.g. not inside an 'associations' array). + //we support that here + if (belongsTo) { + belongsTo = Ext.Array.from(belongsTo); + + for (i = 0, ln = belongsTo.length; i < ln; ++i) { + association = belongsTo[i]; + + if (!Ext.isObject(association)) { + association = {model: association}; + } + + association.type = 'belongsTo'; + associations.push(association); + } + + delete data.belongsTo; + } + + if (hasMany) { + hasMany = Ext.Array.from(hasMany); + for (i = 0, ln = hasMany.length; i < ln; ++i) { + association = hasMany[i]; + + if (!Ext.isObject(association)) { + association = {model: association}; + } + + association.type = 'hasMany'; + associations.push(association); + } + + delete data.hasMany; + } + + if (superAssociations) { + associations = superAssociations.items.concat(associations); + } + + for (i = 0, ln = associations.length; i < ln; ++i) { + dependencies.push('association.' + associations[i].type.toLowerCase()); + } + + if (data.proxy) { + if (typeof data.proxy === 'string') { + dependencies.push('proxy.' + data.proxy); + } + else if (typeof data.proxy.type === 'string') { + dependencies.push('proxy.' + data.proxy.type); + } + } + + Ext.require(dependencies, function() { + Ext.ModelManager.registerType(name, cls); + + for (i = 0, ln = associations.length; i < ln; ++i) { + association = associations[i]; + + Ext.apply(association, { + ownerModel: name, + associatedModel: association.model + }); + + if (Ext.ModelManager.getModel(association.model) === undefined) { + Ext.ModelManager.registerDeferredAssociation(association); + } else { + associationsMixedCollection.add(Ext.data.Association.create(association)); + } + } + + data.associations = associationsMixedCollection; + + onBeforeClassCreated.call(me, cls, data); + + cls.setProxy(cls.prototype.proxy || cls.prototype.defaultProxyType); + + // Fire the onModelDefined template method on ModelManager + Ext.ModelManager.onModelDefined(cls); + }); + } + }, + + inheritableStatics: { + /** + * Sets the Proxy to use for this model. Accepts any options that can be accepted by {@link Ext#createByAlias Ext.createByAlias} + * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy + * @static + */ + setProxy: function(proxy) { + //make sure we have an Ext.data.proxy.Proxy object + if (!proxy.isProxy) { + if (typeof proxy == "string") { + proxy = { + type: proxy + }; + } + proxy = Ext.createByAlias("proxy." + proxy.type, proxy); + } + proxy.setModel(this); + this.proxy = this.prototype.proxy = proxy; + + return proxy; + }, + + /** + * Returns the configured Proxy for this Model + * @return {Ext.data.proxy.Proxy} The proxy + */ + getProxy: function() { + return this.proxy; + }, + + /** + * Static. Asynchronously loads a model instance by id. Sample usage: +

+    MyApp.User = Ext.define('User', {
+        extend: 'Ext.data.Model',
+        fields: [
+            {name: 'id', type: 'int'},
+            {name: 'name', type: 'string'}
+        ]
+    });
+
+    MyApp.User.load(10, {
+        scope: this,
+        failure: function(record, operation) {
+            //do something if the load failed
+        },
+        success: function(record, operation) {
+            //do something if the load succeeded
+        },
+        callback: function(record, operation) {
+            //do something whether the load succeeded or failed
+        }
+    });
+    
+ * @param {Number} id The id of the model to load + * @param {Object} config Optional config object containing success, failure and callback functions, plus optional scope + * @member Ext.data.Model + * @method load + * @static + */ + load: function(id, config) { + config = Ext.apply({}, config); + config = Ext.applyIf(config, { + action: 'read', + id : id + }); + + var operation = Ext.create('Ext.data.Operation', config), + scope = config.scope || this, + record = null, + callback; + + callback = function(operation) { + if (operation.wasSuccessful()) { + record = operation.getRecords()[0]; + Ext.callback(config.success, scope, [record, operation]); + } else { + Ext.callback(config.failure, scope, [record, operation]); + } + Ext.callback(config.callback, scope, [record, operation]); + }; + + this.proxy.read(operation, callback, this); + } + }, + + statics: { + PREFIX : 'ext-record', + AUTO_ID: 1, + EDIT : 'edit', + REJECT : 'reject', + COMMIT : 'commit', + + /** + * Generates a sequential id. This method is typically called when a record is {@link #create}d + * and {@link #Record no id has been specified}. The id will automatically be assigned + * to the record. The returned id takes the form: + * {PREFIX}-{AUTO_ID}.
    + *
  • PREFIX : String

    Ext.data.Model.PREFIX + * (defaults to 'ext-record')

  • + *
  • AUTO_ID : String

    Ext.data.Model.AUTO_ID + * (defaults to 1 initially)

  • + *
+ * @param {Ext.data.Model} rec The record being created. The record does not exist, it's a {@link #phantom}. + * @return {String} auto-generated string id, "ext-record-i++'; + * @static + */ + id: function(rec) { + var id = [this.PREFIX, '-', this.AUTO_ID++].join(''); + rec.phantom = true; + rec.internalId = id; + return id; + } + }, + + /** + * Internal flag used to track whether or not the model instance is currently being edited. Read-only + * @property editing + * @type Boolean + */ + editing : false, + + /** + * Readonly flag - true if this Record has been modified. + * @type Boolean + */ + dirty : false, + + /** + * @cfg {String} persistanceProperty The property on this Persistable object that its data is saved to. + * Defaults to 'data' (e.g. all persistable data resides in this.data.) + */ + persistanceProperty: 'data', + + evented: false, + isModel: true, + + /** + * true when the record does not yet exist in a server-side database (see + * {@link #setDirty}). Any record which has a real database pk set as its id property + * is NOT a phantom -- it's real. + * @property phantom + * @type {Boolean} + */ + phantom : false, + + /** + * @cfg {String} idProperty The name of the field treated as this Model's unique id (defaults to 'id'). + */ + idProperty: 'id', + + /** + * The string type of the default Model Proxy. Defaults to 'ajax' + * @property defaultProxyType + * @type String + */ + defaultProxyType: 'ajax', + + /** + * An array of the fields defined on this model + * @property fields + * @type {Array} + */ + + constructor: function(data, id) { + data = data || {}; + + var me = this, + fields, + length, + field, + name, + i, + isArray = Ext.isArray(data), + newData = isArray ? {} : null; // to hold mapped array data if needed + + /** + * An internal unique ID for each Model instance, used to identify Models that don't have an ID yet + * @property internalId + * @type String + * @private + */ + me.internalId = (id || id === 0) ? id : Ext.data.Model.id(me); + + Ext.applyIf(me, { + data: {} + }); + + /** + * Key: value pairs of all fields whose values have changed + * @property modified + * @type Object + */ + me.modified = {}; + + me[me.persistanceProperty] = {}; + + me.mixins.observable.constructor.call(me); + + //add default field values if present + fields = me.fields.items; + length = fields.length; + + for (i = 0; i < length; i++) { + field = fields[i]; + name = field.name; + + if (isArray){ + // Have to map array data so the values get assigned to the named fields + // rather than getting set as the field names with undefined values. + newData[name] = data[i]; + } + else if (data[name] === undefined) { + data[name] = field.defaultValue; + } + } + + me.set(newData || data); + // clear any dirty/modified since we're initializing + me.dirty = false; + me.modified = {}; + + if (me.getId()) { + me.phantom = false; + } + + if (typeof me.init == 'function') { + me.init(); + } + + me.id = me.modelName + '-' + me.internalId; + + Ext.ModelManager.register(me); + }, + + /** + * Returns the value of the given field + * @param {String} fieldName The field to fetch the value for + * @return {Mixed} The value + */ + get: function(field) { + return this[this.persistanceProperty][field]; + }, + + /** + * Sets the given field to the given value, marks the instance as dirty + * @param {String|Object} fieldName The field to set, or an object containing key/value pairs + * @param {Mixed} value The value to set + */ + set: function(fieldName, value) { + var me = this, + fields = me.fields, + modified = me.modified, + convertFields = [], + field, key, i, currentValue; + + /* + * If we're passed an object, iterate over that object. NOTE: we pull out fields with a convert function and + * set those last so that all other possible data is set before the convert function is called + */ + if (arguments.length == 1 && Ext.isObject(fieldName)) { + for (key in fieldName) { + if (fieldName.hasOwnProperty(key)) { + + //here we check for the custom convert function. Note that if a field doesn't have a convert function, + //we default it to its type's convert function, so we have to check that here. This feels rather dirty. + field = fields.get(key); + if (field && field.convert !== field.type.convert) { + convertFields.push(key); + continue; + } + + me.set(key, fieldName[key]); + } + } + + for (i = 0; i < convertFields.length; i++) { + field = convertFields[i]; + me.set(field, fieldName[field]); + } + + } else { + if (fields) { + field = fields.get(fieldName); + + if (field && field.convert) { + value = field.convert(value, me); + } + } + currentValue = me.get(fieldName); + me[me.persistanceProperty][fieldName] = value; + + if (field && field.persist && !me.isEqual(currentValue, value)) { + me.dirty = true; + me.modified[fieldName] = currentValue; + } + + if (!me.editing) { + me.afterEdit(); + } + } + }, + + /** + * Checks if two values are equal, taking into account certain + * special factors, for example dates. + * @private + * @param {Object} a The first value + * @param {Object} b The second value + * @return {Boolean} True if the values are equal + */ + isEqual: function(a, b){ + if (Ext.isDate(a) && Ext.isDate(b)) { + return a.getTime() === b.getTime(); + } + return a === b; + }, + + /** + * Begin an edit. While in edit mode, no events (e.g.. the update event) + * are relayed to the containing store. When an edit has begun, it must be followed + * by either {@link #endEdit} or {@link #cancelEdit}. + */ + beginEdit : function(){ + var me = this; + if (!me.editing) { + me.editing = true; + me.dirtySave = me.dirty; + me.dataSave = Ext.apply({}, me[me.persistanceProperty]); + me.modifiedSave = Ext.apply({}, me.modified); + } + }, + + /** + * Cancels all changes made in the current edit operation. + */ + cancelEdit : function(){ + var me = this; + if (me.editing) { + me.editing = false; + // reset the modified state, nothing changed since the edit began + me.modified = me.modifiedSave; + me[me.persistanceProperty] = me.dataSave; + me.dirty = me.dirtySave; + delete me.modifiedSave; + delete me.dataSave; + delete me.dirtySave; + } + }, + + /** + * End an edit. If any data was modified, the containing store is notified + * (ie, the store's update event will fire). + * @param {Boolean} silent True to not notify the store of the change + */ + endEdit : function(silent){ + var me = this; + if (me.editing) { + me.editing = false; + delete me.modifiedSave; + delete me.dataSave; + delete me.dirtySave; + if (silent !== true && me.dirty) { + me.afterEdit(); + } + } + }, + + /** + * Gets a hash of only the fields that have been modified since this Model was created or commited. + * @return Object + */ + getChanges : function(){ + var modified = this.modified, + changes = {}, + field; + + for (field in modified) { + if (modified.hasOwnProperty(field)){ + changes[field] = this.get(field); + } + } + + return changes; + }, + + /** + * Returns true if the passed field name has been {@link #modified} + * since the load or last commit. + * @param {String} fieldName {@link Ext.data.Field#name} + * @return {Boolean} + */ + isModified : function(fieldName) { + return this.modified.hasOwnProperty(fieldName); + }, + + /** + *

Marks this Record as {@link #dirty}. This method + * is used interally when adding {@link #phantom} records to a + * {@link Ext.data.Store#writer writer enabled store}.

+ *

Marking a record {@link #dirty} causes the phantom to + * be returned by {@link Ext.data.Store#getModifiedRecords} where it will + * have a create action composed for it during {@link Ext.data.Store#save store save} + * operations.

+ */ + setDirty : function() { + var me = this, + name; + + me.dirty = true; + + me.fields.each(function(field) { + if (field.persist) { + name = field.name; + me.modified[name] = me.get(name); + } + }, me); + }, + + // + markDirty : function() { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.data.Model: markDirty has been deprecated. Use setDirty instead.'); + } + return this.setDirty.apply(this, arguments); + }, + // + + /** + * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. + * Rejects all changes made to the model instance since either creation, or the last commit operation. + * Modified fields are reverted to their original values. + *

Developers should subscribe to the {@link Ext.data.Store#update} event + * to have their code notified of reject operations.

+ * @param {Boolean} silent (optional) True to skip notification of the owning + * store of the change (defaults to false) + */ + reject : function(silent) { + var me = this, + modified = me.modified, + field; + + for (field in modified) { + if (modified.hasOwnProperty(field)) { + if (typeof modified[field] != "function") { + me[me.persistanceProperty][field] = modified[field]; + } + } + } + + me.dirty = false; + me.editing = false; + me.modified = {}; + + if (silent !== true) { + me.afterReject(); + } + }, + + /** + * Usually called by the {@link Ext.data.Store} which owns the model instance. + * Commits all changes made to the instance since either creation or the last commit operation. + *

Developers should subscribe to the {@link Ext.data.Store#update} event + * to have their code notified of commit operations.

+ * @param {Boolean} silent (optional) True to skip notification of the owning + * store of the change (defaults to false) + */ + commit : function(silent) { + var me = this; + + me.dirty = false; + me.editing = false; + + me.modified = {}; + + if (silent !== true) { + me.afterCommit(); + } + }, + + /** + * Creates a copy (clone) of this Model instance. + * @param {String} id (optional) A new id, defaults to the id + * of the instance being copied. See {@link #id}. + * To generate a phantom instance with a new id use:

+var rec = record.copy(); // clone the record
+Ext.data.Model.id(rec); // automatically generate a unique sequential id
+     * 
+ * @return {Record} + */ + copy : function(newId) { + var me = this; + + return new me.self(Ext.apply({}, me[me.persistanceProperty]), newId || me.internalId); + }, + + /** + * Sets the Proxy to use for this model. Accepts any options that can be accepted by {@link Ext#createByAlias Ext.createByAlias} + * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy + * @static + */ + setProxy: function(proxy) { + //make sure we have an Ext.data.proxy.Proxy object + if (!proxy.isProxy) { + if (typeof proxy === "string") { + proxy = { + type: proxy + }; + } + proxy = Ext.createByAlias("proxy." + proxy.type, proxy); + } + proxy.setModel(this.self); + this.proxy = proxy; + + return proxy; + }, + + /** + * Returns the configured Proxy for this Model + * @return {Ext.data.proxy.Proxy} The proxy + */ + getProxy: function() { + return this.proxy; + }, + + /** + * Validates the current data against all of its configured {@link #validations} and returns an + * {@link Ext.data.Errors Errors} object + * @return {Ext.data.Errors} The errors object + */ + validate: function() { + var errors = Ext.create('Ext.data.Errors'), + validations = this.validations, + validators = Ext.data.validations, + length, validation, field, valid, type, i; + + if (validations) { + length = validations.length; + + for (i = 0; i < length; i++) { + validation = validations[i]; + field = validation.field || validation.name; + type = validation.type; + valid = validators[type](validation, this.get(field)); + + if (!valid) { + errors.add({ + field : field, + message: validation.message || validators[type + 'Message'] + }); + } + } + } + + return errors; + }, + + /** + * Checks if the model is valid. See {@link #validate}. + * @return {Boolean} True if the model is valid. + */ + isValid: function(){ + return this.validate().isValid(); + }, + + /** + * Saves the model instance using the configured proxy + * @param {Object} options Options to pass to the proxy + * @return {Ext.data.Model} The Model instance + */ + save: function(options) { + options = Ext.apply({}, options); + + var me = this, + action = me.phantom ? 'create' : 'update', + record = null, + scope = options.scope || me, + operation, + callback; + + Ext.apply(options, { + records: [me], + action : action + }); + + operation = Ext.create('Ext.data.Operation', options); + + callback = function(operation) { + if (operation.wasSuccessful()) { + record = operation.getRecords()[0]; + //we need to make sure we've set the updated data here. Ideally this will be redundant once the + //ModelCache is in place + me.set(record.data); + record.dirty = false; + + Ext.callback(options.success, scope, [record, operation]); + } else { + Ext.callback(options.failure, scope, [record, operation]); + } + + Ext.callback(options.callback, scope, [record, operation]); + }; + + me.getProxy()[action](operation, callback, me); + + return me; + }, + + /** + * Destroys the model using the configured proxy + * @param {Object} options Options to pass to the proxy + * @return {Ext.data.Model} The Model instance + */ + destroy: function(options){ + options = Ext.apply({}, options); + + var me = this, + record = null, + scope = options.scope || me, + operation, + callback; + + Ext.apply(options, { + records: [me], + action : 'destroy' + }); + + operation = Ext.create('Ext.data.Operation', options); + callback = function(operation) { + if (operation.wasSuccessful()) { + Ext.callback(options.success, scope, [record, operation]); + } else { + Ext.callback(options.failure, scope, [record, operation]); + } + Ext.callback(options.callback, scope, [record, operation]); + }; + + me.getProxy().destroy(operation, callback, me); + return me; + }, + + /** + * Returns the unique ID allocated to this model instance as defined by {@link #idProperty} + * @return {Number} The id + */ + getId: function() { + return this.get(this.idProperty); + }, + + /** + * Sets the model instance's id field to the given id + * @param {Number} id The new id + */ + setId: function(id) { + this.set(this.idProperty, id); + }, + + /** + * Tells this model instance that it has been added to a store + * @param {Ext.data.Store} store The store that the model has been added to + */ + join : function(store) { + /** + * The {@link Ext.data.Store} to which this Record belongs. + * @property store + * @type {Ext.data.Store} + */ + this.store = store; + }, + + /** + * Tells this model instance that it has been removed from the store + */ + unjoin: function() { + delete this.store; + }, + + /** + * @private + * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's + * afterEdit method is called + */ + afterEdit : function() { + this.callStore('afterEdit'); + }, + + /** + * @private + * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's + * afterReject method is called + */ + afterReject : function() { + this.callStore("afterReject"); + }, + + /** + * @private + * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's + * afterCommit method is called + */ + afterCommit: function() { + this.callStore('afterCommit'); + }, + + /** + * @private + * Helper function used by afterEdit, afterReject and afterCommit. Calls the given method on the + * {@link Ext.data.Store store} that this instance has {@link #join joined}, if any. The store function + * will always be called with the model instance as its single argument. + * @param {String} fn The function to call on the store + */ + callStore: function(fn) { + var store = this.store; + + if (store !== undefined && typeof store[fn] == "function") { + store[fn](this); + } + }, + + /** + * Gets all of the data from this Models *loaded* associations. + * It does this recursively - for example if we have a User which + * hasMany Orders, and each Order hasMany OrderItems, it will return an object like this: + * { + * orders: [ + * { + * id: 123, + * status: 'shipped', + * orderItems: [ + * ... + * ] + * } + * ] + * } + * @return {Object} The nested data set for the Model's loaded associations + */ + getAssociatedData: function(){ + return this.prepareAssociatedData(this, [], null); + }, + + /** + * @private + * This complex-looking method takes a given Model instance and returns an object containing all data from + * all of that Model's *loaded* associations. See (@link #getAssociatedData} + * @param {Ext.data.Model} record The Model instance + * @param {Array} ids PRIVATE. The set of Model instance internalIds that have already been loaded + * @param {String} associationType (optional) The name of the type of association to limit to. + * @return {Object} The nested data set for the Model's loaded associations + */ + prepareAssociatedData: function(record, ids, associationType) { + //we keep track of all of the internalIds of the models that we have loaded so far in here + var associations = record.associations.items, + associationCount = associations.length, + associationData = {}, + associatedStore, associatedName, associatedRecords, associatedRecord, + associatedRecordCount, association, id, i, j, type, allow; + + for (i = 0; i < associationCount; i++) { + association = associations[i]; + type = association.type; + allow = true; + if (associationType) { + allow = type == associationType; + } + if (allow && type == 'hasMany') { + + //this is the hasMany store filled with the associated data + associatedStore = record[association.storeName]; + + //we will use this to contain each associated record's data + associationData[association.name] = []; + + //if it's loaded, put it into the association data + if (associatedStore && associatedStore.data.length > 0) { + associatedRecords = associatedStore.data.items; + associatedRecordCount = associatedRecords.length; + + //now we're finally iterating over the records in the association. We do this recursively + for (j = 0; j < associatedRecordCount; j++) { + associatedRecord = associatedRecords[j]; + // Use the id, since it is prefixed with the model name, guaranteed to be unique + id = associatedRecord.id; + + //when we load the associations for a specific model instance we add it to the set of loaded ids so that + //we don't load it twice. If we don't do this, we can fall into endless recursive loading failures. + if (Ext.Array.indexOf(ids, id) == -1) { + ids.push(id); + + associationData[association.name][j] = associatedRecord.data; + Ext.apply(associationData[association.name][j], this.prepareAssociatedData(associatedRecord, ids, type)); + } + } + } + } else if (allow && type == 'belongsTo') { + associatedRecord = record[association.instanceName]; + if (associatedRecord !== undefined) { + id = associatedRecord.id; + if (Ext.Array.indexOf(ids, id) == -1) { + ids.push(id); + associationData[association.name] = associatedRecord.data; + Ext.apply(associationData[association.name], this.prepareAssociatedData(associatedRecord, ids, type)); + } + } + } + } + + return associationData; + } +}); + +/** + * @class Ext.Component + * @extends Ext.AbstractComponent + *

Base class for all Ext components. All subclasses of Component may participate in the automated + * Ext component lifecycle of creation, rendering and destruction which is provided by the {@link Ext.container.Container Container} class. + * Components may be added to a Container through the {@link Ext.container.Container#items items} config option at the time the Container is created, + * or they may be added dynamically via the {@link Ext.container.Container#add add} method.

+ *

The Component base class has built-in support for basic hide/show and enable/disable and size control behavior.

+ *

All Components are registered with the {@link Ext.ComponentManager} on construction so that they can be referenced at any time via + * {@link Ext#getCmp Ext.getCmp}, passing the {@link #id}.

+ *

All user-developed visual widgets that are required to participate in automated lifecycle and size management should subclass Component.

+ *

See the Creating new UI controls tutorial for details on how + * and to either extend or augment ExtJs base classes to create custom Components.

+ *

Every component has a specific xtype, which is its Ext-specific type name, along with methods for checking the + * xtype like {@link #getXType} and {@link #isXType}. This is the list of all valid xtypes:

+ *
+xtype            Class
+-------------    ------------------
+button           {@link Ext.button.Button}
+buttongroup      {@link Ext.container.ButtonGroup}
+colorpalette     {@link Ext.picker.Color}
+component        {@link Ext.Component}
+container        {@link Ext.container.Container}
+cycle            {@link Ext.button.Cycle}
+dataview         {@link Ext.view.View}
+datepicker       {@link Ext.picker.Date}
+editor           {@link Ext.Editor}
+editorgrid       {@link Ext.grid.plugin.Editing}
+grid             {@link Ext.grid.Panel}
+multislider      {@link Ext.slider.Multi}
+panel            {@link Ext.panel.Panel}
+progress         {@link Ext.ProgressBar}
+slider           {@link Ext.slider.Single}
+spacer           {@link Ext.toolbar.Spacer}
+splitbutton      {@link Ext.button.Split}
+tabpanel         {@link Ext.tab.Panel}
+treepanel        {@link Ext.tree.Panel}
+viewport         {@link Ext.container.Viewport}
+window           {@link Ext.window.Window}
+
+Toolbar components
+---------------------------------------
+paging           {@link Ext.toolbar.Paging}
+toolbar          {@link Ext.toolbar.Toolbar}
+tbfill           {@link Ext.toolbar.Fill}
+tbitem           {@link Ext.toolbar.Item}
+tbseparator      {@link Ext.toolbar.Separator}
+tbspacer         {@link Ext.toolbar.Spacer}
+tbtext           {@link Ext.toolbar.TextItem}
+
+Menu components
+---------------------------------------
+menu             {@link Ext.menu.Menu}
+menucheckitem    {@link Ext.menu.CheckItem}
+menuitem         {@link Ext.menu.Item}
+menuseparator    {@link Ext.menu.Separator}
+menutextitem     {@link Ext.menu.Item}
+
+Form components
+---------------------------------------
+form             {@link Ext.form.Panel}
+checkbox         {@link Ext.form.field.Checkbox}
+combo            {@link Ext.form.field.ComboBox}
+datefield        {@link Ext.form.field.Date}
+displayfield     {@link Ext.form.field.Display}
+field            {@link Ext.form.field.Base}
+fieldset         {@link Ext.form.FieldSet}
+hidden           {@link Ext.form.field.Hidden}
+htmleditor       {@link Ext.form.field.HtmlEditor}
+label            {@link Ext.form.Label}
+numberfield      {@link Ext.form.field.Number}
+radio            {@link Ext.form.field.Radio}
+radiogroup       {@link Ext.form.RadioGroup}
+textarea         {@link Ext.form.field.TextArea}
+textfield        {@link Ext.form.field.Text}
+timefield        {@link Ext.form.field.Time}
+trigger          {@link Ext.form.field.Trigger}
+
+Chart components
+---------------------------------------
+chart            {@link Ext.chart.Chart}
+barchart         {@link Ext.chart.series.Bar}
+columnchart      {@link Ext.chart.series.Column}
+linechart        {@link Ext.chart.series.Line}
+piechart         {@link Ext.chart.series.Pie}
+
+

+ * It should not usually be necessary to instantiate a Component because there are provided subclasses which implement specialized Component + * use cases which over most application needs. However it is possible to instantiate a base Component, and it will be renderable, + * or will particpate in layouts as the child item of a Container: +{@img Ext.Component/Ext.Component.png Ext.Component component} +


+    Ext.create('Ext.Component', {
+        html: 'Hello world!',
+        width: 300,
+        height: 200,
+        padding: 20,
+        style: {
+            color: '#FFFFFF',
+            backgroundColor:'#000000'
+        },
+        renderTo: Ext.getBody()
+    });
+
+ *

+ *

The Component above creates its encapsulating div upon render, and use the configured HTML as content. More complex + * internal structure may be created using the {@link #renderTpl} configuration, although to display database-derived mass + * data, it is recommended that an ExtJS data-backed Component such as a {Ext.view.DataView DataView}, or {Ext.grid.Panel GridPanel}, + * or {@link Ext.tree.Panel TreePanel} be used.

+ * @constructor + * @param {Ext.core.Element/String/Object} config The configuration options may be specified as either: + *
    + *
  • an element : + *

    it is set as the internal element and its id used as the component id

  • + *
  • a string : + *

    it is assumed to be the id of an existing element and is used as the component id

  • + *
  • anything else : + *

    it is assumed to be a standard config object and is applied to the component

  • + *
+ */ + +Ext.define('Ext.Component', { + + /* Begin Definitions */ + + alias: ['widget.component', 'widget.box'], + + extend: 'Ext.AbstractComponent', + + requires: [ + 'Ext.util.DelayedTask' + ], + + uses: [ + 'Ext.Layer', + 'Ext.resizer.Resizer', + 'Ext.util.ComponentDragger' + ], + + mixins: { + floating: 'Ext.util.Floating' + }, + + statics: { + // Collapse/expand directions + DIRECTION_TOP: 'top', + DIRECTION_RIGHT: 'right', + DIRECTION_BOTTOM: 'bottom', + DIRECTION_LEFT: 'left' + }, + + /* End Definitions */ + + /** + * @cfg {Mixed} resizable + *

Specify as true to apply a {@link Ext.resizer.Resizer Resizer} to this Component + * after rendering.

+ *

May also be specified as a config object to be passed to the constructor of {@link Ext.resizer.Resizer Resizer} + * to override any defaults. By default the Component passes its minimum and maximum size, and uses + * {@link Ext.resizer.Resizer#dynamic}: false

+ */ + + /** + * @cfg {String} resizeHandles + * A valid {@link Ext.resizer.Resizer} handles config string (defaults to 'all'). Only applies when resizable = true. + */ + resizeHandles: 'all', + + /** + * @cfg {Boolean} autoScroll + * true to use overflow:'auto' on the components layout element and show scroll bars automatically when + * necessary, false to clip any overflowing content (defaults to false). + */ + + /** + * @cfg {Boolean} floating + *

Specify as true to float the Component outside of the document flow using CSS absolute positioning.

+ *

Components such as {@link Ext.window.Window Window}s and {@link Ext.menu.Menu Menu}s are floating + * by default.

+ *

Floating Components that are programatically {@link Ext.Component#render rendered} will register themselves with the global + * {@link Ext.WindowManager ZIndexManager}

+ *

Floating Components as child items of a Container

+ *

A floating Component may be used as a child item of a Container. This just allows the floating Component to seek a ZIndexManager by + * examining the ownerCt chain.

+ *

When configured as floating, Components acquire, at render time, a {@link Ext.ZIndexManager ZIndexManager} which manages a stack + * of related floating Components. The ZIndexManager brings a single floating Component to the top of its stack when + * the Component's {@link #toFront} method is called.

+ *

The ZIndexManager is found by traversing up the {@link #ownerCt} chain to find an ancestor which itself is floating. This is so that + * descendant floating Components of floating Containers (Such as a ComboBox dropdown within a Window) can have its zIndex managed relative + * to any siblings, but always above that floating ancestor Container.

+ *

If no floating ancestor is found, a floating Component registers itself with the default {@link Ext.WindowManager ZIndexManager}.

+ *

Floating components do not participate in the Container's layout. Because of this, they are not rendered until you explicitly + * {@link #show} them.

+ *

After rendering, the ownerCt reference is deleted, and the {@link #floatParent} property is set to the found floating ancestor Container. + * If no floating ancestor Container was found the {@link #floatParent} property will not be set.

+ */ + floating: false, + + /** + * @cfg {Boolean} toFrontOnShow + *

True to automatically call {@link #toFront} when the {@link #show} method is called + * on an already visible, floating component (default is true).

+ */ + toFrontOnShow: true, + + /** + *

Optional. Only present for {@link #floating} Components after they have been rendered.

+ *

A reference to the ZIndexManager which is managing this Component's z-index.

+ *

The {@link Ext.ZIndexManager ZIndexManager} maintains a stack of floating Component z-indices, and also provides a single modal + * mask which is insert just beneath the topmost visible modal floating Component.

+ *

Floating Components may be {@link #toFront brought to the front} or {@link #toBack sent to the back} of the z-index stack.

+ *

This defaults to the global {@link Ext.WindowManager ZIndexManager} for floating Components that are programatically + * {@link Ext.Component#render rendered}.

+ *

For {@link #floating} Components which are added to a Container, the ZIndexManager is acquired from the first ancestor Container found + * which is floating, or if not found the global {@link Ext.WindowManager ZIndexManager} is used.

+ *

See {@link #floating} and {@link #floatParent}

+ * @property zIndexManager + * @type Ext.ZIndexManager + */ + + /** + *

Optional. Only present for {@link #floating} Components which were inserted as descendant items of floating Containers.

+ *

Floating Components that are programatically {@link Ext.Component#render rendered} will not have a floatParent property.

+ *

For {@link #floating} Components which are child items of a Container, the floatParent will be the floating ancestor Container which is + * responsible for the base z-index value of all its floating descendants. It provides a {@link Ext.ZIndexManager ZIndexManager} which provides + * z-indexing services for all its descendant floating Components.

+ *

For example, the dropdown {@link Ext.view.BoundList BoundList} of a ComboBox which is in a Window will have the Window as its + * floatParent

+ *

See {@link #floating} and {@link #zIndexManager}

+ * @property floatParent + * @type Ext.Container + */ + + /** + * @cfg {Mixed} draggable + *

Specify as true to make a {@link #floating} Component draggable using the Component's encapsulating element as the drag handle.

+ *

This may also be specified as a config object for the {@link Ext.util.ComponentDragger ComponentDragger} which is instantiated to perform dragging.

+ *

For example to create a Component which may only be dragged around using a certain internal element as the drag handle, + * use the delegate option:

+ *
+new Ext.Component({
+    constrain: true,
+    floating:true,
+    style: {
+        backgroundColor: '#fff',
+        border: '1px solid black'
+    },
+    html: '<h1 style="cursor:move">The title</h1><p>The content</p>',
+    draggable: {
+        delegate: 'h1'
+    }
+}).show();
+
+ */ + + /** + * @cfg {Boolean} maintainFlex + *

Only valid when a sibling element of a {@link Ext.resizer.Splitter Splitter} within a {@link Ext.layout.container.VBox VBox} or + * {@link Ext.layout.container.HBox HBox} layout.

+ *

Specifies that if an immediate sibling Splitter is moved, the Component on the other side is resized, and this + * Component maintains its configured {@link Ext.layout.container.Box#flex flex} value.

+ */ + + hideMode: 'display', + // Deprecate 5.0 + hideParent: false, + + ariaRole: 'presentation', + + bubbleEvents: [], + + actionMode: 'el', + monPropRe: /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate)$/, + + //renderTpl: new Ext.XTemplate( + // '
{uiBase}-{ui}" style="{style}">
', { + // compiled: true, + // disableFormats: true + // } + //), + constructor: function(config) { + config = config || {}; + if (config.initialConfig) { + + // Being initialized from an Ext.Action instance... + if (config.isAction) { + this.baseAction = config; + } + config = config.initialConfig; + // component cloning / action set up + } + else if (config.tagName || config.dom || Ext.isString(config)) { + // element object + config = { + applyTo: config, + id: config.id || config + }; + } + + this.callParent([config]); + + // If we were configured from an instance of Ext.Action, (or configured with a baseAction option), + // register this Component as one of its items + if (this.baseAction){ + this.baseAction.addComponent(this); + } + }, + + initComponent: function() { + var me = this; + + if (me.listeners) { + me.on(me.listeners); + delete me.listeners; + } + me.enableBubble(me.bubbleEvents); + me.mons = []; + }, + + // private + afterRender: function() { + var me = this, + resizable = me.resizable; + + if (me.floating) { + me.makeFloating(me.floating); + } else { + me.el.setVisibilityMode(Ext.core.Element[me.hideMode.toUpperCase()]); + } + + me.setAutoScroll(me.autoScroll); + me.callParent(); + + if (!(me.x && me.y) && (me.pageX || me.pageY)) { + me.setPagePosition(me.pageX, me.pageY); + } + + if (resizable) { + me.initResizable(resizable); + } + + if (me.draggable) { + me.initDraggable(); + } + + me.initAria(); + }, + + initAria: function() { + var actionEl = this.getActionEl(), + role = this.ariaRole; + if (role) { + actionEl.dom.setAttribute('role', role); + } + }, + + /** + * Sets the overflow on the content element of the component. + * @param {Boolean} scroll True to allow the Component to auto scroll. + * @return {Ext.Component} this + */ + setAutoScroll : function(scroll){ + var me = this, + targetEl; + scroll = !!scroll; + if (me.rendered) { + targetEl = me.getTargetEl(); + targetEl.setStyle('overflow', scroll ? 'auto' : ''); + if (scroll && (Ext.isIE6 || Ext.isIE7)) { + // The scrollable container element must be non-statically positioned or IE6/7 will make + // positioned children stay in place rather than scrolling with the rest of the content + targetEl.position(); + } + } + me.autoScroll = scroll; + return me; + }, + + // private + makeFloating : function(cfg){ + this.mixins.floating.constructor.call(this, cfg); + }, + + initResizable: function(resizable) { + resizable = Ext.apply({ + target: this, + dynamic: false, + constrainTo: this.constrainTo, + handles: this.resizeHandles + }, resizable); + resizable.target = this; + this.resizer = Ext.create('Ext.resizer.Resizer', resizable); + }, + + getDragEl: function() { + return this.el; + }, + + initDraggable: function() { + var me = this, + ddConfig = Ext.applyIf({ + el: this.getDragEl(), + constrainTo: me.constrainTo || (me.floatParent ? me.floatParent.getTargetEl() : me.el.dom.parentNode) + }, this.draggable); + + // Add extra configs if Component is specified to be constrained + if (me.constrain || me.constrainDelegate) { + ddConfig.constrain = me.constrain; + ddConfig.constrainDelegate = me.constrainDelegate; + } + + this.dd = Ext.create('Ext.util.ComponentDragger', this, ddConfig); + }, + + /** + * Sets the left and top of the component. To set the page XY position instead, use {@link #setPagePosition}. + * This method fires the {@link #move} event. + * @param {Number} left The new left + * @param {Number} top The new top + * @param {Mixed} animate If true, the Component is animated into its new position. You may also pass an animation configuration. + * @return {Ext.Component} this + */ + setPosition: function(x, y, animate) { + var me = this, + el = me.el, + to = {}, + adj, adjX, adjY, xIsNumber, yIsNumber; + + if (Ext.isArray(x)) { + animate = y; + y = x[1]; + x = x[0]; + } + me.x = x; + me.y = y; + + if (!me.rendered) { + return me; + } + + adj = me.adjustPosition(x, y); + adjX = adj.x; + adjY = adj.y; + xIsNumber = Ext.isNumber(adjX); + yIsNumber = Ext.isNumber(adjY); + + if (xIsNumber || yIsNumber) { + if (animate) { + if (xIsNumber) { + to.left = adjX; + } + if (yIsNumber) { + to.top = adjY; + } + + me.stopAnimation(); + me.animate(Ext.apply({ + duration: 1000, + listeners: { + afteranimate: Ext.Function.bind(me.afterSetPosition, me, [adjX, adjY]) + }, + to: to + }, animate)); + } + else { + if (!xIsNumber) { + el.setTop(adjY); + } + else if (!yIsNumber) { + el.setLeft(adjX); + } + else { + el.setLeftTop(adjX, adjY); + } + me.afterSetPosition(adjX, adjY); + } + } + return me; + }, + + /** + * @private Template method called after a Component has been positioned. + */ + afterSetPosition: function(ax, ay) { + this.onPosition(ax, ay); + this.fireEvent('move', this, ax, ay); + }, + + showAt: function(x, y, animate) { + // A floating Component is positioned relative to its ownerCt if any. + if (this.floating) { + this.setPosition(x, y, animate); + } else { + this.setPagePosition(x, y, animate); + } + this.show(); + }, + + /** + * Sets the page XY position of the component. To set the left and top instead, use {@link #setPosition}. + * This method fires the {@link #move} event. + * @param {Number} x The new x position + * @param {Number} y The new y position + * @param {Mixed} animate If passed, the Component is animated into its new position. If this parameter + * is a number, it is used as the animation duration in milliseconds. + * @return {Ext.Component} this + */ + setPagePosition: function(x, y, animate) { + var me = this, + p; + + if (Ext.isArray(x)) { + y = x[1]; + x = x[0]; + } + me.pageX = x; + me.pageY = y; + if (me.floating && me.floatParent) { + // Floating Components being positioned in their ownerCt have to be made absolute + p = me.floatParent.getTargetEl().getViewRegion(); + if (Ext.isNumber(x) && Ext.isNumber(p.left)) { + x -= p.left; + } + if (Ext.isNumber(y) && Ext.isNumber(p.top)) { + y -= p.top; + } + me.setPosition(x, y, animate); + } + else { + p = me.el.translatePoints(x, y); + me.setPosition(p.left, p.top, animate); + } + return me; + }, + + /** + * Gets the current box measurements of the component's underlying element. + * @param {Boolean} local (optional) If true the element's left and top are returned instead of page XY (defaults to false) + * @return {Object} box An object in the format {x, y, width, height} + */ + getBox : function(local){ + var pos = this.getPosition(local); + var s = this.getSize(); + s.x = pos[0]; + s.y = pos[1]; + return s; + }, + + /** + * Sets the current box measurements of the component's underlying element. + * @param {Object} box An object in the format {x, y, width, height} + * @return {Ext.Component} this + */ + updateBox : function(box){ + this.setSize(box.width, box.height); + this.setPagePosition(box.x, box.y); + return this; + }, + + // Include margins + getOuterSize: function() { + var el = this.el; + return { + width: el.getWidth() + el.getMargin('lr'), + height: el.getHeight() + el.getMargin('tb') + }; + }, + + // private + adjustSize: function(w, h) { + if (this.autoWidth) { + w = 'auto'; + } + + if (this.autoHeight) { + h = 'auto'; + } + + return { + width: w, + height: h + }; + }, + + // private + adjustPosition: function(x, y) { + + // Floating Components being positioned in their ownerCt have to be made absolute + if (this.floating && this.floatParent) { + var o = this.floatParent.getTargetEl().getViewRegion(); + x += o.left; + y += o.top; + } + + return { + x: x, + y: y + }; + }, + + /** + * Gets the current XY position of the component's underlying element. + * @param {Boolean} local (optional) If true the element's left and top are returned instead of page XY (defaults to false) + * @return {Array} The XY position of the element (e.g., [100, 200]) + */ + getPosition: function(local) { + var el = this.el, + xy; + + if (local === true) { + return [el.getLeft(true), el.getTop(true)]; + } + xy = this.xy || el.getXY(); + + // Floating Components in an ownerCt have to have their positions made relative + if (this.floating && this.floatParent) { + var o = this.floatParent.getTargetEl().getViewRegion(); + xy[0] -= o.left; + xy[1] -= o.top; + } + return xy; + }, + + // Todo: add in xtype prefix support + getId: function() { + return this.id || (this.id = (this.getXType() || 'ext-comp') + '-' + this.getAutoId()); + }, + + onEnable: function() { + var actionEl = this.getActionEl(); + actionEl.dom.removeAttribute('aria-disabled'); + actionEl.dom.disabled = false; + this.callParent(); + }, + + onDisable: function() { + var actionEl = this.getActionEl(); + actionEl.dom.setAttribute('aria-disabled', true); + actionEl.dom.disabled = true; + this.callParent(); + }, + + /** + *

Shows this Component, rendering it first if {@link #autoRender} or {{@link "floating} are true.

+ *

After being shown, a {@link #floating} Component (such as a {@link Ext.window.Window}), is activated it and brought to the front of + * its {@link #ZIndexManager z-index stack}.

+ * @param {String/Element} animateTarget Optional, and only valid for {@link #floating} Components such as + * {@link Ext.window.Window Window}s or {@link Ext.tip.ToolTip ToolTip}s, or regular Components which have been configured + * with floating: true. The target from which the Component should + * animate from while opening (defaults to null with no animation) + * @param {Function} callback (optional) A callback function to call after the Component is displayed. Only necessary if animation was specified. + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this Component. + * @return {Component} this + */ + show: function(animateTarget, cb, scope) { + if (this.rendered && this.isVisible()) { + if (this.toFrontOnShow && this.floating) { + this.toFront(); + } + } else if (this.fireEvent('beforeshow', this) !== false) { + this.hidden = false; + + // Render on first show if there is an autoRender config, or if this is a floater (Window, Menu, BoundList etc). + if (!this.rendered && (this.autoRender || this.floating)) { + this.doAutoRender(); + } + if (this.rendered) { + this.beforeShow(); + this.onShow.apply(this, arguments); + + // Notify any owning Container unless it's suspended. + // Floating Components do not participate in layouts. + if (this.ownerCt && !this.floating && !(this.ownerCt.suspendLayout || this.ownerCt.layout.layoutBusy)) { + this.ownerCt.doLayout(); + } + this.afterShow.apply(this, arguments); + } + } + return this; + }, + + beforeShow: Ext.emptyFn, + + // Private. Override in subclasses where more complex behaviour is needed. + onShow: function() { + var me = this; + + me.el.show(); + if (this.floating && this.constrain) { + this.doConstrain(); + } + me.callParent(arguments); + }, + + afterShow: function(animateTarget, cb, scope) { + var me = this, + fromBox, + toBox, + ghostPanel; + + // Default to configured animate target if none passed + animateTarget = animateTarget || me.animateTarget; + + // Need to be able to ghost the Component + if (!me.ghost) { + animateTarget = null; + } + // If we're animating, kick of an animation of the ghost from the target to the *Element* current box + if (animateTarget) { + animateTarget = animateTarget.el ? animateTarget.el : Ext.get(animateTarget); + toBox = me.el.getBox(); + fromBox = animateTarget.getBox(); + fromBox.width += 'px'; + fromBox.height += 'px'; + toBox.width += 'px'; + toBox.height += 'px'; + me.el.addCls(Ext.baseCSSPrefix + 'hide-offsets'); + ghostPanel = me.ghost(); + ghostPanel.el.stopAnimation(); + + ghostPanel.el.animate({ + from: fromBox, + to: toBox, + listeners: { + afteranimate: function() { + delete ghostPanel.componentLayout.lastComponentSize; + me.unghost(); + me.el.removeCls(Ext.baseCSSPrefix + 'hide-offsets'); + if (me.floating) { + me.toFront(); + } + Ext.callback(cb, scope || me); + } + } + }); + } + else { + if (me.floating) { + me.toFront(); + } + Ext.callback(cb, scope || me); + } + me.fireEvent('show', me); + }, + + /** + * Hides this Component, setting it to invisible using the configured {@link #hideMode}. + * @param {String/Element/Component} animateTarget Optional, and only valid for {@link #floating} Components such as + * {@link Ext.window.Window Window}s or {@link Ext.tip.ToolTip ToolTip}s, or regular Components which have been configured + * with floating: true.. + * The target to which the Component should animate while hiding (defaults to null with no animation) + * @param {Function} callback (optional) A callback function to call after the Component is hidden. + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this Component. + * @return {Ext.Component} this + */ + hide: function() { + + // Clear the flag which is set if a floatParent was hidden while this is visible. + // If a hide operation was subsequently called, that pending show must be hidden. + this.showOnParentShow = false; + + if (!(this.rendered && !this.isVisible()) && this.fireEvent('beforehide', this) !== false) { + this.hidden = true; + if (this.rendered) { + this.onHide.apply(this, arguments); + + // Notify any owning Container unless it's suspended. + // Floating Components do not participate in layouts. + if (this.ownerCt && !this.floating && !(this.ownerCt.suspendLayout || this.ownerCt.layout.layoutBusy)) { + this.ownerCt.doLayout(); + } + } + } + return this; + }, + + // Possibly animate down to a target element. + onHide: function(animateTarget, cb, scope) { + var me = this, + ghostPanel, + toBox; + + // Default to configured animate target if none passed + animateTarget = animateTarget || me.animateTarget; + + // Need to be able to ghost the Component + if (!me.ghost) { + animateTarget = null; + } + // If we're animating, kick off an animation of the ghost down to the target + if (animateTarget) { + animateTarget = animateTarget.el ? animateTarget.el : Ext.get(animateTarget); + ghostPanel = me.ghost(); + ghostPanel.el.stopAnimation(); + toBox = animateTarget.getBox(); + toBox.width += 'px'; + toBox.height += 'px'; + ghostPanel.el.animate({ + to: toBox, + listeners: { + afteranimate: function() { + delete ghostPanel.componentLayout.lastComponentSize; + ghostPanel.el.hide(); + me.afterHide(cb, scope); + } + } + }); + } + me.el.hide(); + if (!animateTarget) { + me.afterHide(cb, scope); + } + }, + + afterHide: function(cb, scope) { + Ext.callback(cb, scope || this); + this.fireEvent('hide', this); + }, + + /** + * @private + * Template method to contribute functionality at destroy time. + */ + onDestroy: function() { + var me = this; + + // Ensure that any ancillary components are destroyed. + if (me.rendered) { + Ext.destroy( + me.proxy, + me.resizer + ); + // Different from AbstractComponent + if (me.actionMode == 'container' || me.removeMode == 'container') { + me.container.remove(); + } + } + me.callParent(); + }, + + deleteMembers: function() { + var args = arguments, + len = args.length, + i = 0; + for (; i < len; ++i) { + delete this[args[i]]; + } + }, + + /** + * Try to focus this component. + * @param {Boolean} selectText (optional) If applicable, true to also select the text in this component + * @param {Boolean/Number} delay (optional) Delay the focus this number of milliseconds (true for 10 milliseconds). + * @return {Ext.Component} this + */ + focus: function(selectText, delay) { + var me = this, + focusEl; + + if (delay) { + me.focusTask.delay(Ext.isNumber(delay) ? delay: 10, null, me, [selectText, false]); + return me; + } + + if (me.rendered && !me.isDestroyed) { + // getFocusEl could return a Component. + focusEl = me.getFocusEl(); + focusEl.focus(); + if (focusEl.dom && selectText === true) { + focusEl.dom.select(); + } + + // Focusing a floating Component brings it to the front of its stack. + // this is performed by its zIndexManager. Pass preventFocus true to avoid recursion. + if (me.floating) { + me.toFront(true); + } + } + return me; + }, + + /** + * @private + * Returns the focus holder element associated with this Component. By default, this is the Component's encapsulating + * element. Subclasses which use embedded focusable elements (such as Window and Button) should override this for use + * by the {@link #focus} method. + * @returns {Ext.core.Element} the focus holing element. + */ + getFocusEl: function() { + return this.el; + }, + + // private + blur: function() { + if (this.rendered) { + this.getFocusEl().blur(); + } + return this; + }, + + getEl: function() { + return this.el; + }, + + // Deprecate 5.0 + getResizeEl: function() { + return this.el; + }, + + // Deprecate 5.0 + getPositionEl: function() { + return this.el; + }, + + // Deprecate 5.0 + getActionEl: function() { + return this.el; + }, + + // Deprecate 5.0 + getVisibilityEl: function() { + return this.el; + }, + + // Deprecate 5.0 + onResize: Ext.emptyFn, + + // private + getBubbleTarget: function() { + return this.ownerCt; + }, + + // private + getContentTarget: function() { + return this.el; + }, + + /** + * Clone the current component using the original config values passed into this instance by default. + * @param {Object} overrides A new config containing any properties to override in the cloned version. + * An id property can be passed on this object, otherwise one will be generated to avoid duplicates. + * @return {Ext.Component} clone The cloned copy of this component + */ + cloneConfig: function(overrides) { + overrides = overrides || {}; + var id = overrides.id || Ext.id(); + var cfg = Ext.applyIf(overrides, this.initialConfig); + cfg.id = id; + + var self = Ext.getClass(this); + + // prevent dup id + return new self(cfg); + }, + + /** + * Gets the xtype for this component as registered with {@link Ext.ComponentManager}. For a list of all + * available xtypes, see the {@link Ext.Component} header. Example usage: + *

+var t = new Ext.form.field.Text();
+alert(t.getXType());  // alerts 'textfield'
+
+ * @return {String} The xtype + */ + getXType: function() { + return this.self.xtype; + }, + + /** + * Find a container above this component at any level by a custom function. If the passed function returns + * true, the container will be returned. + * @param {Function} fn The custom function to call with the arguments (container, this component). + * @return {Ext.container.Container} The first Container for which the custom function returns true + */ + findParentBy: function(fn) { + var p; + + // Iterate up the ownerCt chain until there's no ownerCt, or we find an ancestor which matches using the selector function. + for (p = this.ownerCt; p && !fn(p, this); p = p.ownerCt); + return p || null; + }, + + /** + *

Find a container above this component at any level by xtype or class

+ *

See also the {@link Ext.Component#up up} method.

+ * @param {String/Class} xtype The xtype string for a component, or the class of the component directly + * @return {Ext.container.Container} The first Container which matches the given xtype or class + */ + findParentByType: function(xtype) { + return Ext.isFunction(xtype) ? + this.findParentBy(function(p) { + return p.constructor === xtype; + }) + : + this.up(xtype); + }, + + /** + * Bubbles up the component/container heirarchy, calling the specified function with each component. The scope (this) of + * function call will be the scope provided or the current component. The arguments to the function + * will be the args provided or the current component. If the function returns false at any point, + * the bubble is stopped. + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope of the function (defaults to current node) + * @param {Array} args (optional) The args to call the function with (default to passing the current component) + * @return {Ext.Component} this + */ + bubble: function(fn, scope, args) { + var p = this; + while (p) { + if (fn.apply(scope || p, args || [p]) === false) { + break; + } + p = p.ownerCt; + } + return this; + }, + + getProxy: function() { + if (!this.proxy) { + this.proxy = this.el.createProxy(Ext.baseCSSPrefix + 'proxy-el', Ext.getBody(), true); + } + return this.proxy; + } + +}, function() { + + // A single focus delayer for all Components. + this.prototype.focusTask = Ext.create('Ext.util.DelayedTask', this.prototype.focus); + +}); + +/** +* @class Ext.layout.container.Container +* @extends Ext.layout.container.AbstractContainer +* @private +*

This class is intended to be extended or created via the {@link Ext.container.Container#layout layout} +* configuration property. See {@link Ext.container.Container#layout} for additional details.

+*/ +Ext.define('Ext.layout.container.Container', { + + /* Begin Definitions */ + + extend: 'Ext.layout.container.AbstractContainer', + alternateClassName: 'Ext.layout.ContainerLayout', + + /* End Definitions */ + + layoutItem: function(item, box) { + box = box || {}; + if (item.componentLayout.initialized !== true) { + this.setItemSize(item, box.width || item.width || undefined, box.height || item.height || undefined); + // item.doComponentLayout(box.width || item.width || undefined, box.height || item.height || undefined); + } + }, + + getLayoutTargetSize : function() { + var target = this.getTarget(), + ret; + + if (target) { + ret = target.getViewSize(); + + // IE in will sometimes return a width of 0 on the 1st pass of getViewSize. + // Use getStyleSize to verify the 0 width, the adjustment pass will then work properly + // with getViewSize + if (Ext.isIE && ret.width == 0){ + ret = target.getStyleSize(); + } + + ret.width -= target.getPadding('lr'); + ret.height -= target.getPadding('tb'); + } + return ret; + }, + + beforeLayout: function() { + if (this.owner.beforeLayout(arguments) !== false) { + return this.callParent(arguments); + } + else { + return false; + } + }, + + afterLayout: function() { + this.owner.afterLayout(arguments); + this.callParent(arguments); + }, + + /** + * @protected + * Returns all items that are rendered + * @return {Array} All matching items + */ + getRenderedItems: function() { + var me = this, + target = me.getTarget(), + items = me.getLayoutItems(), + ln = items.length, + renderedItems = [], + i, item; + + for (i = 0; i < ln; i++) { + item = items[i]; + if (item.rendered && me.isValidParent(item, target, i)) { + renderedItems.push(item); + } + } + + return renderedItems; + }, + + /** + * @protected + * Returns all items that are both rendered and visible + * @return {Array} All matching items + */ + getVisibleItems: function() { + var target = this.getTarget(), + items = this.getLayoutItems(), + ln = items.length, + visibleItems = [], + i, item; + + for (i = 0; i < ln; i++) { + item = items[i]; + if (item.rendered && this.isValidParent(item, target, i) && item.hidden !== true) { + visibleItems.push(item); + } + } + + return visibleItems; + } +}); +/** + * @class Ext.layout.container.Auto + * @extends Ext.layout.container.Container + * + *

The AutoLayout is the default layout manager delegated by {@link Ext.container.Container} to + * render any child Components when no {@link Ext.container.Container#layout layout} is configured into + * a {@link Ext.container.Container Container}.. AutoLayout provides only a passthrough of any layout calls + * to any child containers.

+ * {@img Ext.layout.container.Auto/Ext.layout.container.Auto.png Ext.layout.container.Auto container layout} + * Example usage: + Ext.create('Ext.Panel', { + width: 500, + height: 280, + title: "AutoLayout Panel", + layout: 'auto', + renderTo: document.body, + items: [{ + xtype: 'panel', + title: 'Top Inner Panel', + width: '75%', + height: 90 + },{ + xtype: 'panel', + title: 'Bottom Inner Panel', + width: '75%', + height: 90 + }] + }); + */ + +Ext.define('Ext.layout.container.Auto', { + + /* Begin Definitions */ + + alias: ['layout.auto', 'layout.autocontainer'], + + extend: 'Ext.layout.container.Container', + + /* End Definitions */ + + type: 'autocontainer', + + fixedLayout: false, + + bindToOwnerCtComponent: true, + + // @private + onLayout : function(owner, target) { + var me = this, + items = me.getLayoutItems(), + ln = items.length, + i; + + // Ensure the Container is only primed with the clear element if there are child items. + if (ln) { + // Auto layout uses natural HTML flow to arrange the child items. + // To ensure that all browsers (I'm looking at you IE!) add the bottom margin of the last child to the + // containing element height, we create a zero-sized element with style clear:both to force a "new line" + if (!me.clearEl) { + me.clearEl = me.getRenderTarget().createChild({ + cls: Ext.baseCSSPrefix + 'clear', + role: 'presentation' + }); + } + + // Auto layout allows CSS to size its child items. + for (i = 0; i < ln; i++) { + me.setItemSize(items[i]); + } + } + } +}); +/** + * @class Ext.container.AbstractContainer + * @extends Ext.Component + *

An abstract base class which provides shared methods for Containers across the Sencha product line.

+ * Please refer to sub class's documentation + */ +Ext.define('Ext.container.AbstractContainer', { + + /* Begin Definitions */ + + extend: 'Ext.Component', + + requires: [ + 'Ext.util.MixedCollection', + 'Ext.layout.container.Auto', + 'Ext.ZIndexManager' + ], + + /* End Definitions */ + /** + * @cfg {String/Object} layout + *

*Important: In order for child items to be correctly sized and + * positioned, typically a layout manager must be specified through + * the layout configuration option.

+ *

The sizing and positioning of child {@link #items} is the responsibility of + * the Container's layout manager which creates and manages the type of layout + * you have in mind. For example:

+ *

If the {@link #layout} configuration is not explicitly specified for + * a general purpose container (e.g. Container or Panel) the + * {@link Ext.layout.container.Auto default layout manager} will be used + * which does nothing but render child components sequentially into the + * Container (no sizing or positioning will be performed in this situation).

+ *

layout may be specified as either as an Object or + * as a String:

    + * + *
  • Specify as an Object
  • + *
      + *
    • Example usage:
    • + *
      
      +layout: {
      +    type: 'vbox',
      +    align: 'left'
      +}
      +       
      + * + *
    • type
    • + *

      The layout type to be used for this container. If not specified, + * a default {@link Ext.layout.container.Auto} will be created and used.

      + *

      Valid layout type values are:

      + *
        + *
      • {@link Ext.layout.container.Auto Auto}     Default
      • + *
      • {@link Ext.layout.container.Card card}
      • + *
      • {@link Ext.layout.container.Fit fit}
      • + *
      • {@link Ext.layout.container.HBox hbox}
      • + *
      • {@link Ext.layout.container.VBox vbox}
      • + *
      • {@link Ext.layout.container.Anchor anchor}
      • + *
      • {@link Ext.layout.container.Table table}
      • + *
      + * + *
    • Layout specific configuration properties
    • + *

      Additional layout specific configuration properties may also be + * specified. For complete details regarding the valid config options for + * each layout type, see the layout class corresponding to the type + * specified.

      + * + *
    + * + *
  • Specify as a String
  • + *
      + *
    • Example usage:
    • + *
      
      +layout: {
      +    type: 'vbox',
      +    padding: '5',
      +    align: 'left'
      +}
      +       
      + *
    • layout
    • + *

      The layout type to be used for this container (see list + * of valid layout type values above).


      + *

      Additional layout specific configuration properties. For complete + * details regarding the valid config options for each layout type, see the + * layout class corresponding to the layout specified.

      + *
+ */ + + /** + * @cfg {String/Number} activeItem + * A string component id or the numeric index of the component that should be initially activated within the + * container's layout on render. For example, activeItem: 'item-1' or activeItem: 0 (index 0 = the first + * item in the container's collection). activeItem only applies to layout styles that can display + * items one at a time (like {@link Ext.layout.container.Card} and {@link Ext.layout.container.Fit}). + */ + /** + * @cfg {Object/Array} items + *

A single item, or an array of child Components to be added to this container

+ *

Unless configured with a {@link #layout}, a Container simply renders child Components serially into + * its encapsulating element and performs no sizing or positioning upon them.

+ *

Example:

+ *

+// specifying a single item
+items: {...},
+layout: 'fit',    // The single items is sized to fit
+
+// specifying multiple items
+items: [{...}, {...}],
+layout: 'hbox', // The items are arranged horizontally
+       
+ *

Each item may be:

+ *
    + *
  • A {@link Ext.Component Component}
  • + *
  • A Component configuration object
  • + *
+ *

If a configuration object is specified, the actual type of Component to be + * instantiated my be indicated by using the {@link Ext.Component#xtype xtype} option.

+ *

Every Component class has its own {@link Ext.Component#xtype xtype}.

+ *

If an {@link Ext.Component#xtype xtype} is not explicitly + * specified, the {@link #defaultType} for the Container is used, which by default is usually panel.

+ *

Notes:

+ *

Ext uses lazy rendering. Child Components will only be rendered + * should it become necessary. Items are automatically laid out when they are first + * shown (no sizing is done while hidden), or in response to a {@link #doLayout} call.

+ *

Do not specify {@link Ext.panel.Panel#contentEl contentEl} or + * {@link Ext.panel.Panel#html html} with items.

+ */ + /** + * @cfg {Object|Function} defaults + *

This option is a means of applying default settings to all added items whether added through the {@link #items} + * config or via the {@link #add} or {@link #insert} methods.

+ *

If an added item is a config object, and not an instantiated Component, then the default properties are + * unconditionally applied. If the added item is an instantiated Component, then the default properties are + * applied conditionally so as not to override existing properties in the item.

+ *

If the defaults option is specified as a function, then the function will be called using this Container as the + * scope (this reference) and passing the added item as the first parameter. Any resulting object + * from that call is then applied to the item as default properties.

+ *

For example, to automatically apply padding to the body of each of a set of + * contained {@link Ext.panel.Panel} items, you could pass: defaults: {bodyStyle:'padding:15px'}.

+ *

Usage:


+defaults: {               // defaults are applied to items, not the container
+    autoScroll:true
+},
+items: [
+    {
+        xtype: 'panel',   // defaults do not have precedence over
+        id: 'panel1',     // options in config objects, so the defaults
+        autoScroll: false // will not be applied here, panel1 will be autoScroll:false
+    },
+    new Ext.panel.Panel({       // defaults do have precedence over options
+        id: 'panel2',     // options in components, so the defaults
+        autoScroll: false // will be applied here, panel2 will be autoScroll:true.
+    })
+]
+ */ + + /** @cfg {Boolean} suspendLayout + * If true, suspend calls to doLayout. Useful when batching multiple adds to a container and not passing them + * as multiple arguments or an array. + */ + suspendLayout : false, + + /** @cfg {Boolean} autoDestroy + * If true the container will automatically destroy any contained component that is removed from it, else + * destruction must be handled manually. + * Defaults to true. + */ + autoDestroy : true, + + /** @cfg {String} defaultType + *

The default {@link Ext.Component xtype} of child Components to create in this Container when + * a child item is specified as a raw configuration object, rather than as an instantiated Component.

+ *

Defaults to 'panel'.

+ */ + defaultType: 'panel', + + isContainer : true, + + baseCls: Ext.baseCSSPrefix + 'container', + + /** + * @cfg {Array} bubbleEvents + *

An array of events that, when fired, should be bubbled to any parent container. + * See {@link Ext.util.Observable#enableBubble}. + * Defaults to ['add', 'remove']. + */ + bubbleEvents: ['add', 'remove'], + + // @private + initComponent : function(){ + var me = this; + me.addEvents( + /** + * @event afterlayout + * Fires when the components in this container are arranged by the associated layout manager. + * @param {Ext.container.Container} this + * @param {ContainerLayout} layout The ContainerLayout implementation for this container + */ + 'afterlayout', + /** + * @event beforeadd + * Fires before any {@link Ext.Component} is added or inserted into the container. + * A handler can return false to cancel the add. + * @param {Ext.container.Container} this + * @param {Ext.Component} component The component being added + * @param {Number} index The index at which the component will be added to the container's items collection + */ + 'beforeadd', + /** + * @event beforeremove + * Fires before any {@link Ext.Component} is removed from the container. A handler can return + * false to cancel the remove. + * @param {Ext.container.Container} this + * @param {Ext.Component} component The component being removed + */ + 'beforeremove', + /** + * @event add + * @bubbles + * Fires after any {@link Ext.Component} is added or inserted into the container. + * @param {Ext.container.Container} this + * @param {Ext.Component} component The component that was added + * @param {Number} index The index at which the component was added to the container's items collection + */ + 'add', + /** + * @event remove + * @bubbles + * Fires after any {@link Ext.Component} is removed from the container. + * @param {Ext.container.Container} this + * @param {Ext.Component} component The component that was removed + */ + 'remove', + /** + * @event beforecardswitch + * Fires before this container switches the active card. This event + * is only available if this container uses a CardLayout. Note that + * TabPanel and Carousel both get a CardLayout by default, so both + * will have this event. + * A handler can return false to cancel the card switch. + * @param {Ext.container.Container} this + * @param {Ext.Component} newCard The card that will be switched to + * @param {Ext.Component} oldCard The card that will be switched from + * @param {Number} index The index of the card that will be switched to + * @param {Boolean} animated True if this cardswitch will be animated + */ + 'beforecardswitch', + /** + * @event cardswitch + * Fires after this container switches the active card. If the card + * is switched using an animation, this event will fire after the + * animation has finished. This event is only available if this container + * uses a CardLayout. Note that TabPanel and Carousel both get a CardLayout + * by default, so both will have this event. + * @param {Ext.container.Container} this + * @param {Ext.Component} newCard The card that has been switched to + * @param {Ext.Component} oldCard The card that has been switched from + * @param {Number} index The index of the card that has been switched to + * @param {Boolean} animated True if this cardswitch was animated + */ + 'cardswitch' + ); + + // layoutOnShow stack + me.layoutOnShow = Ext.create('Ext.util.MixedCollection'); + me.callParent(); + me.initItems(); + }, + + // @private + initItems : function() { + var me = this, + items = me.items; + + /** + * The MixedCollection containing all the child items of this container. + * @property items + * @type Ext.util.MixedCollection + */ + me.items = Ext.create('Ext.util.MixedCollection', false, me.getComponentId); + + if (items) { + if (!Ext.isArray(items)) { + items = [items]; + } + + me.add(items); + } + }, + + // @private + afterRender : function() { + this.getLayout(); + this.callParent(); + }, + + // @private + setLayout : function(layout) { + var currentLayout = this.layout; + + if (currentLayout && currentLayout.isLayout && currentLayout != layout) { + currentLayout.setOwner(null); + } + + this.layout = layout; + layout.setOwner(this); + }, + + /** + * Returns the {@link Ext.layout.container.AbstractContainer layout} instance currently associated with this Container. + * If a layout has not been instantiated yet, that is done first + * @return {Ext.layout.container.AbstractContainer} The layout + */ + getLayout : function() { + var me = this; + if (!me.layout || !me.layout.isLayout) { + me.setLayout(Ext.layout.Layout.create(me.layout, 'autocontainer')); + } + + return me.layout; + }, + + /** + * Manually force this container's layout to be recalculated. The framwork uses this internally to refresh layouts + * form most cases. + * @return {Ext.container.Container} this + */ + doLayout : function() { + var me = this, + layout = me.getLayout(); + + if (me.rendered && layout && !me.suspendLayout) { + // If either dimension is being auto-set, then it requires a ComponentLayout to be run. + if ((!Ext.isNumber(me.width) || !Ext.isNumber(me.height)) && me.componentLayout.type !== 'autocomponent') { + // Only run the ComponentLayout if it is not already in progress + if (me.componentLayout.layoutBusy !== true) { + me.doComponentLayout(); + if (me.componentLayout.layoutCancelled === true) { + layout.layout(); + } + } + } + // Both dimensions defined, run a ContainerLayout + else { + // Only run the ContainerLayout if it is not already in progress + if (layout.layoutBusy !== true) { + layout.layout(); + } + } + } + + return me; + }, + + // @private + afterLayout : function(layout) { + this.fireEvent('afterlayout', this, layout); + }, + + // @private + prepareItems : function(items, applyDefaults) { + if (!Ext.isArray(items)) { + items = [items]; + } + + // Make sure defaults are applied and item is initialized + var i = 0, + len = items.length, + item; + + for (; i < len; i++) { + item = items[i]; + if (applyDefaults) { + item = this.applyDefaults(item); + } + items[i] = this.lookupComponent(item); + } + return items; + }, + + // @private + applyDefaults : function(config) { + var defaults = this.defaults; + + if (defaults) { + if (Ext.isFunction(defaults)) { + defaults = defaults.call(this, config); + } + + if (Ext.isString(config)) { + config = Ext.ComponentManager.get(config); + Ext.applyIf(config, defaults); + } else if (!config.isComponent) { + Ext.applyIf(config, defaults); + } else { + Ext.applyIf(config, defaults); + } + } + + return config; + }, + + // @private + lookupComponent : function(comp) { + return Ext.isString(comp) ? Ext.ComponentManager.get(comp) : this.createComponent(comp); + }, + + // @private + createComponent : function(config, defaultType) { + // // add in ownerCt at creation time but then immediately + // // remove so that onBeforeAdd can handle it + // var component = Ext.create(Ext.apply({ownerCt: this}, config), defaultType || this.defaultType); + // + // delete component.initialConfig.ownerCt; + // delete component.ownerCt; + + return Ext.ComponentManager.create(config, defaultType || this.defaultType); + }, + + // @private - used as the key lookup function for the items collection + getComponentId : function(comp) { + return comp.getItemId(); + }, + + /** + +Adds {@link Ext.Component Component}(s) to this Container. + +##Description:## + +- Fires the {@link #beforeadd} event before adding. +- The Container's {@link #defaults default config values} will be applied + accordingly (see `{@link #defaults}` for details). +- Fires the `{@link #add}` event after the component has been added. + +##Notes:## + +If the Container is __already rendered__ when `add` +is called, it will render the newly added Component into its content area. + +__**If**__ the Container was configured with a size-managing {@link #layout} manager, the Container +will recalculate its internal layout at this time too. + +Note that the default layout manager simply renders child Components sequentially into the content area and thereafter performs no sizing. + +If adding multiple new child Components, pass them as an array to the `add` method, so that only one layout recalculation is performed. + + tb = new {@link Ext.toolbar.Toolbar}({ + renderTo: document.body + }); // toolbar is rendered + tb.add([{text:'Button 1'}, {text:'Button 2'}]); // add multiple items. ({@link #defaultType} for {@link Ext.toolbar.Toolbar Toolbar} is 'button') + +##Warning:## + +Components directly managed by the BorderLayout layout manager +may not be removed or added. See the Notes for {@link Ext.layout.container.Border BorderLayout} +for more details. + + * @param {...Object/Array} Component + * Either one or more Components to add or an Array of Components to add. + * See `{@link #items}` for additional information. + * + * @return {Ext.Component/Array} The Components that were added. + * @markdown + */ + add : function() { + var me = this, + args = Array.prototype.slice.call(arguments), + hasMultipleArgs, + items, + results = [], + i, + ln, + item, + index = -1, + cmp; + + if (typeof args[0] == 'number') { + index = args.shift(); + } + + hasMultipleArgs = args.length > 1; + if (hasMultipleArgs || Ext.isArray(args[0])) { + + items = hasMultipleArgs ? args : args[0]; + // Suspend Layouts while we add multiple items to the container + me.suspendLayout = true; + for (i = 0, ln = items.length; i < ln; i++) { + item = items[i]; + + // + if (!item) { + Ext.Error.raise("Trying to add a null item as a child of Container with itemId/id: " + me.getItemId()); + } + // + + if (index != -1) { + item = me.add(index + i, item); + } else { + item = me.add(item); + } + results.push(item); + } + // Resume Layouts now that all items have been added and do a single layout for all the items just added + me.suspendLayout = false; + me.doLayout(); + return results; + } + + cmp = me.prepareItems(args[0], true)[0]; + + // Floating Components are not added into the items collection + // But they do get an upward ownerCt link so that they can traverse + // up to their z-index parent. + if (cmp.floating) { + cmp.onAdded(me, index); + } else { + index = (index !== -1) ? index : me.items.length; + if (me.fireEvent('beforeadd', me, cmp, index) !== false && me.onBeforeAdd(cmp) !== false) { + me.items.insert(index, cmp); + cmp.onAdded(me, index); + me.onAdd(cmp, index); + me.fireEvent('add', me, cmp, index); + } + me.doLayout(); + } + return cmp; + }, + + /** + * @private + *

Called by Component#doAutoRender

+ *

Register a Container configured floating: true with this Container's {@link Ext.ZIndexManager ZIndexManager}.

+ *

Components added in ths way will not participate in the layout, but will be rendered + * upon first show in the way that {@link Ext.window.Window Window}s are.

+ *

+ */ + registerFloatingItem: function(cmp) { + var me = this; + if (!me.floatingItems) { + me.floatingItems = Ext.create('Ext.ZIndexManager', me); + } + me.floatingItems.register(cmp); + }, + + onAdd : Ext.emptyFn, + onRemove : Ext.emptyFn, + + /** + * Inserts a Component into this Container at a specified index. Fires the + * {@link #beforeadd} event before inserting, then fires the {@link #add} event after the + * Component has been inserted. + * @param {Number} index The index at which the Component will be inserted + * into the Container's items collection + * @param {Ext.Component} component The child Component to insert.

+ * Ext uses lazy rendering, and will only render the inserted Component should + * it become necessary.

+ * A Component config object may be passed in order to avoid the overhead of + * constructing a real Component object if lazy rendering might mean that the + * inserted Component will not be rendered immediately. To take advantage of + * this 'lazy instantiation', set the {@link Ext.Component#xtype} config + * property to the registered type of the Component wanted.

+ * For a list of all available xtypes, see {@link Ext.Component}. + * @return {Ext.Component} component The Component (or config object) that was + * inserted with the Container's default config values applied. + */ + insert : function(index, comp) { + return this.add(index, comp); + }, + + /** + * Moves a Component within the Container + * @param {Number} fromIdx The index the Component you wish to move is currently at. + * @param {Number} toIdx The new index for the Component. + * @return {Ext.Component} component The Component (or config object) that was moved. + */ + move : function(fromIdx, toIdx) { + var items = this.items, + item; + item = items.removeAt(fromIdx); + if (item === false) { + return false; + } + items.insert(toIdx, item); + this.doLayout(); + return item; + }, + + // @private + onBeforeAdd : function(item) { + var me = this; + + if (item.ownerCt) { + item.ownerCt.remove(item, false); + } + + if (me.border === false || me.border === 0) { + item.border = (item.border === true); + } + }, + + /** + * Removes a component from this container. Fires the {@link #beforeremove} event before removing, then fires + * the {@link #remove} event after the component has been removed. + * @param {Component/String} component The component reference or id to remove. + * @param {Boolean} autoDestroy (optional) True to automatically invoke the removed Component's {@link Ext.Component#destroy} function. + * Defaults to the value of this Container's {@link #autoDestroy} config. + * @return {Ext.Component} component The Component that was removed. + */ + remove : function(comp, autoDestroy) { + var me = this, + c = me.getComponent(comp); + // + if (Ext.isDefined(Ext.global.console) && !c) { + console.warn("Attempted to remove a component that does not exist. Ext.container.Container: remove takes an argument of the component to remove. cmp.remove() is incorrect usage."); + } + // + + if (c && me.fireEvent('beforeremove', me, c) !== false) { + me.doRemove(c, autoDestroy); + me.fireEvent('remove', me, c); + } + + return c; + }, + + // @private + doRemove : function(component, autoDestroy) { + var me = this, + layout = me.layout, + hasLayout = layout && me.rendered; + + me.items.remove(component); + component.onRemoved(); + + if (hasLayout) { + layout.onRemove(component); + } + + me.onRemove(component, autoDestroy); + + if (autoDestroy === true || (autoDestroy !== false && me.autoDestroy)) { + component.destroy(); + } + + if (hasLayout && !autoDestroy) { + layout.afterRemove(component); + } + + if (!me.destroying) { + me.doLayout(); + } + }, + + /** + * Removes all components from this container. + * @param {Boolean} autoDestroy (optional) True to automatically invoke the removed Component's {@link Ext.Component#destroy} function. + * Defaults to the value of this Container's {@link #autoDestroy} config. + * @return {Array} Array of the destroyed components + */ + removeAll : function(autoDestroy) { + var me = this, + removeItems = me.items.items.slice(), + items = [], + i = 0, + len = removeItems.length, + item; + + // Suspend Layouts while we remove multiple items from the container + me.suspendLayout = true; + for (; i < len; i++) { + item = removeItems[i]; + me.remove(item, autoDestroy); + + if (item.ownerCt !== me) { + items.push(item); + } + } + + // Resume Layouts now that all items have been removed and do a single layout + me.suspendLayout = false; + me.doLayout(); + return items; + }, + + // Used by ComponentQuery to retrieve all of the items + // which can potentially be considered a child of this Container. + // This should be overriden by components which have child items + // that are not contained in items. For example dockedItems, menu, etc + // IMPORTANT note for maintainers: + // Items are returned in tree traversal order. Each item is appended to the result array + // followed by the results of that child's getRefItems call. + // Floating child items are appended after internal child items. + getRefItems : function(deep) { + var me = this, + items = me.items.items, + len = items.length, + i = 0, + item, + result = []; + + for (; i < len; i++) { + item = items[i]; + result.push(item); + if (deep && item.getRefItems) { + result.push.apply(result, item.getRefItems(true)); + } + } + + // Append floating items to the list. + // These will only be present after they are rendered. + if (me.floatingItems && me.floatingItems.accessList) { + result.push.apply(result, me.floatingItems.accessList); + } + + return result; + }, + + /** + * Cascades down the component/container heirarchy from this component (passed in the first call), calling the specified function with + * each component. The scope (this reference) of the + * function call will be the scope provided or the current component. The arguments to the function + * will be the args provided or the current component. If the function returns false at any point, + * the cascade is stopped on that branch. + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope of the function (defaults to current component) + * @param {Array} args (optional) The args to call the function with. The current component always passed as the last argument. + * @return {Ext.Container} this + */ + cascade : function(fn, scope, origArgs){ + var me = this, + cs = me.items ? me.items.items : [], + len = cs.length, + i = 0, + c, + args = origArgs ? origArgs.concat(me) : [me], + componentIndex = args.length - 1; + + if (fn.apply(scope || me, args) !== false) { + for(; i < len; i++){ + c = cs[i]; + if (c.cascade) { + c.cascade(fn, scope, origArgs); + } else { + args[componentIndex] = c; + fn.apply(scope || cs, args); + } + } + } + return this; + }, + + /** + * Examines this container's {@link #items} property + * and gets a direct child component of this container. + * @param {String/Number} comp This parameter may be any of the following: + *
    + *
  • a String : representing the {@link Ext.Component#itemId itemId} + * or {@link Ext.Component#id id} of the child component
  • + *
  • a Number : representing the position of the child component + * within the {@link #items} property
  • + *
+ *

For additional information see {@link Ext.util.MixedCollection#get}. + * @return Ext.Component The component (if found). + */ + getComponent : function(comp) { + if (Ext.isObject(comp)) { + comp = comp.getItemId(); + } + + return this.items.get(comp); + }, + + /** + * Retrieves all descendant components which match the passed selector. + * Executes an Ext.ComponentQuery.query using this container as its root. + * @param {String} selector Selector complying to an Ext.ComponentQuery selector + * @return {Array} Ext.Component's which matched the selector + */ + query : function(selector) { + return Ext.ComponentQuery.query(selector, this); + }, + + /** + * Retrieves the first direct child of this container which matches the passed selector. + * The passed in selector must comply with an Ext.ComponentQuery selector. + * @param {String} selector An Ext.ComponentQuery selector + * @return Ext.Component + */ + child : function(selector) { + return this.query('> ' + selector)[0] || null; + }, + + /** + * Retrieves the first descendant of this container which matches the passed selector. + * The passed in selector must comply with an Ext.ComponentQuery selector. + * @param {String} selector An Ext.ComponentQuery selector + * @return Ext.Component + */ + down : function(selector) { + return this.query(selector)[0] || null; + }, + + // inherit docs + show : function() { + this.callParent(arguments); + this.performDeferredLayouts(); + return this; + }, + + // Lay out any descendant containers who queued a layout operation during the time this was hidden + // This is also called by Panel after it expands because descendants of a collapsed Panel allso queue any layout ops. + performDeferredLayouts: function() { + var layoutCollection = this.layoutOnShow, + ln = layoutCollection.getCount(), + i = 0, + needsLayout, + item; + + for (; i < ln; i++) { + item = layoutCollection.get(i); + needsLayout = item.needsLayout; + + if (Ext.isObject(needsLayout)) { + item.doComponentLayout(needsLayout.width, needsLayout.height, needsLayout.isSetSize, needsLayout.ownerCt); + } + } + layoutCollection.clear(); + }, + + //@private + // Enable all immediate children that was previously disabled + onEnable: function() { + Ext.Array.each(this.query('[isFormField]'), function(item) { + if (item.resetDisable) { + item.enable(); + delete item.resetDisable; + } + }); + this.callParent(); + }, + + // @private + // Disable all immediate children that was previously disabled + onDisable: function() { + Ext.Array.each(this.query('[isFormField]'), function(item) { + if (item.resetDisable !== false && !item.disabled) { + item.disable(); + item.resetDisable = true; + } + }); + this.callParent(); + }, + + /** + * Occurs before componentLayout is run. Returning false from this method will prevent the containerLayout + * from being executed. + */ + beforeLayout: function() { + return true; + }, + + // @private + beforeDestroy : function() { + var me = this, + items = me.items, + c; + + if (items) { + while ((c = items.first())) { + me.doRemove(c, true); + } + } + + Ext.destroy( + me.layout, + me.floatingItems + ); + me.callParent(); + } +}); +/** + * @class Ext.container.Container + * @extends Ext.container.AbstractContainer + *

Base class for any {@link Ext.Component} that may contain other Components. Containers handle the + * basic behavior of containing items, namely adding, inserting and removing items.

+ * + *

The most commonly used Container classes are {@link Ext.panel.Panel}, {@link Ext.window.Window} and {@link Ext.tab.Panel}. + * If you do not need the capabilities offered by the aforementioned classes you can create a lightweight + * Container to be encapsulated by an HTML element to your specifications by using the + * {@link Ext.Component#autoEl autoEl} config option.

+ * + * {@img Ext.Container/Ext.Container.png Ext.Container component} + *

The code below illustrates how to explicitly create a Container:


+// explicitly create a Container
+Ext.create('Ext.container.Container', {
+    layout: {
+        type: 'hbox'
+    },
+    width: 400,
+    renderTo: Ext.getBody(),
+    border: 1,
+    style: {borderColor:'#000000', borderStyle:'solid', borderWidth:'1px'},
+    defaults: {
+        labelWidth: 80,
+        // implicitly create Container by specifying xtype
+        xtype: 'datefield',
+        flex: 1,
+        style: {
+            padding: '10px'
+        }
+    },
+    items: [{
+        xtype: 'datefield',
+        name: 'startDate',
+        fieldLabel: 'Start date'
+    },{
+        xtype: 'datefield',
+        name: 'endDate',
+        fieldLabel: 'End date'
+    }]
+});
+

+ * + *

Layout

+ *

Container classes delegate the rendering of child Components to a layout + * manager class which must be configured into the Container using the + * {@link #layout} configuration property.

+ *

When either specifying child {@link #items} of a Container, + * or dynamically {@link #add adding} Components to a Container, remember to + * consider how you wish the Container to arrange those child elements, and + * whether those child elements need to be sized using one of Ext's built-in + * {@link #layout} schemes. By default, Containers use the + * {@link Ext.layout.container.Auto Auto} scheme which only + * renders child components, appending them one after the other inside the + * Container, and does not apply any sizing at all.

+ *

A common mistake is when a developer neglects to specify a + * {@link #layout} (e.g. widgets like GridPanels or + * TreePanels are added to Containers for which no {@link #layout} + * has been specified). If a Container is left to use the default + * {Ext.layout.container.Auto Auto} scheme, none of its + * child components will be resized, or changed in any way when the Container + * is resized.

+ *

Certain layout managers allow dynamic addition of child components. + * Those that do include {@link Ext.layout.container.Card}, + * {@link Ext.layout.container.Anchor}, {@link Ext.layout.container.VBox}, {@link Ext.layout.container.HBox}, and + * {@link Ext.layout.container.Table}. For example:


+//  Create the GridPanel.
+var myNewGrid = new Ext.grid.Panel({
+    store: myStore,
+    headers: myHeaders,
+    title: 'Results', // the title becomes the title of the tab
+});
+
+myTabPanel.add(myNewGrid); // {@link Ext.tab.Panel} implicitly uses {@link Ext.layout.container.Card Card}
+myTabPanel.{@link Ext.tab.Panel#setActiveTab setActiveTab}(myNewGrid);
+ * 

+ *

The example above adds a newly created GridPanel to a TabPanel. Note that + * a TabPanel uses {@link Ext.layout.container.Card} as its layout manager which + * means all its child items are sized to {@link Ext.layout.container.Fit fit} + * exactly into its client area. + *

Overnesting is a common problem. + * An example of overnesting occurs when a GridPanel is added to a TabPanel + * by wrapping the GridPanel inside a wrapping Panel (that has no + * {@link #layout} specified) and then add that wrapping Panel + * to the TabPanel. The point to realize is that a GridPanel is a + * Component which can be added directly to a Container. If the wrapping Panel + * has no {@link #layout} configuration, then the overnested + * GridPanel will not be sized as expected.

+ * + *

Adding via remote configuration

+ * + *

A server side script can be used to add Components which are generated dynamically on the server. + * An example of adding a GridPanel to a TabPanel where the GridPanel is generated by the server + * based on certain parameters: + *


+// execute an Ajax request to invoke server side script:
+Ext.Ajax.request({
+    url: 'gen-invoice-grid.php',
+    // send additional parameters to instruct server script
+    params: {
+        startDate: Ext.getCmp('start-date').getValue(),
+        endDate: Ext.getCmp('end-date').getValue()
+    },
+    // process the response object to add it to the TabPanel:
+    success: function(xhr) {
+        var newComponent = eval(xhr.responseText); // see discussion below
+        myTabPanel.add(newComponent); // add the component to the TabPanel
+        myTabPanel.setActiveTab(newComponent);
+    },
+    failure: function() {
+        Ext.Msg.alert("Grid create failed", "Server communication failure");
+    }
+});
+
+ *

The server script needs to return a JSON representation of a configuration object, which, when decoded + * will return a config object with an {@link Ext.Component#xtype xtype}. The server might return the following + * JSON:


+{
+    "xtype": 'grid',
+    "title": 'Invoice Report',
+    "store": {
+        "model": 'Invoice',
+        "proxy": {
+            "type": 'ajax',
+            "url": 'get-invoice-data.php',
+            "reader": {
+                "type": 'json'
+                "record": 'transaction',
+                "idProperty": 'id',
+                "totalRecords": 'total'
+            })
+        },
+        "autoLoad": {
+            "params": {
+                "startDate": '01/01/2008',
+                "endDate": '01/31/2008'
+            }
+        }
+    },
+    "headers": [
+        {"header": "Customer", "width": 250, "dataIndex": 'customer', "sortable": true},
+        {"header": "Invoice Number", "width": 120, "dataIndex": 'invNo', "sortable": true},
+        {"header": "Invoice Date", "width": 100, "dataIndex": 'date', "renderer": Ext.util.Format.dateRenderer('M d, y'), "sortable": true},
+        {"header": "Value", "width": 120, "dataIndex": 'value', "renderer": 'usMoney', "sortable": true}
+    ]
+}
+
+ *

When the above code fragment is passed through the eval function in the success handler + * of the Ajax request, the result will be a config object which, when added to a Container, will cause instantiation + * of a GridPanel. Be sure that the Container is configured with a layout which sizes and positions the child items to your requirements.

+ *

Note: since the code above is generated by a server script, the autoLoad params for + * the Store, the user's preferred date format, the metadata to allow generation of the Model layout, and the ColumnModel + * can all be generated into the code since these are all known on the server.

+ * + * @xtype container + */ +Ext.define('Ext.container.Container', { + extend: 'Ext.container.AbstractContainer', + alias: 'widget.container', + alternateClassName: 'Ext.Container', + + /** + * Return the immediate child Component in which the passed element is located. + * @param el The element to test. + * @return {Component} The child item which contains the passed element. + */ + getChildByElement: function(el) { + var item, + itemEl, + i = 0, + it = this.items.items, + ln = it.length; + + el = Ext.getDom(el); + for (; i < ln; i++) { + item = it[i]; + itemEl = item.getEl(); + if ((itemEl.dom === el) || itemEl.contains(el)) { + return item; + } + } + return null; + } +}); + +/** + * @class Ext.toolbar.Fill + * @extends Ext.Component + * A non-rendering placeholder item which instructs the Toolbar's Layout to begin using + * the right-justified button container. + * + * {@img Ext.toolbar.Fill/Ext.toolbar.Fill.png Toolbar Fill} + * Example usage: +

+    Ext.create('Ext.panel.Panel', {
+        title: 'Toolbar Fill Example',
+        width: 300,
+        height: 200,
+        tbar : [
+            'Item 1',
+            {xtype: 'tbfill'}, // or '->'
+            'Item 2'
+        ],
+        renderTo: Ext.getBody()
+    });
+
+ * @constructor + * Creates a new Fill + * @xtype tbfill + */ +Ext.define('Ext.toolbar.Fill', { + extend: 'Ext.Component', + alias: 'widget.tbfill', + alternateClassName: 'Ext.Toolbar.Fill', + isFill : true, + flex: 1 +}); +/** + * @class Ext.toolbar.Item + * @extends Ext.Component + * The base class that other non-interacting Toolbar Item classes should extend in order to + * get some basic common toolbar item functionality. + * @constructor + * Creates a new Item + * @param {HTMLElement} el + * @xtype tbitem + */ +Ext.define('Ext.toolbar.Item', { + extend: 'Ext.Component', + alias: 'widget.tbitem', + alternateClassName: 'Ext.Toolbar.Item', + enable:Ext.emptyFn, + disable:Ext.emptyFn, + focus:Ext.emptyFn + /** + * @cfg {String} overflowText Text to be used for the menu if the item is overflowed. + */ +}); +/** + * @class Ext.toolbar.Separator + * @extends Ext.toolbar.Item + * A simple class that adds a vertical separator bar between toolbar items + * (css class:'x-toolbar-separator'). + * {@img Ext.toolbar.Separator/Ext.toolbar.Separator.png Toolbar Separator} + * Example usage: + *

+    Ext.create('Ext.panel.Panel', {
+        title: 'Toolbar Seperator Example',
+        width: 300,
+        height: 200,
+        tbar : [
+            'Item 1',
+            {xtype: 'tbseparator'}, // or '-'
+            'Item 2'
+        ],
+        renderTo: Ext.getBody()
+    }); 
+
+ * @constructor + * Creates a new Separator + * @xtype tbseparator + */ +Ext.define('Ext.toolbar.Separator', { + extend: 'Ext.toolbar.Item', + alias: 'widget.tbseparator', + alternateClassName: 'Ext.Toolbar.Separator', + baseCls: Ext.baseCSSPrefix + 'toolbar-separator', + focusable: false +}); +/** + * @class Ext.menu.Manager + * Provides a common registry of all menus on a page. + * @singleton + */ +Ext.define('Ext.menu.Manager', { + singleton: true, + requires: [ + 'Ext.util.MixedCollection', + 'Ext.util.KeyMap' + ], + alternateClassName: 'Ext.menu.MenuMgr', + + uses: ['Ext.menu.Menu'], + + menus: {}, + groups: {}, + attached: false, + lastShow: new Date(), + + init: function() { + var me = this; + + me.active = Ext.create('Ext.util.MixedCollection'); + Ext.getDoc().addKeyListener(27, function() { + if (me.active.length > 0) { + me.hideAll(); + } + }, me); + }, + + /** + * Hides all menus that are currently visible + * @return {Boolean} success True if any active menus were hidden. + */ + hideAll: function() { + var active = this.active, + c; + if (active && active.length > 0) { + c = active.clone(); + c.each(function(m) { + m.hide(); + }); + return true; + } + return false; + }, + + onHide: function(m) { + var me = this, + active = me.active; + active.remove(m); + if (active.length < 1) { + Ext.getDoc().un('mousedown', me.onMouseDown, me); + me.attached = false; + } + }, + + onShow: function(m) { + var me = this, + active = me.active, + last = active.last(), + attached = me.attached, + menuEl = m.getEl(), + zIndex; + + me.lastShow = new Date(); + active.add(m); + if (!attached) { + Ext.getDoc().on('mousedown', me.onMouseDown, me); + me.attached = true; + } + m.toFront(); + }, + + onBeforeHide: function(m) { + if (m.activeChild) { + m.activeChild.hide(); + } + if (m.autoHideTimer) { + clearTimeout(m.autoHideTimer); + delete m.autoHideTimer; + } + }, + + onBeforeShow: function(m) { + var active = this.active, + parentMenu = m.parentMenu; + + active.remove(m); + if (!parentMenu && !m.allowOtherMenus) { + this.hideAll(); + } + else if (parentMenu && parentMenu.activeChild && m != parentMenu.activeChild) { + parentMenu.activeChild.hide(); + } + }, + + // private + onMouseDown: function(e) { + var me = this, + active = me.active, + lastShow = me.lastShow; + + if (Ext.Date.getElapsed(lastShow) > 50 && active.length > 0 && !e.getTarget('.' + Ext.baseCSSPrefix + 'menu')) { + me.hideAll(); + } + }, + + // private + register: function(menu) { + var me = this; + + if (!me.active) { + me.init(); + } + + if (menu.floating) { + me.menus[menu.id] = menu; + menu.on({ + beforehide: me.onBeforeHide, + hide: me.onHide, + beforeshow: me.onBeforeShow, + show: me.onShow, + scope: me + }); + } + }, + + /** + * Returns a {@link Ext.menu.Menu} object + * @param {String/Object} menu The string menu id, an existing menu object reference, or a Menu config that will + * be used to generate and return a new Menu this. + * @return {Ext.menu.Menu} The specified menu, or null if none are found + */ + get: function(menu) { + var menus = this.menus; + + if (typeof menu == 'string') { // menu id + if (!menus) { // not initialized, no menus to return + return null; + } + return menus[menu]; + } else if (menu.isMenu) { // menu instance + return menu; + } else if (Ext.isArray(menu)) { // array of menu items + return Ext.create('Ext.menu.Menu', {items:menu}); + } else { // otherwise, must be a config + return Ext.ComponentManager.create(menu, 'menu'); + } + }, + + // private + unregister: function(menu) { + var me = this, + menus = me.menus, + active = me.active; + + delete menus[menu.id]; + active.remove(menu); + menu.un({ + beforehide: me.onBeforeHide, + hide: me.onHide, + beforeshow: me.onBeforeShow, + show: me.onShow, + scope: me + }); + }, + + // private + registerCheckable: function(menuItem) { + var groups = this.groups, + groupId = menuItem.group; + + if (groupId) { + if (!groups[groupId]) { + groups[groupId] = []; + } + + groups[groupId].push(menuItem); + } + }, + + // private + unregisterCheckable: function(menuItem) { + var groups = this.groups, + groupId = menuItem.group; + + if (groupId) { + Ext.Array.remove(groups[groupId], menuItem); + } + }, + + onCheckChange: function(menuItem, state) { + var groups = this.groups, + groupId = menuItem.group, + i = 0, + group, ln, curr; + + if (groupId && state) { + group = groups[groupId]; + ln = group.length; + for (; i < ln; i++) { + curr = group[i]; + if (curr != menuItem) { + curr.setChecked(false); + } + } + } + } +}); +/** + * @class Ext.button.Button + * @extends Ext.Component + +Create simple buttons with this component. Customisations include {@link #config-iconAlign aligned} +{@link #config-iconCls icons}, {@link #config-menu dropdown menus}, {@link #config-tooltip tooltips} +and {@link #config-scale sizing options}. Specify a {@link #config-handler handler} to run code when +a user clicks the button, or use {@link #config-listeners listeners} for other events such as +{@link #events-mouseover mouseover}. + +{@img Ext.button.Button/Ext.button.Button1.png Ext.button.Button component} +Example usage: + + Ext.create('Ext.Button', { + text: 'Click me', + renderTo: Ext.getBody(), + handler: function() { + alert('You clicked the button!') + } + }); + +The {@link #handler} configuration can also be updated dynamically using the {@link #setHandler} method. +Example usage: + + Ext.create('Ext.Button', { + text : 'Dyanmic Handler Button', + renderTo: Ext.getBody(), + handler : function() { + //this button will spit out a different number every time you click it. + //so firstly we must check if that number is already set: + if (this.clickCount) { + //looks like the property is already set, so lets just add 1 to that number and alert the user + this.clickCount++; + alert('You have clicked the button "' + this.clickCount + '" times.\n\nTry clicking it again..'); + } else { + //if the clickCount property is not set, we will set it and alert the user + this.clickCount = 1; + alert('You just clicked the button for the first time!\n\nTry pressing it again..'); + } + } + }); + +A button within a container: + + Ext.create('Ext.Container', { + renderTo: Ext.getBody(), + items : [ + { + xtype: 'button', + text : 'My Button' + } + ] + }); + +A useful option of Button is the {@link #scale} configuration. This configuration has three different options: +* `'small'` +* `'medium'` +* `'large'` + +{@img Ext.button.Button/Ext.button.Button2.png Ext.button.Button component} +Example usage: + + Ext.create('Ext.Button', { + renderTo: document.body, + text : 'Click me', + scale : 'large' + }); + +Buttons can also be toggled. To enable this, you simple set the {@link #enableToggle} property to `true`. +{@img Ext.button.Button/Ext.button.Button3.png Ext.button.Button component} +Example usage: + + Ext.create('Ext.Button', { + renderTo: Ext.getBody(), + text: 'Click Me', + enableToggle: true + }); + +You can assign a menu to a button by using the {@link #menu} configuration. This standard configuration can either be a reference to a {@link Ext.menu.Menu menu} +object, a {@link Ext.menu.Menu menu} id or a {@link Ext.menu.Menu menu} config blob. When assigning a menu to a button, an arrow is automatically added to the button. +You can change the alignment of the arrow using the {@link #arrowAlign} configuration on button. +{@img Ext.button.Button/Ext.button.Button4.png Ext.button.Button component} +Example usage: + + Ext.create('Ext.Button', { + text : 'Menu button', + renderTo : Ext.getBody(), + arrowAlign: 'bottom', + menu : [ + {text: 'Item 1'}, + {text: 'Item 2'}, + {text: 'Item 3'}, + {text: 'Item 4'} + ] + }); + +Using listeners, you can easily listen to events fired by any component, using the {@link #listeners} configuration or using the {@link #addListener} method. +Button has a variety of different listeners: +* `click` +* `toggle` +* `mouseover` +* `mouseout` +* `mouseshow` +* `menuhide` +* `menutriggerover` +* `menutriggerout` + +Example usage: + + Ext.create('Ext.Button', { + text : 'Button', + renderTo : Ext.getBody(), + listeners: { + click: function() { + //this == the button, as we are in the local scope + this.setText('I was clicked!'); + }, + mouseover: function() { + //set a new config which says we moused over, if not already set + if (!this.mousedOver) { + this.mousedOver = true; + alert('You moused over a button!\n\nI wont do this again.'); + } + } + } + }); + + * @constructor + * Create a new button + * @param {Object} config The config object + * @xtype button + * @markdown + * @docauthor Robert Dougan + */ +Ext.define('Ext.button.Button', { + + /* Begin Definitions */ + alias: 'widget.button', + extend: 'Ext.Component', + + requires: [ + 'Ext.menu.Manager', + 'Ext.util.ClickRepeater', + 'Ext.layout.component.Button', + 'Ext.util.TextMetrics', + 'Ext.util.KeyMap' + ], + + alternateClassName: 'Ext.Button', + /* End Definitions */ + + isButton: true, + componentLayout: 'button', + + /** + * Read-only. True if this button is hidden + * @type Boolean + */ + hidden: false, + + /** + * Read-only. True if this button is disabled + * @type Boolean + */ + disabled: false, + + /** + * Read-only. True if this button is pressed (only if enableToggle = true) + * @type Boolean + */ + pressed: false, + + /** + * @cfg {String} text The button text to be used as innerHTML (html tags are accepted) + */ + + /** + * @cfg {String} icon The path to an image to display in the button (the image will be set as the background-image + * CSS property of the button by default, so if you want a mixed icon/text button, set cls:'x-btn-text-icon') + */ + + /** + * @cfg {Function} handler A function called when the button is clicked (can be used instead of click event). + * The handler is passed the following parameters:
    + *
  • b : Button
    This Button.
  • + *
  • e : EventObject
    The click event.
  • + *
+ */ + + /** + * @cfg {Number} minWidth The minimum width for this button (used to give a set of buttons a common width). + * See also {@link Ext.panel.Panel}.{@link Ext.panel.Panel#minButtonWidth minButtonWidth}. + */ + + /** + * @cfg {String/Object} tooltip The tooltip for the button - can be a string to be used as innerHTML (html tags are accepted) or QuickTips config object + */ + + /** + * @cfg {Boolean} hidden True to start hidden (defaults to false) + */ + + /** + * @cfg {Boolean} disabled True to start disabled (defaults to false) + */ + + /** + * @cfg {Boolean} pressed True to start pressed (only if enableToggle = true) + */ + + /** + * @cfg {String} toggleGroup The group this toggle button is a member of (only 1 per group can be pressed) + */ + + /** + * @cfg {Boolean/Object} repeat True to repeat fire the click event while the mouse is down. This can also be + * a {@link Ext.util.ClickRepeater ClickRepeater} config object (defaults to false). + */ + + /** + * @cfg {Number} tabIndex Set a DOM tabIndex for this button (defaults to undefined) + */ + + /** + * @cfg {Boolean} allowDepress + * False to not allow a pressed Button to be depressed (defaults to undefined). Only valid when {@link #enableToggle} is true. + */ + + /** + * @cfg {Boolean} enableToggle + * True to enable pressed/not pressed toggling (defaults to false) + */ + enableToggle: false, + + /** + * @cfg {Function} toggleHandler + * Function called when a Button with {@link #enableToggle} set to true is clicked. Two arguments are passed:
    + *
  • button : Ext.button.Button
    this Button object
  • + *
  • state : Boolean
    The next state of the Button, true means pressed.
  • + *
+ */ + + /** + * @cfg {Mixed} menu + * Standard menu attribute consisting of a reference to a menu object, a menu id or a menu config blob (defaults to undefined). + */ + + /** + * @cfg {String} menuAlign + * The position to align the menu to (see {@link Ext.core.Element#alignTo} for more details, defaults to 'tl-bl?'). + */ + menuAlign: 'tl-bl?', + + /** + * @cfg {String} overflowText If used in a {@link Ext.toolbar.Toolbar Toolbar}, the + * text to be used if this item is shown in the overflow menu. See also + * {@link Ext.toolbar.Item}.{@link Ext.toolbar.Item#overflowText overflowText}. + */ + + /** + * @cfg {String} iconCls + * A css class which sets a background image to be used as the icon for this button + */ + + /** + * @cfg {String} type + * submit, reset or button - defaults to 'button' + */ + type: 'button', + + /** + * @cfg {String} clickEvent + * The DOM event that will fire the handler of the button. This can be any valid event name (dblclick, contextmenu). + * Defaults to 'click'. + */ + clickEvent: 'click', + + /** + * @cfg {Boolean} preventDefault + * True to prevent the default action when the {@link #clickEvent} is processed. Defaults to true. + */ + preventDefault: true, + + /** + * @cfg {Boolean} handleMouseEvents + * False to disable visual cues on mouseover, mouseout and mousedown (defaults to true) + */ + handleMouseEvents: true, + + /** + * @cfg {String} tooltipType + * The type of tooltip to use. Either 'qtip' (default) for QuickTips or 'title' for title attribute. + */ + tooltipType: 'qtip', + + /** + * @cfg {String} baseCls + * The base CSS class to add to all buttons. (Defaults to 'x-btn') + */ + baseCls: Ext.baseCSSPrefix + 'btn', + + /** + * @cfg {String} pressedCls + * The CSS class to add to a button when it is in the pressed state. (Defaults to 'x-btn-pressed') + */ + pressedCls: 'pressed', + + /** + * @cfg {String} overCls + * The CSS class to add to a button when it is in the over (hovered) state. (Defaults to 'x-btn-over') + */ + overCls: 'over', + + /** + * @cfg {String} focusCls + * The CSS class to add to a button when it is in the focussed state. (Defaults to 'x-btn-focus') + */ + focusCls: 'focus', + + /** + * @cfg {String} menuActiveCls + * The CSS class to add to a button when it's menu is active. (Defaults to 'x-btn-menu-active') + */ + menuActiveCls: 'menu-active', + + ariaRole: 'button', + + // inherited + renderTpl: + '' + + '' + + ' tabIndex="{tabIndex}" role="link">' + + '{text}' + + '' + + '' + + '' + + '' + + '' + + '' , + + /** + * @cfg {String} scale + *

(Optional) The size of the Button. Three values are allowed:

+ *
    + *
  • 'small'
    Results in the button element being 16px high.
  • + *
  • 'medium'
    Results in the button element being 24px high.
  • + *
  • 'large'
    Results in the button element being 32px high.
  • + *
+ *

Defaults to 'small'.

+ */ + scale: 'small', + + /** + * @private An array of allowed scales. + */ + allowedScales: ['small', 'medium', 'large'], + + /** + * @cfg {Object} scope The scope (this reference) in which the + * {@link #handler} and {@link #toggleHandler} is + * executed. Defaults to this Button. + */ + + /** + * @cfg {String} iconAlign + *

(Optional) The side of the Button box to render the icon. Four values are allowed:

+ *
    + *
  • 'top'
  • + *
  • 'right'
  • + *
  • 'bottom'
  • + *
  • 'left'
  • + *
+ *

Defaults to 'left'.

+ */ + iconAlign: 'left', + + /** + * @cfg {String} arrowAlign + *

(Optional) The side of the Button box to render the arrow if the button has an associated {@link #menu}. + * Two values are allowed:

+ *
    + *
  • 'right'
  • + *
  • 'bottom'
  • + *
+ *

Defaults to 'right'.

+ */ + arrowAlign: 'right', + + /** + * @cfg {String} arrowCls + *

(Optional) The className used for the inner arrow element if the button has a menu.

+ */ + arrowCls: 'arrow', + + /** + * @cfg {Ext.Template} template (Optional) + *

A {@link Ext.Template Template} used to create the Button's DOM structure.

+ * Instances, or subclasses which need a different DOM structure may provide a different + * template layout in conjunction with an implementation of {@link #getTemplateArgs}. + * @type Ext.Template + * @property template + */ + + /** + * @cfg {String} cls + * A CSS class string to apply to the button's main element. + */ + + /** + * @property menu + * @type Menu + * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the {@link #menu} config option. + */ + + /** + * @cfg {Boolean} autoWidth + * By default, if a width is not specified the button will attempt to stretch horizontally to fit its content. + * If the button is being managed by a width sizing layout (hbox, fit, anchor), set this to false to prevent + * the button from doing this automatic sizing. + * Defaults to undefined. + */ + + maskOnDisable: false, + + // inherit docs + initComponent: function() { + var me = this; + me.callParent(arguments); + + me.addEvents( + /** + * @event click + * Fires when this button is clicked + * @param {Button} this + * @param {EventObject} e The click event + */ + 'click', + + /** + * @event toggle + * Fires when the 'pressed' state of this button changes (only if enableToggle = true) + * @param {Button} this + * @param {Boolean} pressed + */ + 'toggle', + + /** + * @event mouseover + * Fires when the mouse hovers over the button + * @param {Button} this + * @param {Event} e The event object + */ + 'mouseover', + + /** + * @event mouseout + * Fires when the mouse exits the button + * @param {Button} this + * @param {Event} e The event object + */ + 'mouseout', + + /** + * @event menushow + * If this button has a menu, this event fires when it is shown + * @param {Button} this + * @param {Menu} menu + */ + 'menushow', + + /** + * @event menuhide + * If this button has a menu, this event fires when it is hidden + * @param {Button} this + * @param {Menu} menu + */ + 'menuhide', + + /** + * @event menutriggerover + * If this button has a menu, this event fires when the mouse enters the menu triggering element + * @param {Button} this + * @param {Menu} menu + * @param {EventObject} e + */ + 'menutriggerover', + + /** + * @event menutriggerout + * If this button has a menu, this event fires when the mouse leaves the menu triggering element + * @param {Button} this + * @param {Menu} menu + * @param {EventObject} e + */ + 'menutriggerout' + ); + + if (me.menu) { + // Flag that we'll have a splitCls + me.split = true; + + // retrieve menu by id or instantiate instance if needed + me.menu = Ext.menu.Manager.get(me.menu); + me.menu.ownerCt = me; + } + + // Accept url as a synonym for href + if (me.url) { + me.href = me.url; + } + + // preventDefault defaults to false for links + if (me.href && !me.hasOwnProperty('preventDefault')) { + me.preventDefault = false; + } + + if (Ext.isString(me.toggleGroup)) { + me.enableToggle = true; + } + + }, + + // private + initAria: function() { + this.callParent(); + var actionEl = this.getActionEl(); + if (this.menu) { + actionEl.dom.setAttribute('aria-haspopup', true); + } + }, + + // inherit docs + getActionEl: function() { + return this.btnEl; + }, + + // inherit docs + getFocusEl: function() { + return this.btnEl; + }, + + // private + setButtonCls: function() { + var me = this, + el = me.el, + cls = []; + + if (me.useSetClass) { + if (!Ext.isEmpty(me.oldCls)) { + me.removeClsWithUI(me.oldCls); + me.removeClsWithUI(me.pressedCls); + } + + // Check whether the button has an icon or not, and if it has an icon, what is th alignment + if (me.iconCls || me.icon) { + if (me.text) { + cls.push('icon-text-' + me.iconAlign); + } else { + cls.push('icon'); + } + } else if (me.text) { + cls.push('noicon'); + } + + me.oldCls = cls; + me.addClsWithUI(cls); + me.addClsWithUI(me.pressed ? me.pressedCls : null); + } + }, + + // private + onRender: function(ct, position) { + // classNames for the button + var me = this, + repeater, btn; + + // Apply the renderData to the template args + Ext.applyIf(me.renderData, me.getTemplateArgs()); + + // Extract the button and the button wrapping element + Ext.applyIf(me.renderSelectors, { + btnEl : me.href ? 'a' : 'button', + btnWrap: 'em', + btnInnerEl: '.' + me.baseCls + '-inner' + }); + + if (me.scale) { + me.ui = me.ui + '-' + me.scale; + } + + // Render internal structure + me.callParent(arguments); + + // If it is a split button + has a toolip for the arrow + if (me.split && me.arrowTooltip) { + me.arrowEl.dom[me.tooltipType] = me.arrowTooltip; + } + + // Add listeners to the focus and blur events on the element + me.mon(me.btnEl, { + scope: me, + focus: me.onFocus, + blur : me.onBlur + }); + + // Set btn as a local variable for easy access + btn = me.el; + + if (me.icon) { + me.setIcon(me.icon); + } + + if (me.iconCls) { + me.setIconCls(me.iconCls); + } + + if (me.tooltip) { + me.setTooltip(me.tooltip, true); + } + + // Add the mouse events to the button + if (me.handleMouseEvents) { + me.mon(btn, { + scope: me, + mouseover: me.onMouseOver, + mouseout: me.onMouseOut, + mousedown: me.onMouseDown + }); + + if (me.split) { + me.mon(btn, { + mousemove: me.onMouseMove, + scope: me + }); + } + } + + // Check if the button has a menu + if (me.menu) { + me.mon(me.menu, { + scope: me, + show: me.onMenuShow, + hide: me.onMenuHide + }); + + me.keyMap = Ext.create('Ext.util.KeyMap', me.el, { + key: Ext.EventObject.DOWN, + handler: me.onDownKey, + scope: me + }); + } + + // Check if it is a repeat button + if (me.repeat) { + repeater = Ext.create('Ext.util.ClickRepeater', btn, Ext.isObject(me.repeat) ? me.repeat: {}); + me.mon(repeater, 'click', me.onRepeatClick, me); + } else { + me.mon(btn, me.clickEvent, me.onClick, me); + } + + // Register the button in the toggle manager + Ext.ButtonToggleManager.register(me); + }, + + /** + *

This method returns an object which provides substitution parameters for the {@link #renderTpl XTemplate} used + * to create this Button's DOM structure.

+ *

Instances or subclasses which use a different Template to create a different DOM structure may need to provide their + * own implementation of this method.

+ *

The default implementation which provides data for the default {@link #template} returns an Object containing the + * following properties:

    + *
  • type : The <button>'s {@link #type}
  • + *
  • splitCls : A CSS class to determine the presence and position of an arrow icon. ('x-btn-arrow' or 'x-btn-arrow-bottom' or '')
  • + *
  • cls : A CSS class name applied to the Button's main <tbody> element which determines the button's scale and icon alignment.
  • + *
  • text : The {@link #text} to display ion the Button.
  • + *
  • tabIndex : The tab index within the input flow.
  • + *
+ * @return {Array} Substitution data for a Template. + */ + getTemplateArgs: function() { + var me = this, + persistentPadding = me.getPersistentBtnPadding(), + innerSpanStyle = ''; + + // Create negative margin offsets to counteract persistent button padding if needed + if (Math.max.apply(Math, persistentPadding) > 0) { + innerSpanStyle = 'margin:' + Ext.Array.map(persistentPadding, function(pad) { + return -pad + 'px'; + }).join(' '); + } + + return { + href : me.getHref(), + target : me.target || '_blank', + type : me.type, + splitCls : me.getSplitCls(), + cls : me.cls, + text : me.text || ' ', + tabIndex : me.tabIndex, + innerSpanStyle: innerSpanStyle + }; + }, + + /** + * @private + * If there is a configured href for this Button, returns the href with parameters appended. + * @returns The href string with parameters appended. + */ + getHref: function() { + var me = this; + return me.href ? Ext.urlAppend(me.href, me.params + Ext.Object.toQueryString(Ext.apply(Ext.apply({}, me.baseParams)))) : false; + }, + + /** + *

Only valid if the Button was originally configured with a {@link #url}

+ *

Sets the href of the link dynamically according to the params passed, and any {@link #baseParams} configured.

+ * @param {Object} Parameters to use in the href URL. + */ + setParams: function(p) { + this.params = p; + this.btnEl.dom.href = this.getHref(); + }, + + getSplitCls: function() { + var me = this; + return me.split ? (me.baseCls + '-' + me.arrowCls) + ' ' + (me.baseCls + '-' + me.arrowCls + '-' + me.arrowAlign) : ''; + }, + + // private + afterRender: function() { + var me = this; + me.useSetClass = true; + me.setButtonCls(); + me.doc = Ext.getDoc(); + this.callParent(arguments); + }, + + /** + * Sets the CSS class that provides a background image to use as the button's icon. This method also changes + * the value of the {@link #iconCls} config internally. + * @param {String} cls The CSS class providing the icon image + * @return {Ext.button.Button} this + */ + setIconCls: function(cls) { + var me = this, + btnInnerEl = me.btnInnerEl; + if (btnInnerEl) { + // Remove the previous iconCls from the button + btnInnerEl.removeCls(me.iconCls); + btnInnerEl.addCls(cls || ''); + me.setButtonCls(); + } + me.iconCls = cls; + return me; + }, + + /** + * Sets the tooltip for this Button. + * @param {String/Object} tooltip. This may be:
    + *
  • String : A string to be used as innerHTML (html tags are accepted) to show in a tooltip
  • + *
  • Object : A configuration object for {@link Ext.tip.QuickTipManager#register}.
  • + *
+ * @return {Ext.button.Button} this + */ + setTooltip: function(tooltip, initial) { + var me = this; + + if (me.rendered) { + if (!initial) { + me.clearTip(); + } + if (Ext.isObject(tooltip)) { + Ext.tip.QuickTipManager.register(Ext.apply({ + target: me.btnEl.id + }, + tooltip)); + me.tooltip = tooltip; + } else { + me.btnEl.dom.setAttribute('data-' + this.tooltipType, tooltip); + } + } else { + me.tooltip = tooltip; + } + return me; + }, + + // private + getRefItems: function(deep){ + var menu = this.menu, + items; + + if (menu) { + items = menu.getRefItems(deep); + items.unshift(menu); + } + return items || []; + }, + + // private + clearTip: function() { + if (Ext.isObject(this.tooltip)) { + Ext.tip.QuickTipManager.unregister(this.btnEl); + } + }, + + // private + beforeDestroy: function() { + var me = this; + if (me.rendered) { + me.clearTip(); + } + if (me.menu && me.destroyMenu !== false) { + Ext.destroy(me.btnEl, me.btnInnerEl, me.menu); + } + Ext.destroy(me.repeater); + }, + + // private + onDestroy: function() { + var me = this; + if (me.rendered) { + me.doc.un('mouseover', me.monitorMouseOver, me); + me.doc.un('mouseup', me.onMouseUp, me); + delete me.doc; + delete me.btnEl; + delete me.btnInnerEl; + Ext.ButtonToggleManager.unregister(me); + + Ext.destroy(me.keyMap); + delete me.keyMap; + } + me.callParent(); + }, + + /** + * Assigns this Button's click handler + * @param {Function} handler The function to call when the button is clicked + * @param {Object} scope (optional) The scope (this reference) in which the handler function is executed. + * Defaults to this Button. + * @return {Ext.button.Button} this + */ + setHandler: function(handler, scope) { + this.handler = handler; + this.scope = scope; + return this; + }, + + /** + * Sets this Button's text + * @param {String} text The button text + * @return {Ext.button.Button} this + */ + setText: function(text) { + var me = this; + me.text = text; + if (me.el) { + me.btnInnerEl.update(text || ' '); + me.setButtonCls(); + } + me.doComponentLayout(); + return me; + }, + + /** + * Sets the background image (inline style) of the button. This method also changes + * the value of the {@link #icon} config internally. + * @param {String} icon The path to an image to display in the button + * @return {Ext.button.Button} this + */ + setIcon: function(icon) { + var me = this, + btnInnerEl = me.btnInnerEl; + me.icon = icon; + if (btnInnerEl) { + btnInnerEl.setStyle('background-image', icon ? 'url(' + icon + ')': ''); + me.setButtonCls(); + } + return me; + }, + + /** + * Gets the text for this Button + * @return {String} The button text + */ + getText: function() { + return this.text; + }, + + /** + * If a state it passed, it becomes the pressed state otherwise the current state is toggled. + * @param {Boolean} state (optional) Force a particular state + * @param {Boolean} supressEvent (optional) True to stop events being fired when calling this method. + * @return {Ext.button.Button} this + */ + toggle: function(state, suppressEvent) { + var me = this; + state = state === undefined ? !me.pressed: !!state; + if (state !== me.pressed) { + if (me.rendered) { + me[state ? 'addClsWithUI': 'removeClsWithUI'](me.pressedCls); + } + me.btnEl.dom.setAttribute('aria-pressed', state); + me.pressed = state; + if (!suppressEvent) { + me.fireEvent('toggle', me, state); + Ext.callback(me.toggleHandler, me.scope || me, [me, state]); + } + } + return me; + }, + + /** + * Show this button's menu (if it has one) + */ + showMenu: function() { + var me = this; + if (me.rendered && me.menu) { + if (me.tooltip) { + Ext.tip.QuickTipManager.getQuickTip().cancelShow(me.btnEl); + } + if (me.menu.isVisible()) { + me.menu.hide(); + } + + me.menu.showBy(me.el, me.menuAlign); + } + return me; + }, + + /** + * Hide this button's menu (if it has one) + */ + hideMenu: function() { + if (this.hasVisibleMenu()) { + this.menu.hide(); + } + return this; + }, + + /** + * Returns true if the button has a menu and it is visible + * @return {Boolean} + */ + hasVisibleMenu: function() { + var menu = this.menu; + return menu && menu.rendered && menu.isVisible(); + }, + + // private + onRepeatClick: function(repeat, e) { + this.onClick(e); + }, + + // private + onClick: function(e) { + var me = this; + if (me.preventDefault || (me.disabled && me.getHref()) && e) { + e.preventDefault(); + } + if (e.button !== 0) { + return; + } + if (!me.disabled) { + if (me.enableToggle && (me.allowDepress !== false || !me.pressed)) { + me.toggle(); + } + if (me.menu && !me.hasVisibleMenu() && !me.ignoreNextClick) { + me.showMenu(); + } + me.fireEvent('click', me, e); + if (me.handler) { + me.handler.call(me.scope || me, me, e); + } + me.onBlur(); + } + }, + + /** + * @private mouseover handler called when a mouseover event occurs anywhere within the encapsulating element. + * The targets are interrogated to see what is being entered from where. + * @param e + */ + onMouseOver: function(e) { + var me = this; + if (!me.disabled && !e.within(me.el, true, true)) { + me.onMouseEnter(e); + } + }, + + /** + * @private mouseout handler called when a mouseout event occurs anywhere within the encapsulating element - + * or the mouse leaves the encapsulating element. + * The targets are interrogated to see what is being exited to where. + * @param e + */ + onMouseOut: function(e) { + var me = this; + if (!e.within(me.el, true, true)) { + if (me.overMenuTrigger) { + me.onMenuTriggerOut(e); + } + me.onMouseLeave(e); + } + }, + + /** + * @private mousemove handler called when the mouse moves anywhere within the encapsulating element. + * The position is checked to determine if the mouse is entering or leaving the trigger area. Using + * mousemove to check this is more resource intensive than we'd like, but it is necessary because + * the trigger area does not line up exactly with sub-elements so we don't always get mouseover/out + * events when needed. In the future we should consider making the trigger a separate element that + * is absolutely positioned and sized over the trigger area. + */ + onMouseMove: function(e) { + var me = this, + el = me.el, + over = me.overMenuTrigger, + overlap, btnSize; + + if (me.split) { + if (me.arrowAlign === 'right') { + overlap = e.getX() - el.getX(); + btnSize = el.getWidth(); + } else { + overlap = e.getY() - el.getY(); + btnSize = el.getHeight(); + } + + if (overlap > (btnSize - me.getTriggerSize())) { + if (!over) { + me.onMenuTriggerOver(e); + } + } else { + if (over) { + me.onMenuTriggerOut(e); + } + } + } + }, + + /** + * @private Measures the size of the trigger area for menu and split buttons. Will be a width for + * a right-aligned trigger and a height for a bottom-aligned trigger. Cached after first measurement. + */ + getTriggerSize: function() { + var me = this, + size = me.triggerSize, + side, sideFirstLetter, undef; + + if (size === undef) { + side = me.arrowAlign; + sideFirstLetter = side.charAt(0); + size = me.triggerSize = me.el.getFrameWidth(sideFirstLetter) + me.btnWrap.getFrameWidth(sideFirstLetter) + (me.frameSize && me.frameSize[side] || 0); + } + return size; + }, + + /** + * @private virtual mouseenter handler called when it is detected that the mouseout event + * signified the mouse entering the encapsulating element. + * @param e + */ + onMouseEnter: function(e) { + var me = this; + me.addClsWithUI(me.overCls); + me.fireEvent('mouseover', me, e); + }, + + /** + * @private virtual mouseleave handler called when it is detected that the mouseover event + * signified the mouse entering the encapsulating element. + * @param e + */ + onMouseLeave: function(e) { + var me = this; + me.removeClsWithUI(me.overCls); + me.fireEvent('mouseout', me, e); + }, + + /** + * @private virtual mouseenter handler called when it is detected that the mouseover event + * signified the mouse entering the arrow area of the button - the . + * @param e + */ + onMenuTriggerOver: function(e) { + var me = this; + me.overMenuTrigger = true; + me.fireEvent('menutriggerover', me, me.menu, e); + }, + + /** + * @private virtual mouseleave handler called when it is detected that the mouseout event + * signified the mouse leaving the arrow area of the button - the . + * @param e + */ + onMenuTriggerOut: function(e) { + var me = this; + delete me.overMenuTrigger; + me.fireEvent('menutriggerout', me, me.menu, e); + }, + + // inherit docs + enable : function(silent) { + var me = this; + + me.callParent(arguments); + + me.removeClsWithUI('disabled'); + + return me; + }, + + // inherit docs + disable : function(silent) { + var me = this; + + me.callParent(arguments); + + me.addClsWithUI('disabled'); + + return me; + }, + + /** + * Method to change the scale of the button. See {@link #scale} for allowed configurations. + * @param {String} scale The scale to change to. + */ + setScale: function(scale) { + var me = this, + ui = me.ui.replace('-' + me.scale, ''); + + //check if it is an allowed scale + if (!Ext.Array.contains(me.allowedScales, scale)) { + throw('#setScale: scale must be an allowed scale (' + me.allowedScales.join(', ') + ')'); + } + + me.scale = scale; + me.setUI(ui); + }, + + // inherit docs + setUI: function(ui) { + var me = this; + + //we need to append the scale to the UI, if not already done + if (me.scale && !ui.match(me.scale)) { + ui = ui + '-' + me.scale; + } + + me.callParent([ui]); + + // Set all the state classNames, as they need to include the UI + // me.disabledCls += ' ' + me.baseCls + '-' + me.ui + '-disabled'; + }, + + // private + onFocus: function(e) { + var me = this; + if (!me.disabled) { + me.addClsWithUI(me.focusCls); + } + }, + + // private + onBlur: function(e) { + var me = this; + me.removeClsWithUI(me.focusCls); + }, + + // private + onMouseDown: function(e) { + var me = this; + if (!me.disabled && e.button === 0) { + me.addClsWithUI(me.pressedCls); + me.doc.on('mouseup', me.onMouseUp, me); + } + }, + // private + onMouseUp: function(e) { + var me = this; + if (e.button === 0) { + if (!me.pressed) { + me.removeClsWithUI(me.pressedCls); + } + me.doc.un('mouseup', me.onMouseUp, me); + } + }, + // private + onMenuShow: function(e) { + var me = this; + me.ignoreNextClick = 0; + me.addClsWithUI(me.menuActiveCls); + me.fireEvent('menushow', me, me.menu); + }, + + // private + onMenuHide: function(e) { + var me = this; + me.removeClsWithUI(me.menuActiveCls); + me.ignoreNextClick = Ext.defer(me.restoreClick, 250, me); + me.fireEvent('menuhide', me, me.menu); + }, + + // private + restoreClick: function() { + this.ignoreNextClick = 0; + }, + + // private + onDownKey: function() { + var me = this; + + if (!me.disabled) { + if (me.menu) { + me.showMenu(); + } + } + }, + + /** + * @private Some browsers (notably Safari and older Chromes on Windows) add extra "padding" inside the button + * element that cannot be removed. This method returns the size of that padding with a one-time detection. + * @return Array [top, right, bottom, left] + */ + getPersistentBtnPadding: function() { + var cls = Ext.button.Button, + padding = cls.persistentPadding, + btn, leftTop, btnEl, btnInnerEl; + + if (!padding) { + padding = cls.persistentPadding = [0, 0, 0, 0]; //set early to prevent recursion + + if (!Ext.isIE) { //short-circuit IE as it sometimes gives false positive for padding + // Create auto-size button offscreen and measure its insides + btn = Ext.create('Ext.button.Button', { + renderTo: Ext.getBody(), + text: 'test', + style: 'position:absolute;top:-999px;' + }); + btnEl = btn.btnEl; + btnInnerEl = btn.btnInnerEl; + btnEl.setSize(null, null); //clear any hard dimensions on the button el to see what it does naturally + + leftTop = btnInnerEl.getOffsetsTo(btnEl); + padding[0] = leftTop[1]; + padding[1] = btnEl.getWidth() - btnInnerEl.getWidth() - leftTop[0]; + padding[2] = btnEl.getHeight() - btnInnerEl.getHeight() - leftTop[1]; + padding[3] = leftTop[0]; + + btn.destroy(); + } + } + + return padding; + } + +}, function() { + var groups = {}, + g, i, l; + + function toggleGroup(btn, state) { + if (state) { + g = groups[btn.toggleGroup]; + for (i = 0, l = g.length; i < l; i++) { + if (g[i] !== btn) { + g[i].toggle(false); + } + } + } + } + // Private utility class used by Button + Ext.ButtonToggleManager = { + register: function(btn) { + if (!btn.toggleGroup) { + return; + } + var group = groups[btn.toggleGroup]; + if (!group) { + group = groups[btn.toggleGroup] = []; + } + group.push(btn); + btn.on('toggle', toggleGroup); + }, + + unregister: function(btn) { + if (!btn.toggleGroup) { + return; + } + var group = groups[btn.toggleGroup]; + if (group) { + Ext.Array.remove(group, btn); + btn.un('toggle', toggleGroup); + } + }, + + /** + * Gets the pressed button in the passed group or null + * @param {String} group + * @return Button + */ + getPressed: function(group) { + var g = groups[group], + i = 0, + len; + if (g) { + for (len = g.length; i < len; i++) { + if (g[i].pressed === true) { + return g[i]; + } + } + } + return null; + } + }; +}); + +/** + * @class Ext.layout.container.boxOverflow.Menu + * @extends Ext.layout.container.boxOverflow.None + * @private + */ +Ext.define('Ext.layout.container.boxOverflow.Menu', { + + /* Begin Definitions */ + + extend: 'Ext.layout.container.boxOverflow.None', + requires: ['Ext.toolbar.Separator', 'Ext.button.Button'], + alternateClassName: 'Ext.layout.boxOverflow.Menu', + + /* End Definitions */ + + /** + * @cfg {String} afterCtCls + * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers, + * which must always be present at the rightmost edge of the Container + */ + + /** + * @property noItemsMenuText + * @type String + * HTML fragment to render into the toolbar overflow menu if there are no items to display + */ + noItemsMenuText : '
(None)
', + + constructor: function(layout) { + var me = this; + + me.callParent(arguments); + + // Before layout, we need to re-show all items which we may have hidden due to a previous overflow. + layout.beforeLayout = Ext.Function.createInterceptor(layout.beforeLayout, this.clearOverflow, this); + + me.afterCtCls = me.afterCtCls || Ext.baseCSSPrefix + 'box-menu-' + layout.parallelAfter; + /** + * @property menuItems + * @type Array + * Array of all items that are currently hidden and should go into the dropdown menu + */ + me.menuItems = []; + }, + + handleOverflow: function(calculations, targetSize) { + var me = this, + layout = me.layout, + methodName = 'get' + layout.parallelPrefixCap, + newSize = {}, + posArgs = [null, null]; + + me.callParent(arguments); + this.createMenu(calculations, targetSize); + newSize[layout.perpendicularPrefix] = targetSize[layout.perpendicularPrefix]; + newSize[layout.parallelPrefix] = targetSize[layout.parallelPrefix] - me.afterCt[methodName](); + + // Center the menuTrigger button. + // TODO: Should we emulate align: 'middle' like this, or should we 'stretchmax' the menuTrigger? + posArgs[layout.perpendicularSizeIndex] = (calculations.meta.maxSize - me.menuTrigger['get' + layout.perpendicularPrefixCap]()) / 2; + me.menuTrigger.setPosition.apply(me.menuTrigger, posArgs); + + return { targetSize: newSize }; + }, + + /** + * @private + * Called by the layout, when it determines that there is no overflow. + * Also called as an interceptor to the layout's onLayout method to reshow + * previously hidden overflowing items. + */ + clearOverflow: function(calculations, targetSize) { + var me = this, + newWidth = targetSize ? targetSize.width + (me.afterCt ? me.afterCt.getWidth() : 0) : 0, + items = me.menuItems, + i = 0, + length = items.length, + item; + + me.hideTrigger(); + for (; i < length; i++) { + items[i].show(); + } + items.length = 0; + + return targetSize ? { + targetSize: { + height: targetSize.height, + width : newWidth + } + } : null; + }, + + /** + * @private + */ + showTrigger: function() { + this.menuTrigger.show(); + }, + + /** + * @private + */ + hideTrigger: function() { + if (this.menuTrigger != undefined) { + this.menuTrigger.hide(); + } + }, + + /** + * @private + * Called before the overflow menu is shown. This constructs the menu's items, caching them for as long as it can. + */ + beforeMenuShow: function(menu) { + var me = this, + items = me.menuItems, + i = 0, + len = items.length, + item, + prev; + + var needsSep = function(group, prev){ + return group.isXType('buttongroup') && !(prev instanceof Ext.toolbar.Separator); + }; + + me.clearMenu(); + menu.removeAll(); + + for (; i < len; i++) { + item = items[i]; + + // Do not show a separator as a first item + if (!i && (item instanceof Ext.toolbar.Separator)) { + continue; + } + if (prev && (needsSep(item, prev) || needsSep(prev, item))) { + menu.add('-'); + } + + me.addComponentToMenu(menu, item); + prev = item; + } + + // put something so the menu isn't empty if no compatible items found + if (menu.items.length < 1) { + menu.add(me.noItemsMenuText); + } + }, + + /** + * @private + * Returns a menu config for a given component. This config is used to create a menu item + * to be added to the expander menu + * @param {Ext.Component} component The component to create the config for + * @param {Boolean} hideOnClick Passed through to the menu item + */ + createMenuConfig : function(component, hideOnClick) { + var config = Ext.apply({}, component.initialConfig), + group = component.toggleGroup; + + Ext.copyTo(config, component, [ + 'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu' + ]); + + Ext.apply(config, { + text : component.overflowText || component.text, + hideOnClick: hideOnClick, + destroyMenu: false + }); + + if (group || component.enableToggle) { + Ext.apply(config, { + group : group, + checked: component.pressed, + listeners: { + checkchange: function(item, checked){ + component.toggle(checked); + } + } + }); + } + + delete config.ownerCt; + delete config.xtype; + delete config.id; + return config; + }, + + /** + * @private + * Adds the given Toolbar item to the given menu. Buttons inside a buttongroup are added individually. + * @param {Ext.menu.Menu} menu The menu to add to + * @param {Ext.Component} component The component to add + */ + addComponentToMenu : function(menu, component) { + var me = this; + if (component instanceof Ext.toolbar.Separator) { + menu.add('-'); + } else if (component.isComponent) { + if (component.isXType('splitbutton')) { + menu.add(me.createMenuConfig(component, true)); + + } else if (component.isXType('button')) { + menu.add(me.createMenuConfig(component, !component.menu)); + + } else if (component.isXType('buttongroup')) { + component.items.each(function(item){ + me.addComponentToMenu(menu, item); + }); + } else { + menu.add(Ext.create(Ext.getClassName(component), me.createMenuConfig(component))); + } + } + }, + + /** + * @private + * Deletes the sub-menu of each item in the expander menu. Submenus are created for items such as + * splitbuttons and buttongroups, where the Toolbar item cannot be represented by a single menu item + */ + clearMenu : function() { + var menu = this.moreMenu; + if (menu && menu.items) { + menu.items.each(function(item) { + if (item.menu) { + delete item.menu; + } + }); + } + }, + + /** + * @private + * Creates the overflow trigger and menu used when enableOverflow is set to true and the items + * in the layout are too wide to fit in the space available + */ + createMenu: function(calculations, targetSize) { + var me = this, + layout = me.layout, + startProp = layout.parallelBefore, + sizeProp = layout.parallelPrefix, + available = targetSize[sizeProp], + boxes = calculations.boxes, + i = 0, + len = boxes.length, + box; + + if (!me.menuTrigger) { + me.createInnerElements(); + + /** + * @private + * @property menu + * @type Ext.menu.Menu + * The expand menu - holds items for every item that cannot be shown + * because the container is currently not large enough. + */ + me.menu = Ext.create('Ext.menu.Menu', { + hideMode: 'offsets', + listeners: { + scope: me, + beforeshow: me.beforeMenuShow + } + }); + + /** + * @private + * @property menuTrigger + * @type Ext.button.Button + * The expand button which triggers the overflow menu to be shown + */ + me.menuTrigger = Ext.create('Ext.button.Button', { + ownerCt : me.layout.owner, // To enable the Menu to ascertain a valid zIndexManager owner in the same tree + iconCls : Ext.baseCSSPrefix + layout.owner.getXType() + '-more-icon', + ui : layout.owner instanceof Ext.toolbar.Toolbar ? 'default-toolbar' : 'default', + menu : me.menu, + getSplitCls: function() { return '';}, + renderTo: me.afterCt + }); + } + me.showTrigger(); + available -= me.afterCt.getWidth(); + + // Hide all items which are off the end, and store them to allow them to be restored + // before each layout operation. + me.menuItems.length = 0; + for (; i < len; i++) { + box = boxes[i]; + if (box[startProp] + box[sizeProp] > available) { + me.menuItems.push(box.component); + box.component.hide(); + } + } + }, + + /** + * @private + * Creates the beforeCt, innerCt and afterCt elements if they have not already been created + * @param {Ext.container.Container} container The Container attached to this Layout instance + * @param {Ext.core.Element} target The target Element + */ + createInnerElements: function() { + var me = this, + target = me.layout.getRenderTarget(); + + if (!this.afterCt) { + target.addCls(Ext.baseCSSPrefix + me.layout.direction + '-box-overflow-body'); + this.afterCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + this.afterCtCls}, 'before'); + } + }, + + /** + * @private + */ + destroy: function() { + Ext.destroy(this.menu, this.menuTrigger); + } +}); +/** + * @class Ext.util.Region + * @extends Object + * + * Represents a rectangular region and provides a number of utility methods + * to compare regions. + */ + +Ext.define('Ext.util.Region', { + + /* Begin Definitions */ + + requires: ['Ext.util.Offset'], + + statics: { + /** + * @static + * @param {Mixed} el A string, DomElement or Ext.core.Element representing an element + * on the page. + * @returns {Ext.util.Region} region + * Retrieves an Ext.util.Region for a particular element. + */ + getRegion: function(el) { + return Ext.fly(el).getPageBox(true); + }, + + /** + * @static + * @param {Object} o An object with top, right, bottom, left properties + * @return {Ext.util.Region} region The region constructed based on the passed object + */ + from: function(o) { + return new this(o.top, o.right, o.bottom, o.left); + } + }, + + /* End Definitions */ + + /** + * @constructor + * @param {Number} top Top + * @param {Number} right Right + * @param {Number} bottom Bottom + * @param {Number} left Left + */ + constructor : function(t, r, b, l) { + var me = this; + me.y = me.top = me[1] = t; + me.right = r; + me.bottom = b; + me.x = me.left = me[0] = l; + }, + + /** + * Checks if this region completely contains the region that is passed in. + * @param {Ext.util.Region} region + */ + contains : function(region) { + var me = this; + return (region.x >= me.x && + region.right <= me.right && + region.y >= me.y && + region.bottom <= me.bottom); + + }, + + /** + * Checks if this region intersects the region passed in. + * @param {Ext.util.Region} region + * @return {Ext.util.Region/Boolean} Returns the intersected region or false if there is no intersection. + */ + intersect : function(region) { + var me = this, + t = Math.max(me.y, region.y), + r = Math.min(me.right, region.right), + b = Math.min(me.bottom, region.bottom), + l = Math.max(me.x, region.x); + + if (b > t && r > l) { + return new this.self(t, r, b, l); + } + else { + return false; + } + }, + + /** + * Returns the smallest region that contains the current AND targetRegion. + * @param {Ext.util.Region} region + */ + union : function(region) { + var me = this, + t = Math.min(me.y, region.y), + r = Math.max(me.right, region.right), + b = Math.max(me.bottom, region.bottom), + l = Math.min(me.x, region.x); + + return new this.self(t, r, b, l); + }, + + /** + * Modifies the current region to be constrained to the targetRegion. + * @param {Ext.util.Region} targetRegion + */ + constrainTo : function(r) { + var me = this, + constrain = Ext.Number.constrain; + me.top = me.y = constrain(me.top, r.y, r.bottom); + me.bottom = constrain(me.bottom, r.y, r.bottom); + me.left = me.x = constrain(me.left, r.x, r.right); + me.right = constrain(me.right, r.x, r.right); + return me; + }, + + /** + * Modifies the current region to be adjusted by offsets. + * @param {Number} top top offset + * @param {Number} right right offset + * @param {Number} bottom bottom offset + * @param {Number} left left offset + */ + adjust : function(t, r, b, l) { + var me = this; + me.top = me.y += t; + me.left = me.x += l; + me.right += r; + me.bottom += b; + return me; + }, + + /** + * Get the offset amount of a point outside the region + * @param {String} axis optional + * @param {Ext.util.Point} p the point + * @return {Ext.util.Offset} + */ + getOutOfBoundOffset: function(axis, p) { + if (!Ext.isObject(axis)) { + if (axis == 'x') { + return this.getOutOfBoundOffsetX(p); + } else { + return this.getOutOfBoundOffsetY(p); + } + } else { + p = axis; + var d = Ext.create('Ext.util.Offset'); + d.x = this.getOutOfBoundOffsetX(p.x); + d.y = this.getOutOfBoundOffsetY(p.y); + return d; + } + + }, + + /** + * Get the offset amount on the x-axis + * @param {Number} p the offset + * @return {Number} + */ + getOutOfBoundOffsetX: function(p) { + if (p <= this.x) { + return this.x - p; + } else if (p >= this.right) { + return this.right - p; + } + + return 0; + }, + + /** + * Get the offset amount on the y-axis + * @param {Number} p the offset + * @return {Number} + */ + getOutOfBoundOffsetY: function(p) { + if (p <= this.y) { + return this.y - p; + } else if (p >= this.bottom) { + return this.bottom - p; + } + + return 0; + }, + + /** + * Check whether the point / offset is out of bound + * @param {String} axis optional + * @param {Ext.util.Point/Number} p the point / offset + * @return {Boolean} + */ + isOutOfBound: function(axis, p) { + if (!Ext.isObject(axis)) { + if (axis == 'x') { + return this.isOutOfBoundX(p); + } else { + return this.isOutOfBoundY(p); + } + } else { + p = axis; + return (this.isOutOfBoundX(p.x) || this.isOutOfBoundY(p.y)); + } + }, + + /** + * Check whether the offset is out of bound in the x-axis + * @param {Number} p the offset + * @return {Boolean} + */ + isOutOfBoundX: function(p) { + return (p < this.x || p > this.right); + }, + + /** + * Check whether the offset is out of bound in the y-axis + * @param {Number} p the offset + * @return {Boolean} + */ + isOutOfBoundY: function(p) { + return (p < this.y || p > this.bottom); + }, + + /* + * Restrict a point within the region by a certain factor. + * @param {String} axis Optional + * @param {Ext.util.Point/Ext.util.Offset/Object} p + * @param {Number} factor + * @return {Ext.util.Point/Ext.util.Offset/Object/Number} + */ + restrict: function(axis, p, factor) { + if (Ext.isObject(axis)) { + var newP; + + factor = p; + p = axis; + + if (p.copy) { + newP = p.copy(); + } + else { + newP = { + x: p.x, + y: p.y + }; + } + + newP.x = this.restrictX(p.x, factor); + newP.y = this.restrictY(p.y, factor); + return newP; + } else { + if (axis == 'x') { + return this.restrictX(p, factor); + } else { + return this.restrictY(p, factor); + } + } + }, + + /* + * Restrict an offset within the region by a certain factor, on the x-axis + * @param {Number} p + * @param {Number} factor The factor, optional, defaults to 1 + * @return + */ + restrictX : function(p, factor) { + if (!factor) { + factor = 1; + } + + if (p <= this.x) { + p -= (p - this.x) * factor; + } + else if (p >= this.right) { + p -= (p - this.right) * factor; + } + return p; + }, + + /* + * Restrict an offset within the region by a certain factor, on the y-axis + * @param {Number} p + * @param {Number} factor The factor, optional, defaults to 1 + */ + restrictY : function(p, factor) { + if (!factor) { + factor = 1; + } + + if (p <= this.y) { + p -= (p - this.y) * factor; + } + else if (p >= this.bottom) { + p -= (p - this.bottom) * factor; + } + return p; + }, + + /* + * Get the width / height of this region + * @return {Object} an object with width and height properties + */ + getSize: function() { + return { + width: this.right - this.x, + height: this.bottom - this.y + }; + }, + + /** + * Copy a new instance + * @return {Ext.util.Region} + */ + copy: function() { + return new this.self(this.y, this.right, this.bottom, this.x); + }, + + /** + * Copy the values of another Region to this Region + * @param {Region} The region to copy from. + * @return {Ext.util.Point} this This point + */ + copyFrom: function(p) { + var me = this; + me.top = me.y = me[1] = p.y; + me.right = p.right; + me.bottom = p.bottom; + me.left = me.x = me[0] = p.x; + + return this; + }, + + /** + * Dump this to an eye-friendly string, great for debugging + * @return {String} + */ + toString: function() { + return "Region[" + this.top + "," + this.right + "," + this.bottom + "," + this.left + "]"; + }, + + + /** + * Translate this region by the given offset amount + * @param {Ext.util.Offset/Object} offset Object containing the x and y properties. + * Or the x value is using the two argument form. + * @param {Number} The y value unless using an Offset object. + * @return {Ext.util.Region} this This Region + */ + translateBy: function(x, y) { + if (arguments.length == 1) { + y = x.y; + x = x.x; + } + var me = this; + me.top = me.y += y; + me.right += x; + me.bottom += y; + me.left = me.x += x; + + return me; + }, + + /** + * Round all the properties of this region + * @return {Ext.util.Region} this This Region + */ + round: function() { + var me = this; + me.top = me.y = Math.round(me.y); + me.right = Math.round(me.right); + me.bottom = Math.round(me.bottom); + me.left = me.x = Math.round(me.x); + + return me; + }, + + /** + * Check whether this region is equivalent to the given region + * @param {Ext.util.Region} region The region to compare with + * @return {Boolean} + */ + equals: function(region) { + return (this.top == region.top && this.right == region.right && this.bottom == region.bottom && this.left == region.left); + } +}); + +/* + * This is a derivative of the similarly named class in the YUI Library. + * The original license: + * Copyright (c) 2006, Yahoo! Inc. All rights reserved. + * Code licensed under the BSD License: + * http://developer.yahoo.net/yui/license.txt + */ + + +/** + * @class Ext.dd.DragDropManager + * DragDropManager is a singleton that tracks the element interaction for + * all DragDrop items in the window. Generally, you will not call + * this class directly, but it does have helper methods that could + * be useful in your DragDrop implementations. + * @singleton + */ +Ext.define('Ext.dd.DragDropManager', { + singleton: true, + + requires: ['Ext.util.Region'], + + uses: ['Ext.tip.QuickTipManager'], + + // shorter ClassName, to save bytes and use internally + alternateClassName: ['Ext.dd.DragDropMgr', 'Ext.dd.DDM'], + + /** + * Two dimensional Array of registered DragDrop objects. The first + * dimension is the DragDrop item group, the second the DragDrop + * object. + * @property ids + * @type String[] + * @private + * @static + */ + ids: {}, + + /** + * Array of element ids defined as drag handles. Used to determine + * if the element that generated the mousedown event is actually the + * handle and not the html element itself. + * @property handleIds + * @type String[] + * @private + * @static + */ + handleIds: {}, + + /** + * the DragDrop object that is currently being dragged + * @property dragCurrent + * @type DragDrop + * @private + * @static + **/ + dragCurrent: null, + + /** + * the DragDrop object(s) that are being hovered over + * @property dragOvers + * @type Array + * @private + * @static + */ + dragOvers: {}, + + /** + * the X distance between the cursor and the object being dragged + * @property deltaX + * @type int + * @private + * @static + */ + deltaX: 0, + + /** + * the Y distance between the cursor and the object being dragged + * @property deltaY + * @type int + * @private + * @static + */ + deltaY: 0, + + /** + * Flag to determine if we should prevent the default behavior of the + * events we define. By default this is true, but this can be set to + * false if you need the default behavior (not recommended) + * @property preventDefault + * @type boolean + * @static + */ + preventDefault: true, + + /** + * Flag to determine if we should stop the propagation of the events + * we generate. This is true by default but you may want to set it to + * false if the html element contains other features that require the + * mouse click. + * @property stopPropagation + * @type boolean + * @static + */ + stopPropagation: true, + + /** + * Internal flag that is set to true when drag and drop has been + * intialized + * @property initialized + * @private + * @static + */ + initialized: false, + + /** + * All drag and drop can be disabled. + * @property locked + * @private + * @static + */ + locked: false, + + /** + * Called the first time an element is registered. + * @method init + * @private + * @static + */ + init: function() { + this.initialized = true; + }, + + /** + * In point mode, drag and drop interaction is defined by the + * location of the cursor during the drag/drop + * @property POINT + * @type int + * @static + */ + POINT: 0, + + /** + * In intersect mode, drag and drop interaction is defined by the + * overlap of two or more drag and drop objects. + * @property INTERSECT + * @type int + * @static + */ + INTERSECT: 1, + + /** + * The current drag and drop mode. Default: POINT + * @property mode + * @type int + * @static + */ + mode: 0, + + /** + * Runs method on all drag and drop objects + * @method _execOnAll + * @private + * @static + */ + _execOnAll: function(sMethod, args) { + for (var i in this.ids) { + for (var j in this.ids[i]) { + var oDD = this.ids[i][j]; + if (! this.isTypeOfDD(oDD)) { + continue; + } + oDD[sMethod].apply(oDD, args); + } + } + }, + + /** + * Drag and drop initialization. Sets up the global event handlers + * @method _onLoad + * @private + * @static + */ + _onLoad: function() { + + this.init(); + + var Event = Ext.EventManager; + Event.on(document, "mouseup", this.handleMouseUp, this, true); + Event.on(document, "mousemove", this.handleMouseMove, this, true); + Event.on(window, "unload", this._onUnload, this, true); + Event.on(window, "resize", this._onResize, this, true); + // Event.on(window, "mouseout", this._test); + + }, + + /** + * Reset constraints on all drag and drop objs + * @method _onResize + * @private + * @static + */ + _onResize: function(e) { + this._execOnAll("resetConstraints", []); + }, + + /** + * Lock all drag and drop functionality + * @method lock + * @static + */ + lock: function() { this.locked = true; }, + + /** + * Unlock all drag and drop functionality + * @method unlock + * @static + */ + unlock: function() { this.locked = false; }, + + /** + * Is drag and drop locked? + * @method isLocked + * @return {boolean} True if drag and drop is locked, false otherwise. + * @static + */ + isLocked: function() { return this.locked; }, + + /** + * Location cache that is set for all drag drop objects when a drag is + * initiated, cleared when the drag is finished. + * @property locationCache + * @private + * @static + */ + locationCache: {}, + + /** + * Set useCache to false if you want to force object the lookup of each + * drag and drop linked element constantly during a drag. + * @property useCache + * @type boolean + * @static + */ + useCache: true, + + /** + * The number of pixels that the mouse needs to move after the + * mousedown before the drag is initiated. Default=3; + * @property clickPixelThresh + * @type int + * @static + */ + clickPixelThresh: 3, + + /** + * The number of milliseconds after the mousedown event to initiate the + * drag if we don't get a mouseup event. Default=350 + * @property clickTimeThresh + * @type int + * @static + */ + clickTimeThresh: 350, + + /** + * Flag that indicates that either the drag pixel threshold or the + * mousdown time threshold has been met + * @property dragThreshMet + * @type boolean + * @private + * @static + */ + dragThreshMet: false, + + /** + * Timeout used for the click time threshold + * @property clickTimeout + * @type Object + * @private + * @static + */ + clickTimeout: null, + + /** + * The X position of the mousedown event stored for later use when a + * drag threshold is met. + * @property startX + * @type int + * @private + * @static + */ + startX: 0, + + /** + * The Y position of the mousedown event stored for later use when a + * drag threshold is met. + * @property startY + * @type int + * @private + * @static + */ + startY: 0, + + /** + * Each DragDrop instance must be registered with the DragDropManager. + * This is executed in DragDrop.init() + * @method regDragDrop + * @param {DragDrop} oDD the DragDrop object to register + * @param {String} sGroup the name of the group this element belongs to + * @static + */ + regDragDrop: function(oDD, sGroup) { + if (!this.initialized) { this.init(); } + + if (!this.ids[sGroup]) { + this.ids[sGroup] = {}; + } + this.ids[sGroup][oDD.id] = oDD; + }, + + /** + * Removes the supplied dd instance from the supplied group. Executed + * by DragDrop.removeFromGroup, so don't call this function directly. + * @method removeDDFromGroup + * @private + * @static + */ + removeDDFromGroup: function(oDD, sGroup) { + if (!this.ids[sGroup]) { + this.ids[sGroup] = {}; + } + + var obj = this.ids[sGroup]; + if (obj && obj[oDD.id]) { + delete obj[oDD.id]; + } + }, + + /** + * Unregisters a drag and drop item. This is executed in + * DragDrop.unreg, use that method instead of calling this directly. + * @method _remove + * @private + * @static + */ + _remove: function(oDD) { + for (var g in oDD.groups) { + if (g && this.ids[g] && this.ids[g][oDD.id]) { + delete this.ids[g][oDD.id]; + } + } + delete this.handleIds[oDD.id]; + }, + + /** + * Each DragDrop handle element must be registered. This is done + * automatically when executing DragDrop.setHandleElId() + * @method regHandle + * @param {String} sDDId the DragDrop id this element is a handle for + * @param {String} sHandleId the id of the element that is the drag + * handle + * @static + */ + regHandle: function(sDDId, sHandleId) { + if (!this.handleIds[sDDId]) { + this.handleIds[sDDId] = {}; + } + this.handleIds[sDDId][sHandleId] = sHandleId; + }, + + /** + * Utility function to determine if a given element has been + * registered as a drag drop item. + * @method isDragDrop + * @param {String} id the element id to check + * @return {boolean} true if this element is a DragDrop item, + * false otherwise + * @static + */ + isDragDrop: function(id) { + return ( this.getDDById(id) ) ? true : false; + }, + + /** + * Returns the drag and drop instances that are in all groups the + * passed in instance belongs to. + * @method getRelated + * @param {DragDrop} p_oDD the obj to get related data for + * @param {boolean} bTargetsOnly if true, only return targetable objs + * @return {DragDrop[]} the related instances + * @static + */ + getRelated: function(p_oDD, bTargetsOnly) { + var oDDs = []; + for (var i in p_oDD.groups) { + for (var j in this.ids[i]) { + var dd = this.ids[i][j]; + if (! this.isTypeOfDD(dd)) { + continue; + } + if (!bTargetsOnly || dd.isTarget) { + oDDs[oDDs.length] = dd; + } + } + } + + return oDDs; + }, + + /** + * Returns true if the specified dd target is a legal target for + * the specifice drag obj + * @method isLegalTarget + * @param {DragDrop} oDD the drag obj + * @param {DragDrop} oTargetDD the target + * @return {boolean} true if the target is a legal target for the + * dd obj + * @static + */ + isLegalTarget: function (oDD, oTargetDD) { + var targets = this.getRelated(oDD, true); + for (var i=0, len=targets.length;i this.clickPixelThresh || + diffY > this.clickPixelThresh) { + this.startDrag(this.startX, this.startY); + } + } + + if (this.dragThreshMet) { + this.dragCurrent.b4Drag(e); + this.dragCurrent.onDrag(e); + if(!this.dragCurrent.moveOnly){ + this.fireEvents(e, false); + } + } + + this.stopEvent(e); + + return true; + }, + + /** + * Iterates over all of the DragDrop elements to find ones we are + * hovering over or dropping on + * @method fireEvents + * @param {Event} e the event + * @param {boolean} isDrop is this a drop op or a mouseover op? + * @private + * @static + */ + fireEvents: function(e, isDrop) { + var dc = this.dragCurrent; + + // If the user did the mouse up outside of the window, we could + // get here even though we have ended the drag. + if (!dc || dc.isLocked()) { + return; + } + + var pt = e.getPoint(); + + // cache the previous dragOver array + var oldOvers = []; + + var outEvts = []; + var overEvts = []; + var dropEvts = []; + var enterEvts = []; + + // Check to see if the object(s) we were hovering over is no longer + // being hovered over so we can fire the onDragOut event + for (var i in this.dragOvers) { + + var ddo = this.dragOvers[i]; + + if (! this.isTypeOfDD(ddo)) { + continue; + } + + if (! this.isOverTarget(pt, ddo, this.mode)) { + outEvts.push( ddo ); + } + + oldOvers[i] = true; + delete this.dragOvers[i]; + } + + for (var sGroup in dc.groups) { + + if ("string" != typeof sGroup) { + continue; + } + + for (i in this.ids[sGroup]) { + var oDD = this.ids[sGroup][i]; + if (! this.isTypeOfDD(oDD)) { + continue; + } + + if (oDD.isTarget && !oDD.isLocked() && ((oDD != dc) || (dc.ignoreSelf === false))) { + if (this.isOverTarget(pt, oDD, this.mode)) { + // look for drop interactions + if (isDrop) { + dropEvts.push( oDD ); + // look for drag enter and drag over interactions + } else { + + // initial drag over: dragEnter fires + if (!oldOvers[oDD.id]) { + enterEvts.push( oDD ); + // subsequent drag overs: dragOver fires + } else { + overEvts.push( oDD ); + } + + this.dragOvers[oDD.id] = oDD; + } + } + } + } + } + + if (this.mode) { + if (outEvts.length) { + dc.b4DragOut(e, outEvts); + dc.onDragOut(e, outEvts); + } + + if (enterEvts.length) { + dc.onDragEnter(e, enterEvts); + } + + if (overEvts.length) { + dc.b4DragOver(e, overEvts); + dc.onDragOver(e, overEvts); + } + + if (dropEvts.length) { + dc.b4DragDrop(e, dropEvts); + dc.onDragDrop(e, dropEvts); + } + + } else { + // fire dragout events + var len = 0; + for (i=0, len=outEvts.length; i + * Ext.dd.DragDropManager.refreshCache(ddinstance.groups); + * + * Alternatively: + * + * Ext.dd.DragDropManager.refreshCache({group1:true, group2:true}); + * + * @TODO this really should be an indexed array. Alternatively this + * method could accept both. + * @method refreshCache + * @param {Object} groups an associative array of groups to refresh + * @static + */ + refreshCache: function(groups) { + for (var sGroup in groups) { + if ("string" != typeof sGroup) { + continue; + } + for (var i in this.ids[sGroup]) { + var oDD = this.ids[sGroup][i]; + + if (this.isTypeOfDD(oDD)) { + // if (this.isTypeOfDD(oDD) && oDD.isTarget) { + var loc = this.getLocation(oDD); + if (loc) { + this.locationCache[oDD.id] = loc; + } else { + delete this.locationCache[oDD.id]; + // this will unregister the drag and drop object if + // the element is not in a usable state + // oDD.unreg(); + } + } + } + } + }, + + /** + * This checks to make sure an element exists and is in the DOM. The + * main purpose is to handle cases where innerHTML is used to remove + * drag and drop objects from the DOM. IE provides an 'unspecified + * error' when trying to access the offsetParent of such an element + * @method verifyEl + * @param {HTMLElement} el the element to check + * @return {boolean} true if the element looks usable + * @static + */ + verifyEl: function(el) { + if (el) { + var parent; + if(Ext.isIE){ + try{ + parent = el.offsetParent; + }catch(e){} + }else{ + parent = el.offsetParent; + } + if (parent) { + return true; + } + } + + return false; + }, + + /** + * Returns a Region object containing the drag and drop element's position + * and size, including the padding configured for it + * @method getLocation + * @param {DragDrop} oDD the drag and drop object to get the + * location for + * @return {Ext.util.Region} a Region object representing the total area + * the element occupies, including any padding + * the instance is configured for. + * @static + */ + getLocation: function(oDD) { + if (! this.isTypeOfDD(oDD)) { + return null; + } + + //delegate getLocation method to the + //drag and drop target. + if (oDD.getRegion) { + return oDD.getRegion(); + } + + var el = oDD.getEl(), pos, x1, x2, y1, y2, t, r, b, l; + + try { + pos= Ext.core.Element.getXY(el); + } catch (e) { } + + if (!pos) { + return null; + } + + x1 = pos[0]; + x2 = x1 + el.offsetWidth; + y1 = pos[1]; + y2 = y1 + el.offsetHeight; + + t = y1 - oDD.padding[0]; + r = x2 + oDD.padding[1]; + b = y2 + oDD.padding[2]; + l = x1 - oDD.padding[3]; + + return Ext.create('Ext.util.Region', t, r, b, l); + }, + + /** + * Checks the cursor location to see if it over the target + * @method isOverTarget + * @param {Ext.util.Point} pt The point to evaluate + * @param {DragDrop} oTarget the DragDrop object we are inspecting + * @return {boolean} true if the mouse is over the target + * @private + * @static + */ + isOverTarget: function(pt, oTarget, intersect) { + // use cache if available + var loc = this.locationCache[oTarget.id]; + if (!loc || !this.useCache) { + loc = this.getLocation(oTarget); + this.locationCache[oTarget.id] = loc; + + } + + if (!loc) { + return false; + } + + oTarget.cursorIsOver = loc.contains( pt ); + + // DragDrop is using this as a sanity check for the initial mousedown + // in this case we are done. In POINT mode, if the drag obj has no + // contraints, we are also done. Otherwise we need to evaluate the + // location of the target as related to the actual location of the + // dragged element. + var dc = this.dragCurrent; + if (!dc || !dc.getTargetCoord || + (!intersect && !dc.constrainX && !dc.constrainY)) { + return oTarget.cursorIsOver; + } + + oTarget.overlap = null; + + // Get the current location of the drag element, this is the + // location of the mouse event less the delta that represents + // where the original mousedown happened on the element. We + // need to consider constraints and ticks as well. + var pos = dc.getTargetCoord(pt.x, pt.y); + + var el = dc.getDragEl(); + var curRegion = Ext.create('Ext.util.Region', pos.y, + pos.x + el.offsetWidth, + pos.y + el.offsetHeight, + pos.x ); + + var overlap = curRegion.intersect(loc); + + if (overlap) { + oTarget.overlap = overlap; + return (intersect) ? true : oTarget.cursorIsOver; + } else { + return false; + } + }, + + /** + * unload event handler + * @method _onUnload + * @private + * @static + */ + _onUnload: function(e, me) { + Ext.dd.DragDropManager.unregAll(); + }, + + /** + * Cleans up the drag and drop events and objects. + * @method unregAll + * @private + * @static + */ + unregAll: function() { + + if (this.dragCurrent) { + this.stopDrag(); + this.dragCurrent = null; + } + + this._execOnAll("unreg", []); + + for (var i in this.elementCache) { + delete this.elementCache[i]; + } + + this.elementCache = {}; + this.ids = {}; + }, + + /** + * A cache of DOM elements + * @property elementCache + * @private + * @static + */ + elementCache: {}, + + /** + * Get the wrapper for the DOM element specified + * @method getElWrapper + * @param {String} id the id of the element to get + * @return {Ext.dd.DDM.ElementWrapper} the wrapped element + * @private + * @deprecated This wrapper isn't that useful + * @static + */ + getElWrapper: function(id) { + var oWrapper = this.elementCache[id]; + if (!oWrapper || !oWrapper.el) { + oWrapper = this.elementCache[id] = + new this.ElementWrapper(Ext.getDom(id)); + } + return oWrapper; + }, + + /** + * Returns the actual DOM element + * @method getElement + * @param {String} id the id of the elment to get + * @return {Object} The element + * @deprecated use Ext.lib.Ext.getDom instead + * @static + */ + getElement: function(id) { + return Ext.getDom(id); + }, + + /** + * Returns the style property for the DOM element (i.e., + * document.getElById(id).style) + * @method getCss + * @param {String} id the id of the elment to get + * @return {Object} The style property of the element + * @static + */ + getCss: function(id) { + var el = Ext.getDom(id); + return (el) ? el.style : null; + }, + + /** + * Inner class for cached elements + * @class Ext.dd.DragDropManager.ElementWrapper + * @for DragDropManager + * @private + * @deprecated + */ + ElementWrapper: function(el) { + /** + * The element + * @property el + */ + this.el = el || null; + /** + * The element id + * @property id + */ + this.id = this.el && el.id; + /** + * A reference to the style property + * @property css + */ + this.css = this.el && el.style; + }, + + /** + * Returns the X position of an html element + * @method getPosX + * @param el the element for which to get the position + * @return {int} the X coordinate + * @for DragDropManager + * @static + */ + getPosX: function(el) { + return Ext.core.Element.getX(el); + }, + + /** + * Returns the Y position of an html element + * @method getPosY + * @param el the element for which to get the position + * @return {int} the Y coordinate + * @static + */ + getPosY: function(el) { + return Ext.core.Element.getY(el); + }, + + /** + * Swap two nodes. In IE, we use the native method, for others we + * emulate the IE behavior + * @method swapNode + * @param n1 the first node to swap + * @param n2 the other node to swap + * @static + */ + swapNode: function(n1, n2) { + if (n1.swapNode) { + n1.swapNode(n2); + } else { + var p = n2.parentNode; + var s = n2.nextSibling; + + if (s == n1) { + p.insertBefore(n1, n2); + } else if (n2 == n1.nextSibling) { + p.insertBefore(n2, n1); + } else { + n1.parentNode.replaceChild(n2, n1); + p.insertBefore(n1, s); + } + } + }, + + /** + * Returns the current scroll position + * @method getScroll + * @private + * @static + */ + getScroll: function () { + var doc = window.document, + docEl = doc.documentElement, + body = doc.body, + top = 0, + left = 0; + + if (Ext.isGecko4) { + top = window.scrollYOffset; + left = window.scrollXOffset; + } else { + if (docEl && (docEl.scrollTop || docEl.scrollLeft)) { + top = docEl.scrollTop; + left = docEl.scrollLeft; + } else if (body) { + top = body.scrollTop; + left = body.scrollLeft; + } + } + return { + top: top, + left: left + }; + }, + + /** + * Returns the specified element style property + * @method getStyle + * @param {HTMLElement} el the element + * @param {string} styleProp the style property + * @return {string} The value of the style property + * @static + */ + getStyle: function(el, styleProp) { + return Ext.fly(el).getStyle(styleProp); + }, + + /** + * Gets the scrollTop + * @method getScrollTop + * @return {int} the document's scrollTop + * @static + */ + getScrollTop: function () { + return this.getScroll().top; + }, + + /** + * Gets the scrollLeft + * @method getScrollLeft + * @return {int} the document's scrollTop + * @static + */ + getScrollLeft: function () { + return this.getScroll().left; + }, + + /** + * Sets the x/y position of an element to the location of the + * target element. + * @method moveToEl + * @param {HTMLElement} moveEl The element to move + * @param {HTMLElement} targetEl The position reference element + * @static + */ + moveToEl: function (moveEl, targetEl) { + var aCoord = Ext.core.Element.getXY(targetEl); + Ext.core.Element.setXY(moveEl, aCoord); + }, + + /** + * Numeric array sort function + * @method numericSort + * @static + */ + numericSort: function(a, b) { + return (a - b); + }, + + /** + * Internal counter + * @property _timeoutCount + * @private + * @static + */ + _timeoutCount: 0, + + /** + * Trying to make the load order less important. Without this we get + * an error if this file is loaded before the Event Utility. + * @method _addListeners + * @private + * @static + */ + _addListeners: function() { + if ( document ) { + this._onLoad(); + } else { + if (this._timeoutCount > 2000) { + } else { + setTimeout(this._addListeners, 10); + if (document && document.body) { + this._timeoutCount += 1; + } + } + } + }, + + /** + * Recursively searches the immediate parent and all child nodes for + * the handle element in order to determine wheter or not it was + * clicked. + * @method handleWasClicked + * @param node the html element to inspect + * @static + */ + handleWasClicked: function(node, id) { + if (this.isHandle(id, node.id)) { + return true; + } else { + // check to see if this is a text node child of the one we want + var p = node.parentNode; + + while (p) { + if (this.isHandle(id, p.id)) { + return true; + } else { + p = p.parentNode; + } + } + } + + return false; + } +}, function() { + this._addListeners(); +}); + +/** + * @class Ext.layout.container.Box + * @extends Ext.layout.container.Container + *

Base Class for HBoxLayout and VBoxLayout Classes. Generally it should not need to be used directly.

+ */ + +Ext.define('Ext.layout.container.Box', { + + /* Begin Definitions */ + + alias: ['layout.box'], + extend: 'Ext.layout.container.Container', + alternateClassName: 'Ext.layout.BoxLayout', + + requires: [ + 'Ext.layout.container.boxOverflow.None', + 'Ext.layout.container.boxOverflow.Menu', + 'Ext.layout.container.boxOverflow.Scroller', + 'Ext.util.Format', + 'Ext.dd.DragDropManager' + ], + + /* End Definitions */ + + /** + * @cfg {Mixed} animate + *

If truthy, child Component are animated into position whenever the Container + * is layed out. If this option is numeric, it is used as the animation duration in milliseconds.

+ *

May be set as a property at any time.

+ */ + + /** + * @cfg {Object} defaultMargins + *

If the individual contained items do not have a margins + * property specified or margin specified via CSS, the default margins from this property will be + * applied to each item.

+ *

This property may be specified as an object containing margins + * to apply in the format:


+{
+    top: (top margin),
+    right: (right margin),
+    bottom: (bottom margin),
+    left: (left margin)
+}
+ *

This property may also be specified as a string containing + * space-separated, numeric margin values. The order of the sides associated + * with each value matches the way CSS processes margin values:

+ *
    + *
  • If there is only one value, it applies to all sides.
  • + *
  • If there are two values, the top and bottom borders are set to the + * first value and the right and left are set to the second.
  • + *
  • If there are three values, the top is set to the first value, the left + * and right are set to the second, and the bottom is set to the third.
  • + *
  • If there are four values, they apply to the top, right, bottom, and + * left, respectively.
  • + *
+ *

Defaults to:


+     * {top:0, right:0, bottom:0, left:0}
+     * 
+ */ + defaultMargins: { + top: 0, + right: 0, + bottom: 0, + left: 0 + }, + + /** + * @cfg {String} padding + *

Sets the padding to be applied to all child items managed by this layout.

+ *

This property must be specified as a string containing + * space-separated, numeric padding values. The order of the sides associated + * with each value matches the way CSS processes padding values:

+ *
    + *
  • If there is only one value, it applies to all sides.
  • + *
  • If there are two values, the top and bottom borders are set to the + * first value and the right and left are set to the second.
  • + *
  • If there are three values, the top is set to the first value, the left + * and right are set to the second, and the bottom is set to the third.
  • + *
  • If there are four values, they apply to the top, right, bottom, and + * left, respectively.
  • + *
+ *

Defaults to: "0"

+ */ + padding: '0', + // documented in subclasses + pack: 'start', + + /** + * @cfg {String} pack + * Controls how the child items of the container are packed together. Acceptable configuration values + * for this property are: + *
    + *
  • start : Default
    child items are packed together at + * left side of container
  • + *
  • center :
    child items are packed together at + * mid-width of container
  • + *
  • end :
    child items are packed together at right + * side of container
  • + *
+ */ + /** + * @cfg {Number} flex + * This configuration option is to be applied to child items of the container managed + * by this layout. Each child item with a flex property will be flexed horizontally + * according to each item's relative flex value compared to the sum of all items with + * a flex value specified. Any child items that have either a flex = 0 or + * flex = undefined will not be 'flexed' (the initial size will not be changed). + */ + + type: 'box', + scrollOffset: 0, + itemCls: Ext.baseCSSPrefix + 'box-item', + targetCls: Ext.baseCSSPrefix + 'box-layout-ct', + innerCls: Ext.baseCSSPrefix + 'box-inner', + + bindToOwnerCtContainer: true, + + fixedLayout: false, + + // availableSpaceOffset is used to adjust the availableWidth, typically used + // to reserve space for a scrollbar + availableSpaceOffset: 0, + + // whether or not to reserve the availableSpaceOffset in layout calculations + reserveOffset: true, + + /** + * @cfg {Boolean} clearInnerCtOnLayout + */ + clearInnerCtOnLayout: false, + + flexSortFn: function (a, b) { + var maxParallelPrefix = 'max' + this.parallelPrefixCap, + infiniteValue = Infinity; + a = a.component[maxParallelPrefix] || infiniteValue; + b = b.component[maxParallelPrefix] || infiniteValue; + // IE 6/7 Don't like Infinity - Infinity... + if (!isFinite(a) && !isFinite(b)) { + return false; + } + return a - b; + }, + + // Sort into *descending* order. + minSizeSortFn: function(a, b) { + return b.available - a.available; + }, + + constructor: function(config) { + var me = this; + + me.callParent(arguments); + + // The sort function needs access to properties in this, so must be bound. + me.flexSortFn = Ext.Function.bind(me.flexSortFn, me); + + me.initOverflowHandler(); + }, + + /** + * @private + * Returns the current size and positioning of the passed child item. + * @param {Component} child The child Component to calculate the box for + * @return {Object} Object containing box measurements for the child. Properties are left,top,width,height. + */ + getChildBox: function(child) { + child = child.el || this.owner.getComponent(child).el; + return { + left: child.getLeft(true), + top: child.getTop(true), + width: child.getWidth(), + height: child.getHeight() + }; + }, + + /** + * @private + * Calculates the size and positioning of the passed child item. + * @param {Component} child The child Component to calculate the box for + * @return {Object} Object containing box measurements for the child. Properties are left,top,width,height. + */ + calculateChildBox: function(child) { + var me = this, + boxes = me.calculateChildBoxes(me.getVisibleItems(), me.getLayoutTargetSize()).boxes, + ln = boxes.length, + i = 0; + + child = me.owner.getComponent(child); + for (; i < ln; i++) { + if (boxes[i].component === child) { + return boxes[i]; + } + } + }, + + /** + * @private + * Calculates the size and positioning of each item in the box. This iterates over all of the rendered, + * visible items and returns a height, width, top and left for each, as well as a reference to each. Also + * returns meta data such as maxSize which are useful when resizing layout wrappers such as this.innerCt. + * @param {Array} visibleItems The array of all rendered, visible items to be calculated for + * @param {Object} targetSize Object containing target size and height + * @return {Object} Object containing box measurements for each child, plus meta data + */ + calculateChildBoxes: function(visibleItems, targetSize) { + var me = this, + math = Math, + mmax = math.max, + infiniteValue = Infinity, + undefinedValue, + + parallelPrefix = me.parallelPrefix, + parallelPrefixCap = me.parallelPrefixCap, + perpendicularPrefix = me.perpendicularPrefix, + perpendicularPrefixCap = me.perpendicularPrefixCap, + parallelMinString = 'min' + parallelPrefixCap, + perpendicularMinString = 'min' + perpendicularPrefixCap, + perpendicularMaxString = 'max' + perpendicularPrefixCap, + + parallelSize = targetSize[parallelPrefix] - me.scrollOffset, + perpendicularSize = targetSize[perpendicularPrefix], + padding = me.padding, + parallelOffset = padding[me.parallelBefore], + paddingParallel = parallelOffset + padding[me.parallelAfter], + perpendicularOffset = padding[me.perpendicularLeftTop], + paddingPerpendicular = perpendicularOffset + padding[me.perpendicularRightBottom], + availPerpendicularSize = mmax(0, perpendicularSize - paddingPerpendicular), + + isStart = me.pack == 'start', + isCenter = me.pack == 'center', + isEnd = me.pack == 'end', + + constrain = Ext.Number.constrain, + visibleCount = visibleItems.length, + nonFlexSize = 0, + totalFlex = 0, + desiredSize = 0, + minimumSize = 0, + maxSize = 0, + boxes = [], + minSizes = [], + calculatedWidth, + + i, child, childParallel, childPerpendicular, childMargins, childSize, minParallel, tmpObj, shortfall, + tooNarrow, availableSpace, minSize, item, length, itemIndex, box, oldSize, newSize, reduction, diff, + flexedBoxes, remainingSpace, remainingFlex, flexedSize, parallelMargins, calcs, offset, + perpendicularMargins, stretchSize; + + //gather the total flex of all flexed items and the width taken up by fixed width items + for (i = 0; i < visibleCount; i++) { + child = visibleItems[i]; + childPerpendicular = child[perpendicularPrefix]; + me.layoutItem(child); + childMargins = child.margins; + parallelMargins = childMargins[me.parallelBefore] + childMargins[me.parallelAfter]; + + // Create the box description object for this child item. + tmpObj = { + component: child, + margins: childMargins + }; + + // flex and not 'auto' width + if (child.flex) { + totalFlex += child.flex; + childParallel = undefinedValue; + } + // Not flexed or 'auto' width or undefined width + else { + if (!(child[parallelPrefix] && childPerpendicular)) { + childSize = child.getSize(); + } + childParallel = child[parallelPrefix] || childSize[parallelPrefix]; + childPerpendicular = childPerpendicular || childSize[perpendicularPrefix]; + } + + nonFlexSize += parallelMargins + (childParallel || 0); + desiredSize += parallelMargins + (child.flex ? child[parallelMinString] || 0 : childParallel); + minimumSize += parallelMargins + (child[parallelMinString] || childParallel || 0); + + // Max height for align - force layout of non-laid out subcontainers without a numeric height + if (typeof childPerpendicular != 'number') { + // Clear any static sizing and revert to flow so we can get a proper measurement + // child['set' + perpendicularPrefixCap](null); + childPerpendicular = child['get' + perpendicularPrefixCap](); + } + + // Track the maximum perpendicular size for use by the stretch and stretchmax align config values. + maxSize = mmax(maxSize, childPerpendicular + childMargins[me.perpendicularLeftTop] + childMargins[me.perpendicularRightBottom]); + + tmpObj[parallelPrefix] = childParallel || undefinedValue; + tmpObj[perpendicularPrefix] = childPerpendicular || undefinedValue; + boxes.push(tmpObj); + } + shortfall = desiredSize - parallelSize; + tooNarrow = minimumSize > parallelSize; + + //the space available to the flexed items + availableSpace = mmax(0, parallelSize - nonFlexSize - paddingParallel - (me.reserveOffset ? me.availableSpaceOffset : 0)); + + if (tooNarrow) { + for (i = 0; i < visibleCount; i++) { + box = boxes[i]; + minSize = visibleItems[i][parallelMinString] || visibleItems[i][parallelPrefix] || box[parallelPrefix]; + box.dirtySize = box.dirtySize || box[parallelPrefix] != minSize; + box[parallelPrefix] = minSize; + } + } + else { + //all flexed items should be sized to their minimum size, other items should be shrunk down until + //the shortfall has been accounted for + if (shortfall > 0) { + /* + * When we have a shortfall but are not tooNarrow, we need to shrink the width of each non-flexed item. + * Flexed items are immediately reduced to their minWidth and anything already at minWidth is ignored. + * The remaining items are collected into the minWidths array, which is later used to distribute the shortfall. + */ + for (i = 0; i < visibleCount; i++) { + item = visibleItems[i]; + minSize = item[parallelMinString] || 0; + + //shrink each non-flex tab by an equal amount to make them all fit. Flexed items are all + //shrunk to their minSize because they're flexible and should be the first to lose size + if (item.flex) { + box = boxes[i]; + box.dirtySize = box.dirtySize || box[parallelPrefix] != minSize; + box[parallelPrefix] = minSize; + } + else { + minSizes.push({ + minSize: minSize, + available: boxes[i][parallelPrefix] - minSize, + index: i + }); + } + } + + //sort by descending amount of width remaining before minWidth is reached + Ext.Array.sort(minSizes, me.minSizeSortFn); + + /* + * Distribute the shortfall (difference between total desired size of all items and actual size available) + * between the non-flexed items. We try to distribute the shortfall evenly, but apply it to items with the + * smallest difference between their size and minSize first, so that if reducing the size by the average + * amount would make that item less than its minSize, we carry the remainder over to the next item. + */ + for (i = 0, length = minSizes.length; i < length; i++) { + itemIndex = minSizes[i].index; + + if (itemIndex == undefinedValue) { + continue; + } + item = visibleItems[itemIndex]; + minSize = minSizes[i].minSize; + + box = boxes[itemIndex]; + oldSize = box[parallelPrefix]; + newSize = mmax(minSize, oldSize - math.ceil(shortfall / (length - i))); + reduction = oldSize - newSize; + + box.dirtySize = box.dirtySize || box[parallelPrefix] != newSize; + box[parallelPrefix] = newSize; + shortfall -= reduction; + } + } + else { + remainingSpace = availableSpace; + remainingFlex = totalFlex; + flexedBoxes = []; + + // Create an array containing *just the flexed boxes* for allocation of remainingSpace + for (i = 0; i < visibleCount; i++) { + child = visibleItems[i]; + if (isStart && child.flex) { + flexedBoxes.push(boxes[Ext.Array.indexOf(visibleItems, child)]); + } + } + // The flexed boxes need to be sorted in ascending order of maxSize to work properly + // so that unallocated space caused by maxWidth being less than flexed width + // can be reallocated to subsequent flexed boxes. + Ext.Array.sort(flexedBoxes, me.flexSortFn); + + // Calculate the size of each flexed item, and attempt to set it. + for (i = 0; i < flexedBoxes.length; i++) { + calcs = flexedBoxes[i]; + child = calcs.component; + childMargins = calcs.margins; + + flexedSize = math.ceil((child.flex / remainingFlex) * remainingSpace); + + // Implement maxSize and minSize check + flexedSize = Math.max(child['min' + parallelPrefixCap] || 0, math.min(child['max' + parallelPrefixCap] || infiniteValue, flexedSize)); + + // Remaining space has already had all parallel margins subtracted from it, so just subtract consumed size + remainingSpace -= flexedSize; + remainingFlex -= child.flex; + + calcs.dirtySize = calcs.dirtySize || calcs[parallelPrefix] != flexedSize; + calcs[parallelPrefix] = flexedSize; + } + } + } + + if (isCenter) { + parallelOffset += availableSpace / 2; + } + else if (isEnd) { + parallelOffset += availableSpace; + } + + // Fix for left and right docked Components in a dock component layout. This is for docked Headers and docked Toolbars. + // Older Microsoft browsers do not size a position:absolute element's width to match its content. + // So in this case, in the updateInnerCtSize method we may need to adjust the size of the owning Container's element explicitly based upon + // the discovered max width. So here we put a calculatedWidth property in the metadata to facilitate this. + if (me.owner.dock && (Ext.isIE6 || Ext.isIE7 || Ext.isIEQuirks) && !me.owner.width && me.direction == 'vertical') { + + calculatedWidth = maxSize + me.owner.el.getPadding('lr') + me.owner.el.getBorderWidth('lr'); + if (me.owner.frameSize) { + calculatedWidth += me.owner.frameSize.left + me.owner.frameSize.right; + } + // If the owning element is not sized, calculate the available width to center or stretch in based upon maxSize + availPerpendicularSize = Math.min(availPerpendicularSize, targetSize.width = maxSize + padding.left + padding.right); + } + + //finally, calculate the left and top position of each item + for (i = 0; i < visibleCount; i++) { + child = visibleItems[i]; + calcs = boxes[i]; + + childMargins = calcs.margins; + + perpendicularMargins = childMargins[me.perpendicularLeftTop] + childMargins[me.perpendicularRightBottom]; + + // Advance past the "before" margin + parallelOffset += childMargins[me.parallelBefore]; + + calcs[me.parallelBefore] = parallelOffset; + calcs[me.perpendicularLeftTop] = perpendicularOffset + childMargins[me.perpendicularLeftTop]; + + if (me.align == 'stretch') { + stretchSize = constrain(availPerpendicularSize - perpendicularMargins, child[perpendicularMinString] || 0, child[perpendicularMaxString] || infiniteValue); + calcs.dirtySize = calcs.dirtySize || calcs[perpendicularPrefix] != stretchSize; + calcs[perpendicularPrefix] = stretchSize; + } + else if (me.align == 'stretchmax') { + stretchSize = constrain(maxSize - perpendicularMargins, child[perpendicularMinString] || 0, child[perpendicularMaxString] || infiniteValue); + calcs.dirtySize = calcs.dirtySize || calcs[perpendicularPrefix] != stretchSize; + calcs[perpendicularPrefix] = stretchSize; + } + else if (me.align == me.alignCenteringString) { + // When calculating a centered position within the content box of the innerCt, the width of the borders must be subtracted from + // the size to yield the space available to center within. + // The updateInnerCtSize method explicitly adds the border widths to the set size of the innerCt. + diff = mmax(availPerpendicularSize, maxSize) - me.innerCt.getBorderWidth(me.perpendicularLT + me.perpendicularRB) - calcs[perpendicularPrefix]; + if (diff > 0) { + calcs[me.perpendicularLeftTop] = perpendicularOffset + Math.round(diff / 2); + } + } + + // Advance past the box size and the "after" margin + parallelOffset += (calcs[parallelPrefix] || 0) + childMargins[me.parallelAfter]; + } + + return { + boxes: boxes, + meta : { + calculatedWidth: calculatedWidth, + maxSize: maxSize, + nonFlexSize: nonFlexSize, + desiredSize: desiredSize, + minimumSize: minimumSize, + shortfall: shortfall, + tooNarrow: tooNarrow + } + }; + }, + + /** + * @private + */ + initOverflowHandler: function() { + var handler = this.overflowHandler; + + if (typeof handler == 'string') { + handler = { + type: handler + }; + } + + var handlerType = 'None'; + if (handler && handler.type != undefined) { + handlerType = handler.type; + } + + var constructor = Ext.layout.container.boxOverflow[handlerType]; + if (constructor[this.type]) { + constructor = constructor[this.type]; + } + + this.overflowHandler = Ext.create('Ext.layout.container.boxOverflow.' + handlerType, this, handler); + }, + + /** + * @private + * Runs the child box calculations and caches them in childBoxCache. Subclasses can used these cached values + * when laying out + */ + onLayout: function() { + this.callParent(); + // Clear the innerCt size so it doesn't influence the child items. + if (this.clearInnerCtOnLayout === true && this.adjustmentPass !== true) { + this.innerCt.setSize(null, null); + } + + var me = this, + targetSize = me.getLayoutTargetSize(), + items = me.getVisibleItems(), + calcs = me.calculateChildBoxes(items, targetSize), + boxes = calcs.boxes, + meta = calcs.meta, + handler, method, results; + + if (me.autoSize && calcs.meta.desiredSize) { + targetSize[me.parallelPrefix] = calcs.meta.desiredSize; + } + + //invoke the overflow handler, if one is configured + if (meta.shortfall > 0) { + handler = me.overflowHandler; + method = meta.tooNarrow ? 'handleOverflow': 'clearOverflow'; + + results = handler[method](calcs, targetSize); + + if (results) { + if (results.targetSize) { + targetSize = results.targetSize; + } + + if (results.recalculate) { + items = me.getVisibleItems(owner); + calcs = me.calculateChildBoxes(items, targetSize); + boxes = calcs.boxes; + } + } + } else { + me.overflowHandler.clearOverflow(); + } + + /** + * @private + * @property layoutTargetLastSize + * @type Object + * Private cache of the last measured size of the layout target. This should never be used except by + * BoxLayout subclasses during their onLayout run. + */ + me.layoutTargetLastSize = targetSize; + + /** + * @private + * @property childBoxCache + * @type Array + * Array of the last calculated height, width, top and left positions of each visible rendered component + * within the Box layout. + */ + me.childBoxCache = calcs; + + me.updateInnerCtSize(targetSize, calcs); + me.updateChildBoxes(boxes); + me.handleTargetOverflow(targetSize); + }, + + /** + * Resizes and repositions each child component + * @param {Array} boxes The box measurements + */ + updateChildBoxes: function(boxes) { + var me = this, + i = 0, + length = boxes.length, + animQueue = [], + dd = Ext.dd.DDM.getDDById(me.innerCt.id), // Any DD active on this layout's element (The BoxReorderer plugin does this.) + oldBox, newBox, changed, comp, boxAnim, animCallback; + + for (; i < length; i++) { + newBox = boxes[i]; + comp = newBox.component; + + // If a Component is being drag/dropped, skip positioning it. + // Accomodate the BoxReorderer plugin: Its current dragEl must not be positioned by the layout + if (dd && (dd.getDragEl() === comp.el.dom)) { + continue; + } + + changed = false; + + oldBox = me.getChildBox(comp); + + // If we are animating, we build up an array of Anim config objects, one for each + // child Component which has any changed box properties. Those with unchanged + // properties are not animated. + if (me.animate) { + // Animate may be a config object containing callback. + animCallback = me.animate.callback || me.animate; + boxAnim = { + layoutAnimation: true, // Component Target handler must use set*Calculated*Size + target: comp, + from: {}, + to: {}, + listeners: {} + }; + // Only set from and to properties when there's a change. + // Perform as few Component setter methods as possible. + // Temporarily set the property values that we are not animating + // so that doComponentLayout does not auto-size them. + if (!isNaN(newBox.width) && (newBox.width != oldBox.width)) { + changed = true; + // boxAnim.from.width = oldBox.width; + boxAnim.to.width = newBox.width; + } + if (!isNaN(newBox.height) && (newBox.height != oldBox.height)) { + changed = true; + // boxAnim.from.height = oldBox.height; + boxAnim.to.height = newBox.height; + } + if (!isNaN(newBox.left) && (newBox.left != oldBox.left)) { + changed = true; + // boxAnim.from.left = oldBox.left; + boxAnim.to.left = newBox.left; + } + if (!isNaN(newBox.top) && (newBox.top != oldBox.top)) { + changed = true; + // boxAnim.from.top = oldBox.top; + boxAnim.to.top = newBox.top; + } + if (changed) { + animQueue.push(boxAnim); + } + } else { + if (newBox.dirtySize) { + if (newBox.width !== oldBox.width || newBox.height !== oldBox.height) { + me.setItemSize(comp, newBox.width, newBox.height); + } + } + // Don't set positions to NaN + if (isNaN(newBox.left) || isNaN(newBox.top)) { + continue; + } + comp.setPosition(newBox.left, newBox.top); + } + } + + // Kick off any queued animations + length = animQueue.length; + if (length) { + + // A function which cleans up when a Component's animation is done. + // The last one to finish calls the callback. + var afterAnimate = function(anim) { + // When we've animated all changed boxes into position, clear our busy flag and call the callback. + length -= 1; + if (!length) { + me.layoutBusy = false; + if (Ext.isFunction(animCallback)) { + animCallback(); + } + } + }; + + var beforeAnimate = function() { + me.layoutBusy = true; + }; + + // Start each box animation off + for (i = 0, length = animQueue.length; i < length; i++) { + boxAnim = animQueue[i]; + + // Clean up the Component after. Clean up the *layout* after the last animation finishes + boxAnim.listeners.afteranimate = afterAnimate; + + // The layout is busy during animation, and may not be called, so set the flag when the first animation begins + if (!i) { + boxAnim.listeners.beforeanimate = beforeAnimate; + } + if (me.animate.duration) { + boxAnim.duration = me.animate.duration; + } + comp = boxAnim.target; + delete boxAnim.target; + // Stop any currently running animation + comp.stopAnimation(); + comp.animate(boxAnim); + } + } + }, + + /** + * @private + * Called by onRender just before the child components are sized and positioned. This resizes the innerCt + * to make sure all child items fit within it. We call this before sizing the children because if our child + * items are larger than the previous innerCt size the browser will insert scrollbars and then remove them + * again immediately afterwards, giving a performance hit. + * Subclasses should provide an implementation. + * @param {Object} currentSize The current height and width of the innerCt + * @param {Array} calculations The new box calculations of all items to be laid out + */ + updateInnerCtSize: function(tSize, calcs) { + var me = this, + mmax = Math.max, + align = me.align, + padding = me.padding, + width = tSize.width, + height = tSize.height, + meta = calcs.meta, + innerCtWidth, + innerCtHeight; + + if (me.direction == 'horizontal') { + innerCtWidth = width; + innerCtHeight = meta.maxSize + padding.top + padding.bottom + me.innerCt.getBorderWidth('tb'); + + if (align == 'stretch') { + innerCtHeight = height; + } + else if (align == 'middle') { + innerCtHeight = mmax(height, innerCtHeight); + } + } else { + innerCtHeight = height; + innerCtWidth = meta.maxSize + padding.left + padding.right + me.innerCt.getBorderWidth('lr'); + + if (align == 'stretch') { + innerCtWidth = width; + } + else if (align == 'center') { + innerCtWidth = mmax(width, innerCtWidth); + } + } + me.getRenderTarget().setSize(innerCtWidth || undefined, innerCtHeight || undefined); + + // If a calculated width has been found (and this only happens for auto-width vertical docked Components in old Microsoft browsers) + // then, if the Component has not assumed the size of its content, set it to do so. + if (meta.calculatedWidth && me.owner.el.getWidth() > meta.calculatedWidth) { + me.owner.el.setWidth(meta.calculatedWidth); + } + + if (me.innerCt.dom.scrollTop) { + me.innerCt.dom.scrollTop = 0; + } + }, + + /** + * @private + * This should be called after onLayout of any BoxLayout subclass. If the target's overflow is not set to 'hidden', + * we need to lay out a second time because the scrollbars may have modified the height and width of the layout + * target. Having a Box layout inside such a target is therefore not recommended. + * @param {Object} previousTargetSize The size and height of the layout target before we just laid out + * @param {Ext.container.Container} container The container + * @param {Ext.core.Element} target The target element + * @return True if the layout overflowed, and was reflowed in a secondary onLayout call. + */ + handleTargetOverflow: function(previousTargetSize) { + var target = this.getTarget(), + overflow = target.getStyle('overflow'), + newTargetSize; + + if (overflow && overflow != 'hidden' && !this.adjustmentPass) { + newTargetSize = this.getLayoutTargetSize(); + if (newTargetSize.width != previousTargetSize.width || newTargetSize.height != previousTargetSize.height) { + this.adjustmentPass = true; + this.onLayout(); + return true; + } + } + + delete this.adjustmentPass; + }, + + // private + isValidParent : function(item, target, position) { + // Note: Box layouts do not care about order within the innerCt element because it's an absolutely positioning layout + // We only care whether the item is a direct child of the innerCt element. + var itemEl = item.el ? item.el.dom : Ext.getDom(item); + return (itemEl && this.innerCt && itemEl.parentNode === this.innerCt.dom) || false; + }, + + // Overridden method from AbstractContainer. + // Used in the base AbstractLayout.beforeLayout method to render all items into. + getRenderTarget: function() { + if (!this.innerCt) { + // the innerCt prevents wrapping and shuffling while the container is resizing + this.innerCt = this.getTarget().createChild({ + cls: this.innerCls, + role: 'presentation' + }); + this.padding = Ext.util.Format.parseBox(this.padding); + } + return this.innerCt; + }, + + // private + renderItem: function(item, target) { + this.callParent(arguments); + var me = this, + itemEl = item.getEl(), + style = itemEl.dom.style, + margins = item.margins || item.margin; + + // Parse the item's margin/margins specification + if (margins) { + if (Ext.isString(margins) || Ext.isNumber(margins)) { + margins = Ext.util.Format.parseBox(margins); + } else { + Ext.applyIf(margins, {top: 0, right: 0, bottom: 0, left: 0}); + } + } else { + margins = Ext.apply({}, me.defaultMargins); + } + + // Add any before/after CSS margins to the configured margins, and zero the CSS margins + margins.top += itemEl.getMargin('t'); + margins.right += itemEl.getMargin('r'); + margins.bottom += itemEl.getMargin('b'); + margins.left += itemEl.getMargin('l'); + style.marginTop = style.marginRight = style.marginBottom = style.marginLeft = '0'; + + // Item must reference calculated margins. + item.margins = margins; + }, + + /** + * @private + */ + destroy: function() { + Ext.destroy(this.overflowHandler); + this.callParent(arguments); + } +}); +/** + * @class Ext.layout.container.HBox + * @extends Ext.layout.container.Box + *

A layout that arranges items horizontally across a Container. This layout optionally divides available horizontal + * space between child items containing a numeric flex configuration.

+ * This layout may also be used to set the heights of child items by configuring it with the {@link #align} option. + * {@img Ext.layout.container.HBox/Ext.layout.container.HBox.png Ext.layout.container.HBox container layout} + * Example usage: + Ext.create('Ext.Panel', { + width: 500, + height: 300, + title: "HBoxLayout Panel", + layout: { + type: 'hbox', + align: 'stretch' + }, + renderTo: document.body, + items: [{ + xtype: 'panel', + title: 'Inner Panel One', + flex: 2 + },{ + xtype: 'panel', + title: 'Inner Panel Two', + flex: 1 + },{ + xtype: 'panel', + title: 'Inner Panel Three', + flex: 1 + }] + }); + */ +Ext.define('Ext.layout.container.HBox', { + + /* Begin Definitions */ + + alias: ['layout.hbox'], + extend: 'Ext.layout.container.Box', + alternateClassName: 'Ext.layout.HBoxLayout', + + /* End Definitions */ + + /** + * @cfg {String} align + * Controls how the child items of the container are aligned. Acceptable configuration values for this + * property are: + *
    + *
  • top : Default
    child items are aligned vertically + * at the top of the container
  • + *
  • middle :
    child items are aligned vertically in the + * middle of the container
  • + *
  • stretch :
    child items are stretched vertically to fill + * the height of the container
  • + *
  • stretchmax :
    child items are stretched vertically to + * the height of the largest item.
  • + *
+ */ + align: 'top', // top, middle, stretch, strechmax + + //@private + alignCenteringString: 'middle', + + type : 'hbox', + + direction: 'horizontal', + + // When creating an argument list to setSize, use this order + parallelSizeIndex: 0, + perpendicularSizeIndex: 1, + + parallelPrefix: 'width', + parallelPrefixCap: 'Width', + parallelLT: 'l', + parallelRB: 'r', + parallelBefore: 'left', + parallelBeforeCap: 'Left', + parallelAfter: 'right', + parallelPosition: 'x', + + perpendicularPrefix: 'height', + perpendicularPrefixCap: 'Height', + perpendicularLT: 't', + perpendicularRB: 'b', + perpendicularLeftTop: 'top', + perpendicularRightBottom: 'bottom', + perpendicularPosition: 'y' +}); +/** + * @class Ext.layout.container.VBox + * @extends Ext.layout.container.Box + *

A layout that arranges items vertically down a Container. This layout optionally divides available vertical + * space between child items containing a numeric flex configuration.

+ * This layout may also be used to set the widths of child items by configuring it with the {@link #align} option. + * {@img Ext.layout.container.VBox/Ext.layout.container.VBox.png Ext.layout.container.VBox container layout} + * Example usage: + Ext.create('Ext.Panel', { + width: 500, + height: 400, + title: "VBoxLayout Panel", + layout: { + type: 'vbox', + align: 'center' + }, + renderTo: document.body, + items: [{ + xtype: 'panel', + title: 'Inner Panel One', + width: 250, + flex: 2 + },{ + xtype: 'panel', + title: 'Inner Panel Two', + width: 250, + flex: 4 + },{ + xtype: 'panel', + title: 'Inner Panel Three', + width: '50%', + flex: 4 + }] + }); + */ +Ext.define('Ext.layout.container.VBox', { + + /* Begin Definitions */ + + alias: ['layout.vbox'], + extend: 'Ext.layout.container.Box', + alternateClassName: 'Ext.layout.VBoxLayout', + + /* End Definitions */ + + /** + * @cfg {String} align + * Controls how the child items of the container are aligned. Acceptable configuration values for this + * property are: + *
    + *
  • left : Default
    child items are aligned horizontally + * at the left side of the container
  • + *
  • center :
    child items are aligned horizontally at the + * mid-width of the container
  • + *
  • stretch :
    child items are stretched horizontally to fill + * the width of the container
  • + *
  • stretchmax :
    child items are stretched horizontally to + * the size of the largest item.
  • + *
+ */ + align : 'left', // left, center, stretch, strechmax + + //@private + alignCenteringString: 'center', + + type: 'vbox', + + direction: 'vertical', + + // When creating an argument list to setSize, use this order + parallelSizeIndex: 1, + perpendicularSizeIndex: 0, + + parallelPrefix: 'height', + parallelPrefixCap: 'Height', + parallelLT: 't', + parallelRB: 'b', + parallelBefore: 'top', + parallelBeforeCap: 'Top', + parallelAfter: 'bottom', + parallelPosition: 'y', + + perpendicularPrefix: 'width', + perpendicularPrefixCap: 'Width', + perpendicularLT: 'l', + perpendicularRB: 'r', + perpendicularLeftTop: 'left', + perpendicularRightBottom: 'right', + perpendicularPosition: 'x' +}); +/** + * @class Ext.FocusManager + +The FocusManager is responsible for globally: + +1. Managing component focus +2. Providing basic keyboard navigation +3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame. + +To activate the FocusManager, simply call {@link #enable `Ext.FocusManager.enable();`}. In turn, you may +deactivate the FocusManager by subsequently calling {@link #disable `Ext.FocusManager.disable();`}. The +FocusManager is disabled by default. + +To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}. + +Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container} +that would like to have navigation between its child {@link Ext.Component}'s. The {@link Ext.container.Container} can simply +call {@link #subscribe Ext.FocusManager.subscribe} to take advantage of this feature, and can at any time call +{@link #unsubscribe Ext.FocusManager.unsubscribe} to turn the navigation off. + + * @singleton + * @markdown + * @author Jarred Nicholls + * @docauthor Jarred Nicholls + */ +Ext.define('Ext.FocusManager', { + singleton: true, + alternateClassName: 'Ext.FocusMgr', + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: [ + 'Ext.ComponentManager', + 'Ext.ComponentQuery', + 'Ext.util.HashMap', + 'Ext.util.KeyNav' + ], + + /** + * @property {Boolean} enabled + * Whether or not the FocusManager is currently enabled + */ + enabled: false, + + /** + * @property {Ext.Component} focusedCmp + * The currently focused component. Defaults to `undefined`. + * @markdown + */ + + focusElementCls: Ext.baseCSSPrefix + 'focus-element', + + focusFrameCls: Ext.baseCSSPrefix + 'focus-frame', + + /** + * @property {Array} whitelist + * A list of xtypes that should ignore certain navigation input keys and + * allow for the default browser event/behavior. These input keys include: + * + * 1. Backspace + * 2. Delete + * 3. Left + * 4. Right + * 5. Up + * 6. Down + * + * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof) + * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow + * the user to move the input cursor left and right, and to delete characters, etc. + * + * This whitelist currently defaults to `['textfield']`. + * @markdown + */ + whitelist: [ + 'textfield' + ], + + tabIndexWhitelist: [ + 'a', + 'button', + 'embed', + 'frame', + 'iframe', + 'img', + 'input', + 'object', + 'select', + 'textarea' + ], + + constructor: function() { + var me = this, + CQ = Ext.ComponentQuery; + + me.addEvents( + /** + * @event beforecomponentfocus + * Fires before a component becomes focused. Return `false` to prevent + * the component from gaining focus. + * @param {Ext.FocusManager} fm A reference to the FocusManager singleton + * @param {Ext.Component} cmp The component that is being focused + * @param {Ext.Component} previousCmp The component that was previously focused, + * or `undefined` if there was no previously focused component. + * @markdown + */ + 'beforecomponentfocus', + + /** + * @event componentfocus + * Fires after a component becomes focused. + * @param {Ext.FocusManager} fm A reference to the FocusManager singleton + * @param {Ext.Component} cmp The component that has been focused + * @param {Ext.Component} previousCmp The component that was previously focused, + * or `undefined` if there was no previously focused component. + * @markdown + */ + 'componentfocus', + + /** + * @event disable + * Fires when the FocusManager is disabled + * @param {Ext.FocusManager} fm A reference to the FocusManager singleton + */ + 'disable', + + /** + * @event enable + * Fires when the FocusManager is enabled + * @param {Ext.FocusManager} fm A reference to the FocusManager singleton + */ + 'enable' + ); + + // Setup KeyNav that's bound to document to catch all + // unhandled/bubbled key events for navigation + me.keyNav = Ext.create('Ext.util.KeyNav', Ext.getDoc(), { + disabled: true, + scope: me, + + backspace: me.focusLast, + enter: me.navigateIn, + esc: me.navigateOut, + tab: me.navigateSiblings + + //space: me.navigateIn, + //del: me.focusLast, + //left: me.navigateSiblings, + //right: me.navigateSiblings, + //down: me.navigateSiblings, + //up: me.navigateSiblings + }); + + me.focusData = {}; + me.subscribers = Ext.create('Ext.util.HashMap'); + me.focusChain = {}; + + // Setup some ComponentQuery pseudos + Ext.apply(CQ.pseudos, { + focusable: function(cmps) { + var len = cmps.length, + results = [], + i = 0, + c, + + isFocusable = function(x) { + return x && x.focusable !== false && CQ.is(x, '[rendered]:not([destroying]):not([isDestroyed]):not([disabled]){isVisible(true)}{el && c.el.dom && c.el.isVisible()}'); + }; + + for (; i < len; i++) { + c = cmps[i]; + if (isFocusable(c)) { + results.push(c); + } + } + + return results; + }, + + nextFocus: function(cmps, idx, step) { + step = step || 1; + idx = parseInt(idx, 10); + + var len = cmps.length, + i = idx + step, + c; + + for (; i != idx; i += step) { + if (i >= len) { + i = 0; + } else if (i < 0) { + i = len - 1; + } + + c = cmps[i]; + if (CQ.is(c, ':focusable')) { + return [c]; + } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) { + return [c.placeholder]; + } + } + + return []; + }, + + prevFocus: function(cmps, idx) { + return this.nextFocus(cmps, idx, -1); + }, + + root: function(cmps) { + var len = cmps.length, + results = [], + i = 0, + c; + + for (; i < len; i++) { + c = cmps[i]; + if (!c.ownerCt) { + results.push(c); + } + } + + return results; + } + }); + }, + + /** + * Adds the specified xtype to the {@link #whitelist}. + * @param {String/Array} xtype Adds the xtype(s) to the {@link #whitelist}. + */ + addXTypeToWhitelist: function(xtype) { + var me = this; + + if (Ext.isArray(xtype)) { + Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me); + return; + } + + if (!Ext.Array.contains(me.whitelist, xtype)) { + me.whitelist.push(xtype); + } + }, + + clearComponent: function(cmp) { + clearTimeout(this.cmpFocusDelay); + if (!cmp.isDestroyed) { + cmp.blur(); + } + }, + + /** + * Disables the FocusManager by turning of all automatic focus management and keyboard navigation + */ + disable: function() { + var me = this; + + if (!me.enabled) { + return; + } + + delete me.options; + me.enabled = false; + + Ext.ComponentManager.all.un('add', me.onComponentCreated, me); + + me.removeDOM(); + + // Stop handling key navigation + me.keyNav.disable(); + + // disable focus for all components + me.setFocusAll(false); + + me.fireEvent('disable', me); + }, + + /** + * Enables the FocusManager by turning on all automatic focus management and keyboard navigation + * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object of the following options: + - focusFrame : Boolean + `true` to show the focus frame around a component when it is focused. Defaults to `false`. + * @markdown + */ + enable: function(options) { + var me = this; + + if (options === true) { + options = { focusFrame: true }; + } + me.options = options = options || {}; + + if (me.enabled) { + return; + } + + // Handle components that are newly added after we are enabled + Ext.ComponentManager.all.on('add', me.onComponentCreated, me); + + me.initDOM(options); + + // Start handling key navigation + me.keyNav.enable(); + + // enable focus for all components + me.setFocusAll(true, options); + + // Finally, let's focus our global focus el so we start fresh + me.focusEl.focus(); + delete me.focusedCmp; + + me.enabled = true; + me.fireEvent('enable', me); + }, + + focusLast: function(e) { + var me = this; + + if (me.isWhitelisted(me.focusedCmp)) { + return true; + } + + // Go back to last focused item + if (me.previousFocusedCmp) { + me.previousFocusedCmp.focus(); + } + }, + + getRootComponents: function() { + var me = this, + CQ = Ext.ComponentQuery, + inline = CQ.query(':focusable:root:not([floating])'), + floating = CQ.query(':focusable:root[floating]'); + + // Floating items should go to the top of our root stack, and be ordered + // by their z-index (highest first) + floating.sort(function(a, b) { + return a.el.getZIndex() > b.el.getZIndex(); + }); + + return floating.concat(inline); + }, + + initDOM: function(options) { + var me = this, + sp = ' ', + cls = me.focusFrameCls; + + if (!Ext.isReady) { + Ext.onReady(me.initDOM, me); + return; + } + + // Create global focus element + if (!me.focusEl) { + me.focusEl = Ext.getBody().createChild({ + tabIndex: '-1', + cls: me.focusElementCls, + html: sp + }); + } + + // Create global focus frame + if (!me.focusFrame && options.focusFrame) { + me.focusFrame = Ext.getBody().createChild({ + cls: cls, + children: [ + { cls: cls + '-top' }, + { cls: cls + '-bottom' }, + { cls: cls + '-left' }, + { cls: cls + '-right' } + ], + style: 'top: -100px; left: -100px;' + }); + me.focusFrame.setVisibilityMode(Ext.core.Element.DISPLAY); + me.focusFrameWidth = me.focusFrame.child('.' + cls + '-top').getHeight(); + me.focusFrame.hide().setLeftTop(0, 0); + } + }, + + isWhitelisted: function(cmp) { + return cmp && Ext.Array.some(this.whitelist, function(x) { + return cmp.isXType(x); + }); + }, + + navigateIn: function(e) { + var me = this, + focusedCmp = me.focusedCmp, + rootCmps, + firstChild; + + if (!focusedCmp) { + // No focus yet, so focus the first root cmp on the page + rootCmps = me.getRootComponents(); + if (rootCmps.length) { + rootCmps[0].focus(); + } + } else { + // Drill into child ref items of the focused cmp, if applicable. + // This works for any Component with a getRefItems implementation. + firstChild = Ext.ComponentQuery.query('>:focusable', focusedCmp)[0]; + if (firstChild) { + firstChild.focus(); + } else { + // Let's try to fire a click event, as if it came from the mouse + if (Ext.isFunction(focusedCmp.onClick)) { + e.button = 0; + focusedCmp.onClick(e); + focusedCmp.focus(); + } + } + } + }, + + navigateOut: function(e) { + var me = this, + parent; + + if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) { + me.focusEl.focus(); + return; + } + + parent.focus(); + }, + + navigateSiblings: function(e, source, parent) { + var me = this, + src = source || me, + key = e.getKey(), + EO = Ext.EventObject, + goBack = e.shiftKey || key == EO.LEFT || key == EO.UP, + checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN, + nextSelector = goBack ? 'prev' : 'next', + idx, next, focusedCmp; + + focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp; + if (!focusedCmp && !parent) { + return; + } + + if (checkWhitelist && me.isWhitelisted(focusedCmp)) { + return true; + } + + parent = parent || focusedCmp.up(); + if (parent) { + idx = focusedCmp ? Ext.Array.indexOf(parent.getRefItems(), focusedCmp) : -1; + next = Ext.ComponentQuery.query('>:' + nextSelector + 'Focus(' + idx + ')', parent)[0]; + if (next && focusedCmp !== next) { + next.focus(); + return next; + } + } + }, + + onComponentBlur: function(cmp, e) { + var me = this; + + if (me.focusedCmp === cmp) { + me.previousFocusedCmp = cmp; + delete me.focusedCmp; + } + + if (me.focusFrame) { + me.focusFrame.hide(); + } + }, + + onComponentCreated: function(hash, id, cmp) { + this.setFocus(cmp, true, this.options); + }, + + onComponentDestroy: function(cmp) { + this.setFocus(cmp, false); + }, + + onComponentFocus: function(cmp, e) { + var me = this, + chain = me.focusChain; + + if (!Ext.ComponentQuery.is(cmp, ':focusable')) { + me.clearComponent(cmp); + + // Check our focus chain, so we don't run into a never ending recursion + // If we've attempted (unsuccessfully) to focus this component before, + // then we're caught in a loop of child->parent->...->child and we + // need to cut the loop off rather than feed into it. + if (chain[cmp.id]) { + return; + } + + // Try to focus the parent instead + var parent = cmp.up(); + if (parent) { + // Add component to our focus chain to detect infinite focus loop + // before we fire off an attempt to focus our parent. + // See the comments above. + chain[cmp.id] = true; + parent.focus(); + } + + return; + } + + // Clear our focus chain when we have a focusable component + me.focusChain = {}; + + // Defer focusing for 90ms so components can do a layout/positioning + // and give us an ability to buffer focuses + clearTimeout(me.cmpFocusDelay); + if (arguments.length !== 2) { + me.cmpFocusDelay = Ext.defer(me.onComponentFocus, 90, me, [cmp, e]); + return; + } + + if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) { + me.clearComponent(cmp); + return; + } + + me.focusedCmp = cmp; + + // If we have a focus frame, show it around the focused component + if (me.shouldShowFocusFrame(cmp)) { + var cls = '.' + me.focusFrameCls + '-', + ff = me.focusFrame, + fw = me.focusFrameWidth, + box = cmp.el.getPageBox(), + + // Size the focus frame's t/b/l/r according to the box + // This leaves a hole in the middle of the frame so user + // interaction w/ the mouse can continue + bt = box.top, + bl = box.left, + bw = box.width, + bh = box.height, + ft = ff.child(cls + 'top'), + fb = ff.child(cls + 'bottom'), + fl = ff.child(cls + 'left'), + fr = ff.child(cls + 'right'); + + ft.setWidth(bw - 2).setLeftTop(bl + 1, bt); + fb.setWidth(bw - 2).setLeftTop(bl + 1, bt + bh - fw); + fl.setHeight(bh - 2).setLeftTop(bl, bt + 1); + fr.setHeight(bh - 2).setLeftTop(bl + bw - fw, bt + 1); + + ff.show(); + } + + me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp); + }, + + onComponentHide: function(cmp) { + var me = this, + CQ = Ext.ComponentQuery, + cmpHadFocus = false, + focusedCmp, + parent; + + if (me.focusedCmp) { + focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0]; + cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp; + + if (focusedCmp) { + me.clearComponent(focusedCmp); + } + } + + me.clearComponent(cmp); + + if (cmpHadFocus) { + parent = CQ.query('^:focusable', cmp)[0]; + if (parent) { + parent.focus(); + } + } + }, + + removeDOM: function() { + var me = this; + + // If we are still enabled globally, or there are still subscribers + // then we will halt here, since our DOM stuff is still being used + if (me.enabled || me.subscribers.length) { + return; + } + + Ext.destroy( + me.focusEl, + me.focusFrame + ); + delete me.focusEl; + delete me.focusFrame; + delete me.focusFrameWidth; + }, + + /** + * Removes the specified xtype from the {@link #whitelist}. + * @param {String/Array} xtype Removes the xtype(s) from the {@link #whitelist}. + */ + removeXTypeFromWhitelist: function(xtype) { + var me = this; + + if (Ext.isArray(xtype)) { + Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me); + return; + } + + Ext.Array.remove(me.whitelist, xtype); + }, + + setFocus: function(cmp, focusable, options) { + var me = this, + el, dom, data, + + needsTabIndex = function(n) { + return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase()) + && n.tabIndex <= 0; + }; + + options = options || {}; + + // Come back and do this after the component is rendered + if (!cmp.rendered) { + cmp.on('afterrender', Ext.pass(me.setFocus, arguments, me), me, { single: true }); + return; + } + + el = cmp.getFocusEl(); + dom = el.dom; + + // Decorate the component's focus el for focus-ability + if ((focusable && !me.focusData[cmp.id]) || (!focusable && me.focusData[cmp.id])) { + if (focusable) { + data = { + focusFrame: options.focusFrame + }; + + // Only set -1 tabIndex if we need it + // inputs, buttons, and anchor tags do not need it, + // and neither does any DOM that has it set already + // programmatically or in markup. + if (needsTabIndex(dom)) { + data.tabIndex = dom.tabIndex; + dom.tabIndex = -1; + } + + el.on({ + focus: data.focusFn = Ext.bind(me.onComponentFocus, me, [cmp], 0), + blur: data.blurFn = Ext.bind(me.onComponentBlur, me, [cmp], 0), + scope: me + }); + cmp.on({ + hide: me.onComponentHide, + close: me.onComponentHide, + beforedestroy: me.onComponentDestroy, + scope: me + }); + + me.focusData[cmp.id] = data; + } else { + data = me.focusData[cmp.id]; + if ('tabIndex' in data) { + dom.tabIndex = data.tabIndex; + } + el.un('focus', data.focusFn, me); + el.un('blur', data.blurFn, me); + cmp.un('hide', me.onComponentHide, me); + cmp.un('close', me.onComponentHide, me); + cmp.un('beforedestroy', me.onComponentDestroy, me); + + delete me.focusData[cmp.id]; + } + } + }, + + setFocusAll: function(focusable, options) { + var me = this, + cmps = Ext.ComponentManager.all.getArray(), + len = cmps.length, + cmp, + i = 0; + + for (; i < len; i++) { + me.setFocus(cmps[i], focusable, options); + } + }, + + setupSubscriberKeys: function(container, keys) { + var me = this, + el = container.getFocusEl(), + scope = keys.scope, + handlers = { + backspace: me.focusLast, + enter: me.navigateIn, + esc: me.navigateOut, + scope: me + }, + + navSiblings = function(e) { + if (me.focusedCmp === container) { + // Root the sibling navigation to this container, so that we + // can automatically dive into the container, rather than forcing + // the user to hit the enter key to dive in. + return me.navigateSiblings(e, me, container); + } else { + return me.navigateSiblings(e); + } + }; + + Ext.iterate(keys, function(key, cb) { + handlers[key] = function(e) { + var ret = navSiblings(e); + + if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) { + return true; + } + + return ret; + }; + }, me); + + return Ext.create('Ext.util.KeyNav', el, handlers); + }, + + shouldShowFocusFrame: function(cmp) { + var me = this, + opts = me.options || {}; + + if (!me.focusFrame || !cmp) { + return false; + } + + // Global trumps + if (opts.focusFrame) { + return true; + } + + if (me.focusData[cmp.id].focusFrame) { + return true; + } + + return false; + }, + + /** + * Subscribes an {@link Ext.container.Container} to provide basic keyboard focus navigation between its child {@link Ext.Component}'s. + * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} on which to enable keyboard functionality and focus management. + * @param {Boolean/Object} options An object of the following options: + - keys : Array/Object + An array containing the string names of navigation keys to be supported. The allowed values are: + + - 'left' + - 'right' + - 'up' + - 'down' + + Or, an object containing those key names as keys with `true` or a callback function as their value. A scope may also be passed. E.g.: + + { + left: this.onLeftKey, + right: this.onRightKey, + scope: this + } + + - focusFrame : Boolean (optional) + `true` to show the focus frame around a component when it is focused. Defaults to `false`. + * @markdown + */ + subscribe: function(container, options) { + var me = this, + EA = Ext.Array, + data = {}, + subs = me.subscribers, + + // Recursively add focus ability as long as a descendent container isn't + // itself subscribed to the FocusManager, or else we'd have unwanted side + // effects for subscribing a descendent container twice. + safeSetFocus = function(cmp) { + if (cmp.isContainer && !subs.containsKey(cmp.id)) { + EA.forEach(cmp.query('>'), safeSetFocus); + me.setFocus(cmp, true, options); + cmp.on('add', data.onAdd, me); + } else if (!cmp.isContainer) { + me.setFocus(cmp, true, options); + } + }; + + // We only accept containers + if (!container || !container.isContainer) { + return; + } + + if (!container.rendered) { + container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true }); + return; + } + + // Init the DOM, incase this is the first time it will be used + me.initDOM(options); + + // Create key navigation for subscriber based on keys option + data.keyNav = me.setupSubscriberKeys(container, options.keys); + + // We need to keep track of components being added to our subscriber + // and any containers nested deeply within it (omg), so let's do that. + // Components that are removed are globally handled. + // Also keep track of destruction of our container for auto-unsubscribe. + data.onAdd = function(ct, cmp, idx) { + safeSetFocus(cmp); + }; + container.on('beforedestroy', me.unsubscribe, me); + + // Now we setup focusing abilities for the container and all its components + safeSetFocus(container); + + // Add to our subscribers list + subs.add(container.id, data); + }, + + /** + * Unsubscribes an {@link Ext.container.Container} from keyboard focus management. + * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} to unsubscribe from the FocusManager. + * @markdown + */ + unsubscribe: function(container) { + var me = this, + EA = Ext.Array, + subs = me.subscribers, + data, + + // Recursively remove focus ability as long as a descendent container isn't + // itself subscribed to the FocusManager, or else we'd have unwanted side + // effects for unsubscribing an ancestor container. + safeSetFocus = function(cmp) { + if (cmp.isContainer && !subs.containsKey(cmp.id)) { + EA.forEach(cmp.query('>'), safeSetFocus); + me.setFocus(cmp, false); + cmp.un('add', data.onAdd, me); + } else if (!cmp.isContainer) { + me.setFocus(cmp, false); + } + }; + + if (!container || !subs.containsKey(container.id)) { + return; + } + + data = subs.get(container.id); + data.keyNav.destroy(); + container.un('beforedestroy', me.unsubscribe, me); + subs.removeAtKey(container.id); + safeSetFocus(container); + me.removeDOM(); + } +}); +/** + * @class Ext.toolbar.Toolbar + * @extends Ext.container.Container + +Basic Toolbar class. Although the {@link Ext.container.Container#defaultType defaultType} for Toolbar is {@link Ext.button.Button button}, Toolbar +elements (child items for the Toolbar container) may be virtually any type of Component. Toolbar elements can be created explicitly via their +constructors, or implicitly via their xtypes, and can be {@link #add}ed dynamically. + +__Some items have shortcut strings for creation:__ + +| Shortcut | xtype | Class | Description | +|:---------|:--------------|:------------------------------|:---------------------------------------------------| +| `->` | `tbspacer` | {@link Ext.toolbar.Fill} | begin using the right-justified button container | +| `-` | `tbseparator` | {@link Ext.toolbar.Separator} | add a vertical separator bar between toolbar items | +| ` ` | `tbspacer` | {@link Ext.toolbar.Spacer} | add horiztonal space between elements | + +{@img Ext.toolbar.Toolbar/Ext.toolbar.Toolbar1.png Toolbar component} +Example usage: + + Ext.create('Ext.toolbar.Toolbar", { + renderTo: document.body, + width : 500, + items: [ + { + // xtype: 'button', // default for Toolbars + text: 'Button' + }, + { + xtype: 'splitbutton', + text : 'Split Button' + }, + // begin using the right-justified button container + '->', // same as {xtype: 'tbfill'}, // Ext.toolbar.Fill + { + xtype : 'textfield', + name : 'field1', + emptyText: 'enter search term' + }, + // add a vertical separator bar between toolbar items + '-', // same as {xtype: 'tbseparator'} to create Ext.toolbar.Separator + 'text 1', // same as {xtype: 'tbtext', text: 'text1'} to create Ext.toolbar.TextItem + {xtype: 'tbspacer'},// same as ' ' to create Ext.toolbar.Spacer + 'text 2', + {xtype: 'tbspacer', width: 50}, // add a 50px space + 'text 3' + ] + }); + +Toolbars have {@link #enable} and {@link #disable} methods which when called, will enable/disable all items within your toolbar. + +{@img Ext.toolbar.Toolbar/Ext.toolbar.Toolbar2.png Toolbar component} +Example usage: + + Ext.create('Ext.toolbar.Toolbar', { + renderTo: document.body, + width : 400, + items: [ + { + text: 'Button' + }, + { + xtype: 'splitbutton', + text : 'Split Button' + }, + '->', + { + xtype : 'textfield', + name : 'field1', + emptyText: 'enter search term' + } + ] + }); + +{@img Ext.toolbar.Toolbar/Ext.toolbar.Toolbar3.png Toolbar component} +Example usage: + + var enableBtn = Ext.create('Ext.button.Button', { + text : 'Enable All Items', + disabled: true, + scope : this, + handler : function() { + //disable the enable button and enable the disable button + enableBtn.disable(); + disableBtn.enable(); + + //enable the toolbar + toolbar.enable(); + } + }); + + var disableBtn = Ext.create('Ext.button.Button', { + text : 'Disable All Items', + scope : this, + handler : function() { + //enable the enable button and disable button + disableBtn.disable(); + enableBtn.enable(); + + //disable the toolbar + toolbar.disable(); + } + }); + + var toolbar = Ext.create('Ext.toolbar.Toolbar', { + renderTo: document.body, + width : 400, + margin : '5 0 0 0', + items : [enableBtn, disableBtn] + }); + +Adding items to and removing items from a toolbar is as simple as calling the {@link #add} and {@link #remove} methods. There is also a {@link #removeAll} method +which remove all items within the toolbar. + +{@img Ext.toolbar.Toolbar/Ext.toolbar.Toolbar4.png Toolbar component} +Example usage: + + var toolbar = Ext.create('Ext.toolbar.Toolbar', { + renderTo: document.body, + width : 700, + items: [ + { + text: 'Example Button' + } + ] + }); + + var addedItems = []; + + Ext.create('Ext.toolbar.Toolbar', { + renderTo: document.body, + width : 700, + margin : '5 0 0 0', + items : [ + { + text : 'Add a button', + scope : this, + handler: function() { + var text = prompt('Please enter the text for your button:'); + addedItems.push(toolbar.add({ + text: text + })); + } + }, + { + text : 'Add a text item', + scope : this, + handler: function() { + var text = prompt('Please enter the text for your item:'); + addedItems.push(toolbar.add(text)); + } + }, + { + text : 'Add a toolbar seperator', + scope : this, + handler: function() { + addedItems.push(toolbar.add('-')); + } + }, + { + text : 'Add a toolbar spacer', + scope : this, + handler: function() { + addedItems.push(toolbar.add('->')); + } + }, + '->', + { + text : 'Remove last inserted item', + scope : this, + handler: function() { + if (addedItems.length) { + toolbar.remove(addedItems.pop()); + } else if (toolbar.items.length) { + toolbar.remove(toolbar.items.last()); + } else { + alert('No items in the toolbar'); + } + } + }, + { + text : 'Remove all items', + scope : this, + handler: function() { + toolbar.removeAll(); + } + } + ] + }); + + * @constructor + * Creates a new Toolbar + * @param {Object/Array} config A config object or an array of buttons to {@link #add} + * @xtype toolbar + * @docauthor Robert Dougan + * @markdown + */ +Ext.define('Ext.toolbar.Toolbar', { + extend: 'Ext.container.Container', + requires: [ + 'Ext.toolbar.Fill', + 'Ext.layout.container.HBox', + 'Ext.layout.container.VBox', + 'Ext.FocusManager' + ], + uses: [ + 'Ext.toolbar.Separator' + ], + alias: 'widget.toolbar', + alternateClassName: 'Ext.Toolbar', + + isToolbar: true, + baseCls : Ext.baseCSSPrefix + 'toolbar', + ariaRole : 'toolbar', + + defaultType: 'button', + + /** + * @cfg {Boolean} vertical + * Set to `true` to make the toolbar vertical. The layout will become a `vbox`. + * (defaults to `false`) + */ + vertical: false, + + /** + * @cfg {String/Object} layout + * This class assigns a default layout (layout:'hbox'). + * Developers may override this configuration option if another layout + * is required (the constructor must be passed a configuration object in this + * case instead of an array). + * See {@link Ext.container.Container#layout} for additional information. + */ + + /** + * @cfg {Boolean} enableOverflow + * Defaults to false. Configure true to make the toolbar provide a button + * which activates a dropdown Menu to show items which overflow the Toolbar's width. + */ + enableOverflow: false, + + // private + trackMenus: true, + + itemCls: Ext.baseCSSPrefix + 'toolbar-item', + + initComponent: function() { + var me = this, + keys; + + // check for simplified (old-style) overflow config: + if (!me.layout && me.enableOverflow) { + me.layout = { overflowHandler: 'Menu' }; + } + + if (me.dock === 'right' || me.dock === 'left') { + me.vertical = true; + } + + me.layout = Ext.applyIf(Ext.isString(me.layout) ? { + type: me.layout + } : me.layout || {}, { + type: me.vertical ? 'vbox' : 'hbox', + align: me.vertical ? 'stretchmax' : 'middle' + }); + + if (me.vertical) { + me.addClsWithUI('vertical'); + } + + // @TODO: remove this hack and implement a more general solution + if (me.ui === 'footer') { + me.ignoreBorderManagement = true; + } + + me.callParent(); + + /** + * @event overflowchange + * Fires after the overflow state has changed. + * @param {Object} c The Container + * @param {Boolean} lastOverflow overflow state + */ + me.addEvents('overflowchange'); + + // Subscribe to Ext.FocusManager for key navigation + keys = me.vertical ? ['up', 'down'] : ['left', 'right']; + Ext.FocusManager.subscribe(me, { + keys: keys + }); + }, + + /** + *

Adds element(s) to the toolbar -- this function takes a variable number of + * arguments of mixed type and adds them to the toolbar.

+ *

Note: See the notes within {@link Ext.container.Container#add}.

+ * @param {Mixed} arg1 The following types of arguments are all valid:
+ *
    + *
  • {@link Ext.button.Button} config: A valid button config object (equivalent to {@link #addButton})
  • + *
  • HtmlElement: Any standard HTML element (equivalent to {@link #addElement})
  • + *
  • Field: Any form field (equivalent to {@link #addField})
  • + *
  • Item: Any subclass of {@link Ext.toolbar.Item} (equivalent to {@link #addItem})
  • + *
  • String: Any generic string (gets wrapped in a {@link Ext.toolbar.TextItem}, equivalent to {@link #addText}). + * Note that there are a few special strings that are treated differently as explained next.
  • + *
  • '-': Creates a separator element (equivalent to {@link #addSeparator})
  • + *
  • ' ': Creates a spacer element (equivalent to {@link #addSpacer})
  • + *
  • '->': Creates a fill element (equivalent to {@link #addFill})
  • + *
+ * @param {Mixed} arg2 + * @param {Mixed} etc. + * @method add + */ + + // private + lookupComponent: function(c) { + if (Ext.isString(c)) { + var shortcut = Ext.toolbar.Toolbar.shortcuts[c]; + if (shortcut) { + c = { + xtype: shortcut + }; + } else { + c = { + xtype: 'tbtext', + text: c + }; + } + this.applyDefaults(c); + } + return this.callParent(arguments); + }, + + // private + applyDefaults: function(c) { + if (!Ext.isString(c)) { + c = this.callParent(arguments); + var d = this.internalDefaults; + if (c.events) { + Ext.applyIf(c.initialConfig, d); + Ext.apply(c, d); + } else { + Ext.applyIf(c, d); + } + } + return c; + }, + + // private + trackMenu: function(item, remove) { + if (this.trackMenus && item.menu) { + var method = remove ? 'mun' : 'mon', + me = this; + + me[method](item, 'menutriggerover', me.onButtonTriggerOver, me); + me[method](item, 'menushow', me.onButtonMenuShow, me); + me[method](item, 'menuhide', me.onButtonMenuHide, me); + } + }, + + // private + constructButton: function(item) { + return item.events ? item : this.createComponent(item, item.split ? 'splitbutton' : this.defaultType); + }, + + // private + onBeforeAdd: function(component) { + if (component.is('field') || (component.is('button') && this.ui != 'footer')) { + component.ui = component.ui + '-toolbar'; + } + + // Any separators needs to know if is vertical or not + if (component instanceof Ext.toolbar.Separator) { + component.setUI((this.vertical) ? 'vertical' : 'horizontal'); + } + + this.callParent(arguments); + }, + + // private + onAdd: function(component) { + this.callParent(arguments); + + this.trackMenu(component); + if (this.disabled) { + component.disable(); + } + }, + + // private + onRemove: function(c) { + this.callParent(arguments); + this.trackMenu(c, true); + }, + + // private + onButtonTriggerOver: function(btn){ + if (this.activeMenuBtn && this.activeMenuBtn != btn) { + this.activeMenuBtn.hideMenu(); + btn.showMenu(); + this.activeMenuBtn = btn; + } + }, + + // private + onButtonMenuShow: function(btn) { + this.activeMenuBtn = btn; + }, + + // private + onButtonMenuHide: function(btn) { + delete this.activeMenuBtn; + } +}, function() { + this.shortcuts = { + '-' : 'tbseparator', + ' ' : 'tbspacer', + '->': 'tbfill' + }; +}); +/** + * @class Ext.panel.AbstractPanel + * @extends Ext.container.Container + *

A base class which provides methods common to Panel classes across the Sencha product range.

+ *

Please refer to sub class's documentation

+ * @constructor + * @param {Object} config The config object + */ +Ext.define('Ext.panel.AbstractPanel', { + + /* Begin Definitions */ + + extend: 'Ext.container.Container', + + requires: ['Ext.util.MixedCollection', 'Ext.core.Element', 'Ext.toolbar.Toolbar'], + + /* End Definitions */ + + /** + * @cfg {String} baseCls + * The base CSS class to apply to this panel's element (defaults to 'x-panel'). + */ + baseCls : Ext.baseCSSPrefix + 'panel', + + /** + * @cfg {Number/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. + * Defaults to undefined. + */ + + /** + * @cfg {Boolean} bodyBorder + * A shortcut to add or remove the border on the body of a panel. This only applies to a panel which has the {@link #frame} configuration set to `true`. + * Defaults to undefined. + */ + + /** + * @cfg {String/Object/Function} bodyStyle + * Custom CSS styles to be applied to the panel's body element, which can be supplied as a valid CSS style string, + * an object containing style property name/value pairs or a function that returns such a string or object. + * For example, these two formats are interpreted to be equivalent:

+bodyStyle: 'background:#ffc; padding:10px;'
+
+bodyStyle: {
+    background: '#ffc',
+    padding: '10px'
+}
+     * 
+ */ + + /** + * @cfg {String/Array} bodyCls + * A CSS class, space-delimited string of classes, or array of classes to be applied to the panel's body element. + * The following examples are all valid:

+bodyCls: 'foo'
+bodyCls: 'foo bar'
+bodyCls: ['foo', 'bar']
+     * 
+ */ + + isPanel: true, + + componentLayout: 'dock', + + renderTpl: ['
{bodyCls} {baseCls}-body-{ui} {parent.baseCls}-body-{parent.ui}-{.}" style="{bodyStyle}">
'], + + // TODO: Move code examples into product-specific files. The code snippet below is Touch only. + /** + * @cfg {Object/Array} dockedItems + * A component or series of components to be added as docked items to this panel. + * The docked items can be docked to either the top, right, left or bottom of a panel. + * This is typically used for things like toolbars or tab bars: + *

+var panel = new Ext.panel.Panel({
+    fullscreen: true,
+    dockedItems: [{
+        xtype: 'toolbar',
+        dock: 'top',
+        items: [{
+            text: 'Docked to the top'
+        }]
+    }]
+});
+ */ + + border: true, + + initComponent : function() { + var me = this; + + me.addEvents( + /** + * @event bodyresize + * Fires after the Panel has been resized. + * @param {Ext.panel.Panel} p the Panel which has been resized. + * @param {Number} width The Panel body's new width. + * @param {Number} height The Panel body's new height. + */ + 'bodyresize' + // // inherited + // 'activate', + // // inherited + // 'deactivate' + ); + + Ext.applyIf(me.renderSelectors, { + body: '.' + me.baseCls + '-body' + }); + + //!frame + //!border + + if (me.frame && me.border && me.bodyBorder === undefined) { + me.bodyBorder = false; + } + if (me.frame && me.border && (me.bodyBorder === false || me.bodyBorder === 0)) { + me.manageBodyBorders = true; + } + + me.callParent(); + }, + + // @private + initItems : function() { + var me = this, + items = me.dockedItems; + + me.callParent(); + me.dockedItems = Ext.create('Ext.util.MixedCollection', false, me.getComponentId); + if (items) { + me.addDocked(items); + } + }, + + /** + * Finds a docked component by id, itemId or position. Also see {@link #getDockedItems} + * @param {String/Number} comp The id, itemId or position of the docked component (see {@link #getComponent} for details) + * @return {Ext.Component} The docked component (if found) + */ + getDockedComponent: function(comp) { + if (Ext.isObject(comp)) { + comp = comp.getItemId(); + } + return this.dockedItems.get(comp); + }, + + /** + * Attempts a default component lookup (see {@link Ext.container.Container#getComponent}). If the component is not found in the normal + * items, the dockedItems are searched and the matched component (if any) returned (see {@loink #getDockedComponent}). Note that docked + * items will only be matched by component id or itemId -- if you pass a numeric index only non-docked child components will be searched. + * @param {String/Number} comp The component id, itemId or position to find + * @return {Ext.Component} The component (if found) + */ + getComponent: function(comp) { + var component = this.callParent(arguments); + if (component === undefined && !Ext.isNumber(comp)) { + // If the arg is a numeric index skip docked items + component = this.getDockedComponent(comp); + } + return component; + }, + + /** + * Parses the {@link bodyStyle} config if available to create a style string that will be applied to the body element. + * This also includes {@link bodyPadding} and {@link bodyBorder} if available. + * @return {String} A CSS style string with body styles, padding and border. + * @private + */ + initBodyStyles: function() { + var me = this, + bodyStyle = me.bodyStyle, + styles = [], + Element = Ext.core.Element, + prop; + + if (Ext.isFunction(bodyStyle)) { + bodyStyle = bodyStyle(); + } + if (Ext.isString(bodyStyle)) { + styles = bodyStyle.split(';'); + } else { + for (prop in bodyStyle) { + if (bodyStyle.hasOwnProperty(prop)) { + styles.push(prop + ':' + bodyStyle[prop]); + } + } + } + + if (me.bodyPadding !== undefined) { + styles.push('padding: ' + Element.unitizeBox((me.bodyPadding === true) ? 5 : me.bodyPadding)); + } + if (me.frame && me.bodyBorder) { + if (!Ext.isNumber(me.bodyBorder)) { + me.bodyBorder = 1; + } + styles.push('border-width: ' + Element.unitizeBox(me.bodyBorder)); + } + delete me.bodyStyle; + return styles.length ? styles.join(';') : undefined; + }, + + /** + * Parse the {@link bodyCls} config if available to create a comma-delimited string of + * CSS classes to be applied to the body element. + * @return {String} The CSS class(es) + * @private + */ + initBodyCls: function() { + var me = this, + cls = '', + bodyCls = me.bodyCls; + + if (bodyCls) { + Ext.each(bodyCls, function(v) { + cls += " " + v; + }); + delete me.bodyCls; + } + return cls.length > 0 ? cls : undefined; + }, + + /** + * Initialized the renderData to be used when rendering the renderTpl. + * @return {Object} Object with keys and values that are going to be applied to the renderTpl + * @private + */ + initRenderData: function() { + return Ext.applyIf(this.callParent(), { + bodyStyle: this.initBodyStyles(), + bodyCls: this.initBodyCls() + }); + }, + + /** + * Adds docked item(s) to the panel. + * @param {Object/Array} component The Component or array of components to add. The components + * must include a 'dock' parameter on each component to indicate where it should be docked ('top', 'right', + * 'bottom', 'left'). + * @param {Number} pos (optional) The index at which the Component will be added + */ + addDocked : function(items, pos) { + var me = this, + i = 0, + item, length; + + items = me.prepareItems(items); + length = items.length; + + for (; i < length; i++) { + item = items[i]; + item.dock = item.dock || 'top'; + + // Allow older browsers to target docked items to style without borders + if (me.border === false) { + // item.cls = item.cls || '' + ' ' + me.baseCls + '-noborder-docked-' + item.dock; + } + + if (pos !== undefined) { + me.dockedItems.insert(pos + i, item); + } + else { + me.dockedItems.add(item); + } + item.onAdded(me, i); + me.onDockedAdd(item); + } + if (me.rendered) { + me.doComponentLayout(); + } + return items; + }, + + // Placeholder empty functions + onDockedAdd : Ext.emptyFn, + onDockedRemove : Ext.emptyFn, + + /** + * Inserts docked item(s) to the panel at the indicated position. + * @param {Number} pos The index at which the Component will be inserted + * @param {Object/Array} component. The Component or array of components to add. The components + * must include a 'dock' paramater on each component to indicate where it should be docked ('top', 'right', + * 'bottom', 'left'). + */ + insertDocked : function(pos, items) { + this.addDocked(items, pos); + }, + + /** + * Removes the docked item from the panel. + * @param {Ext.Component} item. The Component to remove. + * @param {Boolean} autoDestroy (optional) Destroy the component after removal. + */ + removeDocked : function(item, autoDestroy) { + var me = this, + layout, + hasLayout; + + if (!me.dockedItems.contains(item)) { + return item; + } + + layout = me.componentLayout; + hasLayout = layout && me.rendered; + + if (hasLayout) { + layout.onRemove(item); + } + + me.dockedItems.remove(item); + item.onRemoved(); + me.onDockedRemove(item); + + if (autoDestroy === true || (autoDestroy !== false && me.autoDestroy)) { + item.destroy(); + } + + if (hasLayout && !autoDestroy) { + layout.afterRemove(item); + } + + if (!this.destroying) { + me.doComponentLayout(); + } + + return item; + }, + + /** + * Retrieve an array of all currently docked Components. + * @param {String} cqSelector A {@link Ext.ComponentQuery ComponentQuery} selector string to filter the returned items. + * @return {Array} An array of components. + */ + getDockedItems : function(cqSelector) { + var me = this, + // Start with a weight of 1, so users can provide <= 0 to come before top items + // Odd numbers, so users can provide a weight to come in between if desired + defaultWeight = { top: 1, left: 3, right: 5, bottom: 7 }, + dockedItems; + + if (me.dockedItems && me.dockedItems.items.length) { + // Allow filtering of returned docked items by CQ selector. + if (cqSelector) { + dockedItems = Ext.ComponentQuery.query(cqSelector, me.dockedItems.items); + } else { + dockedItems = me.dockedItems.items.slice(); + } + + Ext.Array.sort(dockedItems, function(a, b) { + // Docked items are ordered by their visual representation by default (t,l,r,b) + // TODO: Enforce position ordering, and have weights be sub-ordering within positions? + var aw = a.weight || defaultWeight[a.dock], + bw = b.weight || defaultWeight[b.dock]; + if (Ext.isNumber(aw) && Ext.isNumber(bw)) { + return aw - bw; + } + return 0; + }); + + return dockedItems; + } + return []; + }, + + // inherit docs + addUIClsToElement: function(cls, force) { + var me = this; + + me.callParent(arguments); + + if (!force && me.rendered) { + me.body.addCls(Ext.baseCSSPrefix + cls); + me.body.addCls(me.baseCls + '-body-' + cls); + me.body.addCls(me.baseCls + '-body-' + me.ui + '-' + cls); + } + }, + + // inherit docs + removeUIClsFromElement: function(cls, force) { + var me = this; + + me.callParent(arguments); + + if (!force && me.rendered) { + me.body.removeCls(Ext.baseCSSPrefix + cls); + me.body.removeCls(me.baseCls + '-body-' + cls); + me.body.removeCls(me.baseCls + '-body-' + me.ui + '-' + cls); + } + }, + + // inherit docs + addUIToElement: function(force) { + var me = this; + + me.callParent(arguments); + + if (!force && me.rendered) { + me.body.addCls(me.baseCls + '-body-' + me.ui); + } + }, + + // inherit docs + removeUIFromElement: function() { + var me = this; + + me.callParent(arguments); + + if (me.rendered) { + me.body.removeCls(me.baseCls + '-body-' + me.ui); + } + }, + + // @private + getTargetEl : function() { + return this.body; + }, + + getRefItems: function(deep) { + var items = this.callParent(arguments), + // deep fetches all docked items, and their descendants using '*' selector and then '* *' + dockedItems = this.getDockedItems(deep ? '*,* *' : undefined), + ln = dockedItems.length, + i = 0, + item; + + // Find the index where we go from top/left docked items to right/bottom docked items + for (; i < ln; i++) { + item = dockedItems[i]; + if (item.dock === 'right' || item.dock === 'bottom') { + break; + } + } + + // Return docked items in the top/left position before our container items, and + // return right/bottom positioned items after our container items. + // See AbstractDock.renderItems() for more information. + return dockedItems.splice(0, i).concat(items).concat(dockedItems); + }, + + beforeDestroy: function(){ + var docked = this.dockedItems, + c; + + if (docked) { + while ((c = docked.first())) { + this.removeDocked(c, true); + } + } + this.callParent(); + }, + + setBorder: function(border) { + var me = this; + me.border = (border !== undefined) ? border : true; + if (me.rendered) { + me.doComponentLayout(); + } + } +}); +/** + * @class Ext.panel.Header + * @extends Ext.container.Container + * Simple header class which is used for on {@link Ext.panel.Panel} and {@link Ext.window.Window} + * @xtype header + */ +Ext.define('Ext.panel.Header', { + extend: 'Ext.container.Container', + uses: ['Ext.panel.Tool', 'Ext.draw.Component', 'Ext.util.CSS'], + alias: 'widget.header', + + isHeader : true, + defaultType : 'tool', + indicateDrag : false, + weight : -1, + + renderTpl: ['
{bodyCls} {parent.baseCls}-body-{parent.ui}-{.}" style="{bodyStyle}">
'], + + initComponent: function() { + var me = this, + rule, + style, + titleTextEl, + ui; + + me.indicateDragCls = me.baseCls + '-draggable'; + me.title = me.title || ' '; + me.tools = me.tools || []; + me.items = me.items || []; + me.orientation = me.orientation || 'horizontal'; + me.dock = (me.dock) ? me.dock : (me.orientation == 'horizontal') ? 'top' : 'left'; + + //add the dock as a ui + //this is so we support top/right/left/bottom headers + me.addClsWithUI(me.orientation); + me.addClsWithUI(me.dock); + + Ext.applyIf(me.renderSelectors, { + body: '.' + me.baseCls + '-body' + }); + + // Add Icon + if (!Ext.isEmpty(me.iconCls)) { + me.initIconCmp(); + me.items.push(me.iconCmp); + } + + // Add Title + if (me.orientation == 'vertical') { + // Hack for IE6/7's inability to display an inline-block + if (Ext.isIE6 || Ext.isIE7) { + me.width = this.width || 24; + } else if (Ext.isIEQuirks) { + me.width = this.width || 25; + } + + me.layout = { + type : 'vbox', + align: 'center', + clearInnerCtOnLayout: true, + bindToOwnerCtContainer: false + }; + me.textConfig = { + cls: me.baseCls + '-text', + type: 'text', + text: me.title, + rotate: { + degrees: 90 + } + }; + ui = me.ui; + if (Ext.isArray(ui)) { + ui = ui[0]; + } + rule = Ext.util.CSS.getRule('.' + me.baseCls + '-text-' + ui); + if (rule) { + style = rule.style; + } + if (style) { + Ext.apply(me.textConfig, { + 'font-family': style.fontFamily, + 'font-weight': style.fontWeight, + 'font-size': style.fontSize, + fill: style.color + }); + } + me.titleCmp = Ext.create('Ext.draw.Component', { + ariaRole : 'heading', + focusable: false, + viewBox: false, + autoSize: true, + margins: '5 0 0 0', + items: [ me.textConfig ], + renderSelectors: { + textEl: '.' + me.baseCls + '-text' + } + }); + } else { + me.layout = { + type : 'hbox', + align: 'middle', + clearInnerCtOnLayout: true, + bindToOwnerCtContainer: false + }; + me.titleCmp = Ext.create('Ext.Component', { + xtype : 'component', + ariaRole : 'heading', + focusable: false, + renderTpl : ['{title}'], + renderData: { + title: me.title, + cls : me.baseCls, + ui : me.ui + }, + renderSelectors: { + textEl: '.' + me.baseCls + '-text' + } + }); + } + me.items.push(me.titleCmp); + + // Spacer -> + me.items.push({ + xtype: 'component', + html : ' ', + flex : 1, + focusable: false + }); + + // Add Tools + me.items = me.items.concat(me.tools); + this.callParent(); + }, + + initIconCmp: function() { + this.iconCmp = Ext.create('Ext.Component', { + focusable: false, + renderTpl : [''], + renderData: { + blank : Ext.BLANK_IMAGE_URL, + cls : this.baseCls, + iconCls: this.iconCls, + orientation: this.orientation + }, + renderSelectors: { + iconEl: '.' + this.baseCls + '-icon' + }, + iconCls: this.iconCls + }); + }, + + afterRender: function() { + var me = this; + + me.el.unselectable(); + if (me.indicateDrag) { + me.el.addCls(me.indicateDragCls); + } + me.mon(me.el, { + click: me.onClick, + scope: me + }); + me.callParent(); + }, + + afterLayout: function() { + var me = this; + me.callParent(arguments); + + // IE7 needs a forced repaint to make the top framing div expand to full width + if (Ext.isIE7) { + me.el.repaint(); + } + }, + + // inherit docs + addUIClsToElement: function(cls, force) { + var me = this; + + me.callParent(arguments); + + if (!force && me.rendered) { + me.body.addCls(me.baseCls + '-body-' + cls); + me.body.addCls(me.baseCls + '-body-' + me.ui + '-' + cls); + } + }, + + // inherit docs + removeUIClsFromElement: function(cls, force) { + var me = this; + + me.callParent(arguments); + + if (!force && me.rendered) { + me.body.removeCls(me.baseCls + '-body-' + cls); + me.body.removeCls(me.baseCls + '-body-' + me.ui + '-' + cls); + } + }, + + // inherit docs + addUIToElement: function(force) { + var me = this; + + me.callParent(arguments); + + if (!force && me.rendered) { + me.body.addCls(me.baseCls + '-body-' + me.ui); + } + + if (!force && me.titleCmp && me.titleCmp.rendered && me.titleCmp.textEl) { + me.titleCmp.textEl.addCls(me.baseCls + '-text-' + me.ui); + } + }, + + // inherit docs + removeUIFromElement: function() { + var me = this; + + me.callParent(arguments); + + if (me.rendered) { + me.body.removeCls(me.baseCls + '-body-' + me.ui); + } + + if (me.titleCmp && me.titleCmp.rendered && me.titleCmp.textEl) { + me.titleCmp.textEl.removeCls(me.baseCls + '-text-' + me.ui); + } + }, + + onClick: function(e) { + if (!e.getTarget(Ext.baseCSSPrefix + 'tool')) { + this.fireEvent('click', e); + } + }, + + getTargetEl: function() { + return this.body || this.frameBody || this.el; + }, + + /** + * Sets the title of the header. + * @param {String} title The title to be set + */ + setTitle: function(title) { + var me = this; + if (me.rendered) { + if (me.titleCmp.rendered) { + if (me.titleCmp.surface) { + me.title = title || ''; + var sprite = me.titleCmp.surface.items.items[0], + surface = me.titleCmp.surface; + + surface.remove(sprite); + me.textConfig.type = 'text'; + me.textConfig.text = title; + sprite = surface.add(me.textConfig); + sprite.setAttributes({ + rotate: { + degrees: 90 + } + }, true); + me.titleCmp.autoSizeSurface(); + } else { + me.title = title || ' '; + me.titleCmp.textEl.update(me.title); + } + } else { + me.titleCmp.on({ + render: function() { + me.setTitle(title); + }, + single: true + }); + } + } else { + me.on({ + render: function() { + me.layout.layout(); + me.setTitle(title); + }, + single: true + }); + } + }, + + /** + * Sets the CSS class that provides the icon image for this panel. This method will replace any existing + * icon class if one has already been set and fire the {@link #iconchange} event after completion. + * @param {String} cls The new CSS class name + */ + setIconCls: function(cls) { + this.iconCls = cls; + if (!this.iconCmp) { + this.initIconCmp(); + this.insert(0, this.iconCmp); + } + else { + if (!cls || !cls.length) { + this.iconCmp.destroy(); + } + else { + var iconCmp = this.iconCmp, + el = iconCmp.iconEl; + + el.removeCls(iconCmp.iconCls); + el.addCls(cls); + iconCmp.iconCls = cls; + } + } + }, + + /** + * Add a tool to the header + * @param {Object} tool + */ + addTool: function(tool) { + this.tools.push(this.add(tool)); + }, + + /** + * @private + * Set up the tools.<tool type> link in the owning Panel. + * Bind the tool to its owning Panel. + * @param component + * @param index + */ + onAdd: function(component, index) { + this.callParent([arguments]); + if (component instanceof Ext.panel.Tool) { + component.bindTo(this.ownerCt); + this.tools[component.type] = component; + } + } +}); + +/** + * @class Ext.fx.target.Element + * @extends Ext.fx.target.Target + * + * This class represents a animation target for an {@link Ext.core.Element}. In general this class will not be + * created directly, the {@link Ext.core.Element} will be passed to the animation and + * and the appropriate target will be created. + */ +Ext.define('Ext.fx.target.Element', { + + /* Begin Definitions */ + + extend: 'Ext.fx.target.Target', + + /* End Definitions */ + + type: 'element', + + getElVal: function(el, attr, val) { + if (val == undefined) { + if (attr === 'x') { + val = el.getX(); + } + else if (attr === 'y') { + val = el.getY(); + } + else if (attr === 'scrollTop') { + val = el.getScroll().top; + } + else if (attr === 'scrollLeft') { + val = el.getScroll().left; + } + else if (attr === 'height') { + val = el.getHeight(); + } + else if (attr === 'width') { + val = el.getWidth(); + } + else { + val = el.getStyle(attr); + } + } + return val; + }, + + getAttr: function(attr, val) { + var el = this.target; + return [[ el, this.getElVal(el, attr, val)]]; + }, + + setAttr: function(targetData) { + var target = this.target, + ln = targetData.length, + attrs, attr, o, i, j, ln2, element, value; + for (i = 0; i < ln; i++) { + attrs = targetData[i].attrs; + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + ln2 = attrs[attr].length; + for (j = 0; j < ln2; j++) { + o = attrs[attr][j]; + element = o[0]; + value = o[1]; + if (attr === 'x') { + element.setX(value); + } + else if (attr === 'y') { + element.setY(value); + } + else if (attr === 'scrollTop') { + element.scrollTo('top', value); + } + else if (attr === 'scrollLeft') { + element.scrollTo('left',value); + } + else { + element.setStyle(attr, value); + } + } + } + } + } + } +}); + +/** + * @class Ext.fx.target.CompositeElement + * @extends Ext.fx.target.Element + * + * This class represents a animation target for a {@link Ext.CompositeElement}. It allows + * each {@link Ext.core.Element} in the group to be animated as a whole. In general this class will not be + * created directly, the {@link Ext.CompositeElement} will be passed to the animation and + * and the appropriate target will be created. + */ +Ext.define('Ext.fx.target.CompositeElement', { + + /* Begin Definitions */ + + extend: 'Ext.fx.target.Element', + + /* End Definitions */ + + isComposite: true, + + constructor: function(target) { + target.id = target.id || Ext.id(null, 'ext-composite-'); + this.callParent([target]); + }, + + getAttr: function(attr, val) { + var out = [], + target = this.target; + target.each(function(el) { + out.push([el, this.getElVal(el, attr, val)]); + }, this); + return out; + } +}); + +/** + * @class Ext.fx.Manager + * Animation Manager which keeps track of all current animations and manages them on a frame by frame basis. + * @private + * @singleton + */ + +Ext.define('Ext.fx.Manager', { + + /* Begin Definitions */ + + singleton: true, + + requires: ['Ext.util.MixedCollection', + 'Ext.fx.target.Element', + 'Ext.fx.target.CompositeElement', + 'Ext.fx.target.Sprite', + 'Ext.fx.target.CompositeSprite', + 'Ext.fx.target.Component'], + + mixins: { + queue: 'Ext.fx.Queue' + }, + + /* End Definitions */ + + constructor: function() { + this.items = Ext.create('Ext.util.MixedCollection'); + this.mixins.queue.constructor.call(this); + + // this.requestAnimFrame = (function() { + // var raf = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // window.oRequestAnimationFrame || + // window.msRequestAnimationFrame; + // if (raf) { + // return function(callback, element) { + // raf(callback); + // }; + // } + // else { + // return function(callback, element) { + // window.setTimeout(callback, Ext.fx.Manager.interval); + // }; + // } + // })(); + }, + + /** + * @cfg {Number} interval Default interval in miliseconds to calculate each frame. Defaults to 16ms (~60fps) + */ + interval: 16, + + /** + * @cfg {Boolean} forceJS Turn off to not use CSS3 transitions when they are available + */ + forceJS: true, + + // @private Target factory + createTarget: function(target) { + var me = this, + useCSS3 = !me.forceJS && Ext.supports.Transitions, + targetObj; + + me.useCSS3 = useCSS3; + + // dom id + if (Ext.isString(target)) { + target = Ext.get(target); + } + // dom element + if (target && target.tagName) { + target = Ext.get(target); + targetObj = Ext.create('Ext.fx.target.' + 'Element' + (useCSS3 ? 'CSS' : ''), target); + me.targets.add(targetObj); + return targetObj; + } + if (Ext.isObject(target)) { + // Element + if (target.dom) { + targetObj = Ext.create('Ext.fx.target.' + 'Element' + (useCSS3 ? 'CSS' : ''), target); + } + // Element Composite + else if (target.isComposite) { + targetObj = Ext.create('Ext.fx.target.' + 'CompositeElement' + (useCSS3 ? 'CSS' : ''), target); + } + // Draw Sprite + else if (target.isSprite) { + targetObj = Ext.create('Ext.fx.target.Sprite', target); + } + // Draw Sprite Composite + else if (target.isCompositeSprite) { + targetObj = Ext.create('Ext.fx.target.CompositeSprite', target); + } + // Component + else if (target.isComponent) { + targetObj = Ext.create('Ext.fx.target.Component', target); + } + else if (target.isAnimTarget) { + return target; + } + else { + return null; + } + me.targets.add(targetObj); + return targetObj; + } + else { + return null; + } + }, + + /** + * Add an Anim to the manager. This is done automatically when an Anim instance is created. + * @param {Ext.fx.Anim} anim + */ + addAnim: function(anim) { + var items = this.items, + task = this.task; + // var me = this, + // items = me.items, + // cb = function() { + // if (items.length) { + // me.task = true; + // me.runner(); + // me.requestAnimFrame(cb); + // } + // else { + // me.task = false; + // } + // }; + + items.add(anim); + + // Start the timer if not already running + if (!task && items.length) { + task = this.task = { + run: this.runner, + interval: this.interval, + scope: this + }; + Ext.TaskManager.start(task); + } + + // //Start the timer if not already running + // if (!me.task && items.length) { + // me.requestAnimFrame(cb); + // } + }, + + /** + * Remove an Anim from the manager. This is done automatically when an Anim ends. + * @param {Ext.fx.Anim} anim + */ + removeAnim: function(anim) { + // this.items.remove(anim); + var items = this.items, + task = this.task; + items.remove(anim); + // Stop the timer if there are no more managed Anims + if (task && !items.length) { + Ext.TaskManager.stop(task); + delete this.task; + } + }, + + /** + * @private + * Filter function to determine which animations need to be started + */ + startingFilter: function(o) { + return o.paused === false && o.running === false && o.iterations > 0; + }, + + /** + * @private + * Filter function to determine which animations are still running + */ + runningFilter: function(o) { + return o.paused === false && o.running === true && o.isAnimator !== true; + }, + + /** + * @private + * Runner function being called each frame + */ + runner: function() { + var me = this, + items = me.items; + + me.targetData = {}; + me.targetArr = {}; + + // Single timestamp for all animations this interval + me.timestamp = new Date(); + + // Start any items not current running + items.filterBy(me.startingFilter).each(me.startAnim, me); + + // Build the new attributes to be applied for all targets in this frame + items.filterBy(me.runningFilter).each(me.runAnim, me); + + // Apply all the pending changes to their targets + me.applyPendingAttrs(); + }, + + /** + * @private + * Start the individual animation (initialization) + */ + startAnim: function(anim) { + anim.start(this.timestamp); + }, + + /** + * @private + * Run the individual animation for this frame + */ + runAnim: function(anim) { + if (!anim) { + return; + } + var me = this, + targetId = anim.target.getId(), + useCSS3 = me.useCSS3 && anim.target.type == 'element', + elapsedTime = me.timestamp - anim.startTime, + target, o; + + this.collectTargetData(anim, elapsedTime, useCSS3); + + // For CSS3 animation, we need to immediately set the first frame's attributes without any transition + // to get a good initial state, then add the transition properties and set the final attributes. + if (useCSS3) { + // Flush the collected attributes, without transition + anim.target.setAttr(me.targetData[targetId], true); + + // Add the end frame data + me.targetData[targetId] = []; + me.collectTargetData(anim, anim.duration, useCSS3); + + // Pause the animation so runAnim doesn't keep getting called + anim.paused = true; + + target = anim.target.target; + // We only want to attach an event on the last element in a composite + if (anim.target.isComposite) { + target = anim.target.target.last(); + } + + // Listen for the transitionend event + o = {}; + o[Ext.supports.CSS3TransitionEnd] = anim.lastFrame; + o.scope = anim; + o.single = true; + target.on(o); + } + // For JS animation, trigger the lastFrame handler if this is the final frame + else if (elapsedTime >= anim.duration) { + me.applyPendingAttrs(true); + delete me.targetData[targetId]; + delete me.targetArr[targetId]; + anim.lastFrame(); + } + }, + + /** + * Collect target attributes for the given Anim object at the given timestamp + * @param {Ext.fx.Anim} anim The Anim instance + * @param {Number} timestamp Time after the anim's start time + */ + collectTargetData: function(anim, elapsedTime, useCSS3) { + var targetId = anim.target.getId(), + targetData = this.targetData[targetId], + data; + + if (!targetData) { + targetData = this.targetData[targetId] = []; + this.targetArr[targetId] = anim.target; + } + + data = { + duration: anim.duration, + easing: (useCSS3 && anim.reverse) ? anim.easingFn.reverse().toCSS3() : anim.easing, + attrs: {} + }; + Ext.apply(data.attrs, anim.runAnim(elapsedTime)); + targetData.push(data); + }, + + /** + * @private + * Apply all pending attribute changes to their targets + */ + applyPendingAttrs: function(isLastFrame) { + var targetData = this.targetData, + targetArr = this.targetArr, + targetId; + for (targetId in targetData) { + if (targetData.hasOwnProperty(targetId)) { + targetArr[targetId].setAttr(targetData[targetId], false, isLastFrame); + } + } + } +}); + +/** + * @class Ext.fx.Animator + * Animation instance + +This class is used to run keyframe based animations, which follows the CSS3 based animation structure. +Keyframe animations differ from typical from/to animations in that they offer the ability to specify values +at various points throughout the animation. + +__Using Keyframes__ +The {@link #keyframes} option is the most important part of specifying an animation when using this +class. A key frame is a point in a particular animation. We represent this as a percentage of the +total animation duration. At each key frame, we can specify the target values at that time. Note that +you *must* specify the values at 0% and 100%, the start and ending values. There is also a {@link keyframe} +event that fires after each key frame is reached. + +__Example Usage__ +In the example below, we modify the values of the element at each fifth throughout the animation. + + Ext.create('Ext.fx.Animator', { + target: Ext.getBody().createChild({ + style: { + width: '100px', + height: '100px', + 'background-color': 'red' + } + }), + duration: 10000, // 10 seconds + keyframes: { + 0: { + opacity: 1, + backgroundColor: 'FF0000' + }, + 20: { + x: 30, + opacity: 0.5 + }, + 40: { + x: 130, + backgroundColor: '0000FF' + }, + 60: { + y: 80, + opacity: 0.3 + }, + 80: { + width: 200, + y: 200 + }, + 100: { + opacity: 1, + backgroundColor: '00FF00' + } + } + }); + + * @markdown + */ +Ext.define('Ext.fx.Animator', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: ['Ext.fx.Manager'], + + /* End Definitions */ + + isAnimator: true, + + /** + * @cfg {Number} duration + * Time in milliseconds for the animation to last. Defaults to 250. + */ + duration: 250, + + /** + * @cfg {Number} delay + * Time to delay before starting the animation. Defaults to 0. + */ + delay: 0, + + /* private used to track a delayed starting time */ + delayStart: 0, + + /** + * @cfg {Boolean} dynamic + * Currently only for Component Animation: Only set a component's outer element size bypassing layouts. Set to true to do full layouts for every frame of the animation. Defaults to false. + */ + dynamic: false, + + /** + * @cfg {String} easing + +This describes how the intermediate values used during a transition will be calculated. It allows for a transition to change +speed over its duration. + +- backIn +- backOut +- bounceIn +- bounceOut +- ease +- easeIn +- easeOut +- easeInOut +- elasticIn +- elasticOut +- cubic-bezier(x1, y1, x2, y2) + +Note that cubic-bezier will create a custom easing curve following the CSS3 transition-timing-function specification `{@link http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag}`. The four values specify points P1 and P2 of the curve +as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition is invalid. + + * @markdown + */ + easing: 'ease', + + /** + * Flag to determine if the animation has started + * @property running + * @type boolean + */ + running: false, + + /** + * Flag to determine if the animation is paused. Only set this to true if you need to + * keep the Anim instance around to be unpaused later; otherwise call {@link #end}. + * @property paused + * @type boolean + */ + paused: false, + + /** + * @private + */ + damper: 1, + + /** + * @cfg {Number} iterations + * Number of times to execute the animation. Defaults to 1. + */ + iterations: 1, + + /** + * Current iteration the animation is running. + * @property currentIteration + * @type int + */ + currentIteration: 0, + + /** + * Current keyframe step of the animation. + * @property keyframeStep + * @type Number + */ + keyframeStep: 0, + + /** + * @private + */ + animKeyFramesRE: /^(from|to|\d+%?)$/, + + /** + * @cfg {Ext.fx.target} target + * The Ext.fx.target to apply the animation to. If not specified during initialization, this can be passed to the applyAnimator + * method to apply the same animation to many targets. + */ + + /** + * @cfg {Object} keyframes + * Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always considered '0%' and 'to' + * is considered '100%'.Every keyframe declaration must have a keyframe rule for 0% and 100%, possibly defined using + * "from" or "to". A keyframe declaration without these keyframe selectors is invalid and will not be available for + * animation. The keyframe declaration for a keyframe rule consists of properties and values. Properties that are unable to + * be animated are ignored in these rules, with the exception of 'easing' which can be changed at each keyframe. For example: +

+keyframes : {
+    '0%': {
+        left: 100
+    },
+    '40%': {
+        left: 150
+    },
+    '60%': {
+        left: 75
+    },
+    '100%': {
+        left: 100
+    }
+}
+ 
+ */ + constructor: function(config) { + var me = this; + config = Ext.apply(me, config || {}); + me.config = config; + me.id = Ext.id(null, 'ext-animator-'); + me.addEvents( + /** + * @event beforeanimate + * Fires before the animation starts. A handler can return false to cancel the animation. + * @param {Ext.fx.Animator} this + */ + 'beforeanimate', + /** + * @event keyframe + * Fires at each keyframe. + * @param {Ext.fx.Animator} this + * @param {Number} keyframe step number + */ + 'keyframe', + /** + * @event afteranimate + * Fires when the animation is complete. + * @param {Ext.fx.Animator} this + * @param {Date} startTime + */ + 'afteranimate' + ); + me.mixins.observable.constructor.call(me, config); + me.timeline = []; + me.createTimeline(me.keyframes); + if (me.target) { + me.applyAnimator(me.target); + Ext.fx.Manager.addAnim(me); + } + }, + + /** + * @private + */ + sorter: function (a, b) { + return a.pct - b.pct; + }, + + /** + * @private + * Takes the given keyframe configuration object and converts it into an ordered array with the passed attributes per keyframe + * or applying the 'to' configuration to all keyframes. Also calculates the proper animation duration per keyframe. + */ + createTimeline: function(keyframes) { + var me = this, + attrs = [], + to = me.to || {}, + duration = me.duration, + prevMs, ms, i, ln, pct, anim, nextAnim, attr; + + for (pct in keyframes) { + if (keyframes.hasOwnProperty(pct) && me.animKeyFramesRE.test(pct)) { + attr = {attrs: Ext.apply(keyframes[pct], to)}; + // CSS3 spec allow for from/to to be specified. + if (pct == "from") { + pct = 0; + } + else if (pct == "to") { + pct = 100; + } + // convert % values into integers + attr.pct = parseInt(pct, 10); + attrs.push(attr); + } + } + // Sort by pct property + Ext.Array.sort(attrs, me.sorter); + // Only an end + //if (attrs[0].pct) { + // attrs.unshift({pct: 0, attrs: element.attrs}); + //} + + ln = attrs.length; + for (i = 0; i < ln; i++) { + prevMs = (attrs[i - 1]) ? duration * (attrs[i - 1].pct / 100) : 0; + ms = duration * (attrs[i].pct / 100); + me.timeline.push({ + duration: ms - prevMs, + attrs: attrs[i].attrs + }); + } + }, + + /** + * Applies animation to the Ext.fx.target + * @private + * @param target + * @type string/object + */ + applyAnimator: function(target) { + var me = this, + anims = [], + timeline = me.timeline, + reverse = me.reverse, + ln = timeline.length, + anim, easing, damper, initial, attrs, lastAttrs, i; + + if (me.fireEvent('beforeanimate', me) !== false) { + for (i = 0; i < ln; i++) { + anim = timeline[i]; + attrs = anim.attrs; + easing = attrs.easing || me.easing; + damper = attrs.damper || me.damper; + delete attrs.easing; + delete attrs.damper; + anim = Ext.create('Ext.fx.Anim', { + target: target, + easing: easing, + damper: damper, + duration: anim.duration, + paused: true, + to: attrs + }); + anims.push(anim); + } + me.animations = anims; + me.target = anim.target; + for (i = 0; i < ln - 1; i++) { + anim = anims[i]; + anim.nextAnim = anims[i + 1]; + anim.on('afteranimate', function() { + this.nextAnim.paused = false; + }); + anim.on('afteranimate', function() { + this.fireEvent('keyframe', this, ++this.keyframeStep); + }, me); + } + anims[ln - 1].on('afteranimate', function() { + this.lastFrame(); + }, me); + } + }, + + /* + * @private + * Fires beforeanimate and sets the running flag. + */ + start: function(startTime) { + var me = this, + delay = me.delay, + delayStart = me.delayStart, + delayDelta; + if (delay) { + if (!delayStart) { + me.delayStart = startTime; + return; + } + else { + delayDelta = startTime - delayStart; + if (delayDelta < delay) { + return; + } + else { + // Compensate for frame delay; + startTime = new Date(delayStart.getTime() + delay); + } + } + } + if (me.fireEvent('beforeanimate', me) !== false) { + me.startTime = startTime; + me.running = true; + me.animations[me.keyframeStep].paused = false; + } + }, + + /* + * @private + * Perform lastFrame cleanup and handle iterations + * @returns a hash of the new attributes. + */ + lastFrame: function() { + var me = this, + iter = me.iterations, + iterCount = me.currentIteration; + + iterCount++; + if (iterCount < iter) { + me.startTime = new Date(); + me.currentIteration = iterCount; + me.keyframeStep = 0; + me.applyAnimator(me.target); + me.animations[me.keyframeStep].paused = false; + } + else { + me.currentIteration = 0; + me.end(); + } + }, + + /* + * Fire afteranimate event and end the animation. Usually called automatically when the + * animation reaches its final frame, but can also be called manually to pre-emptively + * stop and destroy the running animation. + */ + end: function() { + var me = this; + me.fireEvent('afteranimate', me, me.startTime, new Date() - me.startTime); + } +}); +/** + * @class Ext.fx.Easing + * +This class contains a series of function definitions used to modify values during an animation. +They describe how the intermediate values used during a transition will be calculated. It allows for a transition to change +speed over its duration. The following options are available: + +- linear The default easing type +- backIn +- backOut +- bounceIn +- bounceOut +- ease +- easeIn +- easeOut +- easeInOut +- elasticIn +- elasticOut +- cubic-bezier(x1, y1, x2, y2) + +Note that cubic-bezier will create a custom easing curve following the CSS3 transition-timing-function specification `{@link http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag}`. The four values specify points P1 and P2 of the curve +as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition is invalid. + * @markdown + * @singleton + */ +Ext.ns('Ext.fx'); + +Ext.require('Ext.fx.CubicBezier', function() { + var math = Math, + pi = math.PI, + pow = math.pow, + sin = math.sin, + sqrt = math.sqrt, + abs = math.abs, + backInSeed = 1.70158; + Ext.fx.Easing = { + // ease: Ext.fx.CubicBezier.cubicBezier(0.25, 0.1, 0.25, 1), + // linear: Ext.fx.CubicBezier.cubicBezier(0, 0, 1, 1), + // 'ease-in': Ext.fx.CubicBezier.cubicBezier(0.42, 0, 1, 1), + // 'ease-out': Ext.fx.CubicBezier.cubicBezier(0, 0.58, 1, 1), + // 'ease-in-out': Ext.fx.CubicBezier.cubicBezier(0.42, 0, 0.58, 1), + // 'easeIn': Ext.fx.CubicBezier.cubicBezier(0.42, 0, 1, 1), + // 'easeOut': Ext.fx.CubicBezier.cubicBezier(0, 0.58, 1, 1), + // 'easeInOut': Ext.fx.CubicBezier.cubicBezier(0.42, 0, 0.58, 1) + }; + + Ext.apply(Ext.fx.Easing, { + linear: function(n) { + return n; + }, + ease: function(n) { + var q = 0.07813 - n / 2, + alpha = -0.25, + Q = sqrt(0.0066 + q * q), + x = Q - q, + X = pow(abs(x), 1/3) * (x < 0 ? -1 : 1), + y = -Q - q, + Y = pow(abs(y), 1/3) * (y < 0 ? -1 : 1), + t = X + Y + 0.25; + return pow(1 - t, 2) * 3 * t * 0.1 + (1 - t) * 3 * t * t + t * t * t; + }, + easeIn: function (n) { + return pow(n, 1.7); + }, + easeOut: function (n) { + return pow(n, 0.48); + }, + easeInOut: function(n) { + var q = 0.48 - n / 1.04, + Q = sqrt(0.1734 + q * q), + x = Q - q, + X = pow(abs(x), 1/3) * (x < 0 ? -1 : 1), + y = -Q - q, + Y = pow(abs(y), 1/3) * (y < 0 ? -1 : 1), + t = X + Y + 0.5; + return (1 - t) * 3 * t * t + t * t * t; + }, + backIn: function (n) { + return n * n * ((backInSeed + 1) * n - backInSeed); + }, + backOut: function (n) { + n = n - 1; + return n * n * ((backInSeed + 1) * n + backInSeed) + 1; + }, + elasticIn: function (n) { + if (n === 0 || n === 1) { + return n; + } + var p = 0.3, + s = p / 4; + return pow(2, -10 * n) * sin((n - s) * (2 * pi) / p) + 1; + }, + elasticOut: function (n) { + return 1 - Ext.fx.Easing.elasticIn(1 - n); + }, + bounceIn: function (n) { + return 1 - Ext.fx.Easing.bounceOut(1 - n); + }, + bounceOut: function (n) { + var s = 7.5625, + p = 2.75, + l; + if (n < (1 / p)) { + l = s * n * n; + } else { + if (n < (2 / p)) { + n -= (1.5 / p); + l = s * n * n + 0.75; + } else { + if (n < (2.5 / p)) { + n -= (2.25 / p); + l = s * n * n + 0.9375; + } else { + n -= (2.625 / p); + l = s * n * n + 0.984375; + } + } + } + return l; + } + }); + Ext.apply(Ext.fx.Easing, { + 'back-in': Ext.fx.Easing.backIn, + 'back-out': Ext.fx.Easing.backOut, + 'ease-in': Ext.fx.Easing.easeIn, + 'ease-out': Ext.fx.Easing.easeOut, + 'elastic-in': Ext.fx.Easing.elasticIn, + 'elastic-out': Ext.fx.Easing.elasticIn, + 'bounce-in': Ext.fx.Easing.bounceIn, + 'bounce-out': Ext.fx.Easing.bounceOut, + 'ease-in-out': Ext.fx.Easing.easeInOut + }); +}); +/* + * @class Ext.draw.Draw + * Base Drawing class. Provides base drawing functions. + */ + +Ext.define('Ext.draw.Draw', { + /* Begin Definitions */ + + singleton: true, + + requires: ['Ext.draw.Color'], + + /* End Definitions */ + + pathToStringRE: /,?([achlmqrstvxz]),?/gi, + pathCommandRE: /([achlmqstvz])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?\s*,?\s*)+)/ig, + pathValuesRE: /(-?\d*\.?\d*(?:e[-+]?\d+)?)\s*,?\s*/ig, + stopsRE: /^(\d+%?)$/, + radian: Math.PI / 180, + + availableAnimAttrs: { + along: "along", + blur: null, + "clip-rect": "csv", + cx: null, + cy: null, + fill: "color", + "fill-opacity": null, + "font-size": null, + height: null, + opacity: null, + path: "path", + r: null, + rotation: "csv", + rx: null, + ry: null, + scale: "csv", + stroke: "color", + "stroke-opacity": null, + "stroke-width": null, + translation: "csv", + width: null, + x: null, + y: null + }, + + is: function(o, type) { + type = String(type).toLowerCase(); + return (type == "object" && o === Object(o)) || + (type == "undefined" && typeof o == type) || + (type == "null" && o === null) || + (type == "array" && Array.isArray && Array.isArray(o)) || + (Object.prototype.toString.call(o).toLowerCase().slice(8, -1)) == type; + }, + + ellipsePath: function(sprite) { + var attr = sprite.attr; + return Ext.String.format("M{0},{1}A{2},{3},0,1,1,{0},{4}A{2},{3},0,1,1,{0},{1}z", attr.x, attr.y - attr.ry, attr.rx, attr.ry, attr.y + attr.ry); + }, + + rectPath: function(sprite) { + var attr = sprite.attr; + if (attr.radius) { + return Ext.String.format("M{0},{1}l{2},0a{3},{3},0,0,1,{3},{3}l0,{5}a{3},{3},0,0,1,{4},{3}l{6},0a{3},{3},0,0,1,{4},{4}l0,{7}a{3},{3},0,0,1,{3},{4}z", attr.x + attr.radius, attr.y, attr.width - attr.radius * 2, attr.radius, -attr.radius, attr.height - attr.radius * 2, attr.radius * 2 - attr.width, attr.radius * 2 - attr.height); + } + else { + return Ext.String.format("M{0},{1}l{2},0,0,{3},{4},0z", attr.x, attr.y, attr.width, attr.height, -attr.width); + } + }, + + path2string: function () { + return this.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1"); + }, + + parsePathString: function (pathString) { + if (!pathString) { + return null; + } + var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0}, + data = [], + me = this; + if (me.is(pathString, "array") && me.is(pathString[0], "array")) { // rough assumption + data = me.pathClone(pathString); + } + if (!data.length) { + String(pathString).replace(me.pathCommandRE, function (a, b, c) { + var params = [], + name = b.toLowerCase(); + c.replace(me.pathValuesRE, function (a, b) { + b && params.push(+b); + }); + if (name == "m" && params.length > 2) { + data.push([b].concat(params.splice(0, 2))); + name = "l"; + b = (b == "m") ? "l" : "L"; + } + while (params.length >= paramCounts[name]) { + data.push([b].concat(params.splice(0, paramCounts[name]))); + if (!paramCounts[name]) { + break; + } + } + }); + } + data.toString = me.path2string; + return data; + }, + + mapPath: function (path, matrix) { + if (!matrix) { + return path; + } + var x, y, i, ii, j, jj, pathi; + path = this.path2curve(path); + for (i = 0, ii = path.length; i < ii; i++) { + pathi = path[i]; + for (j = 1, jj = pathi.length; j < jj-1; j += 2) { + x = matrix.x(pathi[j], pathi[j + 1]); + y = matrix.y(pathi[j], pathi[j + 1]); + pathi[j] = x; + pathi[j + 1] = y; + } + } + return path; + }, + + pathClone: function(pathArray) { + var res = [], + j, + jj, + i, + ii; + if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { // rough assumption + pathArray = this.parsePathString(pathArray); + } + for (i = 0, ii = pathArray.length; i < ii; i++) { + res[i] = []; + for (j = 0, jj = pathArray[i].length; j < jj; j++) { + res[i][j] = pathArray[i][j]; + } + } + res.toString = this.path2string; + return res; + }, + + pathToAbsolute: function (pathArray) { + if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { // rough assumption + pathArray = this.parsePathString(pathArray); + } + var res = [], + x = 0, + y = 0, + mx = 0, + my = 0, + start = 0, + i, + ii, + r, + pa, + j, + jj, + k, + kk; + if (pathArray[0][0] == "M") { + x = +pathArray[0][1]; + y = +pathArray[0][2]; + mx = x; + my = y; + start++; + res[0] = ["M", x, y]; + } + for (i = start, ii = pathArray.length; i < ii; i++) { + r = res[i] = []; + pa = pathArray[i]; + if (pa[0] != pa[0].toUpperCase()) { + r[0] = pa[0].toUpperCase(); + switch (r[0]) { + case "A": + r[1] = pa[1]; + r[2] = pa[2]; + r[3] = pa[3]; + r[4] = pa[4]; + r[5] = pa[5]; + r[6] = +(pa[6] + x); + r[7] = +(pa[7] + y); + break; + case "V": + r[1] = +pa[1] + y; + break; + case "H": + r[1] = +pa[1] + x; + break; + case "M": + mx = +pa[1] + x; + my = +pa[2] + y; + default: + for (j = 1, jj = pa.length; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + } + } else { + for (k = 0, kk = pa.length; k < kk; k++) { + res[i][k] = pa[k]; + } + } + switch (r[0]) { + case "Z": + x = mx; + y = my; + break; + case "H": + x = r[1]; + break; + case "V": + y = r[1]; + break; + case "M": + mx = res[i][res[i].length - 2]; + my = res[i][res[i].length - 1]; + default: + x = res[i][res[i].length - 2]; + y = res[i][res[i].length - 1]; + } + } + res.toString = this.path2string; + return res; + }, + + pathToRelative: function (pathArray) { + if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { + pathArray = this.parsePathString(pathArray); + } + var res = [], + x = 0, + y = 0, + mx = 0, + my = 0, + start = 0; + if (pathArray[0][0] == "M") { + x = pathArray[0][1]; + y = pathArray[0][2]; + mx = x; + my = y; + start++; + res.push(["M", x, y]); + } + for (var i = start, ii = pathArray.length; i < ii; i++) { + var r = res[i] = [], + pa = pathArray[i]; + if (pa[0] != pa[0].toLowerCase()) { + r[0] = pa[0].toLowerCase(); + switch (r[0]) { + case "a": + r[1] = pa[1]; + r[2] = pa[2]; + r[3] = pa[3]; + r[4] = pa[4]; + r[5] = pa[5]; + r[6] = +(pa[6] - x).toFixed(3); + r[7] = +(pa[7] - y).toFixed(3); + break; + case "v": + r[1] = +(pa[1] - y).toFixed(3); + break; + case "m": + mx = pa[1]; + my = pa[2]; + default: + for (var j = 1, jj = pa.length; j < jj; j++) { + r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3); + } + } + } else { + r = res[i] = []; + if (pa[0] == "m") { + mx = pa[1] + x; + my = pa[2] + y; + } + for (var k = 0, kk = pa.length; k < kk; k++) { + res[i][k] = pa[k]; + } + } + var len = res[i].length; + switch (res[i][0]) { + case "z": + x = mx; + y = my; + break; + case "h": + x += +res[i][len - 1]; + break; + case "v": + y += +res[i][len - 1]; + break; + default: + x += +res[i][len - 2]; + y += +res[i][len - 1]; + } + } + res.toString = this.path2string; + return res; + }, + + //Returns a path converted to a set of curveto commands + path2curve: function (path) { + var me = this, + points = me.pathToAbsolute(path), + ln = points.length, + attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, + i, seg, segLn, point; + + for (i = 0; i < ln; i++) { + points[i] = me.command2curve(points[i], attrs); + if (points[i].length > 7) { + points[i].shift(); + point = points[i]; + while (point.length) { + points.splice(i++, 0, ["C"].concat(point.splice(0, 6))); + } + points.splice(i, 1); + ln = points.length; + } + seg = points[i]; + segLn = seg.length; + attrs.x = seg[segLn - 2]; + attrs.y = seg[segLn - 1]; + attrs.bx = parseFloat(seg[segLn - 4]) || attrs.x; + attrs.by = parseFloat(seg[segLn - 3]) || attrs.y; + } + return points; + }, + + interpolatePaths: function (path, path2) { + var me = this, + p = me.pathToAbsolute(path), + p2 = me.pathToAbsolute(path2), + attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, + attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, + fixArc = function (pp, i) { + if (pp[i].length > 7) { + pp[i].shift(); + var pi = pp[i]; + while (pi.length) { + pp.splice(i++, 0, ["C"].concat(pi.splice(0, 6))); + } + pp.splice(i, 1); + ii = Math.max(p.length, p2.length || 0); + } + }, + fixM = function (path1, path2, a1, a2, i) { + if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") { + path2.splice(i, 0, ["M", a2.x, a2.y]); + a1.bx = 0; + a1.by = 0; + a1.x = path1[i][1]; + a1.y = path1[i][2]; + ii = Math.max(p.length, p2.length || 0); + } + }; + for (var i = 0, ii = Math.max(p.length, p2.length || 0); i < ii; i++) { + p[i] = me.command2curve(p[i], attrs); + fixArc(p, i); + (p2[i] = me.command2curve(p2[i], attrs2)); + fixArc(p2, i); + fixM(p, p2, attrs, attrs2, i); + fixM(p2, p, attrs2, attrs, i); + var seg = p[i], + seg2 = p2[i], + seglen = seg.length, + seg2len = seg2.length; + attrs.x = seg[seglen - 2]; + attrs.y = seg[seglen - 1]; + attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x; + attrs.by = parseFloat(seg[seglen - 3]) || attrs.y; + attrs2.bx = (parseFloat(seg2[seg2len - 4]) || attrs2.x); + attrs2.by = (parseFloat(seg2[seg2len - 3]) || attrs2.y); + attrs2.x = seg2[seg2len - 2]; + attrs2.y = seg2[seg2len - 1]; + } + return [p, p2]; + }, + + //Returns any path command as a curveto command based on the attrs passed + command2curve: function (pathCommand, d) { + var me = this; + if (!pathCommand) { + return ["C", d.x, d.y, d.x, d.y, d.x, d.y]; + } + if (pathCommand[0] != "T" && pathCommand[0] != "Q") { + d.qx = d.qy = null; + } + switch (pathCommand[0]) { + case "M": + d.X = pathCommand[1]; + d.Y = pathCommand[2]; + break; + case "A": + pathCommand = ["C"].concat(me.arc2curve.apply(me, [d.x, d.y].concat(pathCommand.slice(1)))); + break; + case "S": + pathCommand = ["C", d.x + (d.x - (d.bx || d.x)), d.y + (d.y - (d.by || d.y))].concat(pathCommand.slice(1)); + break; + case "T": + d.qx = d.x + (d.x - (d.qx || d.x)); + d.qy = d.y + (d.y - (d.qy || d.y)); + pathCommand = ["C"].concat(me.quadratic2curve(d.x, d.y, d.qx, d.qy, pathCommand[1], pathCommand[2])); + break; + case "Q": + d.qx = pathCommand[1]; + d.qy = pathCommand[2]; + pathCommand = ["C"].concat(me.quadratic2curve(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[3], pathCommand[4])); + break; + case "L": + pathCommand = ["C"].concat(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[1], pathCommand[2]); + break; + case "H": + pathCommand = ["C"].concat(d.x, d.y, pathCommand[1], d.y, pathCommand[1], d.y); + break; + case "V": + pathCommand = ["C"].concat(d.x, d.y, d.x, pathCommand[1], d.x, pathCommand[1]); + break; + case "Z": + pathCommand = ["C"].concat(d.x, d.y, d.X, d.Y, d.X, d.Y); + break; + } + return pathCommand; + }, + + quadratic2curve: function (x1, y1, ax, ay, x2, y2) { + var _13 = 1 / 3, + _23 = 2 / 3; + return [ + _13 * x1 + _23 * ax, + _13 * y1 + _23 * ay, + _13 * x2 + _23 * ax, + _13 * y2 + _23 * ay, + x2, + y2 + ]; + }, + + rotate: function (x, y, rad) { + var cos = Math.cos(rad), + sin = Math.sin(rad), + X = x * cos - y * sin, + Y = x * sin + y * cos; + return {x: X, y: Y}; + }, + + arc2curve: function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { + // for more information of where this Math came from visit: + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + var me = this, + PI = Math.PI, + radian = me.radian, + _120 = PI * 120 / 180, + rad = radian * (+angle || 0), + res = [], + math = Math, + mcos = math.cos, + msin = math.sin, + msqrt = math.sqrt, + mabs = math.abs, + masin = math.asin, + xy, cos, sin, x, y, h, rx2, ry2, k, cx, cy, f1, f2, df, c1, s1, c2, s2, + t, hx, hy, m1, m2, m3, m4, newres, i, ln, f2old, x2old, y2old; + if (!recursive) { + xy = me.rotate(x1, y1, -rad); + x1 = xy.x; + y1 = xy.y; + xy = me.rotate(x2, y2, -rad); + x2 = xy.x; + y2 = xy.y; + cos = mcos(radian * angle); + sin = msin(radian * angle); + x = (x1 - x2) / 2; + y = (y1 - y2) / 2; + h = (x * x) / (rx * rx) + (y * y) / (ry * ry); + if (h > 1) { + h = msqrt(h); + rx = h * rx; + ry = h * ry; + } + rx2 = rx * rx; + ry2 = ry * ry; + k = (large_arc_flag == sweep_flag ? -1 : 1) * + msqrt(mabs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))); + cx = k * rx * y / ry + (x1 + x2) / 2; + cy = k * -ry * x / rx + (y1 + y2) / 2; + f1 = masin(((y1 - cy) / ry).toFixed(7)); + f2 = masin(((y2 - cy) / ry).toFixed(7)); + + f1 = x1 < cx ? PI - f1 : f1; + f2 = x2 < cx ? PI - f2 : f2; + if (f1 < 0) { + f1 = PI * 2 + f1; + } + if (f2 < 0) { + f2 = PI * 2 + f2; + } + if (sweep_flag && f1 > f2) { + f1 = f1 - PI * 2; + } + if (!sweep_flag && f2 > f1) { + f2 = f2 - PI * 2; + } + } + else { + f1 = recursive[0]; + f2 = recursive[1]; + cx = recursive[2]; + cy = recursive[3]; + } + df = f2 - f1; + if (mabs(df) > _120) { + f2old = f2; + x2old = x2; + y2old = y2; + f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1); + x2 = cx + rx * mcos(f2); + y2 = cy + ry * msin(f2); + res = me.arc2curve(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); + } + df = f2 - f1; + c1 = mcos(f1); + s1 = msin(f1); + c2 = mcos(f2); + s2 = msin(f2); + t = math.tan(df / 4); + hx = 4 / 3 * rx * t; + hy = 4 / 3 * ry * t; + m1 = [x1, y1]; + m2 = [x1 + hx * s1, y1 - hy * c1]; + m3 = [x2 + hx * s2, y2 - hy * c2]; + m4 = [x2, y2]; + m2[0] = 2 * m1[0] - m2[0]; + m2[1] = 2 * m1[1] - m2[1]; + if (recursive) { + return [m2, m3, m4].concat(res); + } + else { + res = [m2, m3, m4].concat(res).join().split(","); + newres = []; + ln = res.length; + for (i = 0; i < ln; i++) { + newres[i] = i % 2 ? me.rotate(res[i - 1], res[i], rad).y : me.rotate(res[i], res[i + 1], rad).x; + } + return newres; + } + }, + + rotatePoint: function (x, y, alpha, cx, cy) { + if (!alpha) { + return { + x: x, + y: y + }; + } + cx = cx || 0; + cy = cy || 0; + x = x - cx; + y = y - cy; + alpha = alpha * this.radian; + var cos = Math.cos(alpha), + sin = Math.sin(alpha); + return { + x: x * cos - y * sin + cx, + y: x * sin + y * cos + cy + }; + }, + + rotateAndTranslatePath: function (sprite) { + var alpha = sprite.rotation.degrees, + cx = sprite.rotation.x, + cy = sprite.rotation.y, + dx = sprite.translation.x, + dy = sprite.translation.y, + path, + i, + p, + xy, + j, + res = []; + if (!alpha && !dx && !dy) { + return this.pathToAbsolute(sprite.attr.path); + } + dx = dx || 0; + dy = dy || 0; + path = this.pathToAbsolute(sprite.attr.path); + for (i = path.length; i--;) { + p = res[i] = path[i].slice(); + if (p[0] == "A") { + xy = this.rotatePoint(p[6], p[7], alpha, cx, cy); + p[6] = xy.x + dx; + p[7] = xy.y + dy; + } else { + j = 1; + while (p[j + 1] != null) { + xy = this.rotatePoint(p[j], p[j + 1], alpha, cx, cy); + p[j] = xy.x + dx; + p[j + 1] = xy.y + dy; + j += 2; + } + } + } + return res; + }, + + pathDimensions: function (path) { + if (!path || !(path + "")) { + return {x: 0, y: 0, width: 0, height: 0}; + } + path = this.path2curve(path); + var x = 0, + y = 0, + X = [], + Y = [], + p, + i, + ii, + xmin, + ymin, + dim; + for (i = 0, ii = path.length; i < ii; i++) { + p = path[i]; + if (p[0] == "M") { + x = p[1]; + y = p[2]; + X.push(x); + Y.push(y); + } + else { + dim = this.curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]); + X = X.concat(dim.min.x, dim.max.x); + Y = Y.concat(dim.min.y, dim.max.y); + x = p[5]; + y = p[6]; + } + } + xmin = Math.min.apply(0, X); + ymin = Math.min.apply(0, Y); + return { + x: xmin, + y: ymin, + path: path, + width: Math.max.apply(0, X) - xmin, + height: Math.max.apply(0, Y) - ymin + }; + }, + + intersect: function(subjectPolygon, clipPolygon) { + var cp1, cp2, s, e, point; + var inside = function(p) { + return (cp2[0]-cp1[0]) * (p[1]-cp1[1]) > (cp2[1]-cp1[1]) * (p[0]-cp1[0]); + }; + var intersection = function() { + var p = []; + var dcx = cp1[0]-cp2[0], + dcy = cp1[1]-cp2[1], + dpx = s[0]-e[0], + dpy = s[1]-e[1], + n1 = cp1[0]*cp2[1] - cp1[1]*cp2[0], + n2 = s[0]*e[1] - s[1]*e[0], + n3 = 1 / (dcx*dpy - dcy*dpx); + + p[0] = (n1*dpx - n2*dcx) * n3; + p[1] = (n1*dpy - n2*dcy) * n3; + return p; + }; + var outputList = subjectPolygon; + cp1 = clipPolygon[clipPolygon.length -1]; + for (var i = 0, l = clipPolygon.length; i < l; ++i) { + cp2 = clipPolygon[i]; + var inputList = outputList; + outputList = []; + s = inputList[inputList.length -1]; + for (var j = 0, ln = inputList.length; j < ln; j++) { + e = inputList[j]; + if (inside(e)) { + if (!inside(s)) { + outputList.push(intersection()); + } + outputList.push(e); + } else if (inside(s)) { + outputList.push(intersection()); + } + s = e; + } + cp1 = cp2; + } + return outputList; + }, + + curveDim: function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) { + var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x), + b = 2 * (c1x - p1x) - 2 * (c2x - c1x), + c = p1x - c1x, + t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a, + t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a, + y = [p1y, p2y], + x = [p1x, p2x], + dot; + if (Math.abs(t1) > 1e12) { + t1 = 0.5; + } + if (Math.abs(t2) > 1e12) { + t2 = 0.5; + } + if (t1 > 0 && t1 < 1) { + dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1); + x.push(dot.x); + y.push(dot.y); + } + if (t2 > 0 && t2 < 1) { + dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2); + x.push(dot.x); + y.push(dot.y); + } + a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y); + b = 2 * (c1y - p1y) - 2 * (c2y - c1y); + c = p1y - c1y; + t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a; + t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a; + if (Math.abs(t1) > 1e12) { + t1 = 0.5; + } + if (Math.abs(t2) > 1e12) { + t2 = 0.5; + } + if (t1 > 0 && t1 < 1) { + dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1); + x.push(dot.x); + y.push(dot.y); + } + if (t2 > 0 && t2 < 1) { + dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2); + x.push(dot.x); + y.push(dot.y); + } + return { + min: {x: Math.min.apply(0, x), y: Math.min.apply(0, y)}, + max: {x: Math.max.apply(0, x), y: Math.max.apply(0, y)} + }; + }, + + getAnchors: function (p1x, p1y, p2x, p2y, p3x, p3y, value) { + value = value || 4; + var l = Math.min(Math.sqrt(Math.pow(p1x - p2x, 2) + Math.pow(p1y - p2y, 2)) / value, Math.sqrt(Math.pow(p3x - p2x, 2) + Math.pow(p3y - p2y, 2)) / value), + a = Math.atan((p2x - p1x) / Math.abs(p2y - p1y)), + b = Math.atan((p3x - p2x) / Math.abs(p2y - p3y)), + pi = Math.PI; + a = p1y < p2y ? pi - a : a; + b = p3y < p2y ? pi - b : b; + var alpha = pi / 2 - ((a + b) % (pi * 2)) / 2; + alpha > pi / 2 && (alpha -= pi); + var dx1 = l * Math.sin(alpha + a), + dy1 = l * Math.cos(alpha + a), + dx2 = l * Math.sin(alpha + b), + dy2 = l * Math.cos(alpha + b), + out = { + x1: p2x - dx1, + y1: p2y + dy1, + x2: p2x + dx2, + y2: p2y + dy2 + }; + return out; + }, + + /* Smoothing function for a path. Converts a path into cubic beziers. Value defines the divider of the distance between points. + * Defaults to a value of 4. + */ + smooth: function (originalPath, value) { + var path = this.path2curve(originalPath), + newp = [path[0]], + x = path[0][1], + y = path[0][2], + j, + points, + i = 1, + ii = path.length, + beg = 1, + mx = x, + my = y, + cx = 0, + cy = 0; + for (; i < ii; i++) { + var pathi = path[i], + pathil = pathi.length, + pathim = path[i - 1], + pathiml = pathim.length, + pathip = path[i + 1], + pathipl = pathip && pathip.length; + if (pathi[0] == "M") { + mx = pathi[1]; + my = pathi[2]; + j = i + 1; + while (path[j][0] != "C") { + j++; + } + cx = path[j][5]; + cy = path[j][6]; + newp.push(["M", mx, my]); + beg = newp.length; + x = mx; + y = my; + continue; + } + if (pathi[pathil - 2] == mx && pathi[pathil - 1] == my && (!pathip || pathip[0] == "M")) { + var begl = newp[beg].length; + points = this.getAnchors(pathim[pathiml - 2], pathim[pathiml - 1], mx, my, newp[beg][begl - 2], newp[beg][begl - 1], value); + newp[beg][1] = points.x2; + newp[beg][2] = points.y2; + } + else if (!pathip || pathip[0] == "M") { + points = { + x1: pathi[pathil - 2], + y1: pathi[pathil - 1] + }; + } else { + points = this.getAnchors(pathim[pathiml - 2], pathim[pathiml - 1], pathi[pathil - 2], pathi[pathil - 1], pathip[pathipl - 2], pathip[pathipl - 1], value); + } + newp.push(["C", x, y, points.x1, points.y1, pathi[pathil - 2], pathi[pathil - 1]]); + x = points.x2; + y = points.y2; + } + return newp; + }, + + findDotAtSegment: function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) { + var t1 = 1 - t; + return { + x: Math.pow(t1, 3) * p1x + Math.pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + Math.pow(t, 3) * p2x, + y: Math.pow(t1, 3) * p1y + Math.pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + Math.pow(t, 3) * p2y + }; + }, + + snapEnds: function (from, to, stepsMax) { + var step = (to - from) / stepsMax, + level = Math.floor(Math.log(step) / Math.LN10) + 1, + m = Math.pow(10, level), + cur, + modulo = Math.round((step % m) * Math.pow(10, 2 - level)), + interval = [[0, 15], [20, 4], [30, 2], [40, 4], [50, 9], [60, 4], [70, 2], [80, 4], [100, 15]], + stepCount = 0, + value, + weight, + i, + topValue, + topWeight = 1e9, + ln = interval.length; + cur = from = Math.floor(from / m) * m; + for (i = 0; i < ln; i++) { + value = interval[i][0]; + weight = (value - modulo) < 0 ? 1e6 : (value - modulo) / interval[i][1]; + if (weight < topWeight) { + topValue = value; + topWeight = weight; + } + } + step = Math.floor(step * Math.pow(10, -level)) * Math.pow(10, level) + topValue * Math.pow(10, level - 2); + while (cur < to) { + cur += step; + stepCount++; + } + to = +cur.toFixed(10); + return { + from: from, + to: to, + power: level, + step: step, + steps: stepCount + }; + }, + + sorter: function (a, b) { + return a.offset - b.offset; + }, + + rad: function(degrees) { + return degrees % 360 * Math.PI / 180; + }, + + degrees: function(radian) { + return radian * 180 / Math.PI % 360; + }, + + withinBox: function(x, y, bbox) { + bbox = bbox || {}; + return (x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height)); + }, + + parseGradient: function(gradient) { + var me = this, + type = gradient.type || 'linear', + angle = gradient.angle || 0, + radian = me.radian, + stops = gradient.stops, + stopsArr = [], + stop, + vector, + max, + stopObj; + + if (type == 'linear') { + vector = [0, 0, Math.cos(angle * radian), Math.sin(angle * radian)]; + max = 1 / (Math.max(Math.abs(vector[2]), Math.abs(vector[3])) || 1); + vector[2] *= max; + vector[3] *= max; + if (vector[2] < 0) { + vector[0] = -vector[2]; + vector[2] = 0; + } + if (vector[3] < 0) { + vector[1] = -vector[3]; + vector[3] = 0; + } + } + + for (stop in stops) { + if (stops.hasOwnProperty(stop) && me.stopsRE.test(stop)) { + stopObj = { + offset: parseInt(stop, 10), + color: Ext.draw.Color.toHex(stops[stop].color) || '#ffffff', + opacity: stops[stop].opacity || 1 + }; + stopsArr.push(stopObj); + } + } + // Sort by pct property + Ext.Array.sort(stopsArr, me.sorter); + if (type == 'linear') { + return { + id: gradient.id, + type: type, + vector: vector, + stops: stopsArr + }; + } + else { + return { + id: gradient.id, + type: type, + centerX: gradient.centerX, + centerY: gradient.centerY, + focalX: gradient.focalX, + focalY: gradient.focalY, + radius: gradient.radius, + vector: vector, + stops: stopsArr + }; + } + } +}); + +/** + * @class Ext.fx.PropertyHandler + * @ignore + */ +Ext.define('Ext.fx.PropertyHandler', { + + /* Begin Definitions */ + + requires: ['Ext.draw.Draw'], + + statics: { + defaultHandler: { + pixelDefaults: ['width', 'height', 'top', 'left'], + unitRE: /^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/, + + computeDelta: function(from, end, damper, initial, attr) { + damper = (typeof damper == 'number') ? damper : 1; + var match = this.unitRE.exec(from), + start, units; + if (match) { + from = match[1]; + units = match[2]; + if (!units && Ext.Array.contains(this.pixelDefaults, attr)) { + units = 'px'; + } + } + from = +from || 0; + + match = this.unitRE.exec(end); + if (match) { + end = match[1]; + units = match[2] || units; + } + end = +end || 0; + start = (initial != null) ? initial : from; + return { + from: from, + delta: (end - start) * damper, + units: units + }; + }, + + get: function(from, end, damper, initialFrom, attr) { + var ln = from.length, + out = [], + i, initial, res, j, len; + for (i = 0; i < ln; i++) { + if (initialFrom) { + initial = initialFrom[i][1].from; + } + if (Ext.isArray(from[i][1]) && Ext.isArray(end)) { + res = []; + j = 0; + len = from[i][1].length; + for (; j < len; j++) { + res.push(this.computeDelta(from[i][1][j], end[j], damper, initial, attr)); + } + out.push([from[i][0], res]); + } + else { + out.push([from[i][0], this.computeDelta(from[i][1], end, damper, initial, attr)]); + } + } + return out; + }, + + set: function(values, easing) { + var ln = values.length, + out = [], + i, val, res, len, j; + for (i = 0; i < ln; i++) { + val = values[i][1]; + if (Ext.isArray(val)) { + res = []; + j = 0; + len = val.length; + for (; j < len; j++) { + res.push(val[j].from + (val[j].delta * easing) + (val[j].units || 0)); + } + out.push([values[i][0], res]); + } else { + out.push([values[i][0], val.from + (val.delta * easing) + (val.units || 0)]); + } + } + return out; + } + }, + color: { + rgbRE: /^rgb\(([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\)$/i, + hexRE: /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i, + hex3RE: /^#?([0-9A-F]{1})([0-9A-F]{1})([0-9A-F]{1})$/i, + + parseColor : function(color, damper) { + damper = (typeof damper == 'number') ? damper : 1; + var base, + out = false, + match; + + Ext.each([this.hexRE, this.rgbRE, this.hex3RE], function(re, idx) { + base = (idx % 2 == 0) ? 16 : 10; + match = re.exec(color); + if (match && match.length == 4) { + if (idx == 2) { + match[1] += match[1]; + match[2] += match[2]; + match[3] += match[3]; + } + out = { + red: parseInt(match[1], base), + green: parseInt(match[2], base), + blue: parseInt(match[3], base) + }; + return false; + } + }); + return out || color; + }, + + computeDelta: function(from, end, damper, initial) { + from = this.parseColor(from); + end = this.parseColor(end, damper); + var start = initial ? initial : from, + tfrom = typeof start, + tend = typeof end; + //Extra check for when the color string is not recognized. + if (tfrom == 'string' || tfrom == 'undefined' + || tend == 'string' || tend == 'undefined') { + return end || start; + } + return { + from: from, + delta: { + red: Math.round((end.red - start.red) * damper), + green: Math.round((end.green - start.green) * damper), + blue: Math.round((end.blue - start.blue) * damper) + } + }; + }, + + get: function(start, end, damper, initialFrom) { + var ln = start.length, + out = [], + i, initial; + for (i = 0; i < ln; i++) { + if (initialFrom) { + initial = initialFrom[i][1].from; + } + out.push([start[i][0], this.computeDelta(start[i][1], end, damper, initial)]); + } + return out; + }, + + set: function(values, easing) { + var ln = values.length, + out = [], + i, val, parsedString, from, delta; + for (i = 0; i < ln; i++) { + val = values[i][1]; + if (val) { + from = val.from; + delta = val.delta; + //multiple checks to reformat the color if it can't recognized by computeDelta. + val = (typeof val == 'object' && 'red' in val)? + 'rgb(' + val.red + ', ' + val.green + ', ' + val.blue + ')' : val; + val = (typeof val == 'object' && val.length)? val[0] : val; + if (typeof val == 'undefined') { + return []; + } + parsedString = typeof val == 'string'? val : + 'rgb(' + [ + (from.red + Math.round(delta.red * easing)) % 256, + (from.green + Math.round(delta.green * easing)) % 256, + (from.blue + Math.round(delta.blue * easing)) % 256 + ].join(',') + ')'; + out.push([ + values[i][0], + parsedString + ]); + } + } + return out; + } + }, + object: { + interpolate: function(prop, damper) { + damper = (typeof damper == 'number') ? damper : 1; + var out = {}, + p; + for(p in prop) { + out[p] = parseInt(prop[p], 10) * damper; + } + return out; + }, + + computeDelta: function(from, end, damper, initial) { + from = this.interpolate(from); + end = this.interpolate(end, damper); + var start = initial ? initial : from, + delta = {}, + p; + + for(p in end) { + delta[p] = end[p] - start[p]; + } + return { + from: from, + delta: delta + }; + }, + + get: function(start, end, damper, initialFrom) { + var ln = start.length, + out = [], + i, initial; + for (i = 0; i < ln; i++) { + if (initialFrom) { + initial = initialFrom[i][1].from; + } + out.push([start[i][0], this.computeDelta(start[i][1], end, damper, initial)]); + } + return out; + }, + + set: function(values, easing) { + var ln = values.length, + out = [], + outObject = {}, + i, from, delta, val, p; + for (i = 0; i < ln; i++) { + val = values[i][1]; + from = val.from; + delta = val.delta; + for (p in from) { + outObject[p] = Math.round(from[p] + delta[p] * easing); + } + out.push([ + values[i][0], + outObject + ]); + } + return out; + } + }, + + path: { + computeDelta: function(from, end, damper, initial) { + damper = (typeof damper == 'number') ? damper : 1; + var start; + from = +from || 0; + end = +end || 0; + start = (initial != null) ? initial : from; + return { + from: from, + delta: (end - start) * damper + }; + }, + + forcePath: function(path) { + if (!Ext.isArray(path) && !Ext.isArray(path[0])) { + path = Ext.draw.Draw.parsePathString(path); + } + return path; + }, + + get: function(start, end, damper, initialFrom) { + var endPath = this.forcePath(end), + out = [], + startLn = start.length, + startPathLn, pointsLn, i, deltaPath, initial, j, k, path, startPath; + for (i = 0; i < startLn; i++) { + startPath = this.forcePath(start[i][1]); + + deltaPath = Ext.draw.Draw.interpolatePaths(startPath, endPath); + startPath = deltaPath[0]; + endPath = deltaPath[1]; + + startPathLn = startPath.length; + path = []; + for (j = 0; j < startPathLn; j++) { + deltaPath = [startPath[j][0]]; + pointsLn = startPath[j].length; + for (k = 1; k < pointsLn; k++) { + initial = initialFrom && initialFrom[0][1][j][k].from; + deltaPath.push(this.computeDelta(startPath[j][k], endPath[j][k], damper, initial)); + } + path.push(deltaPath); + } + out.push([start[i][0], path]); + } + return out; + }, + + set: function(values, easing) { + var ln = values.length, + out = [], + i, j, k, newPath, calcPath, deltaPath, deltaPathLn, pointsLn; + for (i = 0; i < ln; i++) { + deltaPath = values[i][1]; + newPath = []; + deltaPathLn = deltaPath.length; + for (j = 0; j < deltaPathLn; j++) { + calcPath = [deltaPath[j][0]]; + pointsLn = deltaPath[j].length; + for (k = 1; k < pointsLn; k++) { + calcPath.push(deltaPath[j][k].from + deltaPath[j][k].delta * easing); + } + newPath.push(calcPath.join(',')); + } + out.push([values[i][0], newPath.join(',')]); + } + return out; + } + } + /* End Definitions */ + } +}, function() { + Ext.each([ + 'outlineColor', + 'backgroundColor', + 'borderColor', + 'borderTopColor', + 'borderRightColor', + 'borderBottomColor', + 'borderLeftColor', + 'fill', + 'stroke' + ], function(prop) { + this[prop] = this.color; + }, this); +}); +/** + * @class Ext.fx.Anim + * + * This class manages animation for a specific {@link #target}. The animation allows + * animation of various properties on the target, such as size, position, color and others. + * + * ## Starting Conditions + * The starting conditions for the animation are provided by the {@link #from} configuration. + * Any/all of the properties in the {@link #from} configuration can be specified. If a particular + * property is not defined, the starting value for that property will be read directly from the target. + * + * ## End Conditions + * The ending conditions for the animation are provided by the {@link #to} configuration. These mark + * the final values once the animations has finished. The values in the {@link #from} can mirror + * those in the {@link #to} configuration to provide a starting point. + * + * ## Other Options + * - {@link #duration}: Specifies the time period of the animation. + * - {@link #easing}: Specifies the easing of the animation. + * - {@link #iterations}: Allows the animation to repeat a number of times. + * - {@link #alternate}: Used in conjunction with {@link #iterations}, reverses the direction every second iteration. + * + * ## Example Code + * + * var myComponent = Ext.create('Ext.Component', { + * renderTo: document.body, + * width: 200, + * height: 200, + * style: 'border: 1px solid red;' + * }); + * + * new Ext.fx.Anim({ + * target: myComponent, + * duration: 1000, + * from: { + * width: 400 //starting width 400 + * }, + * to: { + * width: 300, //end width 300 + * height: 300 // end width 300 + * } + * }); + */ +Ext.define('Ext.fx.Anim', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: ['Ext.fx.Manager', 'Ext.fx.Animator', 'Ext.fx.Easing', 'Ext.fx.CubicBezier', 'Ext.fx.PropertyHandler'], + + /* End Definitions */ + + isAnimation: true, + /** + * @cfg {Number} duration + * Time in milliseconds for a single animation to last. Defaults to 250. If the {@link #iterations} property is + * specified, then each animate will take the same duration for each iteration. + */ + duration: 250, + + /** + * @cfg {Number} delay + * Time to delay before starting the animation. Defaults to 0. + */ + delay: 0, + + /* private used to track a delayed starting time */ + delayStart: 0, + + /** + * @cfg {Boolean} dynamic + * Currently only for Component Animation: Only set a component's outer element size bypassing layouts. Set to true to do full layouts for every frame of the animation. Defaults to false. + */ + dynamic: false, + + /** + * @cfg {String} easing +This describes how the intermediate values used during a transition will be calculated. It allows for a transition to change +speed over its duration. + + -backIn + -backOut + -bounceIn + -bounceOut + -ease + -easeIn + -easeOut + -easeInOut + -elasticIn + -elasticOut + -cubic-bezier(x1, y1, x2, y2) + +Note that cubic-bezier will create a custom easing curve following the CSS3 transition-timing-function specification `{@link http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag}`. The four values specify points P1 and P2 of the curve +as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition is invalid. + * @markdown + */ + easing: 'ease', + + /** + * @cfg {Object} keyframes + * Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always considered '0%' and 'to' + * is considered '100%'.Every keyframe declaration must have a keyframe rule for 0% and 100%, possibly defined using + * "from" or "to". A keyframe declaration without these keyframe selectors is invalid and will not be available for + * animation. The keyframe declaration for a keyframe rule consists of properties and values. Properties that are unable to + * be animated are ignored in these rules, with the exception of 'easing' which can be changed at each keyframe. For example: +

+keyframes : {
+    '0%': {
+        left: 100
+    },
+    '40%': {
+        left: 150
+    },
+    '60%': {
+        left: 75
+    },
+    '100%': {
+        left: 100
+    }
+}
+ 
+ */ + + /** + * @private + */ + damper: 1, + + /** + * @private + */ + bezierRE: /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/, + + /** + * Run the animation from the end to the beginning + * Defaults to false. + * @cfg {Boolean} reverse + */ + reverse: false, + + /** + * Flag to determine if the animation has started + * @property running + * @type boolean + */ + running: false, + + /** + * Flag to determine if the animation is paused. Only set this to true if you need to + * keep the Anim instance around to be unpaused later; otherwise call {@link #end}. + * @property paused + * @type boolean + */ + paused: false, + + /** + * Number of times to execute the animation. Defaults to 1. + * @cfg {int} iterations + */ + iterations: 1, + + /** + * Used in conjunction with iterations to reverse the animation each time an iteration completes. + * @cfg {Boolean} alternate + * Defaults to false. + */ + alternate: false, + + /** + * Current iteration the animation is running. + * @property currentIteration + * @type int + */ + currentIteration: 0, + + /** + * Starting time of the animation. + * @property startTime + * @type Date + */ + startTime: 0, + + /** + * Contains a cache of the interpolators to be used. + * @private + * @property propHandlers + * @type Object + */ + + /** + * @cfg {String/Object} target + * The {@link Ext.fx.target.Target} to apply the animation to. This should only be specified when creating an Ext.fx.Anim directly. + * The target does not need to be a {@link Ext.fx.target.Target} instance, it can be the underlying object. For example, you can + * pass a Component, Element or Sprite as the target and the Anim will create the appropriate {@link Ext.fx.target.Target} object + * automatically. + */ + + /** + * @cfg {Object} from + * An object containing property/value pairs for the beginning of the animation. If not specified, the current state of the + * Ext.fx.target will be used. For example: +

+from : {
+    opacity: 0,       // Transparent
+    color: '#ffffff', // White
+    left: 0
+}
+
+ */ + + /** + * @cfg {Object} to + * An object containing property/value pairs for the end of the animation. For example: +

+ to : {
+     opacity: 1,       // Opaque
+     color: '#00ff00', // Green
+     left: 500
+ }
+ 
+ */ + + // @private + constructor: function(config) { + var me = this; + config = config || {}; + // If keyframes are passed, they really want an Animator instead. + if (config.keyframes) { + return Ext.create('Ext.fx.Animator', config); + } + config = Ext.apply(me, config); + if (me.from === undefined) { + me.from = {}; + } + me.propHandlers = {}; + me.config = config; + me.target = Ext.fx.Manager.createTarget(me.target); + me.easingFn = Ext.fx.Easing[me.easing]; + me.target.dynamic = me.dynamic; + + // If not a pre-defined curve, try a cubic-bezier + if (!me.easingFn) { + me.easingFn = String(me.easing).match(me.bezierRE); + if (me.easingFn && me.easingFn.length == 5) { + var curve = me.easingFn; + me.easingFn = Ext.fx.cubicBezier(+curve[1], +curve[2], +curve[3], +curve[4]); + } + } + me.id = Ext.id(null, 'ext-anim-'); + Ext.fx.Manager.addAnim(me); + me.addEvents( + /** + * @event beforeanimate + * Fires before the animation starts. A handler can return false to cancel the animation. + * @param {Ext.fx.Anim} this + */ + 'beforeanimate', + /** + * @event afteranimate + * Fires when the animation is complete. + * @param {Ext.fx.Anim} this + * @param {Date} startTime + */ + 'afteranimate', + /** + * @event lastframe + * Fires when the animation's last frame has been set. + * @param {Ext.fx.Anim} this + * @param {Date} startTime + */ + 'lastframe' + ); + me.mixins.observable.constructor.call(me, config); + if (config.callback) { + me.on('afteranimate', config.callback, config.scope); + } + return me; + }, + + /** + * @private + * Helper to the target + */ + setAttr: function(attr, value) { + return Ext.fx.Manager.items.get(this.id).setAttr(this.target, attr, value); + }, + + /* + * @private + * Set up the initial currentAttrs hash. + */ + initAttrs: function() { + var me = this, + from = me.from, + to = me.to, + initialFrom = me.initialFrom || {}, + out = {}, + start, end, propHandler, attr; + + for (attr in to) { + if (to.hasOwnProperty(attr)) { + start = me.target.getAttr(attr, from[attr]); + end = to[attr]; + // Use default (numeric) property handler + if (!Ext.fx.PropertyHandler[attr]) { + if (Ext.isObject(end)) { + propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler.object; + } else { + propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler.defaultHandler; + } + } + // Use custom handler + else { + propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler[attr]; + } + out[attr] = propHandler.get(start, end, me.damper, initialFrom[attr], attr); + } + } + me.currentAttrs = out; + }, + + /* + * @private + * Fires beforeanimate and sets the running flag. + */ + start: function(startTime) { + var me = this, + delay = me.delay, + delayStart = me.delayStart, + delayDelta; + if (delay) { + if (!delayStart) { + me.delayStart = startTime; + return; + } + else { + delayDelta = startTime - delayStart; + if (delayDelta < delay) { + return; + } + else { + // Compensate for frame delay; + startTime = new Date(delayStart.getTime() + delay); + } + } + } + if (me.fireEvent('beforeanimate', me) !== false) { + me.startTime = startTime; + if (!me.paused && !me.currentAttrs) { + me.initAttrs(); + } + me.running = true; + } + }, + + /* + * @private + * Calculate attribute value at the passed timestamp. + * @returns a hash of the new attributes. + */ + runAnim: function(elapsedTime) { + var me = this, + attrs = me.currentAttrs, + duration = me.duration, + easingFn = me.easingFn, + propHandlers = me.propHandlers, + ret = {}, + easing, values, attr, lastFrame; + + if (elapsedTime >= duration) { + elapsedTime = duration; + lastFrame = true; + } + if (me.reverse) { + elapsedTime = duration - elapsedTime; + } + + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + values = attrs[attr]; + easing = lastFrame ? 1 : easingFn(elapsedTime / duration); + ret[attr] = propHandlers[attr].set(values, easing); + } + } + return ret; + }, + + /* + * @private + * Perform lastFrame cleanup and handle iterations + * @returns a hash of the new attributes. + */ + lastFrame: function() { + var me = this, + iter = me.iterations, + iterCount = me.currentIteration; + + iterCount++; + if (iterCount < iter) { + if (me.alternate) { + me.reverse = !me.reverse; + } + me.startTime = new Date(); + me.currentIteration = iterCount; + // Turn off paused for CSS3 Transitions + me.paused = false; + } + else { + me.currentIteration = 0; + me.end(); + me.fireEvent('lastframe', me, me.startTime); + } + }, + + /* + * Fire afteranimate event and end the animation. Usually called automatically when the + * animation reaches its final frame, but can also be called manually to pre-emptively + * stop and destroy the running animation. + */ + end: function() { + var me = this; + me.startTime = 0; + me.paused = false; + me.running = false; + Ext.fx.Manager.removeAnim(me); + me.fireEvent('afteranimate', me, me.startTime); + } +}); +// Set flag to indicate that Fx is available. Class might not be available immediately. +Ext.enableFx = true; + +/* + * This is a derivative of the similarly named class in the YUI Library. + * The original license: + * Copyright (c) 2006, Yahoo! Inc. All rights reserved. + * Code licensed under the BSD License: + * http://developer.yahoo.net/yui/license.txt + */ + + +/** + * @class Ext.dd.DragDrop + * Defines the interface and base operation of items that that can be + * dragged or can be drop targets. It was designed to be extended, overriding + * the event handlers for startDrag, onDrag, onDragOver and onDragOut. + * Up to three html elements can be associated with a DragDrop instance: + *
    + *
  • linked element: the element that is passed into the constructor. + * This is the element which defines the boundaries for interaction with + * other DragDrop objects.
  • + *
  • handle element(s): The drag operation only occurs if the element that + * was clicked matches a handle element. By default this is the linked + * element, but there are times that you will want only a portion of the + * linked element to initiate the drag operation, and the setHandleElId() + * method provides a way to define this.
  • + *
  • drag element: this represents the element that would be moved along + * with the cursor during a drag operation. By default, this is the linked + * element itself as in {@link Ext.dd.DD}. setDragElId() lets you define + * a separate element that would be moved, as in {@link Ext.dd.DDProxy}. + *
  • + *
+ * This class should not be instantiated until the onload event to ensure that + * the associated elements are available. + * The following would define a DragDrop obj that would interact with any + * other DragDrop obj in the "group1" group: + *
+ *  dd = new Ext.dd.DragDrop("div1", "group1");
+ * 
+ * Since none of the event handlers have been implemented, nothing would + * actually happen if you were to run the code above. Normally you would + * override this class or one of the default implementations, but you can + * also override the methods you want on an instance of the class... + *
+ *  dd.onDragDrop = function(e, id) {
+ *    alert("dd was dropped on " + id);
+ *  }
+ * 
+ * @constructor + * @param {String} id of the element that is linked to this instance + * @param {String} sGroup the group of related DragDrop objects + * @param {object} config an object containing configurable attributes + * Valid properties for DragDrop: + * padding, isTarget, maintainOffset, primaryButtonOnly + */ + +Ext.define('Ext.dd.DragDrop', { + requires: ['Ext.dd.DragDropManager'], + constructor: function(id, sGroup, config) { + if(id) { + this.init(id, sGroup, config); + } + }, + + /** + * Set to false to enable a DragDrop object to fire drag events while dragging + * over its own Element. Defaults to true - DragDrop objects do not by default + * fire drag events to themselves. + * @property ignoreSelf + * @type Boolean + */ + + /** + * The id of the element associated with this object. This is what we + * refer to as the "linked element" because the size and position of + * this element is used to determine when the drag and drop objects have + * interacted. + * @property id + * @type String + */ + id: null, + + /** + * Configuration attributes passed into the constructor + * @property config + * @type object + */ + config: null, + + /** + * The id of the element that will be dragged. By default this is same + * as the linked element, but could be changed to another element. Ex: + * Ext.dd.DDProxy + * @property dragElId + * @type String + * @private + */ + dragElId: null, + + /** + * The ID of the element that initiates the drag operation. By default + * this is the linked element, but could be changed to be a child of this + * element. This lets us do things like only starting the drag when the + * header element within the linked html element is clicked. + * @property handleElId + * @type String + * @private + */ + handleElId: null, + + /** + * An object who's property names identify HTML tags to be considered invalid as drag handles. + * A non-null property value identifies the tag as invalid. Defaults to the + * following value which prevents drag operations from being initiated by <a> elements:

+{
+    A: "A"
+}
+ * @property invalidHandleTypes + * @type Object + */ + invalidHandleTypes: null, + + /** + * An object who's property names identify the IDs of elements to be considered invalid as drag handles. + * A non-null property value identifies the ID as invalid. For example, to prevent + * dragging from being initiated on element ID "foo", use:

+{
+    foo: true
+}
+ * @property invalidHandleIds + * @type Object + */ + invalidHandleIds: null, + + /** + * An Array of CSS class names for elements to be considered in valid as drag handles. + * @property invalidHandleClasses + * @type Array + */ + invalidHandleClasses: null, + + /** + * The linked element's absolute X position at the time the drag was + * started + * @property startPageX + * @type int + * @private + */ + startPageX: 0, + + /** + * The linked element's absolute X position at the time the drag was + * started + * @property startPageY + * @type int + * @private + */ + startPageY: 0, + + /** + * The group defines a logical collection of DragDrop objects that are + * related. Instances only get events when interacting with other + * DragDrop object in the same group. This lets us define multiple + * groups using a single DragDrop subclass if we want. + * @property groups + * @type object An object in the format {'group1':true, 'group2':true} + */ + groups: null, + + /** + * Individual drag/drop instances can be locked. This will prevent + * onmousedown start drag. + * @property locked + * @type boolean + * @private + */ + locked: false, + + /** + * Lock this instance + * @method lock + */ + lock: function() { + this.locked = true; + }, + + /** + * When set to true, other DD objects in cooperating DDGroups do not receive + * notification events when this DD object is dragged over them. Defaults to false. + * @property moveOnly + * @type boolean + */ + moveOnly: false, + + /** + * Unlock this instace + * @method unlock + */ + unlock: function() { + this.locked = false; + }, + + /** + * By default, all instances can be a drop target. This can be disabled by + * setting isTarget to false. + * @property isTarget + * @type boolean + */ + isTarget: true, + + /** + * The padding configured for this drag and drop object for calculating + * the drop zone intersection with this object. + * @property padding + * @type int[] An array containing the 4 padding values: [top, right, bottom, left] + */ + padding: null, + + /** + * Cached reference to the linked element + * @property _domRef + * @private + */ + _domRef: null, + + /** + * Internal typeof flag + * @property __ygDragDrop + * @private + */ + __ygDragDrop: true, + + /** + * Set to true when horizontal contraints are applied + * @property constrainX + * @type boolean + * @private + */ + constrainX: false, + + /** + * Set to true when vertical contraints are applied + * @property constrainY + * @type boolean + * @private + */ + constrainY: false, + + /** + * The left constraint + * @property minX + * @type int + * @private + */ + minX: 0, + + /** + * The right constraint + * @property maxX + * @type int + * @private + */ + maxX: 0, + + /** + * The up constraint + * @property minY + * @type int + * @private + */ + minY: 0, + + /** + * The down constraint + * @property maxY + * @type int + * @private + */ + maxY: 0, + + /** + * Maintain offsets when we resetconstraints. Set to true when you want + * the position of the element relative to its parent to stay the same + * when the page changes + * + * @property maintainOffset + * @type boolean + */ + maintainOffset: false, + + /** + * Array of pixel locations the element will snap to if we specified a + * horizontal graduation/interval. This array is generated automatically + * when you define a tick interval. + * @property xTicks + * @type int[] + */ + xTicks: null, + + /** + * Array of pixel locations the element will snap to if we specified a + * vertical graduation/interval. This array is generated automatically + * when you define a tick interval. + * @property yTicks + * @type int[] + */ + yTicks: null, + + /** + * By default the drag and drop instance will only respond to the primary + * button click (left button for a right-handed mouse). Set to true to + * allow drag and drop to start with any mouse click that is propogated + * by the browser + * @property primaryButtonOnly + * @type boolean + */ + primaryButtonOnly: true, + + /** + * The available property is false until the linked dom element is accessible. + * @property available + * @type boolean + */ + available: false, + + /** + * By default, drags can only be initiated if the mousedown occurs in the + * region the linked element is. This is done in part to work around a + * bug in some browsers that mis-report the mousedown if the previous + * mouseup happened outside of the window. This property is set to true + * if outer handles are defined. + * + * @property hasOuterHandles + * @type boolean + * @default false + */ + hasOuterHandles: false, + + /** + * Code that executes immediately before the startDrag event + * @method b4StartDrag + * @private + */ + b4StartDrag: function(x, y) { }, + + /** + * Abstract method called after a drag/drop object is clicked + * and the drag or mousedown time thresholds have beeen met. + * @method startDrag + * @param {int} X click location + * @param {int} Y click location + */ + startDrag: function(x, y) { /* override this */ }, + + /** + * Code that executes immediately before the onDrag event + * @method b4Drag + * @private + */ + b4Drag: function(e) { }, + + /** + * Abstract method called during the onMouseMove event while dragging an + * object. + * @method onDrag + * @param {Event} e the mousemove event + */ + onDrag: function(e) { /* override this */ }, + + /** + * Abstract method called when this element fist begins hovering over + * another DragDrop obj + * @method onDragEnter + * @param {Event} e the mousemove event + * @param {String|DragDrop[]} id In POINT mode, the element + * id this is hovering over. In INTERSECT mode, an array of one or more + * dragdrop items being hovered over. + */ + onDragEnter: function(e, id) { /* override this */ }, + + /** + * Code that executes immediately before the onDragOver event + * @method b4DragOver + * @private + */ + b4DragOver: function(e) { }, + + /** + * Abstract method called when this element is hovering over another + * DragDrop obj + * @method onDragOver + * @param {Event} e the mousemove event + * @param {String|DragDrop[]} id In POINT mode, the element + * id this is hovering over. In INTERSECT mode, an array of dd items + * being hovered over. + */ + onDragOver: function(e, id) { /* override this */ }, + + /** + * Code that executes immediately before the onDragOut event + * @method b4DragOut + * @private + */ + b4DragOut: function(e) { }, + + /** + * Abstract method called when we are no longer hovering over an element + * @method onDragOut + * @param {Event} e the mousemove event + * @param {String|DragDrop[]} id In POINT mode, the element + * id this was hovering over. In INTERSECT mode, an array of dd items + * that the mouse is no longer over. + */ + onDragOut: function(e, id) { /* override this */ }, + + /** + * Code that executes immediately before the onDragDrop event + * @method b4DragDrop + * @private + */ + b4DragDrop: function(e) { }, + + /** + * Abstract method called when this item is dropped on another DragDrop + * obj + * @method onDragDrop + * @param {Event} e the mouseup event + * @param {String|DragDrop[]} id In POINT mode, the element + * id this was dropped on. In INTERSECT mode, an array of dd items this + * was dropped on. + */ + onDragDrop: function(e, id) { /* override this */ }, + + /** + * Abstract method called when this item is dropped on an area with no + * drop target + * @method onInvalidDrop + * @param {Event} e the mouseup event + */ + onInvalidDrop: function(e) { /* override this */ }, + + /** + * Code that executes immediately before the endDrag event + * @method b4EndDrag + * @private + */ + b4EndDrag: function(e) { }, + + /** + * Fired when we are done dragging the object + * @method endDrag + * @param {Event} e the mouseup event + */ + endDrag: function(e) { /* override this */ }, + + /** + * Code executed immediately before the onMouseDown event + * @method b4MouseDown + * @param {Event} e the mousedown event + * @private + */ + b4MouseDown: function(e) { }, + + /** + * Event handler that fires when a drag/drop obj gets a mousedown + * @method onMouseDown + * @param {Event} e the mousedown event + */ + onMouseDown: function(e) { /* override this */ }, + + /** + * Event handler that fires when a drag/drop obj gets a mouseup + * @method onMouseUp + * @param {Event} e the mouseup event + */ + onMouseUp: function(e) { /* override this */ }, + + /** + * Override the onAvailable method to do what is needed after the initial + * position was determined. + * @method onAvailable + */ + onAvailable: function () { + }, + + /** + * Provides default constraint padding to "constrainTo" elements (defaults to {left: 0, right:0, top:0, bottom:0}). + * @type Object + */ + defaultPadding: { + left: 0, + right: 0, + top: 0, + bottom: 0 + }, + + /** + * Initializes the drag drop object's constraints to restrict movement to a certain element. + * + * Usage: +

+ var dd = new Ext.dd.DDProxy("dragDiv1", "proxytest",
+                { dragElId: "existingProxyDiv" });
+ dd.startDrag = function(){
+     this.constrainTo("parent-id");
+ };
+ 
+ * Or you can initalize it using the {@link Ext.core.Element} object: +

+ Ext.get("dragDiv1").initDDProxy("proxytest", {dragElId: "existingProxyDiv"}, {
+     startDrag : function(){
+         this.constrainTo("parent-id");
+     }
+ });
+ 
+ * @param {Mixed} constrainTo The element to constrain to. + * @param {Object/Number} pad (optional) Pad provides a way to specify "padding" of the constraints, + * and can be either a number for symmetrical padding (4 would be equal to {left:4, right:4, top:4, bottom:4}) or + * an object containing the sides to pad. For example: {right:10, bottom:10} + * @param {Boolean} inContent (optional) Constrain the draggable in the content box of the element (inside padding and borders) + */ + constrainTo : function(constrainTo, pad, inContent){ + if(Ext.isNumber(pad)){ + pad = {left: pad, right:pad, top:pad, bottom:pad}; + } + pad = pad || this.defaultPadding; + var b = Ext.get(this.getEl()).getBox(), + ce = Ext.get(constrainTo), + s = ce.getScroll(), + c, + cd = ce.dom; + if(cd == document.body){ + c = { x: s.left, y: s.top, width: Ext.core.Element.getViewWidth(), height: Ext.core.Element.getViewHeight()}; + }else{ + var xy = ce.getXY(); + c = {x : xy[0], y: xy[1], width: cd.clientWidth, height: cd.clientHeight}; + } + + + var topSpace = b.y - c.y, + leftSpace = b.x - c.x; + + this.resetConstraints(); + this.setXConstraint(leftSpace - (pad.left||0), // left + c.width - leftSpace - b.width - (pad.right||0), //right + this.xTickSize + ); + this.setYConstraint(topSpace - (pad.top||0), //top + c.height - topSpace - b.height - (pad.bottom||0), //bottom + this.yTickSize + ); + }, + + /** + * Returns a reference to the linked element + * @method getEl + * @return {HTMLElement} the html element + */ + getEl: function() { + if (!this._domRef) { + this._domRef = Ext.getDom(this.id); + } + + return this._domRef; + }, + + /** + * Returns a reference to the actual element to drag. By default this is + * the same as the html element, but it can be assigned to another + * element. An example of this can be found in Ext.dd.DDProxy + * @method getDragEl + * @return {HTMLElement} the html element + */ + getDragEl: function() { + return Ext.getDom(this.dragElId); + }, + + /** + * Sets up the DragDrop object. Must be called in the constructor of any + * Ext.dd.DragDrop subclass + * @method init + * @param id the id of the linked element + * @param {String} sGroup the group of related items + * @param {object} config configuration attributes + */ + init: function(id, sGroup, config) { + this.initTarget(id, sGroup, config); + Ext.EventManager.on(this.id, "mousedown", this.handleMouseDown, this); + // Ext.EventManager.on(this.id, "selectstart", Event.preventDefault); + }, + + /** + * Initializes Targeting functionality only... the object does not + * get a mousedown handler. + * @method initTarget + * @param id the id of the linked element + * @param {String} sGroup the group of related items + * @param {object} config configuration attributes + */ + initTarget: function(id, sGroup, config) { + + // configuration attributes + this.config = config || {}; + + // create a local reference to the drag and drop manager + this.DDMInstance = Ext.dd.DragDropManager; + // initialize the groups array + this.groups = {}; + + // assume that we have an element reference instead of an id if the + // parameter is not a string + if (typeof id !== "string") { + id = Ext.id(id); + } + + // set the id + this.id = id; + + // add to an interaction group + this.addToGroup((sGroup) ? sGroup : "default"); + + // We don't want to register this as the handle with the manager + // so we just set the id rather than calling the setter. + this.handleElId = id; + + // the linked element is the element that gets dragged by default + this.setDragElId(id); + + // by default, clicked anchors will not start drag operations. + this.invalidHandleTypes = { A: "A" }; + this.invalidHandleIds = {}; + this.invalidHandleClasses = []; + + this.applyConfig(); + + this.handleOnAvailable(); + }, + + /** + * Applies the configuration parameters that were passed into the constructor. + * This is supposed to happen at each level through the inheritance chain. So + * a DDProxy implentation will execute apply config on DDProxy, DD, and + * DragDrop in order to get all of the parameters that are available in + * each object. + * @method applyConfig + */ + applyConfig: function() { + + // configurable properties: + // padding, isTarget, maintainOffset, primaryButtonOnly + this.padding = this.config.padding || [0, 0, 0, 0]; + this.isTarget = (this.config.isTarget !== false); + this.maintainOffset = (this.config.maintainOffset); + this.primaryButtonOnly = (this.config.primaryButtonOnly !== false); + + }, + + /** + * Executed when the linked element is available + * @method handleOnAvailable + * @private + */ + handleOnAvailable: function() { + this.available = true; + this.resetConstraints(); + this.onAvailable(); + }, + + /** + * Configures the padding for the target zone in px. Effectively expands + * (or reduces) the virtual object size for targeting calculations. + * Supports css-style shorthand; if only one parameter is passed, all sides + * will have that padding, and if only two are passed, the top and bottom + * will have the first param, the left and right the second. + * @method setPadding + * @param {int} iTop Top pad + * @param {int} iRight Right pad + * @param {int} iBot Bot pad + * @param {int} iLeft Left pad + */ + setPadding: function(iTop, iRight, iBot, iLeft) { + // this.padding = [iLeft, iRight, iTop, iBot]; + if (!iRight && 0 !== iRight) { + this.padding = [iTop, iTop, iTop, iTop]; + } else if (!iBot && 0 !== iBot) { + this.padding = [iTop, iRight, iTop, iRight]; + } else { + this.padding = [iTop, iRight, iBot, iLeft]; + } + }, + + /** + * Stores the initial placement of the linked element. + * @method setInitPosition + * @param {int} diffX the X offset, default 0 + * @param {int} diffY the Y offset, default 0 + */ + setInitPosition: function(diffX, diffY) { + var el = this.getEl(); + + if (!this.DDMInstance.verifyEl(el)) { + return; + } + + var dx = diffX || 0; + var dy = diffY || 0; + + var p = Ext.core.Element.getXY( el ); + + this.initPageX = p[0] - dx; + this.initPageY = p[1] - dy; + + this.lastPageX = p[0]; + this.lastPageY = p[1]; + + this.setStartPosition(p); + }, + + /** + * Sets the start position of the element. This is set when the obj + * is initialized, the reset when a drag is started. + * @method setStartPosition + * @param pos current position (from previous lookup) + * @private + */ + setStartPosition: function(pos) { + var p = pos || Ext.core.Element.getXY( this.getEl() ); + this.deltaSetXY = null; + + this.startPageX = p[0]; + this.startPageY = p[1]; + }, + + /** + * Add this instance to a group of related drag/drop objects. All + * instances belong to at least one group, and can belong to as many + * groups as needed. + * @method addToGroup + * @param sGroup {string} the name of the group + */ + addToGroup: function(sGroup) { + this.groups[sGroup] = true; + this.DDMInstance.regDragDrop(this, sGroup); + }, + + /** + * Remove's this instance from the supplied interaction group + * @method removeFromGroup + * @param {string} sGroup The group to drop + */ + removeFromGroup: function(sGroup) { + if (this.groups[sGroup]) { + delete this.groups[sGroup]; + } + + this.DDMInstance.removeDDFromGroup(this, sGroup); + }, + + /** + * Allows you to specify that an element other than the linked element + * will be moved with the cursor during a drag + * @method setDragElId + * @param id {string} the id of the element that will be used to initiate the drag + */ + setDragElId: function(id) { + this.dragElId = id; + }, + + /** + * Allows you to specify a child of the linked element that should be + * used to initiate the drag operation. An example of this would be if + * you have a content div with text and links. Clicking anywhere in the + * content area would normally start the drag operation. Use this method + * to specify that an element inside of the content div is the element + * that starts the drag operation. + * @method setHandleElId + * @param id {string} the id of the element that will be used to + * initiate the drag. + */ + setHandleElId: function(id) { + if (typeof id !== "string") { + id = Ext.id(id); + } + this.handleElId = id; + this.DDMInstance.regHandle(this.id, id); + }, + + /** + * Allows you to set an element outside of the linked element as a drag + * handle + * @method setOuterHandleElId + * @param id the id of the element that will be used to initiate the drag + */ + setOuterHandleElId: function(id) { + if (typeof id !== "string") { + id = Ext.id(id); + } + Ext.EventManager.on(id, "mousedown", this.handleMouseDown, this); + this.setHandleElId(id); + + this.hasOuterHandles = true; + }, + + /** + * Remove all drag and drop hooks for this element + * @method unreg + */ + unreg: function() { + Ext.EventManager.un(this.id, "mousedown", this.handleMouseDown, this); + this._domRef = null; + this.DDMInstance._remove(this); + }, + + destroy : function(){ + this.unreg(); + }, + + /** + * Returns true if this instance is locked, or the drag drop mgr is locked + * (meaning that all drag/drop is disabled on the page.) + * @method isLocked + * @return {boolean} true if this obj or all drag/drop is locked, else + * false + */ + isLocked: function() { + return (this.DDMInstance.isLocked() || this.locked); + }, + + /** + * Fired when this object is clicked + * @method handleMouseDown + * @param {Event} e + * @param {Ext.dd.DragDrop} oDD the clicked dd object (this dd obj) + * @private + */ + handleMouseDown: function(e, oDD){ + if (this.primaryButtonOnly && e.button != 0) { + return; + } + + if (this.isLocked()) { + return; + } + + this.DDMInstance.refreshCache(this.groups); + + var pt = e.getPoint(); + if (!this.hasOuterHandles && !this.DDMInstance.isOverTarget(pt, this) ) { + } else { + if (this.clickValidator(e)) { + // set the initial element position + this.setStartPosition(); + this.b4MouseDown(e); + this.onMouseDown(e); + + this.DDMInstance.handleMouseDown(e, this); + + this.DDMInstance.stopEvent(e); + } else { + + + } + } + }, + + clickValidator: function(e) { + var target = e.getTarget(); + return ( this.isValidHandleChild(target) && + (this.id == this.handleElId || + this.DDMInstance.handleWasClicked(target, this.id)) ); + }, + + /** + * Allows you to specify a tag name that should not start a drag operation + * when clicked. This is designed to facilitate embedding links within a + * drag handle that do something other than start the drag. + * @method addInvalidHandleType + * @param {string} tagName the type of element to exclude + */ + addInvalidHandleType: function(tagName) { + var type = tagName.toUpperCase(); + this.invalidHandleTypes[type] = type; + }, + + /** + * Lets you to specify an element id for a child of a drag handle + * that should not initiate a drag + * @method addInvalidHandleId + * @param {string} id the element id of the element you wish to ignore + */ + addInvalidHandleId: function(id) { + if (typeof id !== "string") { + id = Ext.id(id); + } + this.invalidHandleIds[id] = id; + }, + + /** + * Lets you specify a css class of elements that will not initiate a drag + * @method addInvalidHandleClass + * @param {string} cssClass the class of the elements you wish to ignore + */ + addInvalidHandleClass: function(cssClass) { + this.invalidHandleClasses.push(cssClass); + }, + + /** + * Unsets an excluded tag name set by addInvalidHandleType + * @method removeInvalidHandleType + * @param {string} tagName the type of element to unexclude + */ + removeInvalidHandleType: function(tagName) { + var type = tagName.toUpperCase(); + // this.invalidHandleTypes[type] = null; + delete this.invalidHandleTypes[type]; + }, + + /** + * Unsets an invalid handle id + * @method removeInvalidHandleId + * @param {string} id the id of the element to re-enable + */ + removeInvalidHandleId: function(id) { + if (typeof id !== "string") { + id = Ext.id(id); + } + delete this.invalidHandleIds[id]; + }, + + /** + * Unsets an invalid css class + * @method removeInvalidHandleClass + * @param {string} cssClass the class of the element(s) you wish to + * re-enable + */ + removeInvalidHandleClass: function(cssClass) { + for (var i=0, len=this.invalidHandleClasses.length; i= this.minX; i = i - iTickSize) { + if (!tickMap[i]) { + this.xTicks[this.xTicks.length] = i; + tickMap[i] = true; + } + } + + for (i = this.initPageX; i <= this.maxX; i = i + iTickSize) { + if (!tickMap[i]) { + this.xTicks[this.xTicks.length] = i; + tickMap[i] = true; + } + } + + Ext.Array.sort(this.xTicks, this.DDMInstance.numericSort); + }, + + /** + * Create the array of vertical tick marks if an interval was specified in + * setYConstraint(). + * @method setYTicks + * @private + */ + setYTicks: function(iStartY, iTickSize) { + this.yTicks = []; + this.yTickSize = iTickSize; + + var tickMap = {}; + + for (var i = this.initPageY; i >= this.minY; i = i - iTickSize) { + if (!tickMap[i]) { + this.yTicks[this.yTicks.length] = i; + tickMap[i] = true; + } + } + + for (i = this.initPageY; i <= this.maxY; i = i + iTickSize) { + if (!tickMap[i]) { + this.yTicks[this.yTicks.length] = i; + tickMap[i] = true; + } + } + + Ext.Array.sort(this.yTicks, this.DDMInstance.numericSort); + }, + + /** + * By default, the element can be dragged any place on the screen. Use + * this method to limit the horizontal travel of the element. Pass in + * 0,0 for the parameters if you want to lock the drag to the y axis. + * @method setXConstraint + * @param {int} iLeft the number of pixels the element can move to the left + * @param {int} iRight the number of pixels the element can move to the + * right + * @param {int} iTickSize optional parameter for specifying that the + * element + * should move iTickSize pixels at a time. + */ + setXConstraint: function(iLeft, iRight, iTickSize) { + this.leftConstraint = iLeft; + this.rightConstraint = iRight; + + this.minX = this.initPageX - iLeft; + this.maxX = this.initPageX + iRight; + if (iTickSize) { this.setXTicks(this.initPageX, iTickSize); } + + this.constrainX = true; + }, + + /** + * Clears any constraints applied to this instance. Also clears ticks + * since they can't exist independent of a constraint at this time. + * @method clearConstraints + */ + clearConstraints: function() { + this.constrainX = false; + this.constrainY = false; + this.clearTicks(); + }, + + /** + * Clears any tick interval defined for this instance + * @method clearTicks + */ + clearTicks: function() { + this.xTicks = null; + this.yTicks = null; + this.xTickSize = 0; + this.yTickSize = 0; + }, + + /** + * By default, the element can be dragged any place on the screen. Set + * this to limit the vertical travel of the element. Pass in 0,0 for the + * parameters if you want to lock the drag to the x axis. + * @method setYConstraint + * @param {int} iUp the number of pixels the element can move up + * @param {int} iDown the number of pixels the element can move down + * @param {int} iTickSize optional parameter for specifying that the + * element should move iTickSize pixels at a time. + */ + setYConstraint: function(iUp, iDown, iTickSize) { + this.topConstraint = iUp; + this.bottomConstraint = iDown; + + this.minY = this.initPageY - iUp; + this.maxY = this.initPageY + iDown; + if (iTickSize) { this.setYTicks(this.initPageY, iTickSize); } + + this.constrainY = true; + + }, + + /** + * resetConstraints must be called if you manually reposition a dd element. + * @method resetConstraints + * @param {boolean} maintainOffset + */ + resetConstraints: function() { + // Maintain offsets if necessary + if (this.initPageX || this.initPageX === 0) { + // figure out how much this thing has moved + var dx = (this.maintainOffset) ? this.lastPageX - this.initPageX : 0; + var dy = (this.maintainOffset) ? this.lastPageY - this.initPageY : 0; + + this.setInitPosition(dx, dy); + + // This is the first time we have detected the element's position + } else { + this.setInitPosition(); + } + + if (this.constrainX) { + this.setXConstraint( this.leftConstraint, + this.rightConstraint, + this.xTickSize ); + } + + if (this.constrainY) { + this.setYConstraint( this.topConstraint, + this.bottomConstraint, + this.yTickSize ); + } + }, + + /** + * Normally the drag element is moved pixel by pixel, but we can specify + * that it move a number of pixels at a time. This method resolves the + * location when we have it set up like this. + * @method getTick + * @param {int} val where we want to place the object + * @param {int[]} tickArray sorted array of valid points + * @return {int} the closest tick + * @private + */ + getTick: function(val, tickArray) { + if (!tickArray) { + // If tick interval is not defined, it is effectively 1 pixel, + // so we return the value passed to us. + return val; + } else if (tickArray[0] >= val) { + // The value is lower than the first tick, so we return the first + // tick. + return tickArray[0]; + } else { + for (var i=0, len=tickArray.length; i= val) { + var diff1 = val - tickArray[i]; + var diff2 = tickArray[next] - val; + return (diff2 > diff1) ? tickArray[i] : tickArray[next]; + } + } + + // The value is larger than the last tick, so we return the last + // tick. + return tickArray[tickArray.length - 1]; + } + }, + + /** + * toString method + * @method toString + * @return {string} string representation of the dd obj + */ + toString: function() { + return ("DragDrop " + this.id); + } + +}); +/* + * This is a derivative of the similarly named class in the YUI Library. + * The original license: + * Copyright (c) 2006, Yahoo! Inc. All rights reserved. + * Code licensed under the BSD License: + * http://developer.yahoo.net/yui/license.txt + */ + + +/** + * @class Ext.dd.DD + * A DragDrop implementation where the linked element follows the + * mouse cursor during a drag. + * @extends Ext.dd.DragDrop + * @constructor + * @param {String} id the id of the linked element + * @param {String} sGroup the group of related DragDrop items + * @param {object} config an object containing configurable attributes + * Valid properties for DD: + * scroll + */ + +Ext.define('Ext.dd.DD', { + extend: 'Ext.dd.DragDrop', + requires: ['Ext.dd.DragDropManager'], + constructor: function(id, sGroup, config) { + if (id) { + this.init(id, sGroup, config); + } + }, + + /** + * When set to true, the utility automatically tries to scroll the browser + * window when a drag and drop element is dragged near the viewport boundary. + * Defaults to true. + * @property scroll + * @type boolean + */ + scroll: true, + + /** + * Sets the pointer offset to the distance between the linked element's top + * left corner and the location the element was clicked + * @method autoOffset + * @param {int} iPageX the X coordinate of the click + * @param {int} iPageY the Y coordinate of the click + */ + autoOffset: function(iPageX, iPageY) { + var x = iPageX - this.startPageX; + var y = iPageY - this.startPageY; + this.setDelta(x, y); + }, + + /** + * Sets the pointer offset. You can call this directly to force the + * offset to be in a particular location (e.g., pass in 0,0 to set it + * to the center of the object) + * @method setDelta + * @param {int} iDeltaX the distance from the left + * @param {int} iDeltaY the distance from the top + */ + setDelta: function(iDeltaX, iDeltaY) { + this.deltaX = iDeltaX; + this.deltaY = iDeltaY; + }, + + /** + * Sets the drag element to the location of the mousedown or click event, + * maintaining the cursor location relative to the location on the element + * that was clicked. Override this if you want to place the element in a + * location other than where the cursor is. + * @method setDragElPos + * @param {int} iPageX the X coordinate of the mousedown or drag event + * @param {int} iPageY the Y coordinate of the mousedown or drag event + */ + setDragElPos: function(iPageX, iPageY) { + // the first time we do this, we are going to check to make sure + // the element has css positioning + + var el = this.getDragEl(); + this.alignElWithMouse(el, iPageX, iPageY); + }, + + /** + * Sets the element to the location of the mousedown or click event, + * maintaining the cursor location relative to the location on the element + * that was clicked. Override this if you want to place the element in a + * location other than where the cursor is. + * @method alignElWithMouse + * @param {HTMLElement} el the element to move + * @param {int} iPageX the X coordinate of the mousedown or drag event + * @param {int} iPageY the Y coordinate of the mousedown or drag event + */ + alignElWithMouse: function(el, iPageX, iPageY) { + var oCoord = this.getTargetCoord(iPageX, iPageY), + fly = el.dom ? el : Ext.fly(el, '_dd'), + elSize = fly.getSize(), + EL = Ext.core.Element, + vpSize; + + if (!this.deltaSetXY) { + vpSize = this.cachedViewportSize = { width: EL.getDocumentWidth(), height: EL.getDocumentHeight() }; + var aCoord = [ + Math.max(0, Math.min(oCoord.x, vpSize.width - elSize.width)), + Math.max(0, Math.min(oCoord.y, vpSize.height - elSize.height)) + ]; + fly.setXY(aCoord); + var newLeft = fly.getLeft(true); + var newTop = fly.getTop(true); + this.deltaSetXY = [newLeft - oCoord.x, newTop - oCoord.y]; + } else { + vpSize = this.cachedViewportSize; + fly.setLeftTop( + Math.max(0, Math.min(oCoord.x + this.deltaSetXY[0], vpSize.width - elSize.width)), + Math.max(0, Math.min(oCoord.y + this.deltaSetXY[1], vpSize.height - elSize.height)) + ); + } + + this.cachePosition(oCoord.x, oCoord.y); + this.autoScroll(oCoord.x, oCoord.y, el.offsetHeight, el.offsetWidth); + return oCoord; + }, + + /** + * Saves the most recent position so that we can reset the constraints and + * tick marks on-demand. We need to know this so that we can calculate the + * number of pixels the element is offset from its original position. + * @method cachePosition + * @param iPageX the current x position (optional, this just makes it so we + * don't have to look it up again) + * @param iPageY the current y position (optional, this just makes it so we + * don't have to look it up again) + */ + cachePosition: function(iPageX, iPageY) { + if (iPageX) { + this.lastPageX = iPageX; + this.lastPageY = iPageY; + } else { + var aCoord = Ext.core.Element.getXY(this.getEl()); + this.lastPageX = aCoord[0]; + this.lastPageY = aCoord[1]; + } + }, + + /** + * Auto-scroll the window if the dragged object has been moved beyond the + * visible window boundary. + * @method autoScroll + * @param {int} x the drag element's x position + * @param {int} y the drag element's y position + * @param {int} h the height of the drag element + * @param {int} w the width of the drag element + * @private + */ + autoScroll: function(x, y, h, w) { + + if (this.scroll) { + // The client height + var clientH = Ext.core.Element.getViewHeight(); + + // The client width + var clientW = Ext.core.Element.getViewWidth(); + + // The amt scrolled down + var st = this.DDMInstance.getScrollTop(); + + // The amt scrolled right + var sl = this.DDMInstance.getScrollLeft(); + + // Location of the bottom of the element + var bot = h + y; + + // Location of the right of the element + var right = w + x; + + // The distance from the cursor to the bottom of the visible area, + // adjusted so that we don't scroll if the cursor is beyond the + // element drag constraints + var toBot = (clientH + st - y - this.deltaY); + + // The distance from the cursor to the right of the visible area + var toRight = (clientW + sl - x - this.deltaX); + + + // How close to the edge the cursor must be before we scroll + // var thresh = (document.all) ? 100 : 40; + var thresh = 40; + + // How many pixels to scroll per autoscroll op. This helps to reduce + // clunky scrolling. IE is more sensitive about this ... it needs this + // value to be higher. + var scrAmt = (document.all) ? 80 : 30; + + // Scroll down if we are near the bottom of the visible page and the + // obj extends below the crease + if ( bot > clientH && toBot < thresh ) { + window.scrollTo(sl, st + scrAmt); + } + + // Scroll up if the window is scrolled down and the top of the object + // goes above the top border + if ( y < st && st > 0 && y - st < thresh ) { + window.scrollTo(sl, st - scrAmt); + } + + // Scroll right if the obj is beyond the right border and the cursor is + // near the border. + if ( right > clientW && toRight < thresh ) { + window.scrollTo(sl + scrAmt, st); + } + + // Scroll left if the window has been scrolled to the right and the obj + // extends past the left border + if ( x < sl && sl > 0 && x - sl < thresh ) { + window.scrollTo(sl - scrAmt, st); + } + } + }, + + /** + * Finds the location the element should be placed if we want to move + * it to where the mouse location less the click offset would place us. + * @method getTargetCoord + * @param {int} iPageX the X coordinate of the click + * @param {int} iPageY the Y coordinate of the click + * @return an object that contains the coordinates (Object.x and Object.y) + * @private + */ + getTargetCoord: function(iPageX, iPageY) { + var x = iPageX - this.deltaX; + var y = iPageY - this.deltaY; + + if (this.constrainX) { + if (x < this.minX) { + x = this.minX; + } + if (x > this.maxX) { + x = this.maxX; + } + } + + if (this.constrainY) { + if (y < this.minY) { + y = this.minY; + } + if (y > this.maxY) { + y = this.maxY; + } + } + + x = this.getTick(x, this.xTicks); + y = this.getTick(y, this.yTicks); + + + return {x: x, y: y}; + }, + + /** + * Sets up config options specific to this class. Overrides + * Ext.dd.DragDrop, but all versions of this method through the + * inheritance chain are called + */ + applyConfig: function() { + this.callParent(); + this.scroll = (this.config.scroll !== false); + }, + + /** + * Event that fires prior to the onMouseDown event. Overrides + * Ext.dd.DragDrop. + */ + b4MouseDown: function(e) { + // this.resetConstraints(); + this.autoOffset(e.getPageX(), e.getPageY()); + }, + + /** + * Event that fires prior to the onDrag event. Overrides + * Ext.dd.DragDrop. + */ + b4Drag: function(e) { + this.setDragElPos(e.getPageX(), e.getPageY()); + }, + + toString: function() { + return ("DD " + this.id); + } + + ////////////////////////////////////////////////////////////////////////// + // Debugging ygDragDrop events that can be overridden + ////////////////////////////////////////////////////////////////////////// + /* + startDrag: function(x, y) { + }, + + onDrag: function(e) { + }, + + onDragEnter: function(e, id) { + }, + + onDragOver: function(e, id) { + }, + + onDragOut: function(e, id) { + }, + + onDragDrop: function(e, id) { + }, + + endDrag: function(e) { + } + + */ + +}); + +/* + * This is a derivative of the similarly named class in the YUI Library. + * The original license: + * Copyright (c) 2006, Yahoo! Inc. All rights reserved. + * Code licensed under the BSD License: + * http://developer.yahoo.net/yui/license.txt + */ + +/** + * @class Ext.dd.DDProxy + * A DragDrop implementation that inserts an empty, bordered div into + * the document that follows the cursor during drag operations. At the time of + * the click, the frame div is resized to the dimensions of the linked html + * element, and moved to the exact location of the linked element. + * + * References to the "frame" element refer to the single proxy element that + * was created to be dragged in place of all DDProxy elements on the + * page. + * + * @extends Ext.dd.DD + * @constructor + * @param {String} id the id of the linked html element + * @param {String} sGroup the group of related DragDrop objects + * @param {object} config an object containing configurable attributes + * Valid properties for DDProxy in addition to those in DragDrop: + * resizeFrame, centerFrame, dragElId + */ +Ext.define('Ext.dd.DDProxy', { + extend: 'Ext.dd.DD', + + statics: { + /** + * The default drag frame div id + * @property Ext.dd.DDProxy.dragElId + * @type String + * @static + */ + dragElId: "ygddfdiv" + }, + + constructor: function(id, sGroup, config) { + if (id) { + this.init(id, sGroup, config); + this.initFrame(); + } + }, + + /** + * By default we resize the drag frame to be the same size as the element + * we want to drag (this is to get the frame effect). We can turn it off + * if we want a different behavior. + * @property resizeFrame + * @type boolean + */ + resizeFrame: true, + + /** + * By default the frame is positioned exactly where the drag element is, so + * we use the cursor offset provided by Ext.dd.DD. Another option that works only if + * you do not have constraints on the obj is to have the drag frame centered + * around the cursor. Set centerFrame to true for this effect. + * @property centerFrame + * @type boolean + */ + centerFrame: false, + + /** + * Creates the proxy element if it does not yet exist + * @method createFrame + */ + createFrame: function() { + var self = this; + var body = document.body; + + if (!body || !body.firstChild) { + setTimeout( function() { self.createFrame(); }, 50 ); + return; + } + + var div = this.getDragEl(); + + if (!div) { + div = document.createElement("div"); + div.id = this.dragElId; + var s = div.style; + + s.position = "absolute"; + s.visibility = "hidden"; + s.cursor = "move"; + s.border = "2px solid #aaa"; + s.zIndex = 999; + + // appendChild can blow up IE if invoked prior to the window load event + // while rendering a table. It is possible there are other scenarios + // that would cause this to happen as well. + body.insertBefore(div, body.firstChild); + } + }, + + /** + * Initialization for the drag frame element. Must be called in the + * constructor of all subclasses + * @method initFrame + */ + initFrame: function() { + this.createFrame(); + }, + + applyConfig: function() { + this.callParent(); + + this.resizeFrame = (this.config.resizeFrame !== false); + this.centerFrame = (this.config.centerFrame); + this.setDragElId(this.config.dragElId || Ext.dd.DDProxy.dragElId); + }, + + /** + * Resizes the drag frame to the dimensions of the clicked object, positions + * it over the object, and finally displays it + * @method showFrame + * @param {int} iPageX X click position + * @param {int} iPageY Y click position + * @private + */ + showFrame: function(iPageX, iPageY) { + var el = this.getEl(); + var dragEl = this.getDragEl(); + var s = dragEl.style; + + this._resizeProxy(); + + if (this.centerFrame) { + this.setDelta( Math.round(parseInt(s.width, 10)/2), + Math.round(parseInt(s.height, 10)/2) ); + } + + this.setDragElPos(iPageX, iPageY); + + Ext.fly(dragEl).show(); + }, + + /** + * The proxy is automatically resized to the dimensions of the linked + * element when a drag is initiated, unless resizeFrame is set to false + * @method _resizeProxy + * @private + */ + _resizeProxy: function() { + if (this.resizeFrame) { + var el = this.getEl(); + Ext.fly(this.getDragEl()).setSize(el.offsetWidth, el.offsetHeight); + } + }, + + // overrides Ext.dd.DragDrop + b4MouseDown: function(e) { + var x = e.getPageX(); + var y = e.getPageY(); + this.autoOffset(x, y); + this.setDragElPos(x, y); + }, + + // overrides Ext.dd.DragDrop + b4StartDrag: function(x, y) { + // show the drag frame + this.showFrame(x, y); + }, + + // overrides Ext.dd.DragDrop + b4EndDrag: function(e) { + Ext.fly(this.getDragEl()).hide(); + }, + + // overrides Ext.dd.DragDrop + // By default we try to move the element to the last location of the frame. + // This is so that the default behavior mirrors that of Ext.dd.DD. + endDrag: function(e) { + + var lel = this.getEl(); + var del = this.getDragEl(); + + // Show the drag frame briefly so we can get its position + del.style.visibility = ""; + + this.beforeMove(); + // Hide the linked element before the move to get around a Safari + // rendering bug. + lel.style.visibility = "hidden"; + Ext.dd.DDM.moveToEl(lel, del); + del.style.visibility = "hidden"; + lel.style.visibility = ""; + + this.afterDrag(); + }, + + beforeMove : function(){ + + }, + + afterDrag : function(){ + + }, + + toString: function() { + return ("DDProxy " + this.id); + } + +}); + +/** + * @class Ext.dd.DragSource + * @extends Ext.dd.DDProxy + * A simple class that provides the basic implementation needed to make any element draggable. + * @constructor + * @param {Mixed} el The container element + * @param {Object} config + */ +Ext.define('Ext.dd.DragSource', { + extend: 'Ext.dd.DDProxy', + requires: [ + 'Ext.dd.StatusProxy', + 'Ext.dd.DragDropManager' + ], + + /** + * @cfg {String} ddGroup + * A named drag drop group to which this object belongs. If a group is specified, then this object will only + * interact with other drag drop objects in the same group (defaults to undefined). + */ + + /** + * @cfg {String} dropAllowed + * The CSS class returned to the drag source when drop is allowed (defaults to "x-dd-drop-ok"). + */ + + dropAllowed : Ext.baseCSSPrefix + 'dd-drop-ok', + /** + * @cfg {String} dropNotAllowed + * The CSS class returned to the drag source when drop is not allowed (defaults to "x-dd-drop-nodrop"). + */ + dropNotAllowed : Ext.baseCSSPrefix + 'dd-drop-nodrop', + + /** + * @cfg {Boolean} animRepair + * Defaults to true. If true, animates the proxy element back to the position of the handle element used to trigger the drag. + */ + animRepair: true, + + /** + * @cfg {String} repairHighlightColor The color to use when visually highlighting the drag source in the afterRepair + * method after a failed drop (defaults to 'c3daf9' - light blue). The color must be a 6 digit hex value, without + * a preceding '#'. + */ + repairHighlightColor: 'c3daf9', + + constructor: function(el, config) { + this.el = Ext.get(el); + if(!this.dragData){ + this.dragData = {}; + } + + Ext.apply(this, config); + + if(!this.proxy){ + this.proxy = Ext.create('Ext.dd.StatusProxy', { + animRepair: this.animRepair + }); + } + this.callParent([this.el.dom, this.ddGroup || this.group, + {dragElId : this.proxy.id, resizeFrame: false, isTarget: false, scroll: this.scroll === true}]); + + this.dragging = false; + }, + + /** + * Returns the data object associated with this drag source + * @return {Object} data An object containing arbitrary data + */ + getDragData : function(e){ + return this.dragData; + }, + + // private + onDragEnter : function(e, id){ + var target = Ext.dd.DragDropManager.getDDById(id); + this.cachedTarget = target; + if (this.beforeDragEnter(target, e, id) !== false) { + if (target.isNotifyTarget) { + var status = target.notifyEnter(this, e, this.dragData); + this.proxy.setStatus(status); + } else { + this.proxy.setStatus(this.dropAllowed); + } + + if (this.afterDragEnter) { + /** + * An empty function by default, but provided so that you can perform a custom action + * when the dragged item enters the drop target by providing an implementation. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dragged element + * @method afterDragEnter + */ + this.afterDragEnter(target, e, id); + } + } + }, + + /** + * An empty function by default, but provided so that you can perform a custom action + * before the dragged item enters the drop target and optionally cancel the onDragEnter. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dragged element + * @return {Boolean} isValid True if the drag event is valid, else false to cancel + */ + beforeDragEnter: function(target, e, id) { + return true; + }, + + // private + alignElWithMouse: function() { + this.callParent(arguments); + this.proxy.sync(); + }, + + // private + onDragOver: function(e, id) { + var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id); + if (this.beforeDragOver(target, e, id) !== false) { + if(target.isNotifyTarget){ + var status = target.notifyOver(this, e, this.dragData); + this.proxy.setStatus(status); + } + + if (this.afterDragOver) { + /** + * An empty function by default, but provided so that you can perform a custom action + * while the dragged item is over the drop target by providing an implementation. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dragged element + * @method afterDragOver + */ + this.afterDragOver(target, e, id); + } + } + }, + + /** + * An empty function by default, but provided so that you can perform a custom action + * while the dragged item is over the drop target and optionally cancel the onDragOver. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dragged element + * @return {Boolean} isValid True if the drag event is valid, else false to cancel + */ + beforeDragOver: function(target, e, id) { + return true; + }, + + // private + onDragOut: function(e, id) { + var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id); + if (this.beforeDragOut(target, e, id) !== false) { + if (target.isNotifyTarget) { + target.notifyOut(this, e, this.dragData); + } + this.proxy.reset(); + if (this.afterDragOut) { + /** + * An empty function by default, but provided so that you can perform a custom action + * after the dragged item is dragged out of the target without dropping. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dragged element + * @method afterDragOut + */ + this.afterDragOut(target, e, id); + } + } + this.cachedTarget = null; + }, + + /** + * An empty function by default, but provided so that you can perform a custom action before the dragged + * item is dragged out of the target without dropping, and optionally cancel the onDragOut. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dragged element + * @return {Boolean} isValid True if the drag event is valid, else false to cancel + */ + beforeDragOut: function(target, e, id){ + return true; + }, + + // private + onDragDrop: function(e, id){ + var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id); + if (this.beforeDragDrop(target, e, id) !== false) { + if (target.isNotifyTarget) { + if (target.notifyDrop(this, e, this.dragData) !== false) { // valid drop? + this.onValidDrop(target, e, id); + } else { + this.onInvalidDrop(target, e, id); + } + } else { + this.onValidDrop(target, e, id); + } + + if (this.afterDragDrop) { + /** + * An empty function by default, but provided so that you can perform a custom action + * after a valid drag drop has occurred by providing an implementation. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dropped element + * @method afterDragDrop + */ + this.afterDragDrop(target, e, id); + } + } + delete this.cachedTarget; + }, + + /** + * An empty function by default, but provided so that you can perform a custom action before the dragged + * item is dropped onto the target and optionally cancel the onDragDrop. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dragged element + * @return {Boolean} isValid True if the drag drop event is valid, else false to cancel + */ + beforeDragDrop: function(target, e, id){ + return true; + }, + + // private + onValidDrop: function(target, e, id){ + this.hideProxy(); + if(this.afterValidDrop){ + /** + * An empty function by default, but provided so that you can perform a custom action + * after a valid drop has occurred by providing an implementation. + * @param {Object} target The target DD + * @param {Event} e The event object + * @param {String} id The id of the dropped element + * @method afterInvalidDrop + */ + this.afterValidDrop(target, e, id); + } + }, + + // private + getRepairXY: function(e, data){ + return this.el.getXY(); + }, + + // private + onInvalidDrop: function(target, e, id) { + this.beforeInvalidDrop(target, e, id); + if (this.cachedTarget) { + if(this.cachedTarget.isNotifyTarget){ + this.cachedTarget.notifyOut(this, e, this.dragData); + } + this.cacheTarget = null; + } + this.proxy.repair(this.getRepairXY(e, this.dragData), this.afterRepair, this); + + if (this.afterInvalidDrop) { + /** + * An empty function by default, but provided so that you can perform a custom action + * after an invalid drop has occurred by providing an implementation. + * @param {Event} e The event object + * @param {String} id The id of the dropped element + * @method afterInvalidDrop + */ + this.afterInvalidDrop(e, id); + } + }, + + // private + afterRepair: function() { + var me = this; + if (Ext.enableFx) { + me.el.highlight(me.repairHighlightColor); + } + me.dragging = false; + }, + + /** + * An empty function by default, but provided so that you can perform a custom action after an invalid + * drop has occurred. + * @param {Ext.dd.DragDrop} target The drop target + * @param {Event} e The event object + * @param {String} id The id of the dragged element + * @return {Boolean} isValid True if the invalid drop should proceed, else false to cancel + */ + beforeInvalidDrop: function(target, e, id) { + return true; + }, + + // private + handleMouseDown: function(e) { + if (this.dragging) { + return; + } + var data = this.getDragData(e); + if (data && this.onBeforeDrag(data, e) !== false) { + this.dragData = data; + this.proxy.stop(); + this.callParent(arguments); + } + }, + + /** + * An empty function by default, but provided so that you can perform a custom action before the initial + * drag event begins and optionally cancel it. + * @param {Object} data An object containing arbitrary data to be shared with drop targets + * @param {Event} e The event object + * @return {Boolean} isValid True if the drag event is valid, else false to cancel + */ + onBeforeDrag: function(data, e){ + return true; + }, + + /** + * An empty function by default, but provided so that you can perform a custom action once the initial + * drag event has begun. The drag cannot be canceled from this function. + * @param {Number} x The x position of the click on the dragged object + * @param {Number} y The y position of the click on the dragged object + */ + onStartDrag: Ext.emptyFn, + + // private override + startDrag: function(x, y) { + this.proxy.reset(); + this.dragging = true; + this.proxy.update(""); + this.onInitDrag(x, y); + this.proxy.show(); + }, + + // private + onInitDrag: function(x, y) { + var clone = this.el.dom.cloneNode(true); + clone.id = Ext.id(); // prevent duplicate ids + this.proxy.update(clone); + this.onStartDrag(x, y); + return true; + }, + + /** + * Returns the drag source's underlying {@link Ext.dd.StatusProxy} + * @return {Ext.dd.StatusProxy} proxy The StatusProxy + */ + getProxy: function() { + return this.proxy; + }, + + /** + * Hides the drag source's {@link Ext.dd.StatusProxy} + */ + hideProxy: function() { + this.proxy.hide(); + this.proxy.reset(true); + this.dragging = false; + }, + + // private + triggerCacheRefresh: function() { + Ext.dd.DDM.refreshCache(this.groups); + }, + + // private - override to prevent hiding + b4EndDrag: function(e) { + }, + + // private - override to prevent moving + endDrag : function(e){ + this.onEndDrag(this.dragData, e); + }, + + // private + onEndDrag : function(data, e){ + }, + + // private - pin to cursor + autoOffset : function(x, y) { + this.setDelta(-12, -20); + }, + + destroy: function(){ + this.callParent(); + Ext.destroy(this.proxy); + } +}); + +// private - DD implementation for Panels +Ext.define('Ext.panel.DD', { + extend: 'Ext.dd.DragSource', + requires: ['Ext.panel.Proxy'], + + constructor : function(panel, cfg){ + this.panel = panel; + this.dragData = {panel: panel}; + this.proxy = Ext.create('Ext.panel.Proxy', panel, cfg); + + this.callParent([panel.el, cfg]); + + Ext.defer(function() { + var header = panel.header, + el = panel.body; + + if(header){ + this.setHandleElId(header.id); + el = header.el; + } + el.setStyle('cursor', 'move'); + this.scroll = false; + }, 200, this); + }, + + showFrame: Ext.emptyFn, + startDrag: Ext.emptyFn, + b4StartDrag: function(x, y) { + this.proxy.show(); + }, + b4MouseDown: function(e) { + var x = e.getPageX(), + y = e.getPageY(); + this.autoOffset(x, y); + }, + onInitDrag : function(x, y){ + this.onStartDrag(x, y); + return true; + }, + createFrame : Ext.emptyFn, + getDragEl : function(e){ + return this.proxy.ghost.el.dom; + }, + endDrag : function(e){ + this.proxy.hide(); + this.panel.saveState(); + }, + + autoOffset : function(x, y) { + x -= this.startPageX; + y -= this.startPageY; + this.setDelta(x, y); + } +}); + +/** + * @class Ext.layout.component.Dock + * @extends Ext.layout.component.AbstractDock + * @private + */ +Ext.define('Ext.layout.component.Dock', { + + /* Begin Definitions */ + + alias: ['layout.dock'], + + extend: 'Ext.layout.component.AbstractDock' + + /* End Definitions */ + +}); +/** + * @class Ext.panel.Panel + * @extends Ext.panel.AbstractPanel + *

Panel is a container that has specific functionality and structural components that make + * it the perfect building block for application-oriented user interfaces.

+ *

Panels are, by virtue of their inheritance from {@link Ext.container.Container}, capable + * of being configured with a {@link Ext.container.Container#layout layout}, and containing child Components.

+ *

When either specifying child {@link Ext.Component#items items} of a Panel, or dynamically {@link Ext.container.Container#add adding} Components + * to a Panel, remember to consider how you wish the Panel to arrange those child elements, and whether + * those child elements need to be sized using one of Ext's built-in {@link Ext.container.Container#layout layout} schemes. By + * default, Panels use the {@link Ext.layout.container.Auto Auto} scheme. This simply renders + * child components, appending them one after the other inside the Container, and does not apply any sizing + * at all.

+ * {@img Ext.panel.Panel/panel.png Panel components} + *

A Panel may also contain {@link #bbar bottom} and {@link #tbar top} toolbars, along with separate + * {@link #header}, {@link #footer} and {@link #body} sections (see {@link #frame} for additional + * information).

+ *

Panel also provides built-in {@link #collapsible collapsible, expandable} and {@link #closable} behavior. + * Panels can be easily dropped into any {@link Ext.container.Container Container} or layout, and the + * layout and rendering pipeline is {@link Ext.container.Container#add completely managed by the framework}.

+ *

Note: By default, the {@link #closable close} header tool destroys the Panel resulting in removal of the Panel + * and the destruction of any descendant Components. This makes the Panel object, and all its descendants unusable. To enable the close + * tool to simply hide a Panel for later re-use, configure the Panel with {@link #closeAction closeAction: 'hide'}.

+ *

Usually, Panels are used as constituents within an application, in which case, they would be used as child items of Containers, + * and would themselves use Ext.Components as child {@link #items}. However to illustrate simply rendering a Panel into the document, + * here's how to do it:


+Ext.create('Ext.panel.Panel', {
+    title: 'Hello',
+    width: 200,
+    html: '<p>World!</p>',
+    renderTo: document.body
+});
+

+ *

A more realistic scenario is a Panel created to house input fields which will not be rendered, but used as a constituent part of a Container:


+var filterPanel = Ext.create('Ext.panel.Panel', {
+    bodyPadding: 5,  // Don't want content to crunch against the borders
+    title: 'Filters',
+    items: [{
+        xtype: 'datefield',
+        fieldLabel: 'Start date'
+    }, {
+        xtype: 'datefield',
+        fieldLabel: 'End date'
+    }]
+});
+

+ *

Note that the Panel above is not configured to render into the document, nor is it configured with a size or position. In a real world scenario, + * the Container into which the Panel is added will use a {@link #layout} to render, size and position its child Components.

+ *

Panels will often use specific {@link #layout}s to provide an application with shape and structure by containing and arranging child + * Components:


+var resultsPanel = Ext.create('Ext.panel.Panel', {
+    title: 'Results',
+    width: 600,
+    height: 400,
+    renderTo: document.body,
+    layout: {
+        type: 'vbox',       // Arrange child items vertically
+        align: 'stretch',    // Each takes up full width
+        padding: 5
+    },
+    items: [{               // Results grid specified as a config object with an xtype of 'grid'
+        xtype: 'grid',
+        columns: [{header: 'Column One'}],            // One header just for show. There's no data,
+        store: Ext.create('Ext.data.ArrayStore', {}), // A dummy empty data store
+        flex: 1                                       // Use 1/3 of Container's height (hint to Box layout)
+    }, {
+        xtype: 'splitter'   // A splitter between the two child items
+    }, {                    // Details Panel specified as a config object (no xtype defaults to 'panel').
+        title: 'Details',
+        bodyPadding: 5,
+        items: [{
+            fieldLabel: 'Data item',
+            xtype: 'textfield'
+        }], // An array of form fields
+        flex: 2             // Use 2/3 of Container's height (hint to Box layout)
+    }]
+});
+
+ * The example illustrates one possible method of displaying search results. The Panel contains a grid with the resulting data arranged + * in rows. Each selected row may be displayed in detail in the Panel below. The {@link Ext.layout.container.VBox vbox} layout is used + * to arrange the two vertically. It is configured to stretch child items horizontally to full width. Child items may either be configured + * with a numeric height, or with a flex value to distribute available space proportionately.

+ *

This Panel itself may be a child item of, for exaple, a {@link Ext.tab.Panel} which will size its child items to fit within its + * content area.

+ *

Using these techniques, as long as the layout is chosen and configured correctly, an application may have any level of + * nested containment, all dynamically sized according to configuration, the user's preference and available browser size.

+ * @constructor + * @param {Object} config The config object + * @xtype panel + */ +Ext.define('Ext.panel.Panel', { + extend: 'Ext.panel.AbstractPanel', + requires: [ + 'Ext.panel.Header', + 'Ext.fx.Anim', + 'Ext.util.KeyMap', + 'Ext.panel.DD', + 'Ext.XTemplate', + 'Ext.layout.component.Dock' + ], + alias: 'widget.panel', + alternateClassName: 'Ext.Panel', + + /** + * @cfg {String} collapsedCls + * A CSS class to add to the panel's element after it has been collapsed (defaults to + * 'collapsed'). + */ + collapsedCls: 'collapsed', + + /** + * @cfg {Boolean} animCollapse + * true to animate the transition when the panel is collapsed, false to skip the + * animation (defaults to true if the {@link Ext.fx.Anim} class is available, otherwise false). + * May also be specified as the animation duration in milliseconds. + */ + animCollapse: Ext.enableFx, + + /** + * @cfg {Number} minButtonWidth + * Minimum width of all footer toolbar buttons in pixels (defaults to 75). If set, this will + * be used as the default value for the {@link Ext.button.Button#minWidth} config of + * each Button added to the footer toolbar via the {@link #fbar} or {@link #buttons} configurations. + * It will be ignored for buttons that have a minWidth configured some other way, e.g. in their own config + * object or via the {@link Ext.container.Container#config-defaults defaults} of their parent container. + */ + minButtonWidth: 75, + + /** + * @cfg {Boolean} collapsed + * true to render the panel collapsed, false to render it expanded (defaults to + * false). + */ + collapsed: false, + + /** + * @cfg {Boolean} collapseFirst + * true to make sure the collapse/expand toggle button always renders first (to the left of) + * any other tools in the panel's title bar, false to render it last (defaults to true). + */ + collapseFirst: true, + + /** + * @cfg {Boolean} hideCollapseTool + * true to hide the expand/collapse toggle button when {@link #collapsible} == true, + * false to display it (defaults to false). + */ + hideCollapseTool: false, + + /** + * @cfg {Boolean} titleCollapse + * true to allow expanding and collapsing the panel (when {@link #collapsible} = true) + * by clicking anywhere in the header bar, false) to allow it only by clicking to tool button + * (defaults to false)). + */ + titleCollapse: false, + + /** + * @cfg {String} collapseMode + *

Important: this config is only effective for {@link #collapsible} Panels which are direct child items of a {@link Ext.layout.container.Border border layout}.

+ *

When not a direct child item of a {@link Ext.layout.container.Border border layout}, then the Panel's header remains visible, and the body is collapsed to zero dimensions. + * If the Panel has no header, then a new header (orientated correctly depending on the {@link #collapseDirection}) will be inserted to show a the title and a re-expand tool.

+ *

When a child item of a {@link Ext.layout.container.Border border layout}, this config has two options: + *

    + *
  • undefined/omitted
    When collapsed, a placeholder {@link Ext.panel.Header Header} is injected into the layout to represent the Panel + * and to provide a UI with a Tool to allow the user to re-expand the Panel.
  • + *
  • header :
    The Panel collapses to leave its header visible as when not inside a {@link Ext.layout.container.Border border layout}.
  • + *

+ */ + + /** + * @cfg {Mixed} placeholder + *

Important: This config is only effective for {@link #collapsible} Panels which are direct child items of a {@link Ext.layout.container.Border border layout} + * when not using the 'header' {@link #collapseMode}.

+ *

Optional. A Component (or config object for a Component) to show in place of this Panel when this Panel is collapsed by a + * {@link Ext.layout.container.Border border layout}. Defaults to a generated {@link Ext.panel.Header Header} + * containing a {@link Ext.panel.Tool Tool} to re-expand the Panel.

+ */ + + /** + * @cfg {Boolean} floatable + *

Important: This config is only effective for {@link #collapsible} Panels which are direct child items of a {@link Ext.layout.container.Border border layout}.

+ * true to allow clicking a collapsed Panel's {@link #placeholder} to display the Panel floated + * above the layout, false to force the user to fully expand a collapsed region by + * clicking the expand button to see it again (defaults to true). + */ + floatable: true, + + /** + * @cfg {Boolean} collapsible + *

True to make the panel collapsible and have an expand/collapse toggle Tool added into + * the header tool button area. False to keep the panel sized either statically, or by an owning layout manager, with no toggle Tool (defaults to false).

+ * See {@link #collapseMode} and {@link #collapseDirection} + */ + collapsible: false, + + /** + * @cfg {Boolean} collapseDirection + *

The direction to collapse the Panel when the toggle button is clicked.

+ *

Defaults to the {@link #headerPosition}

+ *

Important: This config is ignored for {@link #collapsible} Panels which are direct child items of a {@link Ext.layout.container.Border border layout}.

+ *

Specify as 'top', 'bottom', 'left' or 'right'.

+ */ + + /** + * @cfg {Boolean} closable + *

True to display the 'close' tool button and allow the user to close the window, false to + * hide the button and disallow closing the window (defaults to false).

+ *

By default, when close is requested by clicking the close button in the header, the {@link #close} + * method will be called. This will {@link Ext.Component#destroy destroy} the Panel and its content + * meaning that it may not be reused.

+ *

To make closing a Panel hide the Panel so that it may be reused, set + * {@link #closeAction} to 'hide'.

+ */ + closable: false, + + /** + * @cfg {String} closeAction + *

The action to take when the close header tool is clicked: + *

    + *
  • '{@link #destroy}' : Default
    + * {@link #destroy remove} the window from the DOM and {@link Ext.Component#destroy destroy} + * it and all descendant Components. The window will not be available to be + * redisplayed via the {@link #show} method. + *
  • + *
  • '{@link #hide}' :
    + * {@link #hide} the window by setting visibility to hidden and applying negative offsets. + * The window will be available to be redisplayed via the {@link #show} method. + *
  • + *
+ *

Note: This behavior has changed! setting *does* affect the {@link #close} method + * which will invoke the approriate closeAction. + */ + closeAction: 'destroy', + + /** + * @cfg {Object/Array} dockedItems + * A component or series of components to be added as docked items to this panel. + * The docked items can be docked to either the top, right, left or bottom of a panel. + * This is typically used for things like toolbars or tab bars: + *


+var panel = new Ext.panel.Panel({
+    dockedItems: [{
+        xtype: 'toolbar',
+        dock: 'top',
+        items: [{
+            text: 'Docked to the top'
+        }]
+    }]
+});
+ */ + + /** + * @cfg {Boolean} preventHeader Prevent a Header from being created and shown. Defaults to false. + */ + preventHeader: false, + + /** + * @cfg {String} headerPosition Specify as 'top', 'bottom', 'left' or 'right'. Defaults to 'top'. + */ + headerPosition: 'top', + + /** + * @cfg {Boolean} frame + * True to apply a frame to the panel. + */ + frame: false, + + /** + * @cfg {Boolean} frameHeader + * True to apply a frame to the panel panels header (if 'frame' is true). + */ + frameHeader: true, + + /** + * @cfg {Array} tools + * An array of {@link Ext.panel.Tool} configs/instances to be added to the header tool area. The tools are stored as child + * components of the header container. They can be accessed using {@link #down} and {#query}, as well as the other + * component methods. The toggle tool is automatically created if {@link #collapsible} is set to true. + *

Note that, apart from the toggle tool which is provided when a panel is collapsible, these + * tools only provide the visual button. Any required functionality must be provided by adding + * handlers that implement the necessary behavior.

+ *

Example usage:

+ *

+tools:[{
+    type:'refresh',
+    qtip: 'Refresh form Data',
+    // hidden:true,
+    handler: function(event, toolEl, panel){
+        // refresh logic
+    }
+},
+{
+    type:'help',
+    qtip: 'Get Help',
+    handler: function(event, toolEl, panel){
+        // show help here
+    }
+}]
+
+ */ + + + initComponent: function() { + var me = this, + cls; + + me.addEvents( + /** + * @event titlechange + * Fires after the Panel title has been set or changed. + * @param {Ext.panel.Panel} p the Panel which has been resized. + * @param {String} newTitle The new title. + * @param {String} oldTitle The previous panel title. + */ + 'titlechange', + /** + * @event iconchange + * Fires after the Panel iconCls has been set or changed. + * @param {Ext.panel.Panel} p the Panel which has been resized. + * @param {String} newIconCls The new iconCls. + * @param {String} oldIconCls The previous panel iconCls. + */ + 'iconchange' + ); + + if (me.unstyled) { + me.setUI('plain'); + } + + if (me.frame) { + me.setUI('default-framed'); + } + + me.callParent(); + + me.collapseDirection = me.collapseDirection || me.headerPosition || Ext.Component.DIRECTION_TOP; + + // Backwards compatibility + me.bridgeToolbars(); + }, + + setBorder: function(border) { + // var me = this, + // method = (border === false || border === 0) ? 'addClsWithUI' : 'removeClsWithUI'; + // + // me.callParent(arguments); + // + // if (me.collapsed) { + // me[method](me.collapsedCls + '-noborder'); + // } + // + // if (me.header) { + // me.header.setBorder(border); + // if (me.collapsed) { + // me.header[method](me.collapsedCls + '-noborder'); + // } + // } + + this.callParent(arguments); + }, + + beforeDestroy: function() { + Ext.destroy( + this.ghostPanel, + this.dd + ); + this.callParent(); + }, + + initAria: function() { + this.callParent(); + this.initHeaderAria(); + }, + + initHeaderAria: function() { + var me = this, + el = me.el, + header = me.header; + if (el && header) { + el.dom.setAttribute('aria-labelledby', header.titleCmp.id); + } + }, + + getHeader: function() { + return this.header; + }, + + /** + * Set a title for the panel's header. See {@link Ext.panel.Header#title}. + * @param {String} newTitle + */ + setTitle: function(newTitle) { + var me = this, + oldTitle = this.title; + + me.title = newTitle; + if (me.header) { + me.header.setTitle(newTitle); + } else { + me.updateHeader(); + } + + if (me.reExpander) { + me.reExpander.setTitle(newTitle); + } + me.fireEvent('titlechange', me, newTitle, oldTitle); + }, + + /** + * Set the iconCls for the panel's header. See {@link Ext.panel.Header#iconCls}. + * @param {String} newIconCls + */ + setIconCls: function(newIconCls) { + var me = this, + oldIconCls = me.iconCls; + + me.iconCls = newIconCls; + var header = me.header; + if (header) { + header.setIconCls(newIconCls); + } + me.fireEvent('iconchange', me, newIconCls, oldIconCls); + }, + + bridgeToolbars: function() { + var me = this, + fbar, + fbarDefaults, + minButtonWidth = me.minButtonWidth; + + function initToolbar (toolbar, pos) { + if (Ext.isArray(toolbar)) { + toolbar = { + xtype: 'toolbar', + items: toolbar + }; + } + else if (!toolbar.xtype) { + toolbar.xtype = 'toolbar'; + } + toolbar.dock = pos; + if (pos == 'left' || pos == 'right') { + toolbar.vertical = true; + } + return toolbar; + } + + // Backwards compatibility + + /** + * @cfg {Object/Array} tbar + +Convenience method. Short for 'Top Bar'. + + tbar: [ + { xtype: 'button', text: 'Button 1' } + ] + +is equivalent to + + dockedItems: [{ + xtype: 'toolbar', + dock: 'top', + items: [ + { xtype: 'button', text: 'Button 1' } + ] + }] + + * @markdown + */ + if (me.tbar) { + me.addDocked(initToolbar(me.tbar, 'top')); + me.tbar = null; + } + + /** + * @cfg {Object/Array} bbar + +Convenience method. Short for 'Bottom Bar'. + + bbar: [ + { xtype: 'button', text: 'Button 1' } + ] + +is equivalent to + + dockedItems: [{ + xtype: 'toolbar', + dock: 'bottom', + items: [ + { xtype: 'button', text: 'Button 1' } + ] + }] + + * @markdown + */ + if (me.bbar) { + me.addDocked(initToolbar(me.bbar, 'bottom')); + me.bbar = null; + } + + /** + * @cfg {Object/Array} buttons + +Convenience method used for adding buttons docked to the bottom right of the panel. This is a +synonym for the {@link #fbar} config. + + buttons: [ + { text: 'Button 1' } + ] + +is equivalent to + + dockedItems: [{ + xtype: 'toolbar', + dock: 'bottom', + defaults: {minWidth: {@link #minButtonWidth}}, + items: [ + { xtype: 'component', flex: 1 }, + { xtype: 'button', text: 'Button 1' } + ] + }] + +The {@link #minButtonWidth} is used as the default {@link Ext.button.Button#minWidth minWidth} for +each of the buttons in the buttons toolbar. + + * @markdown + */ + if (me.buttons) { + me.fbar = me.buttons; + me.buttons = null; + } + + /** + * @cfg {Object/Array} fbar + +Convenience method used for adding items to the bottom right of the panel. Short for Footer Bar. + + fbar: [ + { type: 'button', text: 'Button 1' } + ] + +is equivalent to + + dockedItems: [{ + xtype: 'toolbar', + dock: 'bottom', + defaults: {minWidth: {@link #minButtonWidth}}, + items: [ + { xtype: 'component', flex: 1 }, + { xtype: 'button', text: 'Button 1' } + ] + }] + +The {@link #minButtonWidth} is used as the default {@link Ext.button.Button#minWidth minWidth} for +each of the buttons in the fbar. + + * @markdown + */ + if (me.fbar) { + fbar = initToolbar(me.fbar, 'bottom'); + fbar.ui = 'footer'; + + // Apply the minButtonWidth config to buttons in the toolbar + if (minButtonWidth) { + fbarDefaults = fbar.defaults; + fbar.defaults = function(config) { + var defaults = fbarDefaults || {}; + if ((!config.xtype || config.xtype === 'button' || (config.isComponent && config.isXType('button'))) && + !('minWidth' in defaults)) { + defaults = Ext.apply({minWidth: minButtonWidth}, defaults); + } + return defaults; + }; + } + + fbar = me.addDocked(fbar)[0]; + fbar.insert(0, { + flex: 1, + xtype: 'component', + focusable: false + }); + me.fbar = null; + } + + /** + * @cfg {Object/Array} lbar + * + * Convenience method. Short for 'Left Bar' (left-docked, vertical toolbar). + * + * lbar: [ + * { xtype: 'button', text: 'Button 1' } + * ] + * + * is equivalent to + * + * dockedItems: [{ + * xtype: 'toolbar', + * dock: 'left', + * items: [ + * { xtype: 'button', text: 'Button 1' } + * ] + * }] + * + * @markdown + */ + if (me.lbar) { + me.addDocked(initToolbar(me.lbar, 'left')); + me.lbar = null; + } + + /** + * @cfg {Object/Array} rbar + * + * Convenience method. Short for 'Right Bar' (right-docked, vertical toolbar). + * + * rbar: [ + * { xtype: 'button', text: 'Button 1' } + * ] + * + * is equivalent to + * + * dockedItems: [{ + * xtype: 'toolbar', + * dock: 'right', + * items: [ + * { xtype: 'button', text: 'Button 1' } + * ] + * }] + * + * @markdown + */ + if (me.rbar) { + me.addDocked(initToolbar(me.rbar, 'right')); + me.rbar = null; + } + }, + + /** + * @private + * Tools are a Panel-specific capabilty. + * Panel uses initTools. Subclasses may contribute tools by implementing addTools. + */ + initTools: function() { + var me = this; + + me.tools = me.tools || []; + + // Add a collapse tool unless configured to not show a collapse tool + // or to not even show a header. + if (me.collapsible && !(me.hideCollapseTool || me.header === false)) { + me.collapseDirection = me.collapseDirection || me.headerPosition || 'top'; + me.collapseTool = me.expandTool = me.createComponent({ + xtype: 'tool', + type: 'collapse-' + me.collapseDirection, + expandType: me.getOppositeDirection(me.collapseDirection), + handler: me.toggleCollapse, + scope: me + }); + + // Prepend collapse tool is configured to do so. + if (me.collapseFirst) { + me.tools.unshift(me.collapseTool); + } + } + + // Add subclass-specific tools. + me.addTools(); + + // Make Panel closable. + if (me.closable) { + me.addClsWithUI('closable'); + me.addTool({ + type: 'close', + handler: Ext.Function.bind(me.close, this, []) + }); + } + + // Append collapse tool if needed. + if (me.collapseTool && !me.collapseFirst) { + me.tools.push(me.collapseTool); + } + }, + + /** + * @private + * Template method to be implemented in subclasses to add their tools after the collapsible tool. + */ + addTools: Ext.emptyFn, + + /** + *

Closes the Panel. By default, this method, removes it from the DOM, {@link Ext.Component#destroy destroy}s + * the Panel object and all its descendant Components. The {@link #beforeclose beforeclose} + * event is fired before the close happens and will cancel the close action if it returns false.

+ *

Note: This method is not affected by the {@link #closeAction} setting which + * only affects the action triggered when clicking the {@link #closable 'close' tool in the header}. + * To hide the Panel without destroying it, call {@link #hide}.

+ */ + close: function() { + if (this.fireEvent('beforeclose', this) !== false) { + this.doClose(); + } + }, + + // private + doClose: function() { + this.fireEvent('close', this); + this[this.closeAction](); + }, + + onRender: function(ct, position) { + var me = this, + topContainer; + + // Add class-specific header tools. + // Panel adds collapsible and closable. + me.initTools(); + + // Dock the header/title + me.updateHeader(); + + // If initially collapsed, collapsed flag must indicate true current state at this point. + // Do collapse after the first time the Panel's structure has been laid out. + if (me.collapsed) { + me.collapsed = false; + topContainer = me.findLayoutController(); + if (!me.hidden && topContainer) { + topContainer.on({ + afterlayout: function() { + me.collapse(null, false, true); + }, + single: true + }); + } else { + me.afterComponentLayout = function() { + delete me.afterComponentLayout; + Ext.getClass(me).prototype.afterComponentLayout.apply(me, arguments); + me.collapse(null, false, true); + }; + } + } + + // Call to super after adding the header, to prevent an unnecessary re-layout + me.callParent(arguments); + }, + + /** + * Create, hide, or show the header component as appropriate based on the current config. + * @private + * @param {Boolean} force True to force the the header to be created + */ + updateHeader: function(force) { + var me = this, + header = me.header, + title = me.title, + tools = me.tools; + + if (!me.preventHeader && (force || title || (tools && tools.length))) { + if (!header) { + header = me.header = Ext.create('Ext.panel.Header', { + title : title, + orientation : (me.headerPosition == 'left' || me.headerPosition == 'right') ? 'vertical' : 'horizontal', + dock : me.headerPosition || 'top', + textCls : me.headerTextCls, + iconCls : me.iconCls, + baseCls : me.baseCls + '-header', + tools : tools, + ui : me.ui, + indicateDrag: me.draggable, + border : me.border, + frame : me.frame && me.frameHeader, + ignoreParentFrame : me.frame || me.overlapHeader, + ignoreBorderManagement: me.frame || me.ignoreHeaderBorderManagement, + listeners : me.collapsible && me.titleCollapse ? { + click: me.toggleCollapse, + scope: me + } : null + }); + me.addDocked(header, 0); + + // Reference the Header's tool array. + // Header injects named references. + me.tools = header.tools; + } + header.show(); + me.initHeaderAria(); + } else if (header) { + header.hide(); + } + }, + + // inherit docs + setUI: function(ui) { + var me = this; + + me.callParent(arguments); + + if (me.header) { + me.header.setUI(ui); + } + }, + + // private + getContentTarget: function() { + return this.body; + }, + + getTargetEl: function() { + return this.body || this.frameBody || this.el; + }, + + addTool: function(tool) { + this.tools.push(tool); + var header = this.header; + if (header) { + header.addTool(tool); + } + this.updateHeader(); + }, + + getOppositeDirection: function(d) { + var c = Ext.Component; + switch (d) { + case c.DIRECTION_TOP: + return c.DIRECTION_BOTTOM; + case c.DIRECTION_RIGHT: + return c.DIRECTION_LEFT; + case c.DIRECTION_BOTTOM: + return c.DIRECTION_TOP; + case c.DIRECTION_LEFT: + return c.DIRECTION_RIGHT; + } + }, + + /** + * Collapses the panel body so that the body becomes hidden. Docked Components parallel to the + * border towards which the collapse takes place will remain visible. Fires the {@link #beforecollapse} event which will + * cancel the collapse action if it returns false. + * @param {Number} direction. The direction to collapse towards. Must be one of
    + *
  • Ext.Component.DIRECTION_TOP
  • + *
  • Ext.Component.DIRECTION_RIGHT
  • + *
  • Ext.Component.DIRECTION_BOTTOM
  • + *
  • Ext.Component.DIRECTION_LEFT
+ * @param {Boolean} animate True to animate the transition, else false (defaults to the value of the + * {@link #animCollapse} panel config) + * @return {Ext.panel.Panel} this + */ + collapse: function(direction, animate, /* private - passed if called at render time */ internal) { + var me = this, + c = Ext.Component, + height = me.getHeight(), + width = me.getWidth(), + frameInfo, + newSize = 0, + dockedItems = me.dockedItems.items, + dockedItemCount = dockedItems.length, + i = 0, + comp, + pos, + anim = { + from: { + height: height, + width: width + }, + to: { + height: height, + width: width + }, + listeners: { + afteranimate: me.afterCollapse, + scope: me + }, + duration: Ext.Number.from(animate, Ext.fx.Anim.prototype.duration) + }, + reExpander, + reExpanderOrientation, + reExpanderDock, + getDimension, + setDimension, + collapseDimension; + + if (!direction) { + direction = me.collapseDirection; + } + + // If internal (Called because of initial collapsed state), then no animation, and no events. + if (internal) { + animate = false; + } else if (me.collapsed || me.fireEvent('beforecollapse', me, direction, animate) === false) { + return false; + } + + reExpanderDock = direction; + me.expandDirection = me.getOppositeDirection(direction); + + // Track docked items which we hide during collapsed state + me.hiddenDocked = []; + + switch (direction) { + case c.DIRECTION_TOP: + case c.DIRECTION_BOTTOM: + me.expandedSize = me.getHeight(); + reExpanderOrientation = 'horizontal'; + collapseDimension = 'height'; + getDimension = 'getHeight'; + setDimension = 'setHeight'; + + // Collect the height of the visible header. + // Hide all docked items except the header. + // Hide *ALL* docked items if we're going to end up hiding the whole Panel anyway + for (; i < dockedItemCount; i++) { + comp = dockedItems[i]; + if (comp.isVisible()) { + if (comp.isHeader && (!comp.dock || comp.dock == 'top' || comp.dock == 'bottom')) { + reExpander = comp; + } else { + me.hiddenDocked.push(comp); + } + } + } + + if (direction == Ext.Component.DIRECTION_BOTTOM) { + pos = me.getPosition()[1] - Ext.fly(me.el.dom.offsetParent).getRegion().top; + anim.from.top = pos; + } + break; + + case c.DIRECTION_LEFT: + case c.DIRECTION_RIGHT: + me.expandedSize = me.getWidth(); + reExpanderOrientation = 'vertical'; + collapseDimension = 'width'; + getDimension = 'getWidth'; + setDimension = 'setWidth'; + + // Collect the height of the visible header. + // Hide all docked items except the header. + // Hide *ALL* docked items if we're going to end up hiding the whole Panel anyway + for (; i < dockedItemCount; i++) { + comp = dockedItems[i]; + if (comp.isVisible()) { + if (comp.isHeader && (comp.dock == 'left' || comp.dock == 'right')) { + reExpander = comp; + } else { + me.hiddenDocked.push(comp); + } + } + } + + if (direction == Ext.Component.DIRECTION_RIGHT) { + pos = me.getPosition()[0] - Ext.fly(me.el.dom.offsetParent).getRegion().left; + anim.from.left = pos; + } + break; + + default: + throw('Panel collapse must be passed a valid Component collapse direction'); + } + + // No scrollbars when we shrink this Panel + // And no laying out of any children... we're effectively *hiding* the body + me.setAutoScroll(false); + me.suspendLayout = true; + me.body.setVisibilityMode(Ext.core.Element.DISPLAY); + + // Disable toggle tool during animated collapse + if (animate && me.collapseTool) { + me.collapseTool.disable(); + } + + // Add the collapsed class now, so that collapsed CSS rules are applied before measurements are taken. + me.addClsWithUI(me.collapsedCls); + // if (me.border === false) { + // me.addClsWithUI(me.collapsedCls + '-noborder'); + // } + + // We found a header: Measure it to find the collapse-to size. + if (reExpander) { + //we must add the collapsed cls to the header and then remove to get the proper height + reExpander.addClsWithUI(me.collapsedCls); + reExpander.addClsWithUI(me.collapsedCls + '-' + reExpander.dock); + if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) { + reExpander.addClsWithUI(me.collapsedCls + '-border-' + reExpander.dock); + } + + frameInfo = reExpander.getFrameInfo(); + + //get the size + newSize = reExpander[getDimension]() + (frameInfo ? frameInfo[direction] : 0); + + //and remove + reExpander.removeClsWithUI(me.collapsedCls); + reExpander.removeClsWithUI(me.collapsedCls + '-' + reExpander.dock); + if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) { + reExpander.removeClsWithUI(me.collapsedCls + '-border-' + reExpander.dock); + } + } + // No header: Render and insert a temporary one, and then measure it. + else { + reExpander = { + hideMode: 'offsets', + temporary: true, + title: me.title, + orientation: reExpanderOrientation, + dock: reExpanderDock, + textCls: me.headerTextCls, + iconCls: me.iconCls, + baseCls: me.baseCls + '-header', + ui: me.ui, + frame: me.frame && me.frameHeader, + ignoreParentFrame: me.frame || me.overlapHeader, + indicateDrag: me.draggable, + cls: me.baseCls + '-collapsed-placeholder ' + ' ' + Ext.baseCSSPrefix + 'docked ' + me.baseCls + '-' + me.ui + '-collapsed', + renderTo: me.el + }; + reExpander[(reExpander.orientation == 'horizontal') ? 'tools' : 'items'] = [{ + xtype: 'tool', + type: 'expand-' + me.expandDirection, + handler: me.toggleCollapse, + scope: me + }]; + + // Capture the size of the re-expander. + // For vertical headers in IE6 and IE7, this will be sized by a CSS rule in _panel.scss + reExpander = me.reExpander = Ext.create('Ext.panel.Header', reExpander); + newSize = reExpander[getDimension]() + ((reExpander.frame) ? reExpander.frameSize[direction] : 0); + reExpander.hide(); + + // Insert the new docked item + me.insertDocked(0, reExpander); + } + + me.reExpander = reExpander; + me.reExpander.addClsWithUI(me.collapsedCls); + me.reExpander.addClsWithUI(me.collapsedCls + '-' + reExpander.dock); + if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) { + me.reExpander.addClsWithUI(me.collapsedCls + '-border-' + me.reExpander.dock); + } + + // If collapsing right or down, we'll be also animating the left or top. + if (direction == Ext.Component.DIRECTION_RIGHT) { + anim.to.left = pos + (width - newSize); + } else if (direction == Ext.Component.DIRECTION_BOTTOM) { + anim.to.top = pos + (height - newSize); + } + + // Animate to the new size + anim.to[collapseDimension] = newSize; + + // Remove any flex config before we attempt to collapse. + me.savedFlex = me.flex; + me.savedMinWidth = me.minWidth; + me.savedMinHeight = me.minHeight; + me.minWidth = 0; + me.minHeight = 0; + delete me.flex; + + if (animate) { + me.animate(anim); + } else { + me.setSize(anim.to.width, anim.to.height); + if (Ext.isDefined(anim.to.left) || Ext.isDefined(anim.to.top)) { + me.setPosition(anim.to.left, anim.to.top); + } + me.afterCollapse(false, internal); + } + return me; + }, + + afterCollapse: function(animated, internal) { + var me = this, + i = 0, + l = me.hiddenDocked.length; + + me.minWidth = me.savedMinWidth; + me.minHeight = me.savedMinHeight; + + me.body.hide(); + for (; i < l; i++) { + me.hiddenDocked[i].hide(); + } + if (me.reExpander) { + me.reExpander.updateFrame(); + me.reExpander.show(); + } + me.collapsed = true; + + if (!internal) { + me.doComponentLayout(); + } + + if (me.resizer) { + me.resizer.disable(); + } + + // If me Panel was configured with a collapse tool in its header, flip it's type + if (me.collapseTool) { + me.collapseTool.setType('expand-' + me.expandDirection); + } + if (!internal) { + me.fireEvent('collapse', me); + } + + // Re-enable the toggle tool after an animated collapse + if (animated && me.collapseTool) { + me.collapseTool.enable(); + } + }, + + /** + * Expands the panel body so that it becomes visible. Fires the {@link #beforeexpand} event which will + * cancel the expand action if it returns false. + * @param {Boolean} animate True to animate the transition, else false (defaults to the value of the + * {@link #animCollapse} panel config) + * @return {Ext.panel.Panel} this + */ + expand: function(animate) { + if (!this.collapsed || this.fireEvent('beforeexpand', this, animate) === false) { + return false; + } + var me = this, + i = 0, + l = me.hiddenDocked.length, + direction = me.expandDirection, + height = me.getHeight(), + width = me.getWidth(), + pos, anim, satisfyJSLint; + + // Disable toggle tool during animated expand + if (animate && me.collapseTool) { + me.collapseTool.disable(); + } + + // Show any docked items that we hid on collapse + // And hide the injected reExpander Header + for (; i < l; i++) { + me.hiddenDocked[i].hidden = false; + me.hiddenDocked[i].el.show(); + } + if (me.reExpander) { + if (me.reExpander.temporary) { + me.reExpander.hide(); + } else { + me.reExpander.removeClsWithUI(me.collapsedCls); + me.reExpander.removeClsWithUI(me.collapsedCls + '-' + me.reExpander.dock); + if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) { + me.reExpander.removeClsWithUI(me.collapsedCls + '-border-' + me.reExpander.dock); + } + me.reExpander.updateFrame(); + } + } + + // If me Panel was configured with a collapse tool in its header, flip it's type + if (me.collapseTool) { + me.collapseTool.setType('collapse-' + me.collapseDirection); + } + + // Unset the flag before the potential call to calculateChildBox to calculate our newly flexed size + me.collapsed = false; + + // Collapsed means body element was hidden + me.body.show(); + + // Remove any collapsed styling before any animation begins + me.removeClsWithUI(me.collapsedCls); + // if (me.border === false) { + // me.removeClsWithUI(me.collapsedCls + '-noborder'); + // } + + anim = { + to: { + }, + from: { + height: height, + width: width + }, + listeners: { + afteranimate: me.afterExpand, + scope: me + } + }; + + if ((direction == Ext.Component.DIRECTION_TOP) || (direction == Ext.Component.DIRECTION_BOTTOM)) { + + // If autoHeight, measure the height now we have shown the body element. + if (me.autoHeight) { + me.setCalculatedSize(me.width, null); + anim.to.height = me.getHeight(); + + // Must size back down to collapsed for the animation. + me.setCalculatedSize(me.width, anim.from.height); + } + // If we were flexed, then we can't just restore to the saved size. + // We must restore to the currently correct, flexed size, so we much ask the Box layout what that is. + else if (me.savedFlex) { + me.flex = me.savedFlex; + anim.to.height = me.ownerCt.layout.calculateChildBox(me).height; + delete me.flex; + } + // Else, restore to saved height + else { + anim.to.height = me.expandedSize; + } + + // top needs animating upwards + if (direction == Ext.Component.DIRECTION_TOP) { + pos = me.getPosition()[1] - Ext.fly(me.el.dom.offsetParent).getRegion().top; + anim.from.top = pos; + anim.to.top = pos - (anim.to.height - height); + } + } else if ((direction == Ext.Component.DIRECTION_LEFT) || (direction == Ext.Component.DIRECTION_RIGHT)) { + + // If autoWidth, measure the width now we have shown the body element. + if (me.autoWidth) { + me.setCalculatedSize(null, me.height); + anim.to.width = me.getWidth(); + + // Must size back down to collapsed for the animation. + me.setCalculatedSize(anim.from.width, me.height); + } + // If we were flexed, then we can't just restore to the saved size. + // We must restore to the currently correct, flexed size, so we much ask the Box layout what that is. + else if (me.savedFlex) { + me.flex = me.savedFlex; + anim.to.width = me.ownerCt.layout.calculateChildBox(me).width; + delete me.flex; + } + // Else, restore to saved width + else { + anim.to.width = me.expandedSize; + } + + // left needs animating leftwards + if (direction == Ext.Component.DIRECTION_LEFT) { + pos = me.getPosition()[0] - Ext.fly(me.el.dom.offsetParent).getRegion().left; + anim.from.left = pos; + anim.to.left = pos - (anim.to.width - width); + } + } + + if (animate) { + me.animate(anim); + } else { + me.setSize(anim.to.width, anim.to.height); + if (anim.to.x) { + me.setLeft(anim.to.x); + } + if (anim.to.y) { + me.setTop(anim.to.y); + } + me.afterExpand(false); + } + + return me; + }, + + afterExpand: function(animated) { + var me = this; + me.setAutoScroll(me.initialConfig.autoScroll); + + // Restored to a calculated flex. Delete the set width and height properties so that flex works from now on. + if (me.savedFlex) { + me.flex = me.savedFlex; + delete me.savedFlex; + delete me.width; + delete me.height; + } + + // Reinstate layout out after Panel has re-expanded + delete me.suspendLayout; + if (animated && me.ownerCt) { + me.ownerCt.doLayout(); + } + + if (me.resizer) { + me.resizer.enable(); + } + + me.fireEvent('expand', me); + + // Re-enable the toggle tool after an animated expand + if (animated && me.collapseTool) { + me.collapseTool.enable(); + } + }, + + /** + * Shortcut for performing an {@link #expand} or {@link #collapse} based on the current state of the panel. + * @return {Ext.panel.Panel} this + */ + toggleCollapse: function() { + if (this.collapsed) { + this.expand(this.animCollapse); + } else { + this.collapse(this.collapseDirection, this.animCollapse); + } + return this; + }, + + // private + getKeyMap : function(){ + if(!this.keyMap){ + this.keyMap = Ext.create('Ext.util.KeyMap', this.el, this.keys); + } + return this.keyMap; + }, + + // private + initDraggable : function(){ + /** + *

If this Panel is configured {@link #draggable}, this property will contain + * an instance of {@link Ext.dd.DragSource} which handles dragging the Panel.

+ * The developer must provide implementations of the abstract methods of {@link Ext.dd.DragSource} + * in order to supply behaviour for each stage of the drag/drop process. See {@link #draggable}. + * @type Ext.dd.DragSource. + * @property dd + */ + this.dd = Ext.create('Ext.panel.DD', this, Ext.isBoolean(this.draggable) ? null : this.draggable); + }, + + // private - helper function for ghost + ghostTools : function() { + var tools = [], + origTools = this.initialConfig.tools; + + if (origTools) { + Ext.each(origTools, function(tool) { + // Some tools can be full components, and copying them into the ghost + // actually removes them from the owning panel. You could also potentially + // end up with duplicate DOM ids as well. To avoid any issues we just make + // a simple bare-minimum clone of each tool for ghosting purposes. + tools.push({ + type: tool.type + }); + }); + } + else { + tools = [{ + type: 'placeholder' + }]; + } + return tools; + }, + + // private - used for dragging + ghost: function(cls) { + var me = this, + ghostPanel = me.ghostPanel, + box = me.getBox(); + + if (!ghostPanel) { + ghostPanel = Ext.create('Ext.panel.Panel', { + renderTo: document.body, + floating: { + shadow: false + }, + frame: Ext.supports.CSS3BorderRadius ? me.frame : false, + title: me.title, + overlapHeader: me.overlapHeader, + headerPosition: me.headerPosition, + width: me.getWidth(), + height: me.getHeight(), + iconCls: me.iconCls, + baseCls: me.baseCls, + tools: me.ghostTools(), + cls: me.baseCls + '-ghost ' + (cls ||'') + }); + me.ghostPanel = ghostPanel; + } + ghostPanel.floatParent = me.floatParent; + if (me.floating) { + ghostPanel.setZIndex(Ext.Number.from(me.el.getStyle('zIndex'), 0)); + } else { + ghostPanel.toFront(); + } + ghostPanel.el.show(); + ghostPanel.setPosition(box.x, box.y); + ghostPanel.setSize(box.width, box.height); + me.el.hide(); + if (me.floatingItems) { + me.floatingItems.hide(); + } + return ghostPanel; + }, + + // private + unghost: function(show, matchPosition) { + var me = this; + if (!me.ghostPanel) { + return; + } + if (show !== false) { + me.el.show(); + if (matchPosition !== false) { + me.setPosition(me.ghostPanel.getPosition()); + } + if (me.floatingItems) { + me.floatingItems.show(); + } + Ext.defer(me.focus, 10, me); + } + me.ghostPanel.el.hide(); + }, + + initResizable: function(resizable) { + if (this.collapsed) { + resizable.disabled = true; + } + this.callParent([resizable]); + } +}); + +/** + * Component layout for Tip/ToolTip/etc. components + * @class Ext.layout.component.Tip + * @extends Ext.layout.component.Dock + * @private + */ + +Ext.define('Ext.layout.component.Tip', { + + /* Begin Definitions */ + + alias: ['layout.tip'], + + extend: 'Ext.layout.component.Dock', + + /* End Definitions */ + + type: 'tip', + + onLayout: function(width, height) { + var me = this, + owner = me.owner, + el = owner.el, + minWidth, + maxWidth, + naturalWidth, + constrainedWidth, + xy = el.getXY(); + + // Position offscreen so the natural width is not affected by the viewport's right edge + el.setXY([-9999,-9999]); + + // Calculate initial layout + this.callParent(arguments); + + // Handle min/maxWidth for auto-width tips + if (!Ext.isNumber(width)) { + minWidth = owner.minWidth; + maxWidth = owner.maxWidth; + // IE6/7 in strict mode have a problem doing an autoWidth + if (Ext.isStrict && (Ext.isIE6 || Ext.isIE7)) { + constrainedWidth = me.doAutoWidth(); + } else { + naturalWidth = el.getWidth(); + } + if (naturalWidth < minWidth) { + constrainedWidth = minWidth; + } + else if (naturalWidth > maxWidth) { + constrainedWidth = maxWidth; + } + if (constrainedWidth) { + this.callParent([constrainedWidth, height]); + } + } + + // Restore position + el.setXY(xy); + }, + + doAutoWidth: function(){ + var me = this, + owner = me.owner, + body = owner.body, + width = body.getTextWidth(); + + if (owner.header) { + width = Math.max(width, owner.header.getWidth()); + } + if (!Ext.isDefined(me.frameWidth)) { + me.frameWidth = owner.el.getWidth() - body.getWidth(); + } + width += me.frameWidth + body.getPadding('lr'); + return width; + } +}); + +/** + * @class Ext.tip.Tip + * @extends Ext.panel.Panel + * This is the base class for {@link Ext.tip.QuickTip} and {@link Ext.tip.ToolTip} that provides the basic layout and + * positioning that all tip-based classes require. This class can be used directly for simple, statically-positioned + * tips that are displayed programmatically, or it can be extended to provide custom tip implementations. + * @constructor + * Create a new Tip + * @param {Object} config The configuration options + * @xtype tip + */ +Ext.define('Ext.tip.Tip', { + extend: 'Ext.panel.Panel', + requires: [ 'Ext.layout.component.Tip' ], + alternateClassName: 'Ext.Tip', + /** + * @cfg {Boolean} closable True to render a close tool button into the tooltip header (defaults to false). + */ + /** + * @cfg {Number} width + * Width in pixels of the tip (defaults to auto). Width will be ignored if it exceeds the bounds of + * {@link #minWidth} or {@link #maxWidth}. The maximum supported value is 500. + */ + /** + * @cfg {Number} minWidth The minimum width of the tip in pixels (defaults to 40). + */ + minWidth : 40, + /** + * @cfg {Number} maxWidth The maximum width of the tip in pixels (defaults to 300). The maximum supported value is 500. + */ + maxWidth : 300, + /** + * @cfg {Boolean/String} shadow True or "sides" for the default effect, "frame" for 4-way shadow, and "drop" + * for bottom-right shadow (defaults to "sides"). + */ + shadow : "sides", + + /** + * @cfg {String} defaultAlign Experimental. The default {@link Ext.core.Element#alignTo} anchor position value + * for this tip relative to its element of origin (defaults to "tl-bl?"). + */ + defaultAlign : "tl-bl?", + /** + * @cfg {Boolean} constrainPosition If true, then the tooltip will be automatically constrained to stay within + * the browser viewport. Defaults to false. + */ + constrainPosition : true, + + /** + * @inherited + */ + frame: false, + + // private panel overrides + autoRender: true, + hidden: true, + baseCls: Ext.baseCSSPrefix + 'tip', + floating: { + shadow: true, + shim: true, + constrain: true + }, + focusOnToFront: false, + componentLayout: 'tip', + + closeAction: 'hide', + + ariaRole: 'tooltip', + + initComponent: function() { + this.callParent(arguments); + + // Or in the deprecated config. Floating.doConstrain only constrains if the constrain property is truthy. + this.constrain = this.constrain || this.constrainPosition; + }, + + /** + * Shows this tip at the specified XY position. Example usage: + *

+// Show the tip at x:50 and y:100
+tip.showAt([50,100]);
+
+ * @param {Array} xy An array containing the x and y coordinates + */ + showAt : function(xy){ + var me = this; + this.callParent(); + // Show may have been vetoed. + if (me.isVisible()) { + me.setPagePosition(xy[0], xy[1]); + if (me.constrainPosition || me.constrain) { + me.doConstrain(); + } + me.toFront(true); + } + }, + + /** + * Experimental. Shows this tip at a position relative to another element using a standard {@link Ext.core.Element#alignTo} + * anchor position value. Example usage: + *

+// Show the tip at the default position ('tl-br?')
+tip.showBy('my-el');
+
+// Show the tip's top-left corner anchored to the element's top-right corner
+tip.showBy('my-el', 'tl-tr');
+
+ * @param {Mixed} el An HTMLElement, Ext.core.Element or string id of the target element to align to + * @param {String} position (optional) A valid {@link Ext.core.Element#alignTo} anchor position (defaults to 'tl-br?' or + * {@link #defaultAlign} if specified). + */ + showBy : function(el, pos) { + this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign)); + }, + + /** + * @private + * @override + * Set Tip draggable using base Component's draggability + */ + initDraggable : function(){ + var me = this; + me.draggable = { + el: me.getDragEl(), + delegate: me.header.el, + constrain: me, + constrainTo: me.el.dom.parentNode + }; + // Important: Bypass Panel's initDraggable. Call direct to Component's implementation. + Ext.Component.prototype.initDraggable.call(me); + }, + + // Tip does not ghost. Drag is "live" + ghost: undefined, + unghost: undefined +}); + +/** + * @class Ext.tip.ToolTip + * @extends Ext.tip.Tip + * + * ToolTip is a {@link Ext.tip.Tip} implementation that handles the common case of displaying a + * tooltip when hovering over a certain element or elements on the page. It allows fine-grained + * control over the tooltip's alignment relative to the target element or mouse, and the timing + * of when it is automatically shown and hidden. + * + * This implementation does **not** have a built-in method of automatically populating the tooltip's + * text based on the target element; you must either configure a fixed {@link #html} value for each + * ToolTip instance, or implement custom logic (e.g. in a {@link #beforeshow} event listener) to + * generate the appropriate tooltip content on the fly. See {@link Ext.tip.QuickTip} for a more + * convenient way of automatically populating and configuring a tooltip based on specific DOM + * attributes of each target element. + * + * ## Basic Example + * + * var tip = Ext.create('Ext.tip.ToolTip', { + * target: 'clearButton', + * html: 'Press this button to clear the form' + * }); + * + * {@img Ext.tip.ToolTip/Ext.tip.ToolTip1.png Basic Ext.tip.ToolTip} + * + * ## Delegation + * + * In addition to attaching a ToolTip to a single element, you can also use delegation to attach + * one ToolTip to many elements under a common parent. This is more efficient than creating many + * ToolTip instances. To do this, point the {@link #target} config to a common ancestor of all the + * elements, and then set the {@link #delegate} config to a CSS selector that will select all the + * appropriate sub-elements. + * + * When using delegation, it is likely that you will want to programmatically change the content + * of the ToolTip based on each delegate element; you can do this by implementing a custom + * listener for the {@link #beforeshow} event. Example: + * + * var myGrid = Ext.create('Ext.grid.GridPanel', gridConfig); + * myGrid.on('render', function(grid) { + * var view = grid.getView(); // Capture the grid's view. + * grid.tip = Ext.create('Ext.tip.ToolTip', { + * target: view.el, // The overall target element. + * delegate: view.itemSelector, // Each grid row causes its own seperate show and hide. + * trackMouse: true, // Moving within the row should not hide the tip. + * renderTo: Ext.getBody(), // Render immediately so that tip.body can be referenced prior to the first show. + * listeners: { // Change content dynamically depending on which element triggered the show. + * beforeshow: function updateTipBody(tip) { + * tip.update('Over company "' + view.getRecord(tip.triggerElement).get('company') + '"'); + * } + * } + * }); + * }); + * + * {@img Ext.tip.ToolTip/Ext.tip.ToolTip2.png Ext.tip.ToolTip with delegation} + * + * ## Alignment + * + * The following configuration properties allow control over how the ToolTip is aligned relative to + * the target element and/or mouse pointer: + * + * - {@link #anchor} + * - {@link #anchorToTarget} + * - {@link #anchorOffset} + * - {@link #trackMouse} + * - {@link #mouseOffset} + * + * ## Showing/Hiding + * + * The following configuration properties allow control over how and when the ToolTip is automatically + * shown and hidden: + * + * - {@link #autoHide} + * - {@link #showDelay} + * - {@link #hideDelay} + * - {@link #dismissDelay} + * + * @constructor + * Create a new ToolTip instance + * @param {Object} config The configuration options + * @xtype tooltip + * @markdown + * @docauthor Jason Johnston + */ +Ext.define('Ext.tip.ToolTip', { + extend: 'Ext.tip.Tip', + alias: 'widget.tooltip', + alternateClassName: 'Ext.ToolTip', + /** + * When a ToolTip is configured with the {@link #delegate} + * option to cause selected child elements of the {@link #target} + * Element to each trigger a seperate show event, this property is set to + * the DOM element which triggered the show. + * @type DOMElement + * @property triggerElement + */ + /** + * @cfg {Mixed} target The target HTMLElement, Ext.core.Element or id to monitor + * for mouseover events to trigger showing this ToolTip. + */ + /** + * @cfg {Boolean} autoHide True to automatically hide the tooltip after the + * mouse exits the target element or after the {@link #dismissDelay} + * has expired if set (defaults to true). If {@link #closable} = true + * a close tool button will be rendered into the tooltip header. + */ + /** + * @cfg {Number} showDelay Delay in milliseconds before the tooltip displays + * after the mouse enters the target element (defaults to 500) + */ + showDelay: 500, + /** + * @cfg {Number} hideDelay Delay in milliseconds after the mouse exits the + * target element but before the tooltip actually hides (defaults to 200). + * Set to 0 for the tooltip to hide immediately. + */ + hideDelay: 200, + /** + * @cfg {Number} dismissDelay Delay in milliseconds before the tooltip + * automatically hides (defaults to 5000). To disable automatic hiding, set + * dismissDelay = 0. + */ + dismissDelay: 5000, + /** + * @cfg {Array} mouseOffset An XY offset from the mouse position where the + * tooltip should be shown (defaults to [15,18]). + */ + /** + * @cfg {Boolean} trackMouse True to have the tooltip follow the mouse as it + * moves over the target element (defaults to false). + */ + trackMouse: false, + /** + * @cfg {String} anchor If specified, indicates that the tip should be anchored to a + * particular side of the target element or mouse pointer ("top", "right", "bottom", + * or "left"), with an arrow pointing back at the target or mouse pointer. If + * {@link #constrainPosition} is enabled, this will be used as a preferred value + * only and may be flipped as needed. + */ + /** + * @cfg {Boolean} anchorToTarget True to anchor the tooltip to the target + * element, false to anchor it relative to the mouse coordinates (defaults + * to true). When anchorToTarget is true, use + * {@link #defaultAlign} to control tooltip alignment to the + * target element. When anchorToTarget is false, use + * {@link #anchorPosition} instead to control alignment. + */ + anchorToTarget: true, + /** + * @cfg {Number} anchorOffset A numeric pixel value used to offset the + * default position of the anchor arrow (defaults to 0). When the anchor + * position is on the top or bottom of the tooltip, anchorOffset + * will be used as a horizontal offset. Likewise, when the anchor position + * is on the left or right side, anchorOffset will be used as + * a vertical offset. + */ + anchorOffset: 0, + /** + * @cfg {String} delegate

Optional. A {@link Ext.DomQuery DomQuery} + * selector which allows selection of individual elements within the + * {@link #target} element to trigger showing and hiding the + * ToolTip as the mouse moves within the target.

+ *

When specified, the child element of the target which caused a show + * event is placed into the {@link #triggerElement} property + * before the ToolTip is shown.

+ *

This may be useful when a Component has regular, repeating elements + * in it, each of which need a ToolTip which contains information specific + * to that element. For example:


+var myGrid = Ext.create('Ext.grid.GridPanel', gridConfig);
+myGrid.on('render', function(grid) {
+    var view = grid.getView();    // Capture the grid's view.
+    grid.tip = Ext.create('Ext.tip.ToolTip', {
+        target: view.el,          // The overall target element.
+        delegate: view.itemSelector, // Each grid row causes its own seperate show and hide.
+        trackMouse: true,         // Moving within the row should not hide the tip.
+        renderTo: Ext.getBody(),  // Render immediately so that tip.body can be referenced prior to the first show.
+        listeners: {              // Change content dynamically depending on which element triggered the show.
+            beforeshow: function(tip) {
+                tip.update('Over Record ID ' + view.getRecord(tip.triggerElement).id);
+            }
+        }
+    });
+});
+     *
+ */ + + // private + targetCounter: 0, + quickShowInterval: 250, + + // private + initComponent: function() { + var me = this; + me.callParent(arguments); + me.lastActive = new Date(); + me.setTarget(me.target); + me.origAnchor = me.anchor; + }, + + // private + onRender: function(ct, position) { + var me = this; + me.callParent(arguments); + me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition(); + me.anchorEl = me.el.createChild({ + cls: Ext.baseCSSPrefix + 'tip-anchor ' + me.anchorCls + }); + }, + + // private + afterRender: function() { + var me = this, + zIndex; + + me.callParent(arguments); + zIndex = parseInt(me.el.getZIndex(), 10) || 0; + me.anchorEl.setStyle('z-index', zIndex + 1).setVisibilityMode(Ext.core.Element.DISPLAY); + }, + + /** + * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse moves over the element. + * @param {Mixed} t The Element, HtmlElement, or ID of an element to bind to + */ + setTarget: function(target) { + var me = this, + t = Ext.get(target), + tg; + + if (me.target) { + tg = Ext.get(me.target); + me.mun(tg, 'mouseover', me.onTargetOver, me); + me.mun(tg, 'mouseout', me.onTargetOut, me); + me.mun(tg, 'mousemove', me.onMouseMove, me); + } + + me.target = t; + if (t) { + + me.mon(t, { + // TODO - investigate why IE6/7 seem to fire recursive resize in e.getXY + // breaking QuickTip#onTargetOver (EXTJSIV-1608) + freezeEvent: true, + + mouseover: me.onTargetOver, + mouseout: me.onTargetOut, + mousemove: me.onMouseMove, + scope: me + }); + } + if (me.anchor) { + me.anchorTarget = me.target; + } + }, + + // private + onMouseMove: function(e) { + var me = this, + t = me.delegate ? e.getTarget(me.delegate) : me.triggerElement = true, + xy; + if (t) { + me.targetXY = e.getXY(); + if (t === me.triggerElement) { + if (!me.hidden && me.trackMouse) { + xy = me.getTargetXY(); + if (me.constrainPosition) { + xy = me.el.adjustForConstraints(xy, me.el.dom.parentNode); + } + me.setPagePosition(xy); + } + } else { + me.hide(); + me.lastActive = new Date(0); + me.onTargetOver(e); + } + } else if ((!me.closable && me.isVisible()) && me.autoHide !== false) { + me.hide(); + } + }, + + // private + getTargetXY: function() { + var me = this, + mouseOffset; + if (me.delegate) { + me.anchorTarget = me.triggerElement; + } + if (me.anchor) { + me.targetCounter++; + var offsets = me.getOffsets(), + xy = (me.anchorToTarget && !me.trackMouse) ? me.el.getAlignToXY(me.anchorTarget, me.getAnchorAlign()) : me.targetXY, + dw = Ext.core.Element.getViewWidth() - 5, + dh = Ext.core.Element.getViewHeight() - 5, + de = document.documentElement, + bd = document.body, + scrollX = (de.scrollLeft || bd.scrollLeft || 0) + 5, + scrollY = (de.scrollTop || bd.scrollTop || 0) + 5, + axy = [xy[0] + offsets[0], xy[1] + offsets[1]], + sz = me.getSize(), + constrainPosition = me.constrainPosition; + + me.anchorEl.removeCls(me.anchorCls); + + if (me.targetCounter < 2 && constrainPosition) { + if (axy[0] < scrollX) { + if (me.anchorToTarget) { + me.defaultAlign = 'l-r'; + if (me.mouseOffset) { + me.mouseOffset[0] *= -1; + } + } + me.anchor = 'left'; + return me.getTargetXY(); + } + if (axy[0] + sz.width > dw) { + if (me.anchorToTarget) { + me.defaultAlign = 'r-l'; + if (me.mouseOffset) { + me.mouseOffset[0] *= -1; + } + } + me.anchor = 'right'; + return me.getTargetXY(); + } + if (axy[1] < scrollY) { + if (me.anchorToTarget) { + me.defaultAlign = 't-b'; + if (me.mouseOffset) { + me.mouseOffset[1] *= -1; + } + } + me.anchor = 'top'; + return me.getTargetXY(); + } + if (axy[1] + sz.height > dh) { + if (me.anchorToTarget) { + me.defaultAlign = 'b-t'; + if (me.mouseOffset) { + me.mouseOffset[1] *= -1; + } + } + me.anchor = 'bottom'; + return me.getTargetXY(); + } + } + + me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition(); + me.anchorEl.addCls(me.anchorCls); + me.targetCounter = 0; + return axy; + } else { + mouseOffset = me.getMouseOffset(); + return (me.targetXY) ? [me.targetXY[0] + mouseOffset[0], me.targetXY[1] + mouseOffset[1]] : mouseOffset; + } + }, + + getMouseOffset: function() { + var me = this, + offset = me.anchor ? [0, 0] : [15, 18]; + if (me.mouseOffset) { + offset[0] += me.mouseOffset[0]; + offset[1] += me.mouseOffset[1]; + } + return offset; + }, + + // private + getAnchorPosition: function() { + var me = this, + m; + if (me.anchor) { + me.tipAnchor = me.anchor.charAt(0); + } else { + m = me.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/); + // + if (!m) { + Ext.Error.raise('The AnchorTip.defaultAlign value "' + me.defaultAlign + '" is invalid.'); + } + // + me.tipAnchor = m[1].charAt(0); + } + + switch (me.tipAnchor) { + case 't': + return 'top'; + case 'b': + return 'bottom'; + case 'r': + return 'right'; + } + return 'left'; + }, + + // private + getAnchorAlign: function() { + switch (this.anchor) { + case 'top': + return 'tl-bl'; + case 'left': + return 'tl-tr'; + case 'right': + return 'tr-tl'; + default: + return 'bl-tl'; + } + }, + + // private + getOffsets: function() { + var me = this, + mouseOffset, + offsets, + ap = me.getAnchorPosition().charAt(0); + if (me.anchorToTarget && !me.trackMouse) { + switch (ap) { + case 't': + offsets = [0, 9]; + break; + case 'b': + offsets = [0, -13]; + break; + case 'r': + offsets = [ - 13, 0]; + break; + default: + offsets = [9, 0]; + break; + } + } else { + switch (ap) { + case 't': + offsets = [ - 15 - me.anchorOffset, 30]; + break; + case 'b': + offsets = [ - 19 - me.anchorOffset, -13 - me.el.dom.offsetHeight]; + break; + case 'r': + offsets = [ - 15 - me.el.dom.offsetWidth, -13 - me.anchorOffset]; + break; + default: + offsets = [25, -13 - me.anchorOffset]; + break; + } + } + mouseOffset = me.getMouseOffset(); + offsets[0] += mouseOffset[0]; + offsets[1] += mouseOffset[1]; + + return offsets; + }, + + // private + onTargetOver: function(e) { + var me = this, + t; + + if (me.disabled || e.within(me.target.dom, true)) { + return; + } + t = e.getTarget(me.delegate); + if (t) { + me.triggerElement = t; + me.clearTimer('hide'); + me.targetXY = e.getXY(); + me.delayShow(); + } + }, + + // private + delayShow: function() { + var me = this; + if (me.hidden && !me.showTimer) { + if (Ext.Date.getElapsed(me.lastActive) < me.quickShowInterval) { + me.show(); + } else { + me.showTimer = Ext.defer(me.show, me.showDelay, me); + } + } + else if (!me.hidden && me.autoHide !== false) { + me.show(); + } + }, + + // private + onTargetOut: function(e) { + var me = this; + if (me.disabled || e.within(me.target.dom, true)) { + return; + } + me.clearTimer('show'); + if (me.autoHide !== false) { + me.delayHide(); + } + }, + + // private + delayHide: function() { + var me = this; + if (!me.hidden && !me.hideTimer) { + me.hideTimer = Ext.defer(me.hide, me.hideDelay, me); + } + }, + + /** + * Hides this tooltip if visible. + */ + hide: function() { + var me = this; + me.clearTimer('dismiss'); + me.lastActive = new Date(); + if (me.anchorEl) { + me.anchorEl.hide(); + } + me.callParent(arguments); + delete me.triggerElement; + }, + + /** + * Shows this tooltip at the current event target XY position. + */ + show: function() { + var me = this; + + // Show this Component first, so that sizing can be calculated + // pre-show it off screen so that the el will have dimensions + this.callParent(); + if (this.hidden === false) { + me.setPagePosition(-10000, -10000); + + if (me.anchor) { + me.anchor = me.origAnchor; + } + me.showAt(me.getTargetXY()); + + if (me.anchor) { + me.syncAnchor(); + me.anchorEl.show(); + } else { + me.anchorEl.hide(); + } + } + }, + + // inherit docs + showAt: function(xy) { + var me = this; + me.lastActive = new Date(); + me.clearTimers(); + + // Only call if this is hidden. May have been called from show above. + if (!me.isVisible()) { + this.callParent(arguments); + } + + // Show may have been vetoed. + if (me.isVisible()) { + me.setPagePosition(xy[0], xy[1]); + if (me.constrainPosition || me.constrain) { + me.doConstrain(); + } + me.toFront(true); + } + + if (me.dismissDelay && me.autoHide !== false) { + me.dismissTimer = Ext.defer(me.hide, me.dismissDelay, me); + } + if (me.anchor) { + me.syncAnchor(); + if (!me.anchorEl.isVisible()) { + me.anchorEl.show(); + } + } else { + me.anchorEl.hide(); + } + }, + + // private + syncAnchor: function() { + var me = this, + anchorPos, + targetPos, + offset; + switch (me.tipAnchor.charAt(0)) { + case 't': + anchorPos = 'b'; + targetPos = 'tl'; + offset = [20 + me.anchorOffset, 1]; + break; + case 'r': + anchorPos = 'l'; + targetPos = 'tr'; + offset = [ - 1, 12 + me.anchorOffset]; + break; + case 'b': + anchorPos = 't'; + targetPos = 'bl'; + offset = [20 + me.anchorOffset, -1]; + break; + default: + anchorPos = 'r'; + targetPos = 'tl'; + offset = [1, 12 + me.anchorOffset]; + break; + } + me.anchorEl.alignTo(me.el, anchorPos + '-' + targetPos, offset); + }, + + // private + setPagePosition: function(x, y) { + var me = this; + me.callParent(arguments); + if (me.anchor) { + me.syncAnchor(); + } + }, + + // private + clearTimer: function(name) { + name = name + 'Timer'; + clearTimeout(this[name]); + delete this[name]; + }, + + // private + clearTimers: function() { + var me = this; + me.clearTimer('show'); + me.clearTimer('dismiss'); + me.clearTimer('hide'); + }, + + // private + onShow: function() { + var me = this; + me.callParent(); + me.mon(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me); + }, + + // private + onHide: function() { + var me = this; + me.callParent(); + me.mun(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me); + }, + + // private + onDocMouseDown: function(e) { + var me = this; + if (me.autoHide !== true && !me.closable && !e.within(me.el.dom)) { + me.disable(); + Ext.defer(me.doEnable, 100, me); + } + }, + + // private + doEnable: function() { + if (!this.isDestroyed) { + this.enable(); + } + }, + + // private + onDisable: function() { + this.callParent(); + this.clearTimers(); + this.hide(); + }, + + beforeDestroy: function() { + var me = this; + me.clearTimers(); + Ext.destroy(me.anchorEl); + delete me.anchorEl; + delete me.target; + delete me.anchorTarget; + delete me.triggerElement; + me.callParent(); + }, + + // private + onDestroy: function() { + Ext.getDoc().un('mousedown', this.onDocMouseDown, this); + this.callParent(); + } +}); + +/** + * @class Ext.tip.QuickTip + * @extends Ext.tip.ToolTip + * A specialized tooltip class for tooltips that can be specified in markup and automatically managed by the global + * {@link Ext.tip.QuickTipManager} instance. See the QuickTipManager class header for additional usage details and examples. + * @constructor + * Create a new Tip + * @param {Object} config The configuration options + * @xtype quicktip + */ +Ext.define('Ext.tip.QuickTip', { + extend: 'Ext.tip.ToolTip', + alternateClassName: 'Ext.QuickTip', + /** + * @cfg {Mixed} target The target HTMLElement, Ext.core.Element or id to associate with this Quicktip (defaults to the document). + */ + /** + * @cfg {Boolean} interceptTitles True to automatically use the element's DOM title value if available (defaults to false). + */ + interceptTitles : false, + + // Force creation of header Component + title: ' ', + + // private + tagConfig : { + namespace : "data-", + attribute : "qtip", + width : "qwidth", + target : "target", + title : "qtitle", + hide : "hide", + cls : "qclass", + align : "qalign", + anchor : "anchor" + }, + + // private + initComponent : function(){ + var me = this; + + me.target = me.target || Ext.getDoc(); + me.targets = me.targets || {}; + me.callParent(); + }, + + /** + * Configures a new quick tip instance and assigns it to a target element. The following config values are + * supported (for example usage, see the {@link Ext.tip.QuickTipManager} class header): + *
    + *
  • autoHide
  • + *
  • cls
  • + *
  • dismissDelay (overrides the singleton value)
  • + *
  • target (required)
  • + *
  • text (required)
  • + *
  • title
  • + *
  • width
+ * @param {Object} config The config object + */ + register : function(config){ + var configs = Ext.isArray(config) ? config : arguments, + i = 0, + len = configs.length, + target, j, targetLen; + + for (; i < len; i++) { + config = configs[i]; + target = config.target; + if (target) { + if (Ext.isArray(target)) { + for (j = 0, targetLen = target.length; j < targetLen; j++) { + this.targets[Ext.id(target[j])] = config; + } + } else{ + this.targets[Ext.id(target)] = config; + } + } + } + }, + + /** + * Removes this quick tip from its element and destroys it. + * @param {String/HTMLElement/Element} el The element from which the quick tip is to be removed. + */ + unregister : function(el){ + delete this.targets[Ext.id(el)]; + }, + + /** + * Hides a visible tip or cancels an impending show for a particular element. + * @param {String/HTMLElement/Element} el The element that is the target of the tip. + */ + cancelShow: function(el){ + var me = this, + activeTarget = me.activeTarget; + + el = Ext.get(el).dom; + if (me.isVisible()) { + if (activeTarget && activeTarget.el == el) { + me.hide(); + } + } else if (activeTarget && activeTarget.el == el) { + me.clearTimer('show'); + } + }, + + getTipCfg: function(e) { + var t = e.getTarget(), + ttp, + cfg; + + if(this.interceptTitles && t.title && Ext.isString(t.title)){ + ttp = t.title; + t.qtip = ttp; + t.removeAttribute("title"); + e.preventDefault(); + } + else { + cfg = this.tagConfig; + t = e.getTarget('[' + cfg.namespace + cfg.attribute + ']'); + if (t) { + ttp = t.getAttribute(cfg.namespace + cfg.attribute); + } + } + return ttp; + }, + + // private + onTargetOver : function(e){ + var me = this, + target = e.getTarget(), + elTarget, + cfg, + ns, + ttp, + autoHide; + + if (me.disabled) { + return; + } + + // TODO - this causes "e" to be recycled in IE6/7 (EXTJSIV-1608) so ToolTip#setTarget + // was changed to include freezeEvent. The issue seems to be a nested 'resize' event + // that smashed Ext.EventObject. + me.targetXY = e.getXY(); + + if(!target || target.nodeType !== 1 || target == document || target == document.body){ + return; + } + + if (me.activeTarget && ((target == me.activeTarget.el) || Ext.fly(me.activeTarget.el).contains(target))) { + me.clearTimer('hide'); + me.show(); + return; + } + + if (target) { + Ext.Object.each(me.targets, function(key, value) { + var targetEl = Ext.fly(value.target); + if (targetEl && (targetEl.dom === target || targetEl.contains(target))) { + elTarget = targetEl.dom; + return false; + } + }); + if (elTarget) { + me.activeTarget = me.targets[elTarget.id]; + me.activeTarget.el = target; + me.anchor = me.activeTarget.anchor; + if (me.anchor) { + me.anchorTarget = target; + } + me.delayShow(); + return; + } + } + + elTarget = Ext.get(target); + cfg = me.tagConfig; + ns = cfg.namespace; + ttp = me.getTipCfg(e); + + if (ttp) { + autoHide = elTarget.getAttribute(ns + cfg.hide); + + me.activeTarget = { + el: target, + text: ttp, + width: +elTarget.getAttribute(ns + cfg.width) || null, + autoHide: autoHide != "user" && autoHide !== 'false', + title: elTarget.getAttribute(ns + cfg.title), + cls: elTarget.getAttribute(ns + cfg.cls), + align: elTarget.getAttribute(ns + cfg.align) + + }; + me.anchor = elTarget.getAttribute(ns + cfg.anchor); + if (me.anchor) { + me.anchorTarget = target; + } + me.delayShow(); + } + }, + + // private + onTargetOut : function(e){ + var me = this; + + // If moving within the current target, and it does not have a new tip, ignore the mouseout + if (me.activeTarget && e.within(me.activeTarget.el) && !me.getTipCfg(e)) { + return; + } + + me.clearTimer('show'); + if (me.autoHide !== false) { + me.delayHide(); + } + }, + + // inherit docs + showAt : function(xy){ + var me = this, + target = me.activeTarget; + + if (target) { + if (!me.rendered) { + me.render(Ext.getBody()); + me.activeTarget = target; + } + if (target.title) { + me.setTitle(target.title || ''); + me.header.show(); + } else { + me.header.hide(); + } + me.body.update(target.text); + me.autoHide = target.autoHide; + me.dismissDelay = target.dismissDelay || me.dismissDelay; + if (me.lastCls) { + me.el.removeCls(me.lastCls); + delete me.lastCls; + } + if (target.cls) { + me.el.addCls(target.cls); + me.lastCls = target.cls; + } + + me.setWidth(target.width); + + if (me.anchor) { + me.constrainPosition = false; + } else if (target.align) { // TODO: this doesn't seem to work consistently + xy = me.el.getAlignToXY(target.el, target.align); + me.constrainPosition = false; + }else{ + me.constrainPosition = true; + } + } + me.callParent([xy]); + }, + + // inherit docs + hide: function(){ + delete this.activeTarget; + this.callParent(); + } +}); + +/** + * @class Ext.tip.QuickTipManager + *

Provides attractive and customizable tooltips for any element. The QuickTips + * singleton is used to configure and manage tooltips globally for multiple elements + * in a generic manner. To create individual tooltips with maximum customizability, + * you should consider either {@link Ext.tip.Tip} or {@link Ext.tip.ToolTip}.

+ *

Quicktips can be configured via tag attributes directly in markup, or by + * registering quick tips programmatically via the {@link #register} method.

+ *

The singleton's instance of {@link Ext.tip.QuickTip} is available via + * {@link #getQuickTip}, and supports all the methods, and all the all the + * configuration properties of Ext.tip.QuickTip. These settings will apply to all + * tooltips shown by the singleton.

+ *

Below is the summary of the configuration properties which can be used. + * For detailed descriptions see the config options for the {@link Ext.tip.QuickTip QuickTip} class

+ *

QuickTips singleton configs (all are optional)

+ *
  • dismissDelay
  • + *
  • hideDelay
  • + *
  • maxWidth
  • + *
  • minWidth
  • + *
  • showDelay
  • + *
  • trackMouse
+ *

Target element configs (optional unless otherwise noted)

+ *
  • autoHide
  • + *
  • cls
  • + *
  • dismissDelay (overrides singleton value)
  • + *
  • target (required)
  • + *
  • text (required)
  • + *
  • title
  • + *
  • width
+ *

Here is an example showing how some of these config options could be used:

+ * + * {@img Ext.tip.QuickTipManager/Ext.tip.QuickTipManager.png Ext.tip.QuickTipManager component} + * + * ## Code + * // Init the singleton. Any tag-based quick tips will start working. + * Ext.tip.QuickTipManager.init(); + * + * // Apply a set of config properties to the singleton + * Ext.apply(Ext.tip.QuickTipManager.getQuickTip(), { + * maxWidth: 200, + * minWidth: 100, + * showDelay: 50 // Show 50ms after entering target + * }); + * + * // Create a small panel to add a quick tip to + * Ext.create('Ext.container.Container', { + * id: 'quickTipContainer', + * width: 200, + * height: 150, + * style: { + * backgroundColor:'#000000' + * }, + * renderTo: Ext.getBody() + * }); + * + * + * // Manually register a quick tip for a specific element + * Ext.tip.QuickTipManager.register({ + * target: 'quickTipContainer', + * title: 'My Tooltip', + * text: 'This tooltip was added in code', + * width: 100, + * dismissDelay: 10000 // Hide after 10 seconds hover + * }); + + *

To register a quick tip in markup, you simply add one or more of the valid QuickTip attributes prefixed with + * the ext: namespace. The HTML element itself is automatically set as the quick tip target. Here is the summary + * of supported attributes (optional unless otherwise noted):

+ *
  • hide: Specifying "user" is equivalent to setting autoHide = false. Any other value will be the + * same as autoHide = true.
  • + *
  • qclass: A CSS class to be applied to the quick tip (equivalent to the 'cls' target element config).
  • + *
  • qtip (required): The quick tip text (equivalent to the 'text' target element config).
  • + *
  • qtitle: The quick tip title (equivalent to the 'title' target element config).
  • + *
  • qwidth: The quick tip width (equivalent to the 'width' target element config).
+ *

Here is an example of configuring an HTML element to display a tooltip from markup:

+ *

+// Add a quick tip to an HTML button
+<input type="button" value="OK" ext:qtitle="OK Button" ext:qwidth="100"
+     data-qtip="This is a quick tip from markup!"></input>
+
+ * @singleton + */ +Ext.define('Ext.tip.QuickTipManager', function() { + var tip, + disabled = false; + + return { + requires: ['Ext.tip.QuickTip'], + singleton: true, + alternateClassName: 'Ext.QuickTips', + /** + * Initialize the global QuickTips instance and prepare any quick tips. + * @param {Boolean} autoRender True to render the QuickTips container immediately to preload images. (Defaults to true) + */ + init : function(autoRender){ + if (!tip) { + if (!Ext.isReady) { + Ext.onReady(function(){ + Ext.tip.QuickTipManager.init(autoRender); + }); + return; + } + tip = Ext.create('Ext.tip.QuickTip', { + disabled: disabled, + renderTo: autoRender !== false ? document.body : undefined + }); + } + }, + + /** + * Destroy the QuickTips instance. + */ + destroy: function() { + if (tip) { + var undef; + tip.destroy(); + tip = undef; + } + }, + + // Protected method called by the dd classes + ddDisable : function(){ + // don't disable it if we don't need to + if(tip && !disabled){ + tip.disable(); + } + }, + + // Protected method called by the dd classes + ddEnable : function(){ + // only enable it if it hasn't been disabled + if(tip && !disabled){ + tip.enable(); + } + }, + + /** + * Enable quick tips globally. + */ + enable : function(){ + if(tip){ + tip.enable(); + } + disabled = false; + }, + + /** + * Disable quick tips globally. + */ + disable : function(){ + if(tip){ + tip.disable(); + } + disabled = true; + }, + + /** + * Returns true if quick tips are enabled, else false. + * @return {Boolean} + */ + isEnabled : function(){ + return tip !== undefined && !tip.disabled; + }, + + /** + * Gets the single {@link Ext.tip.QuickTip QuickTip} instance used to show tips from all registered elements. + * @return {Ext.tip.QuickTip} + */ + getQuickTip : function(){ + return tip; + }, + + /** + * Configures a new quick tip instance and assigns it to a target element. See + * {@link Ext.tip.QuickTip#register} for details. + * @param {Object} config The config object + */ + register : function(){ + tip.register.apply(tip, arguments); + }, + + /** + * Removes any registered quick tip from the target element and destroys it. + * @param {String/HTMLElement/Element} el The element from which the quick tip is to be removed. + */ + unregister : function(){ + tip.unregister.apply(tip, arguments); + }, + + /** + * Alias of {@link #register}. + * @param {Object} config The config object + */ + tips : function(){ + tip.register.apply(tip, arguments); + } + }; +}()); +/** + * @class Ext.app.Application + * @constructor + * + * Represents an Ext JS 4 application, which is typically a single page app using a {@link Ext.container.Viewport Viewport}. + * A typical Ext.app.Application might look like this: + * + * Ext.application({ + name: 'MyApp', + launch: function() { + Ext.create('Ext.container.Viewport', { + items: { + html: 'My App' + } + }); + } + }); + * + * This does several things. First it creates a global variable called 'MyApp' - all of your Application's classes (such + * as its Models, Views and Controllers) will reside under this single namespace, which drastically lowers the chances + * of colliding global variables. + * + * When the page is ready and all of your JavaScript has loaded, your Application's {@link #launch} function is called, + * at which time you can run the code that starts your app. Usually this consists of creating a Viewport, as we do in + * the example above. + * + * Telling Application about the rest of the app + * + * Because an Ext.app.Application represents an entire app, we should tell it about the other parts of the app - namely + * the Models, Views and Controllers that are bundled with the application. Let's say we have a blog management app; we + * might have Models and Controllers for Posts and Comments, and Views for listing, adding and editing Posts and Comments. + * Here's how we'd tell our Application about all these things: + * + * Ext.application({ + name: 'Blog', + models: ['Post', 'Comment'], + controllers: ['Posts', 'Comments'], + + launch: function() { + ... + } + }); + * + * Note that we didn't actually list the Views directly in the Application itself. This is because Views are managed by + * Controllers, so it makes sense to keep those dependencies there. The Application will load each of the specified + * Controllers using the pathing conventions laid out in the application + * architecture guide - in this case expecting the controllers to reside in app/controller/Posts.js and + * app/controller/Comments.js. In turn, each Controller simply needs to list the Views it uses and they will be + * automatically loaded. Here's how our Posts controller like be defined: + * + * Ext.define('MyApp.controller.Posts', { + extend: 'Ext.app.Controller', + views: ['posts.List', 'posts.Edit'], + + //the rest of the Controller here + }); + * + * Because we told our Application about our Models and Controllers, and our Controllers about their Views, Ext JS will + * automatically load all of our app files for us. This means we don't have to manually add script tags into our html + * files whenever we add a new class, but more importantly it enables us to create a minimized build of our entire + * application using the Ext JS 4 SDK Tools. + * + * For more information about writing Ext JS 4 applications, please see the + * application architecture guide. + * + * @markdown + * @docauthor Ed Spencer + */ +Ext.define('Ext.app.Application', { + extend: 'Ext.app.Controller', + + requires: [ + 'Ext.ModelManager', + 'Ext.data.Model', + 'Ext.data.StoreManager', + 'Ext.tip.QuickTipManager', + 'Ext.ComponentManager', + 'Ext.app.EventBus' + ], + + /** + * @cfg {Object} name The name of your application. This will also be the namespace for your views, controllers + * models and stores. Don't use spaces or special characters in the name. + */ + + /** + * @cfg {Object} scope The scope to execute the {@link #launch} function in. Defaults to the Application + * instance. + */ + scope: undefined, + + /** + * @cfg {Boolean} enableQuickTips True to automatically set up Ext.tip.QuickTip support (defaults to true) + */ + enableQuickTips: true, + + /** + * @cfg {String} defaultUrl When the app is first loaded, this url will be redirected to. Defaults to undefined + */ + + /** + * @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. + * Defaults to 'app' + */ + appFolder: 'app', + + /** + * @cfg {Boolean} autoCreateViewport Automatically loads and instantiates AppName.view.Viewport before firing the launch function. + */ + autoCreateViewport: true, + + constructor: function(config) { + config = config || {}; + Ext.apply(this, config); + + var requires = config.requires || []; + + Ext.Loader.setPath(this.name, this.appFolder); + + if (this.paths) { + Ext.Object.each(this.paths, function(key, value) { + Ext.Loader.setPath(key, value); + }); + } + + this.callParent(arguments); + + this.eventbus = Ext.create('Ext.app.EventBus'); + + var controllers = this.controllers, + ln = controllers.length, + i, controller; + + this.controllers = Ext.create('Ext.util.MixedCollection'); + + if (this.autoCreateViewport) { + requires.push(this.getModuleClassName('Viewport', 'view')); + } + + for (i = 0; i < ln; i++) { + requires.push(this.getModuleClassName(controllers[i], 'controller')); + } + + Ext.require(requires); + + Ext.onReady(function() { + for (i = 0; i < ln; i++) { + controller = this.getController(controllers[i]); + controller.init(this); + } + + this.onBeforeLaunch.call(this); + }, this); + }, + + control: function(selectors, listeners, controller) { + this.eventbus.control(selectors, listeners, controller); + }, + + /** + * Called automatically when the page has completely loaded. This is an empty function that should be + * overridden by each application that needs to take action on page load + * @property launch + * @type Function + * @param {String} profile The detected {@link #profiles application profile} + * @return {Boolean} By default, the Application will dispatch to the configured startup controller and + * action immediately after running the launch function. Return false to prevent this behavior. + */ + launch: Ext.emptyFn, + + /** + * @private + */ + onBeforeLaunch: function() { + if (this.enableQuickTips) { + Ext.tip.QuickTipManager.init(); + } + + if (this.autoCreateViewport) { + this.getView('Viewport').create(); + } + + this.launch.call(this.scope || this); + this.launched = true; + this.fireEvent('launch', this); + + this.controllers.each(function(controller) { + controller.onLaunch(this); + }, this); + }, + + getModuleClassName: function(name, type) { + var namespace = Ext.Loader.getPrefix(name); + + if (namespace.length > 0 && namespace !== name) { + return name; + } + + return this.name + '.' + type + '.' + name; + }, + + getController: function(name) { + var controller = this.controllers.get(name); + + if (!controller) { + controller = Ext.create(this.getModuleClassName(name, 'controller'), { + application: this, + id: name + }); + + this.controllers.add(controller); + } + + return controller; + }, + + getStore: function(name) { + var store = Ext.StoreManager.get(name); + + if (!store) { + store = Ext.create(this.getModuleClassName(name, 'store'), { + storeId: name + }); + } + + return store; + }, + + getModel: function(model) { + model = this.getModuleClassName(model, 'model'); + + return Ext.ModelManager.getModel(model); + }, + + getView: function(view) { + view = this.getModuleClassName(view, 'view'); + + return Ext.ClassManager.get(view); + } +}); + +/** + * @class Ext.chart.Callout + * @ignore + */ +Ext.define('Ext.chart.Callout', { + + /* Begin Definitions */ + + /* End Definitions */ + + constructor: function(config) { + if (config.callouts) { + config.callouts.styles = Ext.applyIf(config.callouts.styles || {}, { + color: "#000", + font: "11px Helvetica, sans-serif" + }); + this.callouts = Ext.apply(this.callouts || {}, config.callouts); + this.calloutsArray = []; + } + }, + + renderCallouts: function() { + if (!this.callouts) { + return; + } + + var me = this, + items = me.items, + animate = me.chart.animate, + config = me.callouts, + styles = config.styles, + group = me.calloutsArray, + store = me.chart.store, + len = store.getCount(), + ratio = items.length / len, + previouslyPlacedCallouts = [], + i, + count, + j, + p; + + for (i = 0, count = 0; i < len; i++) { + for (j = 0; j < ratio; j++) { + var item = items[count], + label = group[count], + storeItem = store.getAt(i), + display; + + display = config.filter(storeItem); + + if (!display && !label) { + count++; + continue; + } + + if (!label) { + group[count] = label = me.onCreateCallout(storeItem, item, i, display, j, count); + } + for (p in label) { + if (label[p] && label[p].setAttributes) { + label[p].setAttributes(styles, true); + } + } + if (!display) { + for (p in label) { + if (label[p]) { + if (label[p].setAttributes) { + label[p].setAttributes({ + hidden: true + }, true); + } else if(label[p].setVisible) { + label[p].setVisible(false); + } + } + } + } + config.renderer(label, storeItem); + me.onPlaceCallout(label, storeItem, item, i, display, animate, + j, count, previouslyPlacedCallouts); + previouslyPlacedCallouts.push(label); + count++; + } + } + this.hideCallouts(count); + }, + + onCreateCallout: function(storeItem, item, i, display) { + var me = this, + group = me.calloutsGroup, + config = me.callouts, + styles = config.styles, + width = styles.width, + height = styles.height, + chart = me.chart, + surface = chart.surface, + calloutObj = { + //label: false, + //box: false, + lines: false + }; + + calloutObj.lines = surface.add(Ext.apply({}, + { + type: 'path', + path: 'M0,0', + stroke: me.getLegendColor() || '#555' + }, + styles)); + + if (config.items) { + calloutObj.panel = Ext.create('widget.panel', { + style: "position: absolute;", + width: width, + height: height, + items: config.items, + renderTo: chart.el + }); + } + + return calloutObj; + }, + + hideCallouts: function(index) { + var calloutsArray = this.calloutsArray, + len = calloutsArray.length, + co, + p; + while (len-->index) { + co = calloutsArray[len]; + for (p in co) { + if (co[p]) { + co[p].hide(true); + } + } + } + } +}); + +/** + * @class Ext.draw.CompositeSprite + * @extends Ext.util.MixedCollection + * + * A composite Sprite handles a group of sprites with common methods to a sprite + * such as `hide`, `show`, `setAttributes`. These methods are applied to the set of sprites + * added to the group. + * + * CompositeSprite extends {@link Ext.util.MixedCollection} so you can use the same methods + * in `MixedCollection` to iterate through sprites, add and remove elements, etc. + * + * In order to create a CompositeSprite, one has to provide a handle to the surface where it is + * rendered: + * + * var group = Ext.create('Ext.draw.CompositeSprite', { + * surface: drawComponent.surface + * }); + * + * Then just by using `MixedCollection` methods it's possible to add {@link Ext.draw.Sprite}s: + * + * group.add(sprite1); + * group.add(sprite2); + * group.add(sprite3); + * + * And then apply common Sprite methods to them: + * + * group.setAttributes({ + * fill: '#f00' + * }, true); + */ +Ext.define('Ext.draw.CompositeSprite', { + + /* Begin Definitions */ + + extend: 'Ext.util.MixedCollection', + mixins: { + animate: 'Ext.util.Animate' + }, + + /* End Definitions */ + isCompositeSprite: true, + constructor: function(config) { + var me = this; + + config = config || {}; + Ext.apply(me, config); + + me.addEvents( + 'mousedown', + 'mouseup', + 'mouseover', + 'mouseout', + 'click' + ); + me.id = Ext.id(null, 'ext-sprite-group-'); + me.callParent(); + }, + + // @private + onClick: function(e) { + this.fireEvent('click', e); + }, + + // @private + onMouseUp: function(e) { + this.fireEvent('mouseup', e); + }, + + // @private + onMouseDown: function(e) { + this.fireEvent('mousedown', e); + }, + + // @private + onMouseOver: function(e) { + this.fireEvent('mouseover', e); + }, + + // @private + onMouseOut: function(e) { + this.fireEvent('mouseout', e); + }, + + attachEvents: function(o) { + var me = this; + + o.on({ + scope: me, + mousedown: me.onMouseDown, + mouseup: me.onMouseUp, + mouseover: me.onMouseOver, + mouseout: me.onMouseOut, + click: me.onClick + }); + }, + + /** Add a Sprite to the Group */ + add: function(key, o) { + var result = this.callParent(arguments); + this.attachEvents(result); + return result; + }, + + insert: function(index, key, o) { + return this.callParent(arguments); + }, + + /** Remove a Sprite from the Group */ + remove: function(o) { + var me = this; + + o.un({ + scope: me, + mousedown: me.onMouseDown, + mouseup: me.onMouseUp, + mouseover: me.onMouseOver, + mouseout: me.onMouseOut, + click: me.onClick + }); + me.callParent(arguments); + }, + + /** + * Returns the group bounding box. + * Behaves like {@link Ext.draw.Sprite} getBBox method. + */ + getBBox: function() { + var i = 0, + sprite, + bb, + items = this.items, + len = this.length, + infinity = Infinity, + minX = infinity, + maxHeight = -infinity, + minY = infinity, + maxWidth = -infinity, + maxWidthBBox, maxHeightBBox; + + for (; i < len; i++) { + sprite = items[i]; + if (sprite.el) { + bb = sprite.getBBox(); + minX = Math.min(minX, bb.x); + minY = Math.min(minY, bb.y); + maxHeight = Math.max(maxHeight, bb.height + bb.y); + maxWidth = Math.max(maxWidth, bb.width + bb.x); + } + } + + return { + x: minX, + y: minY, + height: maxHeight - minY, + width: maxWidth - minX + }; + }, + + /** + * Iterates through all sprites calling + * `setAttributes` on each one. For more information + * {@link Ext.draw.Sprite} provides a description of the + * attributes that can be set with this method. + */ + setAttributes: function(attrs, redraw) { + var i = 0, + items = this.items, + len = this.length; + + for (; i < len; i++) { + items[i].setAttributes(attrs, redraw); + } + return this; + }, + + /** + * Hides all sprites. If the first parameter of the method is true + * then a redraw will be forced for each sprite. + */ + hide: function(attrs) { + var i = 0, + items = this.items, + len = this.length; + + for (; i < len; i++) { + items[i].hide(); + } + return this; + }, + + /** + * Shows all sprites. If the first parameter of the method is true + * then a redraw will be forced for each sprite. + */ + show: function(attrs) { + var i = 0, + items = this.items, + len = this.length; + + for (; i < len; i++) { + items[i].show(); + } + return this; + }, + + redraw: function() { + var me = this, + i = 0, + items = me.items, + surface = me.getSurface(), + len = me.length; + + if (surface) { + for (; i < len; i++) { + surface.renderItem(items[i]); + } + } + return me; + }, + + setStyle: function(obj) { + var i = 0, + items = this.items, + len = this.length, + item, el; + + for (; i < len; i++) { + item = items[i]; + el = item.el; + if (el) { + el.setStyle(obj); + } + } + }, + + addCls: function(obj) { + var i = 0, + items = this.items, + surface = this.getSurface(), + len = this.length; + + if (surface) { + for (; i < len; i++) { + surface.addCls(items[i], obj); + } + } + }, + + removeCls: function(obj) { + var i = 0, + items = this.items, + surface = this.getSurface(), + len = this.length; + + if (surface) { + for (; i < len; i++) { + surface.removeCls(items[i], obj); + } + } + }, + + /** + * Grab the surface from the items + * @private + * @return {Ext.draw.Surface} The surface, null if not found + */ + getSurface: function(){ + var first = this.first(); + if (first) { + return first.surface; + } + return null; + }, + + /** + * Destroys the SpriteGroup + */ + destroy: function(){ + var me = this, + surface = me.getSurface(), + item; + + if (surface) { + while (me.getCount() > 0) { + item = me.first(); + me.remove(item); + surface.remove(item); + } + } + me.clearListeners(); + } +}); + +/** + * @class Ext.layout.component.Draw + * @extends Ext.layout.component.Component + * @private + * + */ + +Ext.define('Ext.layout.component.Draw', { + + /* Begin Definitions */ + + alias: 'layout.draw', + + extend: 'Ext.layout.component.Auto', + + /* End Definitions */ + + type: 'draw', + + onLayout : function(width, height) { + this.owner.surface.setSize(width, height); + this.callParent(arguments); + } +}); +/** + * @class Ext.chart.theme.Theme + * @ignore + */ +Ext.define('Ext.chart.theme.Theme', { + + /* Begin Definitions */ + + requires: ['Ext.draw.Color'], + + /* End Definitions */ + + theme: 'Base', + themeAttrs: false, + + initTheme: function(theme) { + var me = this, + themes = Ext.chart.theme, + key, gradients; + if (theme) { + theme = theme.split(':'); + for (key in themes) { + if (key == theme[0]) { + gradients = theme[1] == 'gradients'; + me.themeAttrs = new themes[key]({ + useGradients: gradients + }); + if (gradients) { + me.gradients = me.themeAttrs.gradients; + } + if (me.themeAttrs.background) { + me.background = me.themeAttrs.background; + } + return; + } + } + // + Ext.Error.raise('No theme found named "' + theme + '"'); + // + } + } +}, +// This callback is executed right after when the class is created. This scope refers to the newly created class itself +function() { + /* Theme constructor: takes either a complex object with styles like: + + { + axis: { + fill: '#000', + 'stroke-width': 1 + }, + axisLabelTop: { + fill: '#000', + font: '11px Arial' + }, + axisLabelLeft: { + fill: '#000', + font: '11px Arial' + }, + axisLabelRight: { + fill: '#000', + font: '11px Arial' + }, + axisLabelBottom: { + fill: '#000', + font: '11px Arial' + }, + axisTitleTop: { + fill: '#000', + font: '11px Arial' + }, + axisTitleLeft: { + fill: '#000', + font: '11px Arial' + }, + axisTitleRight: { + fill: '#000', + font: '11px Arial' + }, + axisTitleBottom: { + fill: '#000', + font: '11px Arial' + }, + series: { + 'stroke-width': 1 + }, + seriesLabel: { + font: '12px Arial', + fill: '#333' + }, + marker: { + stroke: '#555', + fill: '#000', + radius: 3, + size: 3 + }, + seriesThemes: [{ + fill: '#C6DBEF' + }, { + fill: '#9ECAE1' + }, { + fill: '#6BAED6' + }, { + fill: '#4292C6' + }, { + fill: '#2171B5' + }, { + fill: '#084594' + }], + markerThemes: [{ + fill: '#084594', + type: 'circle' + }, { + fill: '#2171B5', + type: 'cross' + }, { + fill: '#4292C6', + type: 'plus' + }] + } + + ...or also takes just an array of colors and creates the complex object: + + { + colors: ['#aaa', '#bcd', '#eee'] + } + + ...or takes just a base color and makes a theme from it + + { + baseColor: '#bce' + } + + To create a new theme you may add it to the Themes object: + + Ext.chart.theme.MyNewTheme = Ext.extend(Object, { + constructor: function(config) { + Ext.chart.theme.call(this, config, { + baseColor: '#mybasecolor' + }); + } + }); + + //Proposal: + Ext.chart.theme.MyNewTheme = Ext.chart.createTheme('#basecolor'); + + ...and then to use it provide the name of the theme (as a lower case string) in the chart config. + + { + theme: 'mynewtheme' + } + */ + +(function() { + Ext.chart.theme = function(config, base) { + config = config || {}; + var i = 0, l, colors, color, + seriesThemes, markerThemes, + seriesTheme, markerTheme, + key, gradients = [], + midColor, midL; + + if (config.baseColor) { + midColor = Ext.draw.Color.fromString(config.baseColor); + midL = midColor.getHSL()[2]; + if (midL < 0.15) { + midColor = midColor.getLighter(0.3); + } else if (midL < 0.3) { + midColor = midColor.getLighter(0.15); + } else if (midL > 0.85) { + midColor = midColor.getDarker(0.3); + } else if (midL > 0.7) { + midColor = midColor.getDarker(0.15); + } + config.colors = [ midColor.getDarker(0.3).toString(), + midColor.getDarker(0.15).toString(), + midColor.toString(), + midColor.getLighter(0.15).toString(), + midColor.getLighter(0.3).toString()]; + + delete config.baseColor; + } + if (config.colors) { + colors = config.colors.slice(); + markerThemes = base.markerThemes; + seriesThemes = base.seriesThemes; + l = colors.length; + base.colors = colors; + for (; i < l; i++) { + color = colors[i]; + markerTheme = markerThemes[i] || {}; + seriesTheme = seriesThemes[i] || {}; + markerTheme.fill = seriesTheme.fill = markerTheme.stroke = seriesTheme.stroke = color; + markerThemes[i] = markerTheme; + seriesThemes[i] = seriesTheme; + } + base.markerThemes = markerThemes.slice(0, l); + base.seriesThemes = seriesThemes.slice(0, l); + //the user is configuring something in particular (either markers, series or pie slices) + } + for (key in base) { + if (key in config) { + if (Ext.isObject(config[key]) && Ext.isObject(base[key])) { + Ext.apply(base[key], config[key]); + } else { + base[key] = config[key]; + } + } + } + if (config.useGradients) { + colors = base.colors || (function () { + var ans = []; + for (i = 0, seriesThemes = base.seriesThemes, l = seriesThemes.length; i < l; i++) { + ans.push(seriesThemes[i].fill || seriesThemes[i].stroke); + } + return ans; + })(); + for (i = 0, l = colors.length; i < l; i++) { + midColor = Ext.draw.Color.fromString(colors[i]); + if (midColor) { + color = midColor.getDarker(0.1).toString(); + midColor = midColor.toString(); + key = 'theme-' + midColor.substr(1) + '-' + color.substr(1); + gradients.push({ + id: key, + angle: 45, + stops: { + 0: { + color: midColor.toString() + }, + 100: { + color: color.toString() + } + } + }); + colors[i] = 'url(#' + key + ')'; + } + } + base.gradients = gradients; + base.colors = colors; + } + /* + base.axis = Ext.apply(base.axis || {}, config.axis || {}); + base.axisLabel = Ext.apply(base.axisLabel || {}, config.axisLabel || {}); + base.axisTitle = Ext.apply(base.axisTitle || {}, config.axisTitle || {}); + */ + Ext.apply(this, base); + }; +})(); +}); + +/** + * @class Ext.chart.Mask + * + * Defines a mask for a chart's series. + * The 'chart' member must be set prior to rendering. + * + * A Mask can be used to select a certain region in a chart. + * When enabled, the `select` event will be triggered when a + * region is selected by the mask, allowing the user to perform + * other tasks like zooming on that region, etc. + * + * In order to use the mask one has to set the Chart `mask` option to + * `true`, `vertical` or `horizontal`. Then a possible configuration for the + * listener could be: + * + items: { + xtype: 'chart', + animate: true, + store: store1, + mask: 'horizontal', + listeners: { + select: { + fn: function(me, selection) { + me.setZoom(selection); + me.mask.hide(); + } + } + }, + + * In this example we zoom the chart to that particular region. You can also get + * a handle to a mask instance from the chart object. The `chart.mask` element is a + * `Ext.Panel`. + * + * @constructor + */ +Ext.define('Ext.chart.Mask', { + constructor: function(config) { + var me = this; + + me.addEvents('select'); + + if (config) { + Ext.apply(me, config); + } + if (me.mask) { + me.on('afterrender', function() { + //create a mask layer component + var comp = Ext.create('Ext.chart.MaskLayer', { + renderTo: me.el + }); + comp.el.on({ + 'mousemove': function(e) { + me.onMouseMove(e); + }, + 'mouseup': function(e) { + me.resized(e); + } + }); + //create a resize handler for the component + var resizeHandler = Ext.create('Ext.resizer.Resizer', { + el: comp.el, + handles: 'all', + pinned: true + }); + resizeHandler.on({ + 'resize': function(e) { + me.resized(e); + } + }); + comp.initDraggable(); + me.maskType = me.mask; + me.mask = comp; + me.maskSprite = me.surface.add({ + type: 'path', + path: ['M', 0, 0], + zIndex: 1001, + opacity: 0.7, + hidden: true, + stroke: '#444' + }); + }, me, { single: true }); + } + }, + + resized: function(e) { + var me = this, + bbox = me.bbox || me.chartBBox, + x = bbox.x, + y = bbox.y, + width = bbox.width, + height = bbox.height, + box = me.mask.getBox(true), + max = Math.max, + min = Math.min, + staticX = box.x - x, + staticY = box.y - y; + + staticX = max(staticX, x); + staticY = max(staticY, y); + staticX = min(staticX, width); + staticY = min(staticY, height); + box.x = staticX; + box.y = staticY; + me.fireEvent('select', me, box); + }, + + onMouseUp: function(e) { + var me = this, + bbox = me.bbox || me.chartBBox, + sel = me.maskSelection; + me.maskMouseDown = false; + me.mouseDown = false; + if (me.mouseMoved) { + me.onMouseMove(e); + me.mouseMoved = false; + me.fireEvent('select', me, { + x: sel.x - bbox.x, + y: sel.y - bbox.y, + width: sel.width, + height: sel.height + }); + } + }, + + onMouseDown: function(e) { + var me = this; + me.mouseDown = true; + me.mouseMoved = false; + me.maskMouseDown = { + x: e.getPageX() - me.el.getX(), + y: e.getPageY() - me.el.getY() + }; + }, + + onMouseMove: function(e) { + var me = this, + mask = me.maskType, + bbox = me.bbox || me.chartBBox, + x = bbox.x, + y = bbox.y, + math = Math, + floor = math.floor, + abs = math.abs, + min = math.min, + max = math.max, + height = floor(y + bbox.height), + width = floor(x + bbox.width), + posX = e.getPageX(), + posY = e.getPageY(), + staticX = posX - me.el.getX(), + staticY = posY - me.el.getY(), + maskMouseDown = me.maskMouseDown, + path; + + me.mouseMoved = me.mouseDown; + staticX = max(staticX, x); + staticY = max(staticY, y); + staticX = min(staticX, width); + staticY = min(staticY, height); + if (maskMouseDown && me.mouseDown) { + if (mask == 'horizontal') { + staticY = y; + maskMouseDown.y = height; + posY = me.el.getY() + bbox.height + me.insetPadding; + } + else if (mask == 'vertical') { + staticX = x; + maskMouseDown.x = width; + } + width = maskMouseDown.x - staticX; + height = maskMouseDown.y - staticY; + path = ['M', staticX, staticY, 'l', width, 0, 0, height, -width, 0, 'z']; + me.maskSelection = { + x: width > 0 ? staticX : staticX + width, + y: height > 0 ? staticY : staticY + height, + width: abs(width), + height: abs(height) + }; + me.mask.updateBox({ + x: posX - abs(width), + y: posY - abs(height), + width: abs(width), + height: abs(height) + }); + me.mask.show(); + me.maskSprite.setAttributes({ + hidden: true + }, true); + } + else { + if (mask == 'horizontal') { + path = ['M', staticX, y, 'L', staticX, height]; + } + else if (mask == 'vertical') { + path = ['M', x, staticY, 'L', width, staticY]; + } + else { + path = ['M', staticX, y, 'L', staticX, height, 'M', x, staticY, 'L', width, staticY]; + } + me.maskSprite.setAttributes({ + path: path, + fill: me.maskMouseDown ? me.maskSprite.stroke : false, + 'stroke-width': mask === true ? 1 : 3, + hidden: false + }, true); + } + }, + + onMouseLeave: function(e) { + var me = this; + me.mouseMoved = false; + me.mouseDown = false; + me.maskMouseDown = false; + me.mask.hide(); + me.maskSprite.hide(true); + } +}); + +/** + * @class Ext.chart.Navigation + * + * Handles panning and zooming capabilities. + * + * @ignore + */ +Ext.define('Ext.chart.Navigation', { + + constructor: function() { + this.originalStore = this.store; + }, + + //filters the store to the specified interval(s) + setZoom: function(zoomConfig) { + var me = this, + store = me.substore || me.store, + bbox = me.chartBBox, + len = store.getCount(), + from = (zoomConfig.x / bbox.width * len) >> 0, + to = Math.ceil(((zoomConfig.x + zoomConfig.width) / bbox.width * len)), + recFieldsLen, recFields = [], curField, json = [], obj; + + store.each(function(rec, i) { + if (i < from || i > to) { + return; + } + obj = {}; + //get all record field names in a simple array + if (!recFields.length) { + rec.fields.each(function(f) { + recFields.push(f.name); + }); + recFieldsLen = recFields.length; + } + //append record values to an aggregation record + for (i = 0; i < recFieldsLen; i++) { + curField = recFields[i]; + obj[curField] = rec.get(curField); + } + json.push(obj); + }); + me.store = me.substore = Ext.create('Ext.data.JsonStore', { + fields: recFields, + data: json + }); + me.redraw(true); + }, + + restoreZoom: function() { + this.store = this.substore = this.originalStore; + this.redraw(true); + } + +}); +/** + * @class Ext.chart.Shape + * @ignore + */ +Ext.define('Ext.chart.Shape', { + + /* Begin Definitions */ + + singleton: true, + + /* End Definitions */ + + circle: function (surface, opts) { + return surface.add(Ext.apply({ + type: 'circle', + x: opts.x, + y: opts.y, + stroke: null, + radius: opts.radius + }, opts)); + }, + line: function (surface, opts) { + return surface.add(Ext.apply({ + type: 'rect', + x: opts.x - opts.radius, + y: opts.y - opts.radius, + height: 2 * opts.radius, + width: 2 * opts.radius / 5 + }, opts)); + }, + square: function (surface, opts) { + return surface.add(Ext.applyIf({ + type: 'rect', + x: opts.x - opts.radius, + y: opts.y - opts.radius, + height: 2 * opts.radius, + width: 2 * opts.radius, + radius: null + }, opts)); + }, + triangle: function (surface, opts) { + opts.radius *= 1.75; + return surface.add(Ext.apply({ + type: 'path', + stroke: null, + path: "M".concat(opts.x, ",", opts.y, "m0-", opts.radius * 0.58, "l", opts.radius * 0.5, ",", opts.radius * 0.87, "-", opts.radius, ",0z") + }, opts)); + }, + diamond: function (surface, opts) { + var r = opts.radius; + r *= 1.5; + return surface.add(Ext.apply({ + type: 'path', + stroke: null, + path: ["M", opts.x, opts.y - r, "l", r, r, -r, r, -r, -r, r, -r, "z"] + }, opts)); + }, + cross: function (surface, opts) { + var r = opts.radius; + r = r / 1.7; + return surface.add(Ext.apply({ + type: 'path', + stroke: null, + path: "M".concat(opts.x - r, ",", opts.y, "l", [-r, -r, r, -r, r, r, r, -r, r, r, -r, r, r, r, -r, r, -r, -r, -r, r, -r, -r, "z"]) + }, opts)); + }, + plus: function (surface, opts) { + var r = opts.radius / 1.3; + return surface.add(Ext.apply({ + type: 'path', + stroke: null, + path: "M".concat(opts.x - r / 2, ",", opts.y - r / 2, "l", [0, -r, r, 0, 0, r, r, 0, 0, r, -r, 0, 0, r, -r, 0, 0, -r, -r, 0, 0, -r, "z"]) + }, opts)); + }, + arrow: function (surface, opts) { + var r = opts.radius; + return surface.add(Ext.apply({ + type: 'path', + path: "M".concat(opts.x - r * 0.7, ",", opts.y - r * 0.4, "l", [r * 0.6, 0, 0, -r * 0.4, r, r * 0.8, -r, r * 0.8, 0, -r * 0.4, -r * 0.6, 0], "z") + }, opts)); + }, + drop: function (surface, x, y, text, size, angle) { + size = size || 30; + angle = angle || 0; + surface.add({ + type: 'path', + path: ['M', x, y, 'l', size, 0, 'A', size * 0.4, size * 0.4, 0, 1, 0, x + size * 0.7, y - size * 0.7, 'z'], + fill: '#000', + stroke: 'none', + rotate: { + degrees: 22.5 - angle, + x: x, + y: y + } + }); + angle = (angle + 90) * Math.PI / 180; + surface.add({ + type: 'text', + x: x + size * Math.sin(angle) - 10, // Shift here, Not sure why. + y: y + size * Math.cos(angle) + 5, + text: text, + 'font-size': size * 12 / 40, + stroke: 'none', + fill: '#fff' + }); + } +}); +/** + * @class Ext.draw.Surface + * @extends Object + * + * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}. + * A Surface contains methods to render sprites, get bounding boxes of sprites, add + * sprites to the canvas, initialize other graphic components, etc. One of the most used + * methods for this class is the `add` method, to add Sprites to the surface. + * + * Most of the Surface methods are abstract and they have a concrete implementation + * in VML or SVG engines. + * + * A Surface instance can be accessed as a property of a draw component. For example: + * + * drawComponent.surface.add({ + * type: 'circle', + * fill: '#ffc', + * radius: 100, + * x: 100, + * y: 100 + * }); + * + * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.Sprite} + * class documentation. + * + * ### Listeners + * + * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are: + * + * - mousedown + * - mouseup + * - mouseover + * - mouseout + * - mousemove + * - mouseenter + * - mouseleave + * - click + * + * For example: + * + drawComponent.surface.on({ + 'mousemove': function() { + console.log('moving the mouse over the surface'); + } + }); + */ +Ext.define('Ext.draw.Surface', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: ['Ext.draw.CompositeSprite'], + uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'], + + separatorRe: /[, ]+/, + + statics: { + /** + * Create and return a new concrete Surface instance appropriate for the current environment. + * @param {Object} config Initial configuration for the Surface instance + * @param {Array} enginePriority Optional order of implementations to use; the first one that is + * available in the current environment will be used. Defaults to + * ['Svg', 'Vml']. + */ + create: function(config, enginePriority) { + enginePriority = enginePriority || ['Svg', 'Vml']; + + var i = 0, + len = enginePriority.length, + surfaceClass; + + for (; i < len; i++) { + if (Ext.supports[enginePriority[i]]) { + return Ext.create('Ext.draw.engine.' + enginePriority[i], config); + } + } + return false; + } + }, + + /* End Definitions */ + + // @private + availableAttrs: { + blur: 0, + "clip-rect": "0 0 1e9 1e9", + cursor: "default", + cx: 0, + cy: 0, + 'dominant-baseline': 'auto', + fill: "none", + "fill-opacity": 1, + font: '10px "Arial"', + "font-family": '"Arial"', + "font-size": "10", + "font-style": "normal", + "font-weight": 400, + gradient: "", + height: 0, + hidden: false, + href: "http://sencha.com/", + opacity: 1, + path: "M0,0", + radius: 0, + rx: 0, + ry: 0, + scale: "1 1", + src: "", + stroke: "#000", + "stroke-dasharray": "", + "stroke-linecap": "butt", + "stroke-linejoin": "butt", + "stroke-miterlimit": 0, + "stroke-opacity": 1, + "stroke-width": 1, + target: "_blank", + text: "", + "text-anchor": "middle", + title: "Ext Draw", + width: 0, + x: 0, + y: 0, + zIndex: 0 + }, + + /** + * @cfg {Number} height + * The height of this component in pixels (defaults to auto). + * Note to express this dimension as a percentage or offset see {@link Ext.Component#anchor}. + */ + /** + * @cfg {Number} width + * The width of this component in pixels (defaults to auto). + * Note to express this dimension as a percentage or offset see {@link Ext.Component#anchor}. + */ + container: undefined, + height: 352, + width: 512, + x: 0, + y: 0, + + constructor: function(config) { + var me = this; + config = config || {}; + Ext.apply(me, config); + + me.domRef = Ext.getDoc().dom; + + me.customAttributes = {}; + + me.addEvents( + 'mousedown', + 'mouseup', + 'mouseover', + 'mouseout', + 'mousemove', + 'mouseenter', + 'mouseleave', + 'click' + ); + + me.mixins.observable.constructor.call(me); + + me.getId(); + me.initGradients(); + me.initItems(); + if (me.renderTo) { + me.render(me.renderTo); + delete me.renderTo; + } + me.initBackground(config.background); + }, + + // @private called to initialize components in the surface + // this is dependent on the underlying implementation. + initSurface: Ext.emptyFn, + + // @private called to setup the surface to render an item + //this is dependent on the underlying implementation. + renderItem: Ext.emptyFn, + + // @private + renderItems: Ext.emptyFn, + + // @private + setViewBox: Ext.emptyFn, + + /** + * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out. + * + * For example: + * + * drawComponent.surface.addCls(sprite, 'x-visible'); + * + * @param {Object} sprite The sprite to add the class to. + * @param {String/Array} className The CSS class to add, or an array of classes + */ + addCls: Ext.emptyFn, + + /** + * Removes one or more CSS classes from the element. + * + * For example: + * + * drawComponent.surface.removeCls(sprite, 'x-visible'); + * + * @param {Object} sprite The sprite to remove the class from. + * @param {String/Array} className The CSS class to remove, or an array of classes + */ + removeCls: Ext.emptyFn, + + /** + * Sets CSS style attributes to an element. + * + * For example: + * + * drawComponent.surface.setStyle(sprite, { + * 'cursor': 'pointer' + * }); + * + * @param {Object} sprite The sprite to add, or an array of classes to + * @param {Object} styles An Object with CSS styles. + */ + setStyle: Ext.emptyFn, + + // @private + initGradients: function() { + var gradients = this.gradients; + if (gradients) { + Ext.each(gradients, this.addGradient, this); + } + }, + + // @private + initItems: function() { + var items = this.items; + this.items = Ext.create('Ext.draw.CompositeSprite'); + this.groups = Ext.create('Ext.draw.CompositeSprite'); + if (items) { + this.add(items); + } + }, + + // @private + initBackground: function(config) { + var gradientId, + gradient, + backgroundSprite, + width = this.width, + height = this.height; + if (config) { + if (config.gradient) { + gradient = config.gradient; + gradientId = gradient.id; + this.addGradient(gradient); + this.background = this.add({ + type: 'rect', + x: 0, + y: 0, + width: width, + height: height, + fill: 'url(#' + gradientId + ')' + }); + } else if (config.fill) { + this.background = this.add({ + type: 'rect', + x: 0, + y: 0, + width: width, + height: height, + fill: config.fill + }); + } else if (config.image) { + this.background = this.add({ + type: 'image', + x: 0, + y: 0, + width: width, + height: height, + src: config.image + }); + } + } + }, + + /** + * Sets the size of the surface. Accomodates the background (if any) to fit the new size too. + * + * For example: + * + * drawComponent.surface.setSize(500, 500); + * + * This method is generally called when also setting the size of the draw Component. + * + * @param {Number} w The new width of the canvas. + * @param {Number} h The new height of the canvas. + */ + setSize: function(w, h) { + if (this.background) { + this.background.setAttributes({ + width: w, + height: h, + hidden: false + }, true); + } + }, + + // @private + scrubAttrs: function(sprite) { + var i, + attrs = {}, + exclude = {}, + sattr = sprite.attr; + for (i in sattr) { + // Narrow down attributes to the main set + if (this.translateAttrs.hasOwnProperty(i)) { + // Translated attr + attrs[this.translateAttrs[i]] = sattr[i]; + exclude[this.translateAttrs[i]] = true; + } + else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) { + // Passtrhough attr + attrs[i] = sattr[i]; + } + } + return attrs; + }, + + // @private + onClick: function(e) { + this.processEvent('click', e); + }, + + // @private + onMouseUp: function(e) { + this.processEvent('mouseup', e); + }, + + // @private + onMouseDown: function(e) { + this.processEvent('mousedown', e); + }, + + // @private + onMouseOver: function(e) { + this.processEvent('mouseover', e); + }, + + // @private + onMouseOut: function(e) { + this.processEvent('mouseout', e); + }, + + // @private + onMouseMove: function(e) { + this.fireEvent('mousemove', e); + }, + + // @private + onMouseEnter: Ext.emptyFn, + + // @private + onMouseLeave: Ext.emptyFn, + + /** + * Add a gradient definition to the Surface. Note that in some surface engines, adding + * a gradient via this method will not take effect if the surface has already been rendered. + * Therefore, it is preferred to pass the gradients as an item to the surface config, rather + * than calling this method, especially if the surface is rendered immediately (e.g. due to + * 'renderTo' in its config). For more information on how to create gradients in the Chart + * configuration object please refer to {@link Ext.chart.Chart}. + * + * The gradient object to be passed into this method is composed by: + * + * + * - **id** - string - The unique name of the gradient. + * - **angle** - number, optional - The angle of the gradient in degrees. + * - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values. + * + * + For example: + drawComponent.surface.addGradient({ + id: 'gradientId', + angle: 45, + stops: { + 0: { + color: '#555' + }, + 100: { + color: '#ddd' + } + } + }); + */ + addGradient: Ext.emptyFn, + + /** + * Add a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be passed into this method. + * + * For example: + * + * drawComponent.surface.add({ + * type: 'circle', + * fill: '#ffc', + * radius: 100, + * x: 100, + * y: 100 + * }); + * + */ + add: function() { + var args = Array.prototype.slice.call(arguments), + sprite, + index; + + var hasMultipleArgs = args.length > 1; + if (hasMultipleArgs || Ext.isArray(args[0])) { + var items = hasMultipleArgs ? args : args[0], + results = [], + i, ln, item; + + for (i = 0, ln = items.length; i < ln; i++) { + item = items[i]; + item = this.add(item); + results.push(item); + } + + return results; + } + sprite = this.prepareItems(args[0], true)[0]; + this.normalizeSpriteCollection(sprite); + this.onAdd(sprite); + return sprite; + }, + + /** + * @private + * Insert or move a given sprite into the correct position in the items + * MixedCollection, according to its zIndex. Will be inserted at the end of + * an existing series of sprites with the same or lower zIndex. If the sprite + * is already positioned within an appropriate zIndex group, it will not be moved. + * This ordering can be used by subclasses to assist in rendering the sprites in + * the correct order for proper z-index stacking. + * @param {Ext.draw.Sprite} sprite + * @return {Number} the sprite's new index in the list + */ + normalizeSpriteCollection: function(sprite) { + var items = this.items, + zIndex = sprite.attr.zIndex, + idx = items.indexOf(sprite); + + if (idx < 0 || (idx > 0 && items.getAt(idx - 1).attr.zIndex > zIndex) || + (idx < items.length - 1 && items.getAt(idx + 1).attr.zIndex < zIndex)) { + items.removeAt(idx); + idx = items.findIndexBy(function(otherSprite) { + return otherSprite.attr.zIndex > zIndex; + }); + if (idx < 0) { + idx = items.length; + } + items.insert(idx, sprite); + } + return idx; + }, + + onAdd: function(sprite) { + var group = sprite.group, + draggable = sprite.draggable, + groups, ln, i; + if (group) { + groups = [].concat(group); + ln = groups.length; + for (i = 0; i < ln; i++) { + group = groups[i]; + this.getGroup(group).add(sprite); + } + delete sprite.group; + } + if (draggable) { + sprite.initDraggable(); + } + }, + + /** + * Remove a given sprite from the surface, optionally destroying the sprite in the process. + * You can also call the sprite own `remove` method. + * + * For example: + * + * drawComponent.surface.remove(sprite); + * //or... + * sprite.remove(); + * + * @param {Ext.draw.Sprite} sprite + * @param {Boolean} destroySprite + * @return {Number} the sprite's new index in the list + */ + remove: function(sprite, destroySprite) { + if (sprite) { + this.items.remove(sprite); + this.groups.each(function(item) { + item.remove(sprite); + }); + sprite.onRemove(); + if (destroySprite === true) { + sprite.destroy(); + } + } + }, + + /** + * Remove all sprites from the surface, optionally destroying the sprites in the process. + * + * For example: + * + * drawComponent.surface.removeAll(); + * + * @param {Boolean} destroySprites Whether to destroy all sprites when removing them. + * @return {Number} The sprite's new index in the list. + */ + removeAll: function(destroySprites) { + var items = this.items.items, + ln = items.length, + i; + for (i = ln - 1; i > -1; i--) { + this.remove(items[i], destroySprites); + } + }, + + onRemove: Ext.emptyFn, + + onDestroy: Ext.emptyFn, + + // @private + applyTransformations: function(sprite) { + sprite.bbox.transform = 0; + this.transform(sprite); + + var me = this, + dirty = false, + attr = sprite.attr; + + if (attr.translation.x != null || attr.translation.y != null) { + me.translate(sprite); + dirty = true; + } + if (attr.scaling.x != null || attr.scaling.y != null) { + me.scale(sprite); + dirty = true; + } + if (attr.rotation.degrees != null) { + me.rotate(sprite); + dirty = true; + } + if (dirty) { + sprite.bbox.transform = 0; + this.transform(sprite); + sprite.transformations = []; + } + }, + + // @private + rotate: function (sprite) { + var bbox, + deg = sprite.attr.rotation.degrees, + centerX = sprite.attr.rotation.x, + centerY = sprite.attr.rotation.y; + if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) { + bbox = this.getBBox(sprite); + centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX; + centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY; + } + sprite.transformations.push({ + type: "rotate", + degrees: deg, + x: centerX, + y: centerY + }); + }, + + // @private + translate: function(sprite) { + var x = sprite.attr.translation.x || 0, + y = sprite.attr.translation.y || 0; + sprite.transformations.push({ + type: "translate", + x: x, + y: y + }); + }, + + // @private + scale: function(sprite) { + var bbox, + x = sprite.attr.scaling.x || 1, + y = sprite.attr.scaling.y || 1, + centerX = sprite.attr.scaling.centerX, + centerY = sprite.attr.scaling.centerY; + + if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) { + bbox = this.getBBox(sprite); + centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX; + centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY; + } + sprite.transformations.push({ + type: "scale", + x: x, + y: y, + centerX: centerX, + centerY: centerY + }); + }, + + // @private + rectPath: function (x, y, w, h, r) { + if (r) { + return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]]; + } + return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]]; + }, + + // @private + ellipsePath: function (x, y, rx, ry) { + if (ry == null) { + ry = rx; + } + return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]]; + }, + + // @private + getPathpath: function (el) { + return el.attr.path; + }, + + // @private + getPathcircle: function (el) { + var a = el.attr; + return this.ellipsePath(a.x, a.y, a.radius, a.radius); + }, + + // @private + getPathellipse: function (el) { + var a = el.attr; + return this.ellipsePath(a.x, a.y, a.radiusX, a.radiusY); + }, + + // @private + getPathrect: function (el) { + var a = el.attr; + return this.rectPath(a.x, a.y, a.width, a.height, a.r); + }, + + // @private + getPathimage: function (el) { + var a = el.attr; + return this.rectPath(a.x || 0, a.y || 0, a.width, a.height); + }, + + // @private + getPathtext: function (el) { + var bbox = this.getBBoxText(el); + return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height); + }, + + createGroup: function(id) { + var group = this.groups.get(id); + if (!group) { + group = Ext.create('Ext.draw.CompositeSprite', { + surface: this + }); + group.id = id || Ext.id(null, 'ext-surface-group-'); + this.groups.add(group); + } + return group; + }, + + /** + * Returns a new group or an existent group associated with the current surface. + * The group returned is a {@link Ext.draw.CompositeSprite} group. + * + * For example: + * + * var spriteGroup = drawComponent.surface.getGroup('someGroupId'); + * + * @param {String} id The unique identifier of the group. + * @return {Object} The {@link Ext.draw.CompositeSprite}. + */ + getGroup: function(id) { + if (typeof id == "string") { + var group = this.groups.get(id); + if (!group) { + group = this.createGroup(id); + } + } else { + group = id; + } + return group; + }, + + // @private + prepareItems: function(items, applyDefaults) { + items = [].concat(items); + // Make sure defaults are applied and item is initialized + var item, i, ln; + for (i = 0, ln = items.length; i < ln; i++) { + item = items[i]; + if (!(item instanceof Ext.draw.Sprite)) { + // Temporary, just take in configs... + item.surface = this; + items[i] = this.createItem(item); + } else { + item.surface = this; + } + } + return items; + }, + + /** + * Changes the text in the sprite element. The sprite must be a `text` sprite. + * This method can also be called from {@link Ext.draw.Sprite}. + * + * For example: + * + * var spriteGroup = drawComponent.surface.setText(sprite, 'my new text'); + * + * @param {Object} sprite The Sprite to change the text. + * @param {String} text The new text to be set. + */ + setText: Ext.emptyFn, + + //@private Creates an item and appends it to the surface. Called + //as an internal method when calling `add`. + createItem: Ext.emptyFn, + + /** + * Retrieves the id of this component. + * Will autogenerate an id if one has not already been set. + */ + getId: function() { + return this.id || (this.id = Ext.id(null, 'ext-surface-')); + }, + + /** + * Destroys the surface. This is done by removing all components from it and + * also removing its reference to a DOM element. + * + * For example: + * + * drawComponent.surface.destroy(); + */ + destroy: function() { + delete this.domRef; + this.removeAll(); + } +}); +/** + * @class Ext.draw.Component + * @extends Ext.Component + * + * The Draw Component is a surface in which sprites can be rendered. The Draw Component + * manages and holds a `Surface` instance: an interface that has + * an SVG or VML implementation depending on the browser capabilities and where + * Sprites can be appended. + * {@img Ext.draw.Component/Ext.draw.Component.png Ext.draw.Component component} + * One way to create a draw component is: + * + * var drawComponent = Ext.create('Ext.draw.Component', { + * viewBox: false, + * items: [{ + * type: 'circle', + * fill: '#79BB3F', + * radius: 100, + * x: 100, + * y: 100 + * }] + * }); + * + * Ext.create('Ext.Window', { + * width: 215, + * height: 235, + * layout: 'fit', + * items: [drawComponent] + * }).show(); + * + * In this case we created a draw component and added a sprite to it. + * The *type* of the sprite is *circle* so if you run this code you'll see a yellow-ish + * circle in a Window. When setting `viewBox` to `false` we are responsible for setting the object's position and + * dimensions accordingly. + * + * You can also add sprites by using the surface's add method: + * + * drawComponent.surface.add({ + * type: 'circle', + * fill: '#79BB3F', + * radius: 100, + * x: 100, + * y: 100 + * }); + * + * For more information on Sprites, the core elements added to a draw component's surface, + * refer to the Ext.draw.Sprite documentation. + */ +Ext.define('Ext.draw.Component', { + + /* Begin Definitions */ + + alias: 'widget.draw', + + extend: 'Ext.Component', + + requires: [ + 'Ext.draw.Surface', + 'Ext.layout.component.Draw' + ], + + /* End Definitions */ + + /** + * @cfg {Array} enginePriority + * Defines the priority order for which Surface implementation to use. The first + * one supported by the current environment will be used. + */ + enginePriority: ['Svg', 'Vml'], + + baseCls: Ext.baseCSSPrefix + 'surface', + + componentLayout: 'draw', + + /** + * @cfg {Boolean} viewBox + * Turn on view box support which will scale and position items in the draw component to fit to the component while + * maintaining aspect ratio. Note that this scaling can override other sizing settings on yor items. Defaults to true. + */ + viewBox: true, + + /** + * @cfg {Boolean} autoSize + * Turn on autoSize support which will set the bounding div's size to the natural size of the contents. Defaults to false. + */ + autoSize: false, + + /** + * @cfg {Array} gradients (optional) Define a set of gradients that can be used as `fill` property in sprites. + * The gradients array is an array of objects with the following properties: + * + *
    + *
  • id - string - The unique name of the gradient.
  • + *
  • angle - number, optional - The angle of the gradient in degrees.
  • + *
  • stops - object - An object with numbers as keys (from 0 to 100) and style objects + * as values
  • + *
+ * + + For example: + +

+        gradients: [{
+            id: 'gradientId',
+            angle: 45,
+            stops: {
+                0: {
+                    color: '#555'
+                },
+                100: {
+                    color: '#ddd'
+                }
+            }
+        },  {
+            id: 'gradientId2',
+            angle: 0,
+            stops: {
+                0: {
+                    color: '#590'
+                },
+                20: {
+                    color: '#599'
+                },
+                100: {
+                    color: '#ddd'
+                }
+            }
+        }]
+     
+ + Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example: + +

+        sprite.setAttributes({
+            fill: 'url(#gradientId)'
+        }, true);
+     
+ + */ + + initComponent: function() { + this.callParent(arguments); + + this.addEvents( + 'mousedown', + 'mouseup', + 'mousemove', + 'mouseenter', + 'mouseleave', + 'click' + ); + }, + + /** + * @private + * + * Create the Surface on initial render + */ + onRender: function() { + var me = this, + viewBox = me.viewBox, + autoSize = me.autoSize, + bbox, items, width, height, x, y; + me.callParent(arguments); + + me.createSurface(); + + items = me.surface.items; + + if (viewBox || autoSize) { + bbox = items.getBBox(); + width = bbox.width; + height = bbox.height; + x = bbox.x; + y = bbox.y; + if (me.viewBox) { + me.surface.setViewBox(x, y, width, height); + } + else { + // AutoSized + me.autoSizeSurface(); + } + } + }, + + //@private + autoSizeSurface: function() { + var me = this, + items = me.surface.items, + bbox = items.getBBox(), + width = bbox.width, + height = bbox.height; + items.setAttributes({ + translate: { + x: -bbox.x, + //Opera has a slight offset in the y axis. + y: -bbox.y + (+Ext.isOpera) + } + }, true); + if (me.rendered) { + me.setSize(width, height); + } + else { + me.surface.setSize(width, height); + } + me.el.setSize(width, height); + }, + + /** + * Create the Surface instance. Resolves the correct Surface implementation to + * instantiate based on the 'enginePriority' config. Once the Surface instance is + * created you can use the handle to that instance to add sprites. For example: + * +

+        drawComponent.surface.add(sprite);
+     
+ */ + createSurface: function() { + var surface = Ext.draw.Surface.create(Ext.apply({}, { + width: this.width, + height: this.height, + renderTo: this.el + }, this.initialConfig)); + this.surface = surface; + + function refire(eventName) { + return function(e) { + this.fireEvent(eventName, e); + }; + } + + surface.on({ + scope: this, + mouseup: refire('mouseup'), + mousedown: refire('mousedown'), + mousemove: refire('mousemove'), + mouseenter: refire('mouseenter'), + mouseleave: refire('mouseleave'), + click: refire('click') + }); + }, + + + /** + * @private + * + * Clean up the Surface instance on component destruction + */ + onDestroy: function() { + var surface = this.surface; + if (surface) { + surface.destroy(); + } + this.callParent(arguments); + } + +}); + +/** + * @class Ext.chart.LegendItem + * @extends Ext.draw.CompositeSprite + * A single item of a legend (marker plus label) + * @constructor + */ +Ext.define('Ext.chart.LegendItem', { + + /* Begin Definitions */ + + extend: 'Ext.draw.CompositeSprite', + + requires: ['Ext.chart.Shape'], + + /* End Definitions */ + + // Position of the item, relative to the upper-left corner of the legend box + x: 0, + y: 0, + zIndex: 500, + + constructor: function(config) { + this.callParent(arguments); + this.createLegend(config); + }, + + /** + * Creates all the individual sprites for this legend item + */ + createLegend: function(config) { + var me = this, + index = config.yFieldIndex, + series = me.series, + seriesType = series.type, + idx = me.yFieldIndex, + legend = me.legend, + surface = me.surface, + refX = legend.x + me.x, + refY = legend.y + me.y, + bbox, z = me.zIndex, + markerConfig, label, mask, + radius, toggle = false, + seriesStyle = Ext.apply(series.seriesStyle, series.style); + + function getSeriesProp(name) { + var val = series[name]; + return (Ext.isArray(val) ? val[idx] : val); + } + + label = me.add('label', surface.add({ + type: 'text', + x: 20, + y: 0, + zIndex: z || 0, + font: legend.labelFont, + text: getSeriesProp('title') || getSeriesProp('yField') + })); + + // Line series - display as short line with optional marker in the middle + if (seriesType === 'line' || seriesType === 'scatter') { + if(seriesType === 'line') { + me.add('line', surface.add({ + type: 'path', + path: 'M0.5,0.5L16.5,0.5', + zIndex: z, + "stroke-width": series.lineWidth, + "stroke-linejoin": "round", + "stroke-dasharray": series.dash, + stroke: seriesStyle.stroke || '#000', + style: { + cursor: 'pointer' + } + })); + } + if (series.showMarkers || seriesType === 'scatter') { + markerConfig = Ext.apply(series.markerStyle, series.markerConfig || {}); + me.add('marker', Ext.chart.Shape[markerConfig.type](surface, { + fill: markerConfig.fill, + x: 8.5, + y: 0.5, + zIndex: z, + radius: markerConfig.radius || markerConfig.size, + style: { + cursor: 'pointer' + } + })); + } + } + // All other series types - display as filled box + else { + me.add('box', surface.add({ + type: 'rect', + zIndex: z, + x: 0, + y: 0, + width: 12, + height: 12, + fill: series.getLegendColor(index), + style: { + cursor: 'pointer' + } + })); + } + + me.setAttributes({ + hidden: false + }, true); + + bbox = me.getBBox(); + + mask = me.add('mask', surface.add({ + type: 'rect', + x: bbox.x, + y: bbox.y, + width: bbox.width || 20, + height: bbox.height || 20, + zIndex: (z || 0) + 1000, + fill: '#f00', + opacity: 0, + style: { + 'cursor': 'pointer' + } + })); + + //add toggle listener + me.on('mouseover', function() { + label.setStyle({ + 'font-weight': 'bold' + }); + mask.setStyle({ + 'cursor': 'pointer' + }); + series._index = index; + series.highlightItem(); + }, me); + + me.on('mouseout', function() { + label.setStyle({ + 'font-weight': 'normal' + }); + series._index = index; + series.unHighlightItem(); + }, me); + + if (!series.visibleInLegend(index)) { + toggle = true; + label.setAttributes({ + opacity: 0.5 + }, true); + } + + me.on('mousedown', function() { + if (!toggle) { + series.hideAll(); + label.setAttributes({ + opacity: 0.5 + }, true); + } else { + series.showAll(); + label.setAttributes({ + opacity: 1 + }, true); + } + toggle = !toggle; + }, me); + me.updatePosition({x:0, y:0}); //Relative to 0,0 at first so that the bbox is calculated correctly + }, + + /** + * Update the positions of all this item's sprites to match the root position + * of the legend box. + * @param {Object} relativeTo (optional) If specified, this object's 'x' and 'y' values will be used + * as the reference point for the relative positioning. Defaults to the Legend. + */ + updatePosition: function(relativeTo) { + var me = this, + items = me.items, + ln = items.length, + i = 0, + item; + if (!relativeTo) { + relativeTo = me.legend; + } + for (; i < ln; i++) { + item = items[i]; + switch (item.type) { + case 'text': + item.setAttributes({ + x: 20 + relativeTo.x + me.x, + y: relativeTo.y + me.y + }, true); + break; + case 'rect': + item.setAttributes({ + translate: { + x: relativeTo.x + me.x, + y: relativeTo.y + me.y - 6 + } + }, true); + break; + default: + item.setAttributes({ + translate: { + x: relativeTo.x + me.x, + y: relativeTo.y + me.y + } + }, true); + } + } + } +}); +/** + * @class Ext.chart.Legend + * + * Defines a legend for a chart's series. + * The 'chart' member must be set prior to rendering. + * The legend class displays a list of legend items each of them related with a + * series being rendered. In order to render the legend item of the proper series + * the series configuration object must have `showInSeries` set to true. + * + * The legend configuration object accepts a `position` as parameter. + * The `position` parameter can be `left`, `right` + * `top` or `bottom`. For example: + * + * legend: { + * position: 'right' + * }, + * + * Full example: +

+    var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        animate: true,
+        store: store,
+        shadow: true,
+        theme: 'Category1',
+        legend: {
+            position: 'top'
+        },
+         axes: [{
+                type: 'Numeric',
+                grid: true,
+                position: 'left',
+                fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+                title: 'Sample Values',
+                grid: {
+                    odd: {
+                        opacity: 1,
+                        fill: '#ddd',
+                        stroke: '#bbb',
+                        'stroke-width': 1
+                    }
+                },
+                minimum: 0,
+                adjustMinimumByMajorUnit: 0
+            }, {
+                type: 'Category',
+                position: 'bottom',
+                fields: ['name'],
+                title: 'Sample Metrics',
+                grid: true,
+                label: {
+                    rotate: {
+                        degrees: 315
+                    }
+                }
+        }],
+        series: [{
+            type: 'area',
+            highlight: false,
+            axis: 'left',
+            xField: 'name',
+            yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+            style: {
+                opacity: 0.93
+            }
+        }]
+    });    
+    
+ * + * @constructor + */ +Ext.define('Ext.chart.Legend', { + + /* Begin Definitions */ + + requires: ['Ext.chart.LegendItem'], + + /* End Definitions */ + + /** + * @cfg {Boolean} visible + * Whether or not the legend should be displayed. + */ + visible: true, + + /** + * @cfg {String} position + * The position of the legend in relation to the chart. One of: "top", + * "bottom", "left", "right", or "float". If set to "float", then the legend + * box will be positioned at the point denoted by the x and y parameters. + */ + position: 'bottom', + + /** + * @cfg {Number} x + * X-position of the legend box. Used directly if position is set to "float", otherwise + * it will be calculated dynamically. + */ + x: 0, + + /** + * @cfg {Number} y + * Y-position of the legend box. Used directly if position is set to "float", otherwise + * it will be calculated dynamically. + */ + y: 0, + + /** + * @cfg {String} labelFont + * Font to be used for the legend labels, eg '12px Helvetica' + */ + labelFont: '12px Helvetica, sans-serif', + + /** + * @cfg {String} boxStroke + * Style of the stroke for the legend box + */ + boxStroke: '#000', + + /** + * @cfg {String} boxStrokeWidth + * Width of the stroke for the legend box + */ + boxStrokeWidth: 1, + + /** + * @cfg {String} boxFill + * Fill style for the legend box + */ + boxFill: '#FFF', + + /** + * @cfg {Number} itemSpacing + * Amount of space between legend items + */ + itemSpacing: 10, + + /** + * @cfg {Number} padding + * Amount of padding between the legend box's border and its items + */ + padding: 5, + + // @private + width: 0, + // @private + height: 0, + + /** + * @cfg {Number} boxZIndex + * Sets the z-index for the legend. Defaults to 100. + */ + boxZIndex: 100, + + constructor: function(config) { + var me = this; + if (config) { + Ext.apply(me, config); + } + me.items = []; + /** + * Whether the legend box is oriented vertically, i.e. if it is on the left or right side or floating. + * @type {Boolean} + */ + me.isVertical = ("left|right|float".indexOf(me.position) !== -1); + + // cache these here since they may get modified later on + me.origX = me.x; + me.origY = me.y; + }, + + /** + * @private Create all the sprites for the legend + */ + create: function() { + var me = this; + me.createItems(); + if (!me.created && me.isDisplayed()) { + me.createBox(); + me.created = true; + + // Listen for changes to series titles to trigger regeneration of the legend + me.chart.series.each(function(series) { + series.on('titlechange', function() { + me.create(); + me.updatePosition(); + }); + }); + } + }, + + /** + * @private Determine whether the legend should be displayed. Looks at the legend's 'visible' config, + * and also the 'showInLegend' config for each of the series. + */ + isDisplayed: function() { + return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1; + }, + + /** + * @private Create the series markers and labels + */ + createItems: function() { + var me = this, + chart = me.chart, + surface = chart.surface, + items = me.items, + padding = me.padding, + itemSpacing = me.itemSpacing, + spacingOffset = 2, + maxWidth = 0, + maxHeight = 0, + totalWidth = 0, + totalHeight = 0, + vertical = me.isVertical, + math = Math, + mfloor = math.floor, + mmax = math.max, + index = 0, + i = 0, + len = items ? items.length : 0, + x, y, spacing, item, bbox, height, width; + + //remove all legend items + if (len) { + for (; i < len; i++) { + items[i].destroy(); + } + } + //empty array + items.length = []; + // Create all the item labels, collecting their dimensions and positioning each one + // properly in relation to the previous item + chart.series.each(function(series, i) { + if (series.showInLegend) { + Ext.each([].concat(series.yField), function(field, j) { + item = Ext.create('Ext.chart.LegendItem', { + legend: this, + series: series, + surface: chart.surface, + yFieldIndex: j + }); + bbox = item.getBBox(); + + //always measure from x=0, since not all markers go all the way to the left + width = bbox.width; + height = bbox.height; + + if (i + j === 0) { + spacing = vertical ? padding + height / 2 : padding; + } + else { + spacing = itemSpacing / (vertical ? 2 : 1); + } + // Set the item's position relative to the legend box + item.x = mfloor(vertical ? padding : totalWidth + spacing); + item.y = mfloor(vertical ? totalHeight + spacing : padding + height / 2); + + // Collect cumulative dimensions + totalWidth += width + spacing; + totalHeight += height + spacing; + maxWidth = mmax(maxWidth, width); + maxHeight = mmax(maxHeight, height); + + items.push(item); + }, this); + } + }, me); + + // Store the collected dimensions for later + me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2); + if (vertical && items.length === 1) { + spacingOffset = 1; + } + me.height = mfloor((vertical ? totalHeight - spacingOffset * spacing : maxHeight) + (padding * 2)); + me.itemHeight = maxHeight; + }, + + /** + * @private Get the bounds for the legend's outer box + */ + getBBox: function() { + var me = this; + return { + x: Math.round(me.x) - me.boxStrokeWidth / 2, + y: Math.round(me.y) - me.boxStrokeWidth / 2, + width: me.width, + height: me.height + }; + }, + + /** + * @private Create the box around the legend items + */ + createBox: function() { + var me = this, + box = me.boxSprite = me.chart.surface.add(Ext.apply({ + type: 'rect', + stroke: me.boxStroke, + "stroke-width": me.boxStrokeWidth, + fill: me.boxFill, + zIndex: me.boxZIndex + }, me.getBBox())); + box.redraw(); + }, + + /** + * @private Update the position of all the legend's sprites to match its current x/y values + */ + updatePosition: function() { + var me = this, + x, y, + legendWidth = me.width, + legendHeight = me.height, + padding = me.padding, + chart = me.chart, + chartBBox = chart.chartBBox, + insets = chart.insetPadding, + chartWidth = chartBBox.width - (insets * 2), + chartHeight = chartBBox.height - (insets * 2), + chartX = chartBBox.x + insets, + chartY = chartBBox.y + insets, + surface = chart.surface, + mfloor = Math.floor; + + if (me.isDisplayed()) { + // Find the position based on the dimensions + switch(me.position) { + case "left": + x = insets; + y = mfloor(chartY + chartHeight / 2 - legendHeight / 2); + break; + case "right": + x = mfloor(surface.width - legendWidth) - insets; + y = mfloor(chartY + chartHeight / 2 - legendHeight / 2); + break; + case "top": + x = mfloor(chartX + chartWidth / 2 - legendWidth / 2); + y = insets; + break; + case "bottom": + x = mfloor(chartX + chartWidth / 2 - legendWidth / 2); + y = mfloor(surface.height - legendHeight) - insets; + break; + default: + x = mfloor(me.origX) + insets; + y = mfloor(me.origY) + insets; + } + me.x = x; + me.y = y; + + // Update the position of each item + Ext.each(me.items, function(item) { + item.updatePosition(); + }); + // Update the position of the outer box + me.boxSprite.setAttributes(me.getBBox(), true); + } + } +}); +/** + * @class Ext.chart.Chart + * @extends Ext.draw.Component + * + * The Ext.chart package provides the capability to visualize data. + * Each chart binds directly to an Ext.data.Store enabling automatic updates of the chart. + * A chart configuration object has some overall styling options as well as an array of axes + * and series. A chart instance example could look like: + * +

+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 800,
+        height: 600,
+        animate: true,
+        store: store1,
+        shadow: true,
+        theme: 'Category1',
+        legend: {
+            position: 'right'
+        },
+        axes: [ ...some axes options... ],
+        series: [ ...some series options... ]
+    });
+  
+ * + * In this example we set the `width` and `height` of the chart, we decide whether our series are + * animated or not and we select a store to be bound to the chart. We also turn on shadows for all series, + * select a color theme `Category1` for coloring the series, set the legend to the right part of the chart and + * then tell the chart to render itself in the body element of the document. For more information about the axes and + * series configurations please check the documentation of each series (Line, Bar, Pie, etc). + * + * @xtype chart + */ + +Ext.define('Ext.chart.Chart', { + + /* Begin Definitions */ + + alias: 'widget.chart', + + extend: 'Ext.draw.Component', + + mixins: { + themeManager: 'Ext.chart.theme.Theme', + mask: 'Ext.chart.Mask', + navigation: 'Ext.chart.Navigation' + }, + + requires: [ + 'Ext.util.MixedCollection', + 'Ext.data.StoreManager', + 'Ext.chart.Legend', + 'Ext.util.DelayedTask' + ], + + /* End Definitions */ + + // @private + viewBox: false, + + /** + * @cfg {String} theme (optional) The name of the theme to be used. A theme defines the colors and + * other visual displays of tick marks on axis, text, title text, line colors, marker colors and styles, etc. + * Possible theme values are 'Base', 'Green', 'Sky', 'Red', 'Purple', 'Blue', 'Yellow' and also six category themes + * 'Category1' to 'Category6'. Default value is 'Base'. + */ + + /** + * @cfg {Boolean/Object} animate (optional) true for the default animation (easing: 'ease' and duration: 500) + * or a standard animation config object to be used for default chart animations. Defaults to false. + */ + animate: false, + + /** + * @cfg {Boolean/Object} legend (optional) true for the default legend display or a legend config object. Defaults to false. + */ + legend: false, + + /** + * @cfg {integer} insetPadding (optional) Set the amount of inset padding in pixels for the chart. Defaults to 10. + */ + insetPadding: 10, + + /** + * @cfg {Array} enginePriority + * Defines the priority order for which Surface implementation to use. The first + * one supported by the current environment will be used. + */ + enginePriority: ['Svg', 'Vml'], + + /** + * @cfg {Object|Boolean} background (optional) Set the chart background. This can be a gradient object, image, or color. + * Defaults to false for no background. + * + * For example, if `background` were to be a color we could set the object as + * +

+        background: {
+            //color string
+            fill: '#ccc'
+        }
+     
+ + You can specify an image by using: + +

+        background: {
+            image: 'http://path.to.image/'
+        }
+     
+ + Also you can specify a gradient by using the gradient object syntax: + +

+        background: {
+            gradient: {
+                id: 'gradientId',
+                angle: 45,
+                stops: {
+                    0: {
+                        color: '#555'
+                    }
+                    100: {
+                        color: '#ddd'
+                    }
+                }
+            }
+        }
+     
+ */ + background: false, + + /** + * @cfg {Array} gradients (optional) Define a set of gradients that can be used as `fill` property in sprites. + * The gradients array is an array of objects with the following properties: + * + *
    + *
  • id - string - The unique name of the gradient.
  • + *
  • angle - number, optional - The angle of the gradient in degrees.
  • + *
  • stops - object - An object with numbers as keys (from 0 to 100) and style objects + * as values
  • + *
+ * + + For example: + +

+        gradients: [{
+            id: 'gradientId',
+            angle: 45,
+            stops: {
+                0: {
+                    color: '#555'
+                },
+                100: {
+                    color: '#ddd'
+                }
+            }
+        },  {
+            id: 'gradientId2',
+            angle: 0,
+            stops: {
+                0: {
+                    color: '#590'
+                },
+                20: {
+                    color: '#599'
+                },
+                100: {
+                    color: '#ddd'
+                }
+            }
+        }]
+     
+ + Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example: + +

+        sprite.setAttributes({
+            fill: 'url(#gradientId)'
+        }, true);
+     
+ + */ + + + constructor: function(config) { + var me = this, + defaultAnim; + me.initTheme(config.theme || me.theme); + if (me.gradients) { + Ext.apply(config, { gradients: me.gradients }); + } + if (me.background) { + Ext.apply(config, { background: me.background }); + } + if (config.animate) { + defaultAnim = { + easing: 'ease', + duration: 500 + }; + if (Ext.isObject(config.animate)) { + config.animate = Ext.applyIf(config.animate, defaultAnim); + } + else { + config.animate = defaultAnim; + } + } + me.mixins.mask.constructor.call(me, config); + me.mixins.navigation.constructor.call(me, config); + me.callParent([config]); + }, + + initComponent: function() { + var me = this, + axes, + series; + me.callParent(); + me.addEvents( + 'itemmousedown', + 'itemmouseup', + 'itemmouseover', + 'itemmouseout', + 'itemclick', + 'itemdoubleclick', + 'itemdragstart', + 'itemdrag', + 'itemdragend', + /** + * @event beforerefresh + * Fires before a refresh to the chart data is called. If the beforerefresh handler returns + * false the {@link #refresh} action will be cancelled. + * @param {Chart} this + */ + 'beforerefresh', + /** + * @event refresh + * Fires after the chart data has been refreshed. + * @param {Chart} this + */ + 'refresh' + ); + Ext.applyIf(me, { + zoom: { + width: 1, + height: 1, + x: 0, + y: 0 + } + }); + me.maxGutter = [0, 0]; + me.store = Ext.data.StoreManager.lookup(me.store); + axes = me.axes; + me.axes = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.position; }); + if (axes) { + me.axes.addAll(axes); + } + series = me.series; + me.series = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); }); + if (series) { + me.series.addAll(series); + } + if (me.legend !== false) { + me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend)); + } + + me.on({ + mousemove: me.onMouseMove, + mouseleave: me.onMouseLeave, + mousedown: me.onMouseDown, + mouseup: me.onMouseUp, + scope: me + }); + }, + + // @private overrides the component method to set the correct dimensions to the chart. + afterComponentLayout: function(width, height) { + var me = this; + if (Ext.isNumber(width) && Ext.isNumber(height)) { + me.curWidth = width; + me.curHeight = height; + me.redraw(true); + } + this.callParent(arguments); + }, + + /** + * Redraw the chart. If animations are set this will animate the chart too. + * @cfg {boolean} resize Optional flag which changes the default origin points of the chart for animations. + */ + redraw: function(resize) { + var me = this, + chartBBox = me.chartBBox = { + x: 0, + y: 0, + height: me.curHeight, + width: me.curWidth + }, + legend = me.legend; + me.surface.setSize(chartBBox.width, chartBBox.height); + // Instantiate Series and Axes + me.series.each(me.initializeSeries, me); + me.axes.each(me.initializeAxis, me); + //process all views (aggregated data etc) on stores + //before rendering. + me.axes.each(function(axis) { + axis.processView(); + }); + me.axes.each(function(axis) { + axis.drawAxis(true); + }); + + // Create legend if not already created + if (legend !== false) { + legend.create(); + } + + // Place axes properly, including influence from each other + me.alignAxes(); + + // Reposition legend based on new axis alignment + if (me.legend !== false) { + legend.updatePosition(); + } + + // Find the max gutter + me.getMaxGutter(); + + // Draw axes and series + me.resizing = !!resize; + + me.axes.each(me.drawAxis, me); + me.series.each(me.drawCharts, me); + me.resizing = false; + }, + + // @private set the store after rendering the chart. + afterRender: function() { + var ref, + me = this; + this.callParent(); + + if (me.categoryNames) { + me.setCategoryNames(me.categoryNames); + } + + if (me.tipRenderer) { + ref = me.getFunctionRef(me.tipRenderer); + me.setTipRenderer(ref.fn, ref.scope); + } + me.bindStore(me.store, true); + me.refresh(); + }, + + // @private get x and y position of the mouse cursor. + getEventXY: function(e) { + var me = this, + box = this.surface.getRegion(), + pageXY = e.getXY(), + x = pageXY[0] - box.left, + y = pageXY[1] - box.top; + return [x, y]; + }, + + // @private wrap the mouse down position to delegate the event to the series. + onClick: function(e) { + var me = this, + position = me.getEventXY(e), + item; + + // Ask each series if it has an item corresponding to (not necessarily exactly + // on top of) the current mouse coords. Fire itemclick event. + me.series.each(function(series) { + if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { + if (series.getItemForPoint) { + item = series.getItemForPoint(position[0], position[1]); + if (item) { + series.fireEvent('itemclick', item); + } + } + } + }, me); + }, + + // @private wrap the mouse down position to delegate the event to the series. + onMouseDown: function(e) { + var me = this, + position = me.getEventXY(e), + item; + + if (me.mask) { + me.mixins.mask.onMouseDown.call(me, e); + } + // Ask each series if it has an item corresponding to (not necessarily exactly + // on top of) the current mouse coords. Fire mousedown event. + me.series.each(function(series) { + if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { + if (series.getItemForPoint) { + item = series.getItemForPoint(position[0], position[1]); + if (item) { + series.fireEvent('itemmousedown', item); + } + } + } + }, me); + }, + + // @private wrap the mouse up event to delegate it to the series. + onMouseUp: function(e) { + var me = this, + position = me.getEventXY(e), + item; + + if (me.mask) { + me.mixins.mask.onMouseUp.call(me, e); + } + // Ask each series if it has an item corresponding to (not necessarily exactly + // on top of) the current mouse coords. Fire mousedown event. + me.series.each(function(series) { + if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { + if (series.getItemForPoint) { + item = series.getItemForPoint(position[0], position[1]); + if (item) { + series.fireEvent('itemmouseup', item); + } + } + } + }, me); + }, + + // @private wrap the mouse move event so it can be delegated to the series. + onMouseMove: function(e) { + var me = this, + position = me.getEventXY(e), + item, last, storeItem, storeField; + + if (me.mask) { + me.mixins.mask.onMouseMove.call(me, e); + } + // Ask each series if it has an item corresponding to (not necessarily exactly + // on top of) the current mouse coords. Fire itemmouseover/out events. + me.series.each(function(series) { + if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { + if (series.getItemForPoint) { + item = series.getItemForPoint(position[0], position[1]); + last = series._lastItemForPoint; + storeItem = series._lastStoreItem; + storeField = series._lastStoreField; + + + if (item !== last || item && (item.storeItem != storeItem || item.storeField != storeField)) { + if (last) { + series.fireEvent('itemmouseout', last); + delete series._lastItemForPoint; + delete series._lastStoreField; + delete series._lastStoreItem; + } + if (item) { + series.fireEvent('itemmouseover', item); + series._lastItemForPoint = item; + series._lastStoreItem = item.storeItem; + series._lastStoreField = item.storeField; + } + } + } + } else { + last = series._lastItemForPoint; + if (last) { + series.fireEvent('itemmouseout', last); + delete series._lastItemForPoint; + delete series._lastStoreField; + delete series._lastStoreItem; + } + } + }, me); + }, + + // @private handle mouse leave event. + onMouseLeave: function(e) { + var me = this; + if (me.mask) { + me.mixins.mask.onMouseLeave.call(me, e); + } + me.series.each(function(series) { + delete series._lastItemForPoint; + }); + }, + + // @private buffered refresh for when we update the store + delayRefresh: function() { + var me = this; + if (!me.refreshTask) { + me.refreshTask = Ext.create('Ext.util.DelayedTask', me.refresh, me); + } + me.refreshTask.delay(me.refreshBuffer); + }, + + // @private + refresh: function() { + var me = this; + if (me.rendered && me.curWidth != undefined && me.curHeight != undefined) { + if (me.fireEvent('beforerefresh', me) !== false) { + me.redraw(); + me.fireEvent('refresh', me); + } + } + }, + + /** + * Changes the data store bound to this chart and refreshes it. + * @param {Store} store The store to bind to this chart + */ + bindStore: function(store, initial) { + var me = this; + if (!initial && me.store) { + if (store !== me.store && me.store.autoDestroy) { + me.store.destroy(); + } + else { + me.store.un('datachanged', me.refresh, me); + me.store.un('add', me.delayRefresh, me); + me.store.un('remove', me.delayRefresh, me); + me.store.un('update', me.delayRefresh, me); + me.store.un('clear', me.refresh, me); + } + } + if (store) { + store = Ext.data.StoreManager.lookup(store); + store.on({ + scope: me, + datachanged: me.refresh, + add: me.delayRefresh, + remove: me.delayRefresh, + update: me.delayRefresh, + clear: me.refresh + }); + } + me.store = store; + if (store && !initial) { + me.refresh(); + } + }, + + // @private Create Axis + initializeAxis: function(axis) { + var me = this, + chartBBox = me.chartBBox, + w = chartBBox.width, + h = chartBBox.height, + x = chartBBox.x, + y = chartBBox.y, + themeAttrs = me.themeAttrs, + config = { + chart: me + }; + if (themeAttrs) { + config.axisStyle = Ext.apply({}, themeAttrs.axis); + config.axisLabelLeftStyle = Ext.apply({}, themeAttrs.axisLabelLeft); + config.axisLabelRightStyle = Ext.apply({}, themeAttrs.axisLabelRight); + config.axisLabelTopStyle = Ext.apply({}, themeAttrs.axisLabelTop); + config.axisLabelBottomStyle = Ext.apply({}, themeAttrs.axisLabelBottom); + config.axisTitleLeftStyle = Ext.apply({}, themeAttrs.axisTitleLeft); + config.axisTitleRightStyle = Ext.apply({}, themeAttrs.axisTitleRight); + config.axisTitleTopStyle = Ext.apply({}, themeAttrs.axisTitleTop); + config.axisTitleBottomStyle = Ext.apply({}, themeAttrs.axisTitleBottom); + } + switch (axis.position) { + case 'top': + Ext.apply(config, { + length: w, + width: h, + x: x, + y: y + }); + break; + case 'bottom': + Ext.apply(config, { + length: w, + width: h, + x: x, + y: h + }); + break; + case 'left': + Ext.apply(config, { + length: h, + width: w, + x: x, + y: h + }); + break; + case 'right': + Ext.apply(config, { + length: h, + width: w, + x: w, + y: h + }); + break; + } + if (!axis.chart) { + Ext.apply(config, axis); + axis = me.axes.replace(Ext.createByAlias('axis.' + axis.type.toLowerCase(), config)); + } + else { + Ext.apply(axis, config); + } + }, + + + /** + * @private Adjust the dimensions and positions of each axis and the chart body area after accounting + * for the space taken up on each side by the axes and legend. + */ + alignAxes: function() { + var me = this, + axes = me.axes, + legend = me.legend, + edges = ['top', 'right', 'bottom', 'left'], + chartBBox, + insetPadding = me.insetPadding, + insets = { + top: insetPadding, + right: insetPadding, + bottom: insetPadding, + left: insetPadding + }; + + function getAxis(edge) { + var i = axes.findIndex('position', edge); + return (i < 0) ? null : axes.getAt(i); + } + + // Find the space needed by axes and legend as a positive inset from each edge + Ext.each(edges, function(edge) { + var isVertical = (edge === 'left' || edge === 'right'), + axis = getAxis(edge), + bbox; + + // Add legend size if it's on this edge + if (legend !== false) { + if (legend.position === edge) { + bbox = legend.getBBox(); + insets[edge] += (isVertical ? bbox.width : bbox.height) + insets[edge]; + } + } + + // Add axis size if there's one on this edge only if it has been + //drawn before. + if (axis && axis.bbox) { + bbox = axis.bbox; + insets[edge] += (isVertical ? bbox.width : bbox.height); + } + }); + // Build the chart bbox based on the collected inset values + chartBBox = { + x: insets.left, + y: insets.top, + width: me.curWidth - insets.left - insets.right, + height: me.curHeight - insets.top - insets.bottom + }; + me.chartBBox = chartBBox; + + // Go back through each axis and set its length and position based on the + // corresponding edge of the chartBBox + axes.each(function(axis) { + var pos = axis.position, + isVertical = (pos === 'left' || pos === 'right'); + + axis.x = (pos === 'right' ? chartBBox.x + chartBBox.width : chartBBox.x); + axis.y = (pos === 'top' ? chartBBox.y : chartBBox.y + chartBBox.height); + axis.width = (isVertical ? chartBBox.width : chartBBox.height); + axis.length = (isVertical ? chartBBox.height : chartBBox.width); + }); + }, + + // @private initialize the series. + initializeSeries: function(series, idx) { + var me = this, + themeAttrs = me.themeAttrs, + seriesObj, markerObj, seriesThemes, st, + markerThemes, colorArrayStyle = [], + i = 0, l, + config = { + chart: me, + seriesId: series.seriesId + }; + if (themeAttrs) { + seriesThemes = themeAttrs.seriesThemes; + markerThemes = themeAttrs.markerThemes; + seriesObj = Ext.apply({}, themeAttrs.series); + markerObj = Ext.apply({}, themeAttrs.marker); + config.seriesStyle = Ext.apply(seriesObj, seriesThemes[idx % seriesThemes.length]); + config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel); + config.markerStyle = Ext.apply(markerObj, markerThemes[idx % markerThemes.length]); + if (themeAttrs.colors) { + config.colorArrayStyle = themeAttrs.colors; + } else { + colorArrayStyle = []; + for (l = seriesThemes.length; i < l; i++) { + st = seriesThemes[i]; + if (st.fill || st.stroke) { + colorArrayStyle.push(st.fill || st.stroke); + } + } + if (colorArrayStyle.length) { + config.colorArrayStyle = colorArrayStyle; + } + } + config.seriesIdx = idx; + } + if (series instanceof Ext.chart.series.Series) { + Ext.apply(series, config); + } else { + Ext.applyIf(config, series); + series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config)); + } + if (series.initialize) { + series.initialize(); + } + }, + + // @private + getMaxGutter: function() { + var me = this, + maxGutter = [0, 0]; + me.series.each(function(s) { + var gutter = s.getGutters && s.getGutters() || [0, 0]; + maxGutter[0] = Math.max(maxGutter[0], gutter[0]); + maxGutter[1] = Math.max(maxGutter[1], gutter[1]); + }); + me.maxGutter = maxGutter; + }, + + // @private draw axis. + drawAxis: function(axis) { + axis.drawAxis(); + }, + + // @private draw series. + drawCharts: function(series) { + series.triggerafterrender = false; + series.drawSeries(); + if (!this.animate) { + series.fireEvent('afterrender'); + } + }, + + // @private remove gently. + destroy: function() { + this.surface.destroy(); + this.bindStore(null); + this.callParent(arguments); + } +}); + +/** + * @class Ext.chart.Highlight + * @ignore + */ +Ext.define('Ext.chart.Highlight', { + + /* Begin Definitions */ + + requires: ['Ext.fx.Anim'], + + /* End Definitions */ + + /** + * Highlight the given series item. + * @param {Boolean|Object} Default's false. Can also be an object width style properties (i.e fill, stroke, radius) + * or just use default styles per series by setting highlight = true. + */ + highlight: false, + + highlightCfg : null, + + constructor: function(config) { + if (config.highlight) { + if (config.highlight !== true) { //is an object + this.highlightCfg = Ext.apply({}, config.highlight); + } + else { + this.highlightCfg = { + fill: '#fdd', + radius: 20, + lineWidth: 5, + stroke: '#f55' + }; + } + } + }, + + /** + * Highlight the given series item. + * @param {Object} item Info about the item; same format as returned by #getItemForPoint. + */ + highlightItem: function(item) { + if (!item) { + return; + } + + var me = this, + sprite = item.sprite, + opts = me.highlightCfg, + surface = me.chart.surface, + animate = me.chart.animate, + p, + from, + to, + pi; + + if (!me.highlight || !sprite || sprite._highlighted) { + return; + } + if (sprite._anim) { + sprite._anim.paused = true; + } + sprite._highlighted = true; + if (!sprite._defaults) { + sprite._defaults = Ext.apply(sprite._defaults || {}, + sprite.attr); + from = {}; + to = {}; + for (p in opts) { + if (! (p in sprite._defaults)) { + sprite._defaults[p] = surface.availableAttrs[p]; + } + from[p] = sprite._defaults[p]; + to[p] = opts[p]; + if (Ext.isObject(opts[p])) { + from[p] = {}; + to[p] = {}; + Ext.apply(sprite._defaults[p], sprite.attr[p]); + Ext.apply(from[p], sprite._defaults[p]); + for (pi in sprite._defaults[p]) { + if (! (pi in opts[p])) { + to[p][pi] = from[p][pi]; + } else { + to[p][pi] = opts[p][pi]; + } + } + for (pi in opts[p]) { + if (! (pi in to[p])) { + to[p][pi] = opts[p][pi]; + } + } + } + } + sprite._from = from; + sprite._to = to; + } + if (animate) { + sprite._anim = Ext.create('Ext.fx.Anim', { + target: sprite, + from: sprite._from, + to: sprite._to, + duration: 150 + }); + } else { + sprite.setAttributes(sprite._to, true); + } + }, + + /** + * Un-highlight any existing highlights + */ + unHighlightItem: function() { + if (!this.highlight || !this.items) { + return; + } + + var me = this, + items = me.items, + len = items.length, + opts = me.highlightCfg, + animate = me.chart.animate, + i = 0, + obj, + p, + sprite; + + for (; i < len; i++) { + if (!items[i]) { + continue; + } + sprite = items[i].sprite; + if (sprite && sprite._highlighted) { + if (sprite._anim) { + sprite._anim.paused = true; + } + obj = {}; + for (p in opts) { + if (Ext.isObject(sprite._defaults[p])) { + obj[p] = {}; + Ext.apply(obj[p], sprite._defaults[p]); + } + else { + obj[p] = sprite._defaults[p]; + } + } + if (animate) { + sprite._anim = Ext.create('Ext.fx.Anim', { + target: sprite, + to: obj, + duration: 150 + }); + } + else { + sprite.setAttributes(obj, true); + } + delete sprite._highlighted; + //delete sprite._defaults; + } + } + }, + + cleanHighlights: function() { + if (!this.highlight) { + return; + } + + var group = this.group, + markerGroup = this.markerGroup, + i = 0, + l; + for (l = group.getCount(); i < l; i++) { + delete group.getAt(i)._defaults; + } + if (markerGroup) { + for (l = markerGroup.getCount(); i < l; i++) { + delete markerGroup.getAt(i)._defaults; + } + } + } +}); +/** + * @class Ext.chart.Label + * + * Labels is a mixin whose methods are appended onto the Series class. Labels is an interface with methods implemented + * in each of the Series (Pie, Bar, etc) for label creation and label placement. + * + * The methods implemented by the Series are: + * + * - **`onCreateLabel(storeItem, item, i, display)`** Called each time a new label is created. + * The arguments of the method are: + * - *`storeItem`* The element of the store that is related to the label sprite. + * - *`item`* The item related to the label sprite. An item is an object containing the position of the shape + * used to describe the visualization and also pointing to the actual shape (circle, rectangle, path, etc). + * - *`i`* The index of the element created (i.e the first created label, second created label, etc) + * - *`display`* The display type. May be false if the label is hidden + * + * - **`onPlaceLabel(label, storeItem, item, i, display, animate)`** Called for updating the position of the label. + * The arguments of the method are: + * - *`label`* The sprite label. + * - *`storeItem`* The element of the store that is related to the label sprite + * - *`item`* The item related to the label sprite. An item is an object containing the position of the shape + * used to describe the visualization and also pointing to the actual shape (circle, rectangle, path, etc). + * - *`i`* The index of the element to be updated (i.e. whether it is the first, second, third from the labelGroup) + * - *`display`* The display type. May be false if the label is hidden. + * - *`animate`* A boolean value to set or unset animations for the labels. + */ +Ext.define('Ext.chart.Label', { + + /* Begin Definitions */ + + requires: ['Ext.draw.Color'], + + /* End Definitions */ + + /** + * @cfg {String} display + * Specifies the presence and position of labels for each pie slice. Either "rotate", "middle", "insideStart", + * "insideEnd", "outside", "over", "under", or "none" to prevent label rendering. + * Default value: 'none'. + */ + + /** + * @cfg {String} color + * The color of the label text. + * Default value: '#000' (black). + */ + + /** + * @cfg {String} field + * The name of the field to be displayed in the label. + * Default value: 'name'. + */ + + /** + * @cfg {Number} minMargin + * Specifies the minimum distance from a label to the origin of the visualization. + * This parameter is useful when using PieSeries width variable pie slice lengths. + * Default value: 50. + */ + + /** + * @cfg {String} font + * The font used for the labels. + * Defautl value: "11px Helvetica, sans-serif". + */ + + /** + * @cfg {String} orientation + * Either "horizontal" or "vertical". + * Dafault value: "horizontal". + */ + + /** + * @cfg {Function} renderer + * Optional function for formatting the label into a displayable value. + * Default value: function(v) { return v; } + * @param v + */ + + //@private a regex to parse url type colors. + colorStringRe: /url\s*\(\s*#([^\/)]+)\s*\)/, + + //@private the mixin constructor. Used internally by Series. + constructor: function(config) { + var me = this; + me.label = Ext.applyIf(me.label || {}, + { + display: "none", + color: "#000", + field: "name", + minMargin: 50, + font: "11px Helvetica, sans-serif", + orientation: "horizontal", + renderer: function(v) { + return v; + } + }); + + if (me.label.display !== 'none') { + me.labelsGroup = me.chart.surface.getGroup(me.seriesId + '-labels'); + } + }, + + //@private a method to render all labels in the labelGroup + renderLabels: function() { + var me = this, + chart = me.chart, + gradients = chart.gradients, + gradient, + items = me.items, + animate = chart.animate, + config = me.label, + display = config.display, + color = config.color, + field = [].concat(config.field), + group = me.labelsGroup, + store = me.chart.store, + len = store.getCount(), + ratio = items.length / len, + i, count, j, + k, gradientsCount = (gradients || 0) && gradients.length, + colorStopTotal, colorStopIndex, colorStop, + item, label, storeItem, + sprite, spriteColor, spriteBrightness, labelColor, + Color = Ext.draw.Color, + colorString; + + if (display == 'none') { + return; + } + + for (i = 0, count = 0; i < len; i++) { + for (j = 0; j < ratio; j++) { + item = items[count]; + label = group.getAt(count); + storeItem = store.getAt(i); + + if (!item && label) { + label.hide(true); + } + + if (item && field[j]) { + if (!label) { + label = me.onCreateLabel(storeItem, item, i, display, j, count); + } + me.onPlaceLabel(label, storeItem, item, i, display, animate, j, count); + + //set contrast + if (config.contrast && item.sprite) { + sprite = item.sprite; + colorString = sprite._to && sprite._to.fill || sprite.attr.fill; + spriteColor = Color.fromString(colorString); + //color wasn't parsed property maybe because it's a gradient id + if (colorString && !spriteColor) { + colorString = colorString.match(me.colorStringRe)[1]; + for (k = 0; k < gradientsCount; k++) { + gradient = gradients[k]; + if (gradient.id == colorString) { + //avg color stops + colorStop = 0; colorStopTotal = 0; + for (colorStopIndex in gradient.stops) { + colorStop++; + colorStopTotal += Color.fromString(gradient.stops[colorStopIndex].color).getGrayscale(); + } + spriteBrightness = (colorStopTotal / colorStop) / 255; + break; + } + } + } + else { + spriteBrightness = spriteColor.getGrayscale() / 255; + } + labelColor = Color.fromString(label.attr.color || label.attr.fill).getHSL(); + + labelColor[2] = spriteBrightness > 0.5? 0.2 : 0.8; + label.setAttributes({ + fill: String(Color.fromHSL.apply({}, labelColor)) + }, true); + } + } + count++; + } + } + me.hideLabels(count); + }, + + //@private a method to hide labels. + hideLabels: function(index) { + var labelsGroup = this.labelsGroup, len; + if (labelsGroup) { + len = labelsGroup.getCount(); + while (len-->index) { + labelsGroup.getAt(len).hide(true); + } + } + } +}); +Ext.define('Ext.chart.MaskLayer', { + extend: 'Ext.Component', + + constructor: function(config) { + config = Ext.apply(config || {}, { + style: 'position:absolute;background-color:#888;cursor:move;opacity:0.6;border:1px solid #222;' + }); + this.callParent([config]); + }, + + initComponent: function() { + var me = this; + me.callParent(arguments); + me.addEvents( + 'mousedown', + 'mouseup', + 'mousemove', + 'mouseenter', + 'mouseleave' + ); + }, + + initDraggable: function() { + this.callParent(arguments); + this.dd.onStart = function (e) { + var me = this, + comp = me.comp; + + // Cache the start [X, Y] array + this.startPosition = comp.getPosition(true); + + // If client Component has a ghost method to show a lightweight version of itself + // then use that as a drag proxy unless configured to liveDrag. + if (comp.ghost && !comp.liveDrag) { + me.proxy = comp.ghost(); + me.dragTarget = me.proxy.header.el; + } + + // Set the constrainTo Region before we start dragging. + if (me.constrain || me.constrainDelegate) { + me.constrainTo = me.calculateConstrainRegion(); + } + }; + } +}); +/** + * @class Ext.chart.TipSurface + * @ignore + */ +Ext.define('Ext.chart.TipSurface', { + + /* Begin Definitions */ + + extend: 'Ext.draw.Component', + + /* End Definitions */ + + spriteArray: false, + renderFirst: true, + + constructor: function(config) { + this.callParent([config]); + if (config.sprites) { + this.spriteArray = [].concat(config.sprites); + delete config.sprites; + } + }, + + onRender: function() { + var me = this, + i = 0, + l = 0, + sp, + sprites; + this.callParent(arguments); + sprites = me.spriteArray; + if (me.renderFirst && sprites) { + me.renderFirst = false; + for (l = sprites.length; i < l; i++) { + sp = me.surface.add(sprites[i]); + sp.setAttributes({ + hidden: false + }, + true); + } + } + } +}); + +/** + * @class Ext.chart.Tip + * @ignore + */ +Ext.define('Ext.chart.Tip', { + + /* Begin Definitions */ + + requires: ['Ext.tip.ToolTip', 'Ext.chart.TipSurface'], + + /* End Definitions */ + + constructor: function(config) { + var me = this, + surface, + sprites, + tipSurface; + if (config.tips) { + me.tipTimeout = null; + me.tipConfig = Ext.apply({}, config.tips, { + renderer: Ext.emptyFn, + constrainPosition: false + }); + me.tooltip = Ext.create('Ext.tip.ToolTip', me.tipConfig); + Ext.getBody().on('mousemove', me.tooltip.onMouseMove, me.tooltip); + if (me.tipConfig.surface) { + //initialize a surface + surface = me.tipConfig.surface; + sprites = surface.sprites; + tipSurface = Ext.create('Ext.chart.TipSurface', { + id: 'tipSurfaceComponent', + sprites: sprites + }); + if (surface.width && surface.height) { + tipSurface.setSize(surface.width, surface.height); + } + me.tooltip.add(tipSurface); + me.spriteTip = tipSurface; + } + } + }, + + showTip: function(item) { + var me = this; + if (!me.tooltip) { + return; + } + clearTimeout(me.tipTimeout); + var tooltip = me.tooltip, + spriteTip = me.spriteTip, + tipConfig = me.tipConfig, + trackMouse = tooltip.trackMouse, + sprite, surface, surfaceExt, pos, x, y; + if (!trackMouse) { + tooltip.trackMouse = true; + sprite = item.sprite; + surface = sprite.surface; + surfaceExt = Ext.get(surface.getId()); + if (surfaceExt) { + pos = surfaceExt.getXY(); + x = pos[0] + (sprite.attr.x || 0) + (sprite.attr.translation && sprite.attr.translation.x || 0); + y = pos[1] + (sprite.attr.y || 0) + (sprite.attr.translation && sprite.attr.translation.y || 0); + tooltip.targetXY = [x, y]; + } + } + if (spriteTip) { + tipConfig.renderer.call(tooltip, item.storeItem, item, spriteTip.surface); + } else { + tipConfig.renderer.call(tooltip, item.storeItem, item); + } + tooltip.show(); + tooltip.trackMouse = trackMouse; + }, + + hideTip: function(item) { + var tooltip = this.tooltip; + if (!tooltip) { + return; + } + clearTimeout(this.tipTimeout); + this.tipTimeout = setTimeout(function() { + tooltip.hide(); + }, 0); + } +}); +/** + * @class Ext.chart.axis.Abstract + * @ignore + */ +Ext.define('Ext.chart.axis.Abstract', { + + /* Begin Definitions */ + + requires: ['Ext.chart.Chart'], + + /* End Definitions */ + + constructor: function(config) { + config = config || {}; + + var me = this, + pos = config.position || 'left'; + + pos = pos.charAt(0).toUpperCase() + pos.substring(1); + //axisLabel(Top|Bottom|Right|Left)Style + config.label = Ext.apply(config['axisLabel' + pos + 'Style'] || {}, config.label || {}); + config.axisTitleStyle = Ext.apply(config['axisTitle' + pos + 'Style'] || {}, config.labelTitle || {}); + Ext.apply(me, config); + me.fields = [].concat(me.fields); + this.callParent(); + me.labels = []; + me.getId(); + me.labelGroup = me.chart.surface.getGroup(me.axisId + "-labels"); + }, + + alignment: null, + grid: false, + steps: 10, + x: 0, + y: 0, + minValue: 0, + maxValue: 0, + + getId: function() { + return this.axisId || (this.axisId = Ext.id(null, 'ext-axis-')); + }, + + /* + Called to process a view i.e to make aggregation and filtering over + a store creating a substore to be used to render the axis. Since many axes + may do different things on the data and we want the final result of all these + operations to be rendered we need to call processView on all axes before drawing + them. + */ + processView: Ext.emptyFn, + + drawAxis: Ext.emptyFn, + addDisplayAndLabels: Ext.emptyFn +}); + +/** + * @class Ext.chart.axis.Axis + * @extends Ext.chart.axis.Abstract + * + * Defines axis for charts. The axis position, type, style can be configured. + * The axes are defined in an axes array of configuration objects where the type, + * field, grid and other configuration options can be set. To know more about how + * to create a Chart please check the Chart class documentation. Here's an example for the axes part: + * 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',
+        grid: true,
+        position: 'left',
+        fields: ['data1', 'data2', 'data3'],
+        title: 'Number of Hits',
+        grid: {
+            odd: {
+                opacity: 1,
+                fill: '#ddd',
+                stroke: '#bbb',
+                'stroke-width': 1
+            }
+        },
+        minimum: 0
+    }, {
+        type: 'Category',
+        position: 'bottom',
+        fields: ['name'],
+        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', { + + /* Begin Definitions */ + + extend: 'Ext.chart.axis.Abstract', + + alternateClassName: 'Ext.chart.Axis', + + requires: ['Ext.draw.Draw'], + + /* End Definitions */ + + /** + * @cfg {Number} majorTickSteps + * If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value. + */ + + /** + * @cfg {Number} minorTickSteps + * The number of small ticks between two major ticks. Default is zero. + */ + + /** + * @cfg {Number} dashSize + * The size of the dash marker. Default's 3. + */ + dashSize: 3, + + /** + * @cfg {String} position + * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`. + */ + position: 'bottom', + + // @private + skipFirst: false, + + /** + * @cfg {Number} length + * Offset axis position. Default's 0. + */ + length: 0, + + /** + * @cfg {Number} width + * Offset axis width. Default's 0. + */ + width: 0, + + majorTickSteps: false, + + // @private + applyData: Ext.emptyFn, + + // @private creates a structure with start, end and step points. + calcEnds: function() { + var me = this, + math = Math, + mmax = math.max, + mmin = math.min, + store = me.chart.substore || me.chart.store, + series = me.chart.series.items, + fields = me.fields, + ln = fields.length, + min = isNaN(me.minimum) ? Infinity : me.minimum, + max = isNaN(me.maximum) ? -Infinity : me.maximum, + prevMin = me.prevMin, + prevMax = me.prevMax, + aggregate = false, + total = 0, + excludes = [], + outfrom, outto, + i, l, values, rec, out; + + //if one series is stacked I have to aggregate the values + //for the scale. + for (i = 0, l = series.length; !aggregate && i < l; i++) { + aggregate = aggregate || series[i].stacked; + excludes = series[i].__excludes || excludes; + } + store.each(function(record) { + if (aggregate) { + if (!isFinite(min)) { + min = 0; + } + for (values = [0, 0], i = 0; i < ln; i++) { + if (excludes[i]) { + continue; + } + rec = record.get(fields[i]); + values[+(rec > 0)] += math.abs(rec); + } + max = mmax(max, -values[0], values[1]); + min = mmin(min, -values[0], values[1]); + } + else { + for (i = 0; i < ln; i++) { + if (excludes[i]) { + continue; + } + value = record.get(fields[i]); + max = mmax(max, value); + min = mmin(min, value); + } + } + }); + if (!isFinite(max)) { + max = me.prevMax || 0; + } + if (!isFinite(min)) { + min = me.prevMin || 0; + } + //normalize min max for snapEnds. + if (min != max && (max != (max >> 0))) { + max = (max >> 0) + 1; + } + out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ? (me.majorTickSteps +1) : me.steps); + outfrom = out.from; + outto = out.to; + if (!isNaN(me.maximum)) { + //TODO(nico) users are responsible for their own minimum/maximum values set. + //Clipping should be added to remove lines in the chart which are below the axis. + out.to = me.maximum; + } + if (!isNaN(me.minimum)) { + //TODO(nico) users are responsible for their own minimum/maximum values set. + //Clipping should be added to remove lines in the chart which are below the axis. + out.from = me.minimum; + } + + //Adjust after adjusting minimum and maximum + out.step = (out.to - out.from) / (outto - outfrom) * out.step; + + if (me.adjustMaximumByMajorUnit) { + out.to += out.step; + } + if (me.adjustMinimumByMajorUnit) { + out.from -= out.step; + } + me.prevMin = min == max? 0 : min; + me.prevMax = max; + return out; + }, + + /** + * Renders the axis into the screen and updates it's position. + */ + drawAxis: function (init) { + var me = this, + i, j, + x = me.x, + y = me.y, + gutterX = me.chart.maxGutter[0], + gutterY = me.chart.maxGutter[1], + dashSize = me.dashSize, + subDashesX = me.minorTickSteps || 0, + subDashesY = me.minorTickSteps || 0, + length = me.length, + position = me.position, + inflections = [], + calcLabels = false, + stepCalcs = me.applyData(), + step = stepCalcs.step, + steps = stepCalcs.steps, + from = stepCalcs.from, + to = stepCalcs.to, + trueLength, + currentX, + currentY, + path, + prev, + dashesX, + dashesY, + delta; + + //If no steps are specified + //then don't draw the axis. This generally happens + //when an empty store. + if (me.hidden || isNaN(step) || (from == to)) { + return; + } + + me.from = stepCalcs.from; + me.to = stepCalcs.to; + if (position == 'left' || position == 'right') { + currentX = Math.floor(x) + 0.5; + path = ["M", currentX, y, "l", 0, -length]; + trueLength = length - (gutterY * 2); + } + else { + currentY = Math.floor(y) + 0.5; + path = ["M", x, currentY, "l", length, 0]; + trueLength = length - (gutterX * 2); + } + + delta = trueLength / (steps || 1); + dashesX = Math.max(subDashesX +1, 0); + dashesY = Math.max(subDashesY +1, 0); + if (me.type == 'Numeric') { + calcLabels = true; + me.labels = [stepCalcs.from]; + } + if (position == 'right' || position == 'left') { + currentY = y - gutterY; + currentX = x - ((position == 'left') * dashSize * 2); + while (currentY >= y - gutterY - trueLength) { + path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashSize * 2 + 1, 0); + if (currentY != y - gutterY) { + for (i = 1; i < dashesY; i++) { + path.push("M", currentX + dashSize, Math.floor(currentY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0); + } + } + inflections.push([ Math.floor(x), Math.floor(currentY) ]); + currentY -= delta; + if (calcLabels) { + me.labels.push(me.labels[me.labels.length -1] + step); + } + if (delta === 0) { + break; + } + } + if (Math.round(currentY + delta - (y - gutterY - trueLength))) { + path.push("M", currentX, Math.floor(y - length + gutterY) + 0.5, "l", dashSize * 2 + 1, 0); + for (i = 1; i < dashesY; i++) { + path.push("M", currentX + dashSize, Math.floor(y - length + gutterY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0); + } + inflections.push([ Math.floor(x), Math.floor(currentY) ]); + if (calcLabels) { + me.labels.push(me.labels[me.labels.length -1] + step); + } + } + } else { + currentX = x + gutterX; + currentY = y - ((position == 'top') * dashSize * 2); + while (currentX <= x + gutterX + trueLength) { + path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashSize * 2 + 1); + if (currentX != x + gutterX) { + for (i = 1; i < dashesX; i++) { + path.push("M", Math.floor(currentX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1); + } + } + inflections.push([ Math.floor(currentX), Math.floor(y) ]); + currentX += delta; + if (calcLabels) { + me.labels.push(me.labels[me.labels.length -1] + step); + } + if (delta === 0) { + break; + } + } + if (Math.round(currentX - delta - (x + gutterX + trueLength))) { + path.push("M", Math.floor(x + length - gutterX) + 0.5, currentY, "l", 0, dashSize * 2 + 1); + for (i = 1; i < dashesX; i++) { + path.push("M", Math.floor(x + length - gutterX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1); + } + inflections.push([ Math.floor(currentX), Math.floor(y) ]); + if (calcLabels) { + me.labels.push(me.labels[me.labels.length -1] + step); + } + } + } + if (!me.axis) { + me.axis = me.chart.surface.add(Ext.apply({ + type: 'path', + path: path + }, me.axisStyle)); + } + me.axis.setAttributes({ + path: path + }, true); + me.inflections = inflections; + if (!init && me.grid) { + me.drawGrid(); + } + me.axisBBox = me.axis.getBBox(); + me.drawLabel(); + }, + + /** + * Renders an horizontal and/or vertical grid into the Surface. + */ + drawGrid: function() { + var me = this, + surface = me.chart.surface, + grid = me.grid, + odd = grid.odd, + even = grid.even, + inflections = me.inflections, + ln = inflections.length - ((odd || even)? 0 : 1), + position = me.position, + gutter = me.chart.maxGutter, + width = me.width - 2, + vert = false, + point, prevPoint, + i = 1, + path = [], styles, lineWidth, dlineWidth, + oddPath = [], evenPath = []; + + if ((gutter[1] !== 0 && (position == 'left' || position == 'right')) || + (gutter[0] !== 0 && (position == 'top' || position == 'bottom'))) { + i = 0; + ln++; + } + for (; i < ln; i++) { + point = inflections[i]; + prevPoint = inflections[i - 1]; + if (odd || even) { + path = (i % 2)? oddPath : evenPath; + styles = ((i % 2)? odd : even) || {}; + lineWidth = (styles.lineWidth || styles['stroke-width'] || 0) / 2; + dlineWidth = 2 * lineWidth; + if (position == 'left') { + path.push("M", prevPoint[0] + 1 + lineWidth, prevPoint[1] + 0.5 - lineWidth, + "L", prevPoint[0] + 1 + width - lineWidth, prevPoint[1] + 0.5 - lineWidth, + "L", point[0] + 1 + width - lineWidth, point[1] + 0.5 + lineWidth, + "L", point[0] + 1 + lineWidth, point[1] + 0.5 + lineWidth, "Z"); + } + else if (position == 'right') { + path.push("M", prevPoint[0] - lineWidth, prevPoint[1] + 0.5 - lineWidth, + "L", prevPoint[0] - width + lineWidth, prevPoint[1] + 0.5 - lineWidth, + "L", point[0] - width + lineWidth, point[1] + 0.5 + lineWidth, + "L", point[0] - lineWidth, point[1] + 0.5 + lineWidth, "Z"); + } + else if (position == 'top') { + path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + lineWidth, + "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + width - lineWidth, + "L", point[0] + 0.5 - lineWidth, point[1] + 1 + width - lineWidth, + "L", point[0] + 0.5 - lineWidth, point[1] + 1 + lineWidth, "Z"); + } + else { + path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - lineWidth, + "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - width + lineWidth, + "L", point[0] + 0.5 - lineWidth, point[1] - width + lineWidth, + "L", point[0] + 0.5 - lineWidth, point[1] - lineWidth, "Z"); + } + } else { + if (position == 'left') { + path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", width, 0]); + } + else if (position == 'right') { + path = path.concat(["M", point[0] - 0.5, point[1] + 0.5, "l", -width, 0]); + } + else if (position == 'top') { + path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", 0, width]); + } + else { + path = path.concat(["M", point[0] + 0.5, point[1] - 0.5, "l", 0, -width]); + } + } + } + if (odd || even) { + if (oddPath.length) { + if (!me.gridOdd && oddPath.length) { + me.gridOdd = surface.add({ + type: 'path', + path: oddPath + }); + } + me.gridOdd.setAttributes(Ext.apply({ + path: oddPath, + hidden: false + }, odd || {}), true); + } + if (evenPath.length) { + if (!me.gridEven) { + me.gridEven = surface.add({ + type: 'path', + path: evenPath + }); + } + me.gridEven.setAttributes(Ext.apply({ + path: evenPath, + hidden: false + }, even || {}), true); + } + } + else { + if (path.length) { + if (!me.gridLines) { + me.gridLines = me.chart.surface.add({ + type: 'path', + path: path, + "stroke-width": me.lineWidth || 1, + stroke: me.gridColor || '#ccc' + }); + } + me.gridLines.setAttributes({ + hidden: false, + path: path + }, true); + } + else if (me.gridLines) { + me.gridLines.hide(true); + } + } + }, + + //@private + getOrCreateLabel: function(i, text) { + var me = this, + labelGroup = me.labelGroup, + textLabel = labelGroup.getAt(i), + surface = me.chart.surface; + if (textLabel) { + if (text != textLabel.attr.text) { + textLabel.setAttributes(Ext.apply({ + text: text + }, me.label), true); + textLabel._bbox = textLabel.getBBox(); + } + } + else { + textLabel = surface.add(Ext.apply({ + group: labelGroup, + type: 'text', + x: 0, + y: 0, + text: text + }, me.label)); + surface.renderItem(textLabel); + textLabel._bbox = textLabel.getBBox(); + } + //get untransformed bounding box + if (me.label.rotation) { + textLabel.setAttributes({ + rotation: { + degrees: 0 + } + }, true); + textLabel._ubbox = textLabel.getBBox(); + textLabel.setAttributes(me.label, true); + } else { + textLabel._ubbox = textLabel._bbox; + } + return textLabel; + }, + + rect2pointArray: function(sprite) { + var surface = this.chart.surface, + rect = surface.getBBox(sprite, true), + p1 = [rect.x, rect.y], + p1p = p1.slice(), + p2 = [rect.x + rect.width, rect.y], + p2p = p2.slice(), + p3 = [rect.x + rect.width, rect.y + rect.height], + p3p = p3.slice(), + p4 = [rect.x, rect.y + rect.height], + p4p = p4.slice(), + matrix = sprite.matrix; + //transform the points + p1[0] = matrix.x.apply(matrix, p1p); + p1[1] = matrix.y.apply(matrix, p1p); + + p2[0] = matrix.x.apply(matrix, p2p); + p2[1] = matrix.y.apply(matrix, p2p); + + p3[0] = matrix.x.apply(matrix, p3p); + p3[1] = matrix.y.apply(matrix, p3p); + + p4[0] = matrix.x.apply(matrix, p4p); + p4[1] = matrix.y.apply(matrix, p4p); + return [p1, p2, p3, p4]; + }, + + intersect: function(l1, l2) { + var r1 = this.rect2pointArray(l1), + r2 = this.rect2pointArray(l2); + return !!Ext.draw.Draw.intersect(r1, r2).length; + }, + + drawHorizontalLabels: function() { + var me = this, + labelConf = me.label, + floor = Math.floor, + max = Math.max, + axes = me.chart.axes, + position = me.position, + inflections = me.inflections, + ln = inflections.length, + labels = me.labels, + labelGroup = me.labelGroup, + maxHeight = 0, + ratio, + gutterY = me.chart.maxGutter[1], + ubbox, bbox, point, prevX, prevLabel, + projectedWidth = 0, + textLabel, attr, textRight, text, + label, last, x, y, i, firstLabel; + + last = ln - 1; + //get a reference to the first text label dimensions + point = inflections[0]; + firstLabel = me.getOrCreateLabel(0, me.label.renderer(labels[0])); + ratio = Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)) >> 0; + + for (i = 0; i < ln; i++) { + point = inflections[i]; + text = me.label.renderer(labels[i]); + textLabel = me.getOrCreateLabel(i, text); + bbox = textLabel._bbox; + maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding); + x = floor(point[0] - (ratio? bbox.height : bbox.width) / 2); + if (me.chart.maxGutter[0] == 0) { + if (i == 0 && axes.findIndex('position', 'left') == -1) { + x = point[0]; + } + else if (i == last && axes.findIndex('position', 'right') == -1) { + x = point[0] - bbox.width; + } + } + if (position == 'top') { + y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2); + } + else { + y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2); + } + + textLabel.setAttributes({ + hidden: false, + x: x, + y: y + }, true); + + // Skip label if there isn't available minimum space + if (i != 0 && (me.intersect(textLabel, prevLabel) + || me.intersect(textLabel, firstLabel))) { + textLabel.hide(true); + continue; + } + + prevLabel = textLabel; + } + + return maxHeight; + }, + + drawVerticalLabels: function() { + var me = this, + inflections = me.inflections, + position = me.position, + ln = inflections.length, + labels = me.labels, + maxWidth = 0, + max = Math.max, + floor = Math.floor, + ceil = Math.ceil, + axes = me.chart.axes, + gutterY = me.chart.maxGutter[1], + ubbox, bbox, point, prevLabel, + projectedWidth = 0, + textLabel, attr, textRight, text, + label, last, x, y, i; + + last = ln; + for (i = 0; i < last; i++) { + point = inflections[i]; + text = me.label.renderer(labels[i]); + textLabel = me.getOrCreateLabel(i, text); + bbox = textLabel._bbox; + + maxWidth = max(maxWidth, bbox.width + me.dashSize + me.label.padding); + y = point[1]; + if (gutterY < bbox.height / 2) { + if (i == last - 1 && axes.findIndex('position', 'top') == -1) { + y = me.y - me.length + ceil(bbox.height / 2); + } + else if (i == 0 && axes.findIndex('position', 'bottom') == -1) { + y = me.y - floor(bbox.height / 2); + } + } + if (position == 'left') { + x = point[0] - bbox.width - me.dashSize - me.label.padding - 2; + } + else { + x = point[0] + me.dashSize + me.label.padding + 2; + } + textLabel.setAttributes(Ext.apply({ + hidden: false, + x: x, + y: y + }, me.label), true); + // Skip label if there isn't available minimum space + if (i != 0 && me.intersect(textLabel, prevLabel)) { + textLabel.hide(true); + continue; + } + prevLabel = textLabel; + } + + return maxWidth; + }, + + /** + * Renders the labels in the axes. + */ + drawLabel: function() { + var me = this, + position = me.position, + labelGroup = me.labelGroup, + inflections = me.inflections, + maxWidth = 0, + maxHeight = 0, + ln, i; + + if (position == 'left' || position == 'right') { + maxWidth = me.drawVerticalLabels(); + } else { + maxHeight = me.drawHorizontalLabels(); + } + + // Hide unused bars + ln = labelGroup.getCount(); + i = inflections.length; + for (; i < ln; i++) { + labelGroup.getAt(i).hide(true); + } + + me.bbox = {}; + Ext.apply(me.bbox, me.axisBBox); + me.bbox.height = maxHeight; + me.bbox.width = maxWidth; + if (Ext.isString(me.title)) { + me.drawTitle(maxWidth, maxHeight); + } + }, + + // @private creates the elipsis for the text. + elipsis: function(sprite, text, desiredWidth, minWidth, center) { + var bbox, + x; + + if (desiredWidth < minWidth) { + sprite.hide(true); + return false; + } + while (text.length > 4) { + text = text.substr(0, text.length - 4) + "..."; + sprite.setAttributes({ + text: text + }, true); + bbox = sprite.getBBox(); + if (bbox.width < desiredWidth) { + if (typeof center == 'number') { + sprite.setAttributes({ + x: Math.floor(center - (bbox.width / 2)) + }, true); + } + break; + } + } + return true; + }, + + /** + * Updates the {@link #title} of this axis. + * @param {String} title + */ + setTitle: function(title) { + this.title = title; + this.drawLabel(); + }, + + // @private draws the title for the axis. + drawTitle: function(maxWidth, maxHeight) { + var me = this, + position = me.position, + surface = me.chart.surface, + displaySprite = me.displaySprite, + title = me.title, + rotate = (position == 'left' || position == 'right'), + x = me.x, + y = me.y, + base, bbox, pad; + + if (displaySprite) { + displaySprite.setAttributes({text: title}, true); + } else { + base = { + type: 'text', + x: 0, + y: 0, + text: title + }; + displaySprite = me.displaySprite = surface.add(Ext.apply(base, me.axisTitleStyle, me.labelTitle)); + surface.renderItem(displaySprite); + } + bbox = displaySprite.getBBox(); + pad = me.dashSize + me.label.padding; + + if (rotate) { + y -= ((me.length / 2) - (bbox.height / 2)); + if (position == 'left') { + x -= (maxWidth + pad + (bbox.width / 2)); + } + else { + x += (maxWidth + pad + bbox.width - (bbox.width / 2)); + } + me.bbox.width += bbox.width + 10; + } + else { + x += (me.length / 2) - (bbox.width * 0.5); + if (position == 'top') { + y -= (maxHeight + pad + (bbox.height * 0.3)); + } + else { + y += (maxHeight + pad + (bbox.height * 0.8)); + } + me.bbox.height += bbox.height + 10; + } + displaySprite.setAttributes({ + translate: { + x: x, + y: y + } + }, true); + } +}); +/** + * @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 Number + * axis are more suitable. + * + * As with other axis you can set the position of the axis and its title. For example: + * {@img Ext.chart.axis.Category/Ext.chart.axis.Category.png Ext.chart.axis.Category chart axis} +

+   var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        store: store,
+        axes: [{
+            type: 'Numeric',
+            grid: true,
+            position: 'left',
+            fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+            title: 'Sample Values',
+            grid: {
+                odd: {
+                    opacity: 1,
+                    fill: '#ddd',
+                    stroke: '#bbb',
+                    'stroke-width': 1
+                }
+            },
+            minimum: 0,
+            adjustMinimumByMajorUnit: 0
+        }, {
+            type: 'Category',
+            position: 'bottom',
+            fields: ['name'],
+            title: 'Sample Metrics',
+            grid: true,
+            label: {
+                rotate: {
+                    degrees: 315
+                }
+            }
+        }],
+        series: [{
+            type: 'area',
+            highlight: false,
+            axis: 'left',
+            xField: 'name',
+            yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+            style: {
+                opacity: 0.93
+            }
+        }]
+    });
+    
+ + 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 Month of the Year. + */ + +Ext.define('Ext.chart.axis.Category', { + + /* Begin Definitions */ + + extend: 'Ext.chart.axis.Axis', + + alternateClassName: 'Ext.chart.CategoryAxis', + + alias: 'axis.category', + + /* End Definitions */ + + /** + * A list of category names to display along this axis. + * + * @property categoryNames + * @type Array + */ + categoryNames: null, + + /** + * Indicates whether or not to calculate the number of categories (ticks and + * labels) when there is not enough room to display all labels on the axis. + * If set to true, the axis will determine the number of categories to plot. + * If not, all categories will be plotted. + * + * @property calculateCategoryCount + * @type Boolean + */ + calculateCategoryCount: false, + + // @private creates an array of labels to be used when rendering. + setLabels: function() { + var store = this.chart.store, + fields = this.fields, + ln = fields.length, + i; + + this.labels = []; + store.each(function(record) { + for (i = 0; i < ln; i++) { + this.labels.push(record.get(fields[i])); + } + }, this); + }, + + // @private calculates labels positions and marker positions for rendering. + applyData: function() { + this.callParent(); + this.setLabels(); + var count = this.chart.store.getCount(); + return { + from: 0, + to: count, + power: 1, + step: 1, + steps: count - 1 + }; + } +}); + +/** + * @class Ext.chart.axis.Gauge + * @extends Ext.chart.axis.Abstract + * + * Gauge Axis is the axis to be used with a Gauge series. The Gauge axis + * displays numeric data from an interval defined by the `minimum`, `maximum` and + * `step` configuration properties. The placement of the numeric data can be changed + * by altering the `margin` option that is set to `10` by default. + * + * A possible configuration for this axis would look like: + * + axes: [{ + type: 'gauge', + position: 'gauge', + minimum: 0, + maximum: 100, + steps: 10, + margin: 7 + }], + * + */ +Ext.define('Ext.chart.axis.Gauge', { + + /* Begin Definitions */ + + extend: 'Ext.chart.axis.Abstract', + + /* End Definitions */ + + /** + * @cfg {Number} minimum (required) the minimum value of the interval to be displayed in the axis. + */ + + /** + * @cfg {Number} maximum (required) the maximum value of the interval to be displayed in the axis. + */ + + /** + * @cfg {Number} steps (required) the number of steps and tick marks to add to the interval. + */ + + /** + * @cfg {Number} margin (optional) the offset positioning of the tick marks and labels in pixels. Default's 10. + */ + + position: 'gauge', + + alias: 'axis.gauge', + + drawAxis: function(init) { + var chart = this.chart, + surface = chart.surface, + bbox = chart.chartBBox, + centerX = bbox.x + (bbox.width / 2), + centerY = bbox.y + bbox.height, + margin = this.margin || 10, + rho = Math.min(bbox.width, 2 * bbox.height) /2 + margin, + sprites = [], sprite, + steps = this.steps, + i, pi = Math.PI, + cos = Math.cos, + sin = Math.sin; + + if (this.sprites && !chart.resizing) { + this.drawLabel(); + return; + } + + if (this.margin >= 0) { + if (!this.sprites) { + //draw circles + for (i = 0; i <= steps; i++) { + sprite = surface.add({ + type: 'path', + path: ['M', centerX + (rho - margin) * cos(i / steps * pi - pi), + centerY + (rho - margin) * sin(i / steps * pi - pi), + 'L', centerX + rho * cos(i / steps * pi - pi), + centerY + rho * sin(i / steps * pi - pi), 'Z'], + stroke: '#ccc' + }); + sprite.setAttributes({ + hidden: false + }, true); + sprites.push(sprite); + } + } else { + sprites = this.sprites; + //draw circles + for (i = 0; i <= steps; i++) { + sprites[i].setAttributes({ + path: ['M', centerX + (rho - margin) * cos(i / steps * pi - pi), + centerY + (rho - margin) * sin(i / steps * pi - pi), + 'L', centerX + rho * cos(i / steps * pi - pi), + centerY + rho * sin(i / steps * pi - pi), 'Z'], + stroke: '#ccc' + }, true); + } + } + } + this.sprites = sprites; + this.drawLabel(); + if (this.title) { + this.drawTitle(); + } + }, + + drawTitle: function() { + var me = this, + chart = me.chart, + surface = chart.surface, + bbox = chart.chartBBox, + labelSprite = me.titleSprite, + labelBBox; + + if (!labelSprite) { + me.titleSprite = labelSprite = surface.add({ + type: 'text', + zIndex: 2 + }); + } + labelSprite.setAttributes(Ext.apply({ + text: me.title + }, me.label || {}), true); + labelBBox = labelSprite.getBBox(); + labelSprite.setAttributes({ + x: bbox.x + (bbox.width / 2) - (labelBBox.width / 2), + y: bbox.y + bbox.height - (labelBBox.height / 2) - 4 + }, true); + }, + + /** + * Updates the {@link #title} of this axis. + * @param {String} title + */ + setTitle: function(title) { + this.title = title; + this.drawTitle(); + }, + + drawLabel: function() { + var chart = this.chart, + surface = chart.surface, + bbox = chart.chartBBox, + centerX = bbox.x + (bbox.width / 2), + centerY = bbox.y + bbox.height, + margin = this.margin || 10, + rho = Math.min(bbox.width, 2 * bbox.height) /2 + 2 * margin, + round = Math.round, + labelArray = [], label, + maxValue = this.maximum || 0, + steps = this.steps, i = 0, + adjY, + pi = Math.PI, + cos = Math.cos, + sin = Math.sin, + labelConf = this.label, + renderer = labelConf.renderer || function(v) { return v; }; + + if (!this.labelArray) { + //draw scale + for (i = 0; i <= steps; i++) { + // TODO Adjust for height of text / 2 instead + adjY = (i === 0 || i === steps) ? 7 : 0; + label = surface.add({ + type: 'text', + text: renderer(round(i / steps * maxValue)), + x: centerX + rho * cos(i / steps * pi - pi), + y: centerY + rho * sin(i / steps * pi - pi) - adjY, + 'text-anchor': 'middle', + 'stroke-width': 0.2, + zIndex: 10, + stroke: '#333' + }); + label.setAttributes({ + hidden: false + }, true); + labelArray.push(label); + } + } + else { + labelArray = this.labelArray; + //draw values + for (i = 0; i <= steps; i++) { + // TODO Adjust for height of text / 2 instead + adjY = (i === 0 || i === steps) ? 7 : 0; + labelArray[i].setAttributes({ + text: renderer(round(i / steps * maxValue)), + x: centerX + rho * cos(i / steps * pi - pi), + y: centerY + rho * sin(i / steps * pi - pi) - adjY + }, true); + } + } + this.labelArray = labelArray; + } +}); +/** + * @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 mininum 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. + * {@img Ext.chart.axis.Numeric/Ext.chart.axis.Numeric.png Ext.chart.axis.Numeric chart axis} + * For example: + +

+   var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        store: store,
+        axes: [{
+            type: 'Numeric',
+            grid: true,
+            position: 'left',
+            fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+            title: 'Sample Values',
+            grid: {
+                odd: {
+                    opacity: 1,
+                    fill: '#ddd',
+                    stroke: '#bbb',
+                    'stroke-width': 1
+                }
+            },
+            minimum: 0,
+            adjustMinimumByMajorUnit: 0
+        }, {
+            type: 'Category',
+            position: 'bottom',
+            fields: ['name'],
+            title: 'Sample Metrics',
+            grid: true,
+            label: {
+                rotate: {
+                    degrees: 315
+                }
+            }
+        }],
+        series: [{
+            type: 'area',
+            highlight: false,
+            axis: 'left',
+            xField: 'name',
+            yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+            style: {
+                opacity: 0.93
+            }
+        }]
+    });
+    
+ + * + * 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. + * + * + * @constructor + */ +Ext.define('Ext.chart.axis.Numeric', { + + /* Begin Definitions */ + + extend: 'Ext.chart.axis.Axis', + + alternateClassName: 'Ext.chart.NumericAxis', + + /* End Definitions */ + + type: 'numeric', + + alias: 'axis.numeric', + + constructor: function(config) { + var me = this, label, f; + me.callParent([config]); + label = me.label; + if (me.roundToDecimal === false) { + return; + } + if (label.renderer) { + f = label.renderer; + label.renderer = function(v) { + return me.roundToDecimal( f(v), me.decimals ); + }; + } else { + label.renderer = function(v) { + return me.roundToDecimal(v, me.decimals); + }; + } + }, + + roundToDecimal: function(v, dec) { + var val = Math.pow(10, dec || 0); + return ((v * val) >> 0) / val; + }, + + /** + * The minimum value drawn by the axis. If not set explicitly, the axis + * minimum will be calculated automatically. + * + * @property minimum + * @type Number + */ + minimum: NaN, + + /** + * The maximum value drawn by the axis. If not set explicitly, the axis + * maximum will be calculated automatically. + * + * @property maximum + * @type Number + */ + maximum: NaN, + + /** + * The number of decimals to round the value to. + * Default's 2. + * + * @property decimals + * @type Number + */ + decimals: 2, + + /** + * The scaling algorithm to use on this axis. May be "linear" or + * "logarithmic". + * + * @property scale + * @type String + */ + scale: "linear", + + /** + * Indicates the position of the axis relative to the chart + * + * @property position + * @type String + */ + position: 'left', + + /** + * Indicates whether to extend maximum beyond data's maximum to the nearest + * majorUnit. + * + * @property adjustMaximumByMajorUnit + * @type Boolean + */ + adjustMaximumByMajorUnit: false, + + /** + * Indicates whether to extend the minimum beyond data's minimum to the + * nearest majorUnit. + * + * @property adjustMinimumByMajorUnit + * @type Boolean + */ + adjustMinimumByMajorUnit: false, + + // @private apply data. + applyData: function() { + this.callParent(); + return this.calcEnds(); + } +}); + +/** + * @class Ext.chart.axis.Radial + * @extends Ext.chart.axis.Abstract + * @ignore + */ +Ext.define('Ext.chart.axis.Radial', { + + /* Begin Definitions */ + + extend: 'Ext.chart.axis.Abstract', + + /* End Definitions */ + + position: 'radial', + + alias: 'axis.radial', + + drawAxis: function(init) { + var chart = this.chart, + surface = chart.surface, + bbox = chart.chartBBox, + store = chart.store, + l = store.getCount(), + centerX = bbox.x + (bbox.width / 2), + centerY = bbox.y + (bbox.height / 2), + rho = Math.min(bbox.width, bbox.height) /2, + sprites = [], sprite, + steps = this.steps, + i, j, pi2 = Math.PI * 2, + cos = Math.cos, sin = Math.sin; + + if (this.sprites && !chart.resizing) { + this.drawLabel(); + return; + } + + if (!this.sprites) { + //draw circles + for (i = 1; i <= steps; i++) { + sprite = surface.add({ + type: 'circle', + x: centerX, + y: centerY, + radius: Math.max(rho * i / steps, 0), + stroke: '#ccc' + }); + sprite.setAttributes({ + hidden: false + }, true); + sprites.push(sprite); + } + //draw lines + store.each(function(rec, i) { + sprite = surface.add({ + type: 'path', + path: ['M', centerX, centerY, 'L', centerX + rho * cos(i / l * pi2), centerY + rho * sin(i / l * pi2), 'Z'], + stroke: '#ccc' + }); + sprite.setAttributes({ + hidden: false + }, true); + sprites.push(sprite); + }); + } else { + sprites = this.sprites; + //draw circles + for (i = 0; i < steps; i++) { + sprites[i].setAttributes({ + x: centerX, + y: centerY, + radius: Math.max(rho * (i + 1) / steps, 0), + stroke: '#ccc' + }, true); + } + //draw lines + store.each(function(rec, j) { + sprites[i + j].setAttributes({ + path: ['M', centerX, centerY, 'L', centerX + rho * cos(j / l * pi2), centerY + rho * sin(j / l * pi2), 'Z'], + stroke: '#ccc' + }, true); + }); + } + this.sprites = sprites; + + this.drawLabel(); + }, + + drawLabel: function() { + var chart = this.chart, + surface = chart.surface, + bbox = chart.chartBBox, + store = chart.store, + centerX = bbox.x + (bbox.width / 2), + centerY = bbox.y + (bbox.height / 2), + rho = Math.min(bbox.width, bbox.height) /2, + max = Math.max, round = Math.round, + labelArray = [], label, + fields = [], nfields, + categories = [], xField, + aggregate = !this.maximum, + maxValue = this.maximum || 0, + steps = this.steps, i = 0, j, dx, dy, + pi2 = Math.PI * 2, + cos = Math.cos, sin = Math.sin, + display = this.label.display, + draw = display !== 'none', + margin = 10; + + if (!draw) { + return; + } + + //get all rendered fields + chart.series.each(function(series) { + fields.push(series.yField); + xField = series.xField; + }); + + //get maxValue to interpolate + store.each(function(record, i) { + if (aggregate) { + for (i = 0, nfields = fields.length; i < nfields; i++) { + maxValue = max(+record.get(fields[i]), maxValue); + } + } + categories.push(record.get(xField)); + }); + if (!this.labelArray) { + if (display != 'categories') { + //draw scale + for (i = 1; i <= steps; i++) { + label = surface.add({ + type: 'text', + text: round(i / steps * maxValue), + x: centerX, + y: centerY - rho * i / steps, + 'text-anchor': 'middle', + 'stroke-width': 0.1, + stroke: '#333' + }); + label.setAttributes({ + hidden: false + }, true); + labelArray.push(label); + } + } + if (display != 'scale') { + //draw text + for (j = 0, steps = categories.length; j < steps; j++) { + dx = cos(j / steps * pi2) * (rho + margin); + dy = sin(j / steps * pi2) * (rho + margin); + label = surface.add({ + type: 'text', + text: categories[j], + x: centerX + dx, + y: centerY + dy, + 'text-anchor': dx * dx <= 0.001? 'middle' : (dx < 0? 'end' : 'start') + }); + label.setAttributes({ + hidden: false + }, true); + labelArray.push(label); + } + } + } + else { + labelArray = this.labelArray; + if (display != 'categories') { + //draw values + for (i = 0; i < steps; i++) { + labelArray[i].setAttributes({ + text: round((i + 1) / steps * maxValue), + x: centerX, + y: centerY - rho * (i + 1) / steps, + 'text-anchor': 'middle', + 'stroke-width': 0.1, + stroke: '#333' + }, true); + } + } + if (display != 'scale') { + //draw text + for (j = 0, steps = categories.length; j < steps; j++) { + dx = cos(j / steps * pi2) * (rho + margin); + dy = sin(j / steps * pi2) * (rho + margin); + if (labelArray[i + j]) { + labelArray[i + j].setAttributes({ + type: 'text', + text: categories[j], + x: centerX + dx, + y: centerY + dy, + 'text-anchor': dx * dx <= 0.001? 'middle' : (dx < 0? 'end' : 'start') + }, true); + } + } + } + } + this.labelArray = labelArray; + } +}); +/** + * @author Ed Spencer + * @class Ext.data.AbstractStore + * + *

AbstractStore is a superclass of {@link Ext.data.Store} and {@link Ext.data.TreeStore}. It's never used directly, + * but offers a set of methods used by both of those subclasses.

+ * + *

We've left it here in the docs for reference purposes, but unless you need to make a whole new type of Store, what + * you're probably looking for is {@link Ext.data.Store}. If you're still interested, here's a brief description of what + * AbstractStore is and is not.

+ * + *

AbstractStore provides the basic configuration for anything that can be considered a Store. It expects to be + * given a {@link Ext.data.Model Model} that represents the type of data in the Store. It also expects to be given a + * {@link Ext.data.proxy.Proxy Proxy} that handles the loading of data into the Store.

+ * + *

AbstractStore provides a few helpful methods such as {@link #load} and {@link #sync}, which load and save data + * respectively, passing the requests through the configured {@link #proxy}. Both built-in Store subclasses add extra + * behavior to each of these functions. Note also that each AbstractStore subclass has its own way of storing data - + * in {@link Ext.data.Store} the data is saved as a flat {@link Ext.util.MixedCollection MixedCollection}, whereas in + * {@link Ext.data.TreeStore TreeStore} we use a {@link Ext.data.Tree} to maintain the data's hierarchy.

+ * + * TODO: Update these docs to explain about the sortable and filterable mixins. + *

Finally, AbstractStore provides an API for sorting and filtering data via its {@link #sorters} and {@link #filters} + * {@link Ext.util.MixedCollection MixedCollections}. Although this functionality is provided by AbstractStore, there's a + * good description of how to use it in the introduction of {@link Ext.data.Store}. + * + */ +Ext.define('Ext.data.AbstractStore', { + requires: ['Ext.util.MixedCollection', 'Ext.data.Operation', 'Ext.util.Filter'], + + mixins: { + observable: 'Ext.util.Observable', + sortable: 'Ext.util.Sortable' + }, + + statics: { + create: function(store){ + if (!store.isStore) { + if (!store.type) { + store.type = 'store'; + } + store = Ext.createByAlias('store.' + store.type, store); + } + return store; + } + }, + + remoteSort : false, + remoteFilter: false, + + /** + * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config + * object or a Proxy instance - see {@link #setProxy} for details. + */ + + /** + * @cfg {Boolean/Object} autoLoad If data is not specified, and if autoLoad is true or an Object, this store's load method + * is automatically called after creation. If the value of autoLoad is an Object, this Object will be passed to the store's + * load method. Defaults to false. + */ + autoLoad: false, + + /** + * @cfg {Boolean} autoSync True to automatically sync the Store with its Proxy after every edit to one of its Records. + * Defaults to false. + */ + autoSync: false, + + /** + * Sets the updating behavior based on batch synchronization. 'operation' (the default) will update the Store's + * internal representation of the data after each operation of the batch has completed, 'complete' will wait until + * the entire batch has been completed before updating the Store's data. 'complete' is a good choice for local + * storage proxies, 'operation' is better for remote proxies, where there is a comparatively high latency. + * @property batchUpdateMode + * @type String + */ + batchUpdateMode: 'operation', + + /** + * If true, any filters attached to this Store will be run after loading data, before the datachanged event is fired. + * Defaults to true, ignored if {@link #remoteFilter} is true + * @property filterOnLoad + * @type Boolean + */ + filterOnLoad: true, + + /** + * If true, any sorters attached to this Store will be run after loading data, before the datachanged event is fired. + * Defaults to true, igored if {@link #remoteSort} is true + * @property sortOnLoad + * @type Boolean + */ + sortOnLoad: true, + + /** + * True if a model was created implicitly for this Store. This happens if a fields array is passed to the Store's constructor + * instead of a model constructor or name. + * @property implicitModel + * @type Boolean + * @private + */ + implicitModel: false, + + /** + * The string type of the Proxy to create if none is specified. This defaults to creating a {@link Ext.data.proxy.Memory memory proxy}. + * @property defaultProxyType + * @type String + */ + defaultProxyType: 'memory', + + /** + * True if the Store has already been destroyed via {@link #destroyStore}. If this is true, the reference to Store should be deleted + * as it will not function correctly any more. + * @property isDestroyed + * @type Boolean + */ + isDestroyed: false, + + isStore: true, + + /** + * @cfg {String} storeId Optional unique identifier for this store. If present, this Store will be registered with + * the {@link Ext.data.StoreManager}, making it easy to reuse elsewhere. Defaults to undefined. + */ + + /** + * @cfg {Array} fields + * This may be used in place of specifying a {@link #model} configuration. The fields should be a + * set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model} + * with these fields. In general this configuration option should be avoided, it exists for the purposes of + * backwards compatibility. For anything more complicated, such as specifying a particular id property or + * assocations, a {@link Ext.data.Model} should be defined and specified for the {@link #model} config. + */ + + sortRoot: 'data', + + //documented above + constructor: function(config) { + var me = this; + + me.addEvents( + /** + * @event add + * Fired when a Model instance has been added to this Store + * @param {Ext.data.Store} store The store + * @param {Array} records The Model instances that were added + * @param {Number} index The index at which the instances were inserted + */ + 'add', + + /** + * @event remove + * Fired when a Model instance has been removed from this Store + * @param {Ext.data.Store} store The Store object + * @param {Ext.data.Model} record The record that was removed + * @param {Number} index The index of the record that was removed + */ + 'remove', + + /** + * @event update + * Fires when a Record has been updated + * @param {Store} this + * @param {Ext.data.Model} record The Model instance that was updated + * @param {String} operation The update operation being performed. Value may be one of: + *


+               Ext.data.Model.EDIT
+               Ext.data.Model.REJECT
+               Ext.data.Model.COMMIT
+             * 
+ */ + 'update', + + /** + * @event datachanged + * Fires whenever the records in the Store have changed in some way - this could include adding or removing records, + * or updating the data in existing records + * @param {Ext.data.Store} this The data store + */ + 'datachanged', + + /** + * @event beforeload + * Event description + * @param {Ext.data.Store} store This Store + * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to load the Store + */ + 'beforeload', + + /** + * @event load + * Fires whenever the store reads data from a remote data source. + * @param {Ext.data.Store} this + * @param {Array} records An array of records + * @param {Boolean} successful True if the operation was successful. + */ + 'load', + + /** + * @event beforesync + * Called before a call to {@link #sync} is executed. Return false from any listener to cancel the synv + * @param {Object} options Hash of all records to be synchronized, broken down into create, update and destroy + */ + 'beforesync', + /** + * @event clear + * Fired after the {@link #removeAll} method is called. + * @param {Ext.data.Store} this + */ + 'clear' + ); + + Ext.apply(me, config); + + /** + * Temporary cache in which removed model instances are kept until successfully synchronised with a Proxy, + * at which point this is cleared. + * @private + * @property removed + * @type Array + */ + me.removed = []; + + me.mixins.observable.constructor.apply(me, arguments); + me.model = Ext.ModelManager.getModel(config.model || me.model); + + /** + * @property modelDefaults + * @type Object + * @private + * A set of default values to be applied to every model instance added via {@link #insert} or created via {@link #create}. + * This is used internally by associations to set foreign keys and other fields. See the Association classes source code + * for examples. This should not need to be used by application developers. + */ + Ext.applyIf(me, { + modelDefaults: {} + }); + + //Supports the 3.x style of simply passing an array of fields to the store, implicitly creating a model + if (!me.model && me.fields) { + me.model = Ext.define('Ext.data.Store.ImplicitModel-' + (me.storeId || Ext.id()), { + extend: 'Ext.data.Model', + fields: me.fields, + proxy: me.proxy || me.defaultProxyType + }); + + delete me.fields; + + me.implicitModel = true; + } + + //ensures that the Proxy is instantiated correctly + me.setProxy(config.proxy || me.proxy || me.model.getProxy()); + + if (me.id && !me.storeId) { + me.storeId = me.id; + delete me.id; + } + + if (me.storeId) { + Ext.data.StoreManager.register(me); + } + + me.mixins.sortable.initSortable.call(me); + + /** + * The collection of {@link Ext.util.Filter Filters} currently applied to this Store + * @property filters + * @type Ext.util.MixedCollection + */ + me.filters = Ext.create('Ext.util.MixedCollection'); + me.filters.addAll(me.decodeFilters(config.filters)); + }, + + /** + * Sets the Store's Proxy by string, config object or Proxy instance + * @param {String|Object|Ext.data.proxy.Proxy} proxy The new Proxy, which can be either a type string, a configuration object + * or an Ext.data.proxy.Proxy instance + * @return {Ext.data.proxy.Proxy} The attached Proxy object + */ + setProxy: function(proxy) { + var me = this; + + if (proxy instanceof Ext.data.proxy.Proxy) { + proxy.setModel(me.model); + } else { + if (Ext.isString(proxy)) { + proxy = { + type: proxy + }; + } + Ext.applyIf(proxy, { + model: me.model + }); + + proxy = Ext.createByAlias('proxy.' + proxy.type, proxy); + } + + me.proxy = proxy; + + return me.proxy; + }, + + /** + * Returns the proxy currently attached to this proxy instance + * @return {Ext.data.proxy.Proxy} The Proxy instance + */ + getProxy: function() { + return this.proxy; + }, + + //saves any phantom records + create: function(data, options) { + var me = this, + instance = Ext.ModelManager.create(Ext.applyIf(data, me.modelDefaults), me.model.modelName), + operation; + + options = options || {}; + + Ext.applyIf(options, { + action : 'create', + records: [instance] + }); + + operation = Ext.create('Ext.data.Operation', options); + + me.proxy.create(operation, me.onProxyWrite, me); + + return instance; + }, + + read: function() { + return this.load.apply(this, arguments); + }, + + onProxyRead: Ext.emptyFn, + + update: function(options) { + var me = this, + operation; + options = options || {}; + + Ext.applyIf(options, { + action : 'update', + records: me.getUpdatedRecords() + }); + + operation = Ext.create('Ext.data.Operation', options); + + return me.proxy.update(operation, me.onProxyWrite, me); + }, + + /** + * @private + * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect + * the updates provided by the Proxy + */ + onProxyWrite: function(operation) { + var me = this, + success = operation.wasSuccessful(), + records = operation.getRecords(); + + switch (operation.action) { + case 'create': + me.onCreateRecords(records, operation, success); + break; + case 'update': + me.onUpdateRecords(records, operation, success); + break; + case 'destroy': + me.onDestroyRecords(records, operation, success); + break; + } + + if (success) { + me.fireEvent('write', me, operation); + me.fireEvent('datachanged', me); + } + //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional + Ext.callback(operation.callback, operation.scope || me, [records, operation, success]); + }, + + + //tells the attached proxy to destroy the given records + destroy: function(options) { + var me = this, + operation; + + options = options || {}; + + Ext.applyIf(options, { + action : 'destroy', + records: me.getRemovedRecords() + }); + + operation = Ext.create('Ext.data.Operation', options); + + return me.proxy.destroy(operation, me.onProxyWrite, me); + }, + + /** + * @private + * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default just calls through + * to onProxyWrite. + */ + onBatchOperationComplete: function(batch, operation) { + return this.onProxyWrite(operation); + }, + + /** + * @private + * Attached as the 'complete' event listener to a proxy's Batch object. Iterates over the batch operations + * and updates the Store's internal data MixedCollection. + */ + onBatchComplete: function(batch, operation) { + var me = this, + operations = batch.operations, + length = operations.length, + i; + + me.suspendEvents(); + + for (i = 0; i < length; i++) { + me.onProxyWrite(operations[i]); + } + + me.resumeEvents(); + + me.fireEvent('datachanged', me); + }, + + onBatchException: function(batch, operation) { + // //decide what to do... could continue with the next operation + // batch.start(); + // + // //or retry the last operation + // batch.retry(); + }, + + /** + * @private + * Filter function for new records. + */ + filterNew: function(item) { + // only want phantom records that are valid + return item.phantom === true && item.isValid(); + }, + + /** + * Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not + * yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one) + * @return {Array} The Model instances + */ + getNewRecords: function() { + return []; + }, + + /** + * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy + * @return {Array} The updated Model instances + */ + getUpdatedRecords: function() { + return []; + }, + + /** + * @private + * Filter function for updated records. + */ + filterUpdated: function(item) { + // only want dirty records, not phantoms that are valid + return item.dirty === true && item.phantom !== true && item.isValid(); + }, + + //returns any records that have been removed from the store but not yet destroyed on the proxy + getRemovedRecords: function() { + return this.removed; + }, + + filter: function(filters, value) { + + }, + + /** + * @private + * Normalizes an array of filter objects, ensuring that they are all Ext.util.Filter instances + * @param {Array} filters The filters array + * @return {Array} Array of Ext.util.Filter objects + */ + decodeFilters: function(filters) { + if (!Ext.isArray(filters)) { + if (filters === undefined) { + filters = []; + } else { + filters = [filters]; + } + } + + var length = filters.length, + Filter = Ext.util.Filter, + config, i; + + for (i = 0; i < length; i++) { + config = filters[i]; + + if (!(config instanceof Filter)) { + Ext.apply(config, { + root: 'data' + }); + + //support for 3.x style filters where a function can be defined as 'fn' + if (config.fn) { + config.filterFn = config.fn; + } + + //support a function to be passed as a filter definition + if (typeof config == 'function') { + config = { + filterFn: config + }; + } + + filters[i] = new Filter(config); + } + } + + return filters; + }, + + clearFilter: function(supressEvent) { + + }, + + isFiltered: function() { + + }, + + filterBy: function(fn, scope) { + + }, + + /** + * Synchronizes the Store with its Proxy. This asks the Proxy to batch together any new, updated + * and deleted records in the store, updating the Store's internal representation of the records + * as each operation completes. + */ + sync: function() { + var me = this, + options = {}, + toCreate = me.getNewRecords(), + toUpdate = me.getUpdatedRecords(), + toDestroy = me.getRemovedRecords(), + needsSync = false; + + if (toCreate.length > 0) { + options.create = toCreate; + needsSync = true; + } + + if (toUpdate.length > 0) { + options.update = toUpdate; + needsSync = true; + } + + if (toDestroy.length > 0) { + options.destroy = toDestroy; + needsSync = true; + } + + if (needsSync && me.fireEvent('beforesync', options) !== false) { + me.proxy.batch(options, me.getBatchListeners()); + } + }, + + + /** + * @private + * Returns an object which is passed in as the listeners argument to proxy.batch inside this.sync. + * This is broken out into a separate function to allow for customisation of the listeners + * @return {Object} The listeners object + */ + getBatchListeners: function() { + var me = this, + listeners = { + scope: me, + exception: me.onBatchException + }; + + if (me.batchUpdateMode == 'operation') { + listeners.operationcomplete = me.onBatchOperationComplete; + } else { + listeners.complete = me.onBatchComplete; + } + + return listeners; + }, + + //deprecated, will be removed in 5.0 + save: function() { + return this.sync.apply(this, arguments); + }, + + /** + * Loads the Store using its configured {@link #proxy}. + * @param {Object} options Optional config object. This is passed into the {@link Ext.data.Operation Operation} + * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function + */ + load: function(options) { + var me = this, + operation; + + options = options || {}; + + Ext.applyIf(options, { + action : 'read', + filters: me.filters.items, + sorters: me.getSorters() + }); + + operation = Ext.create('Ext.data.Operation', options); + + if (me.fireEvent('beforeload', me, operation) !== false) { + me.loading = true; + me.proxy.read(operation, me.onProxyLoad, me); + } + + return me; + }, + + /** + * @private + * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to. + * @param {Ext.data.Model} record The model instance that was edited + */ + afterEdit : function(record) { + var me = this; + + if (me.autoSync) { + me.sync(); + } + + me.fireEvent('update', me, record, Ext.data.Model.EDIT); + }, + + /** + * @private + * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.. + * @param {Ext.data.Model} record The model instance that was edited + */ + afterReject : function(record) { + this.fireEvent('update', this, record, Ext.data.Model.REJECT); + }, + + /** + * @private + * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to. + * @param {Ext.data.Model} record The model instance that was edited + */ + afterCommit : function(record) { + this.fireEvent('update', this, record, Ext.data.Model.COMMIT); + }, + + clearData: Ext.emptyFn, + + destroyStore: function() { + var me = this; + + if (!me.isDestroyed) { + if (me.storeId) { + Ext.data.StoreManager.unregister(me); + } + me.clearData(); + me.data = null; + me.tree = null; + // Ext.destroy(this.proxy); + me.reader = me.writer = null; + me.clearListeners(); + me.isDestroyed = true; + + if (me.implicitModel) { + Ext.destroy(me.model); + } + } + }, + + doSort: function(sorterFn) { + var me = this; + if (me.remoteSort) { + //the load function will pick up the new sorters and request the sorted data from the proxy + me.load(); + } else { + me.data.sortBy(sorterFn); + me.fireEvent('datachanged', me); + } + }, + + getCount: Ext.emptyFn, + + getById: Ext.emptyFn, + + /** + * Removes all records from the store. This method does a "fast remove", + * individual remove events are not called. The {@link #clear} event is + * fired upon completion. + */ + removeAll: Ext.emptyFn, + // individual substores should implement a "fast" remove + // and fire a clear event afterwards + + /** + * Returns true if the Store is currently performing a load operation + * @return {Boolean} True if the Store is currently loading + */ + isLoading: function() { + return this.loading; + } +}); + +/** + * @class Ext.util.Grouper + * @extends Ext.util.Sorter + */ + +Ext.define('Ext.util.Grouper', { + + /* Begin Definitions */ + + extend: 'Ext.util.Sorter', + + /* End Definitions */ + + /** + * Function description + * @param {Ext.data.Model} instance The Model instance + * @return {String} The group string for this model + */ + getGroupString: function(instance) { + return instance.get(this.property); + } +}); +/** + * @author Ed Spencer + * @class Ext.data.Store + * @extends Ext.data.AbstractStore + * + *

The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load + * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting}, + * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.

+ * + *

Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:

+ * +

+// Set up a {@link Ext.data.Model model} to use in our Store
+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'firstName', type: 'string'},
+        {name: 'lastName',  type: 'string'},
+        {name: 'age',       type: 'int'},
+        {name: 'eyeColor',  type: 'string'}
+    ]
+});
+
+var myStore = new Ext.data.Store({
+    model: 'User',
+    proxy: {
+        type: 'ajax',
+        url : '/users.json',
+        reader: {
+            type: 'json',
+            root: 'users'
+        }
+    },
+    autoLoad: true
+});
+
+ + *

In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy + * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object - + * {@link Ext.data.reader.Json see the docs on JsonReader} for details.

+ * + *

Inline data

+ * + *

Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #data} + * into Model instances:

+ * +

+new Ext.data.Store({
+    model: 'User',
+    data : [
+        {firstName: 'Ed',    lastName: 'Spencer'},
+        {firstName: 'Tommy', lastName: 'Maintz'},
+        {firstName: 'Aaron', lastName: 'Conran'},
+        {firstName: 'Jamie', lastName: 'Avins'}
+    ]
+});
+
+ * + *

Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need + * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure, + * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).

+ * + *

Additional data can also be loaded locally using {@link #add}.

+ * + *

Loading Nested Data

+ * + *

Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders. + * Instead of issuing an AJAX request for the User and a series of additional AJAX requests for each Order, we can load a nested dataset + * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro + * docs for a full explanation:

+ * +

+var store = new Ext.data.Store({
+    autoLoad: true,
+    model: "User",
+    proxy: {
+        type: 'ajax',
+        url : 'users.json',
+        reader: {
+            type: 'json',
+            root: 'users'
+        }
+    }
+});
+
+ * + *

Which would consume a response like this:

+ * +

+{
+    "users": [
+        {
+            "id": 1,
+            "name": "Ed",
+            "orders": [
+                {
+                    "id": 10,
+                    "total": 10.76,
+                    "status": "invoiced"
+                },
+                {
+                    "id": 11,
+                    "total": 13.45,
+                    "status": "shipped"
+                }
+            ]
+        }
+    ]
+}
+
+ * + *

See the {@link Ext.data.reader.Reader} intro docs for a full explanation.

+ * + *

Filtering and Sorting

+ * + *

Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are + * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to + * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}: + * +


+var store = new Ext.data.Store({
+    model: 'User',
+    sorters: [
+        {
+            property : 'age',
+            direction: 'DESC'
+        },
+        {
+            property : 'firstName',
+            direction: 'ASC'
+        }
+    ],
+
+    filters: [
+        {
+            property: 'firstName',
+            value   : /Ed/
+        }
+    ]
+});
+
+ * + *

The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting + * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to + * perform these operations instead.

+ * + *

Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store + * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by + * default {@link #sortOnFilter} is set to true, which means that your sorters are automatically reapplied if using local sorting.

+ * +

+store.filter('eyeColor', 'Brown');
+
+ * + *

Change the sorting at any time by calling {@link #sort}:

+ * +

+store.sort('height', 'ASC');
+
+ * + *

Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments, + * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them + * to the MixedCollection:

+ * +

+store.sorters.add(new Ext.util.Sorter({
+    property : 'shoeSize',
+    direction: 'ASC'
+}));
+
+store.sort();
+
+ * + *

Registering with StoreManager

+ * + *

Any Store that is instantiated with a {@link #storeId} will automatically be registed with the {@link Ext.data.StoreManager StoreManager}. + * This makes it easy to reuse the same store in multiple views:

+ * +

+//this store can be used several times
+new Ext.data.Store({
+    model: 'User',
+    storeId: 'usersStore'
+});
+
+new Ext.List({
+    store: 'usersStore',
+
+    //other config goes here
+});
+
+new Ext.view.View({
+    store: 'usersStore',
+
+    //other config goes here
+});
+
+ * + *

Further Reading

+ * + *

Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these + * pieces and how they fit together, see:

+ * + *
    + *
  • {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
  • + *
  • {@link Ext.data.Model Model} - the core class in the data package
  • + *
  • {@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response
  • + *
+ * + * @constructor + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.Store', { + extend: 'Ext.data.AbstractStore', + + alias: 'store.store', + + requires: ['Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'], + uses: ['Ext.data.proxy.Memory'], + + /** + * @cfg {Boolean} remoteSort + * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to false. + */ + remoteSort: false, + + /** + * @cfg {Boolean} remoteFilter + * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to false. + */ + remoteFilter: false, + + /** + * @cfg {Boolean} remoteGroup + * True if the grouping should apply on the server side, false if it is local only (defaults to false). If the + * grouping is local, it can be applied immediately to the data. If it is remote, then it will simply act as a + * helper, automatically sending the grouping information to the server. + */ + remoteGroup : false, + + /** + * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config + * object or a Proxy instance - see {@link #setProxy} for details. + */ + + /** + * @cfg {Array} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details. + */ + + /** + * @cfg {String} model The {@link Ext.data.Model} associated with this store + */ + + /** + * The (optional) field by which to group data in the store. Internally, grouping is very similar to sorting - the + * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single + * level of grouping, and groups can be fetched via the {@link #getGroups} method. + * @property groupField + * @type String + */ + groupField: undefined, + + /** + * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC" + * @property groupDir + * @type String + */ + groupDir: "ASC", + + /** + * The number of records considered to form a 'page'. This is used to power the built-in + * paging using the nextPage and previousPage functions. Defaults to 25. + * @property pageSize + * @type Number + */ + pageSize: 25, + + /** + * The page that the Store has most recently loaded (see {@link #loadPage}) + * @property currentPage + * @type Number + */ + currentPage: 1, + + /** + * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage}, + * {@link #nextPage} or {@link #previousPage} (defaults to true). Setting to false keeps existing records, allowing + * large data sets to be loaded one page at a time but rendered all together. + */ + clearOnPageLoad: true, + + /** + * True if the Store is currently loading via its Proxy + * @property loading + * @type Boolean + * @private + */ + loading: false, + + /** + * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called, + * causing the sorters to be reapplied after filtering. Defaults to true + */ + sortOnFilter: true, + + /** + * @cfg {Boolean} buffered + * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will + * tell the store to pre-fetch records ahead of a time. + */ + buffered: false, + + /** + * @cfg {Number} purgePageCount + * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data. + * This option is only relevant when the {@link #buffered} option is set to true. + */ + purgePageCount: 5, + + isStore: true, + + //documented above + constructor: function(config) { + config = config || {}; + + var me = this, + groupers = config.groupers, + proxy, + data; + + if (config.buffered || me.buffered) { + me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) { + return record.index; + }); + me.pendingRequests = []; + me.pagesRequested = []; + + me.sortOnLoad = false; + me.filterOnLoad = false; + } + + me.addEvents( + /** + * @event beforeprefetch + * Fires before a prefetch occurs. Return false to cancel. + * @param {Ext.data.store} this + * @param {Ext.data.Operation} operation The associated operation + */ + 'beforeprefetch', + /** + * @event groupchange + * Fired whenever the grouping in the grid changes + * @param {Ext.data.Store} store The store + * @param {Array} groupers The array of grouper objects + */ + 'groupchange', + /** + * @event load + * Fires whenever records have been prefetched + * @param {Ext.data.store} this + * @param {Array} records An array of records + * @param {Boolean} successful True if the operation was successful. + * @param {Ext.data.Operation} operation The associated operation + */ + 'prefetch' + ); + data = config.data || me.data; + + /** + * The MixedCollection that holds this store's local cache of records + * @property data + * @type Ext.util.MixedCollection + */ + me.data = Ext.create('Ext.util.MixedCollection', false, function(record) { + return record.internalId; + }); + + if (data) { + me.inlineData = data; + delete config.data; + } + + if (!groupers && config.groupField) { + groupers = [{ + property : config.groupField, + direction: config.groupDir + }]; + } + delete config.groupers; + + /** + * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store + * @property groupers + * @type Ext.util.MixedCollection + */ + me.groupers = Ext.create('Ext.util.MixedCollection'); + me.groupers.addAll(me.decodeGroupers(groupers)); + + this.callParent([config]); + + if (me.groupers.items.length) { + me.sort(me.groupers.items, 'prepend', false); + } + + proxy = me.proxy; + data = me.inlineData; + + if (data) { + if (proxy instanceof Ext.data.proxy.Memory) { + proxy.data = data; + me.read(); + } else { + me.add.apply(me, data); + } + + me.sort(); + delete me.inlineData; + } else if (me.autoLoad) { + Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]); + // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here. + // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined); + } + }, + + onBeforeSort: function() { + this.sort(this.groupers.items, 'prepend', false); + }, + + /** + * @private + * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances + * @param {Array} groupers The groupers array + * @return {Array} Array of Ext.util.Grouper objects + */ + decodeGroupers: function(groupers) { + if (!Ext.isArray(groupers)) { + if (groupers === undefined) { + groupers = []; + } else { + groupers = [groupers]; + } + } + + var length = groupers.length, + Grouper = Ext.util.Grouper, + config, i; + + for (i = 0; i < length; i++) { + config = groupers[i]; + + if (!(config instanceof Grouper)) { + if (Ext.isString(config)) { + config = { + property: config + }; + } + + Ext.applyIf(config, { + root : 'data', + direction: "ASC" + }); + + //support for 3.x style sorters where a function can be defined as 'fn' + if (config.fn) { + config.sorterFn = config.fn; + } + + //support a function to be passed as a sorter definition + if (typeof config == 'function') { + config = { + sorterFn: config + }; + } + + groupers[i] = new Grouper(config); + } + } + + return groupers; + }, + + /** + * Group data in the store + * @param {String|Array} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model}, + * or an Array of grouper configurations. + * @param {String} direction The overall direction to group the data by. Defaults to "ASC". + */ + group: function(groupers, direction) { + var me = this, + grouper, + newGroupers; + + if (Ext.isArray(groupers)) { + newGroupers = groupers; + } else if (Ext.isObject(groupers)) { + newGroupers = [groupers]; + } else if (Ext.isString(groupers)) { + grouper = me.groupers.get(groupers); + + if (!grouper) { + grouper = { + property : groupers, + direction: direction + }; + newGroupers = [grouper]; + } else if (direction === undefined) { + grouper.toggle(); + } else { + grouper.setDirection(direction); + } + } + + if (newGroupers && newGroupers.length) { + newGroupers = me.decodeGroupers(newGroupers); + me.groupers.clear(); + me.groupers.addAll(newGroupers); + } + + if (me.remoteGroup) { + me.load({ + scope: me, + callback: me.fireGroupChange + }); + } else { + me.sort(); + me.fireEvent('groupchange', me, me.groupers); + } + }, + + /** + * Clear any groupers in the store + */ + clearGrouping: function(){ + var me = this; + // Clear any groupers we pushed on to the sorters + me.groupers.each(function(grouper){ + me.sorters.remove(grouper); + }); + me.groupers.clear(); + if (me.remoteGroup) { + me.load({ + scope: me, + callback: me.fireGroupChange + }); + } else { + me.sort(); + me.fireEvent('groupchange', me, me.groupers); + } + }, + + /** + * Checks if the store is currently grouped + * @return {Boolean} True if the store is grouped. + */ + isGrouped: function() { + return this.groupers.getCount() > 0; + }, + + /** + * Fires the groupchange event. Abstracted out so we can use it + * as a callback + * @private + */ + fireGroupChange: function(){ + this.fireEvent('groupchange', this, this.groupers); + }, + + /** + * Returns an object containing the result of applying grouping to the records in this store. See {@link #groupField}, + * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field: +

+var myStore = new Ext.data.Store({
+    groupField: 'color',
+    groupDir  : 'DESC'
+});
+
+myStore.getGroups(); //returns:
+[
+    {
+        name: 'yellow',
+        children: [
+            //all records where the color field is 'yellow'
+        ]
+    },
+    {
+        name: 'red',
+        children: [
+            //all records where the color field is 'red'
+        ]
+    }
+]
+
+ * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString} + * @return {Array} The grouped data + */ + getGroups: function(requestGroupString) { + var records = this.data.items, + length = records.length, + groups = [], + pointers = {}, + record, + groupStr, + group, + i; + + for (i = 0; i < length; i++) { + record = records[i]; + groupStr = this.getGroupString(record); + group = pointers[groupStr]; + + if (group === undefined) { + group = { + name: groupStr, + children: [] + }; + + groups.push(group); + pointers[groupStr] = group; + } + + group.children.push(record); + } + + return requestGroupString ? pointers[requestGroupString] : groups; + }, + + /** + * @private + * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records + * matching a certain group. + */ + getGroupsForGrouper: function(records, grouper) { + var length = records.length, + groups = [], + oldValue, + newValue, + record, + group, + i; + + for (i = 0; i < length; i++) { + record = records[i]; + newValue = grouper.getGroupString(record); + + if (newValue !== oldValue) { + group = { + name: newValue, + grouper: grouper, + records: [] + }; + groups.push(group); + } + + group.records.push(record); + + oldValue = newValue; + } + + return groups; + }, + + /** + * @private + * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for + * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by + * {@link #getGroupsForGrouper} - this function largely just handles the recursion. + * @param {Array} records The set or subset of records to group + * @param {Number} grouperIndex The grouper index to retrieve + * @return {Array} The grouped records + */ + getGroupsForGrouperIndex: function(records, grouperIndex) { + var me = this, + groupers = me.groupers, + grouper = groupers.getAt(grouperIndex), + groups = me.getGroupsForGrouper(records, grouper), + length = groups.length, + i; + + if (grouperIndex + 1 < groupers.length) { + for (i = 0; i < length; i++) { + groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1); + } + } + + for (i = 0; i < length; i++) { + groups[i].depth = grouperIndex; + } + + return groups; + }, + + /** + * @private + *

Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in + * this case grouping by genre and then author in a fictional books dataset):

+

+[
+    {
+        name: 'Fantasy',
+        depth: 0,
+        records: [
+            //book1, book2, book3, book4
+        ],
+        children: [
+            {
+                name: 'Rowling',
+                depth: 1,
+                records: [
+                    //book1, book2
+                ]
+            },
+            {
+                name: 'Tolkein',
+                depth: 1,
+                records: [
+                    //book3, book4
+                ]
+            }
+        ]
+    }
+]
+
+ * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping + * function correctly so this should only be set to false if the Store is known to already be sorted correctly + * (defaults to true) + * @return {Array} The group data + */ + getGroupData: function(sort) { + var me = this; + if (sort !== false) { + me.sort(); + } + + return me.getGroupsForGrouperIndex(me.data.items, 0); + }, + + /** + *

Returns the string to group on for a given model instance. The default implementation of this method returns + * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to + * group by the first letter of a model's 'name' field, use the following code:

+

+new Ext.data.Store({
+    groupDir: 'ASC',
+    getGroupString: function(instance) {
+        return instance.get('name')[0];
+    }
+});
+
+ * @param {Ext.data.Model} instance The model instance + * @return {String} The string to compare when forming groups + */ + getGroupString: function(instance) { + var group = this.groupers.first(); + if (group) { + return instance.get(group.property); + } + return ''; + }, + /** + * Inserts Model instances into the Store at the given index and fires the {@link #add} event. + * See also {@link #add}. + * @param {Number} index The start index at which to insert the passed Records. + * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache. + */ + insert: function(index, records) { + var me = this, + sync = false, + i, + record, + len; + + records = [].concat(records); + for (i = 0, len = records.length; i < len; i++) { + record = me.createModel(records[i]); + record.set(me.modelDefaults); + // reassign the model in the array in case it wasn't created yet + records[i] = record; + + me.data.insert(index + i, record); + record.join(me); + + sync = sync || record.phantom === true; + } + + if (me.snapshot) { + me.snapshot.addAll(records); + } + + me.fireEvent('add', me, records, index); + me.fireEvent('datachanged', me); + if (me.autoSync && sync) { + me.sync(); + } + }, + + /** + * Adds Model instances to the Store by instantiating them based on a JavaScript object. When adding already- + * instantiated Models, use {@link #insert} instead. The instances will be added at the end of the existing collection. + * This method accepts either a single argument array of Model instances or any number of model instance arguments. + * Sample usage: + * +

+myStore.add({some: 'data'}, {some: 'other data'});
+
+ * + * @param {Object} data The data for each model + * @return {Array} The array of newly created model instances + */ + add: function(records) { + //accept both a single-argument array of records, or any number of record arguments + if (!Ext.isArray(records)) { + records = Array.prototype.slice.apply(arguments); + } + + var me = this, + i = 0, + length = records.length, + record; + + for (; i < length; i++) { + record = me.createModel(records[i]); + // reassign the model in the array in case it wasn't created yet + records[i] = record; + } + + me.insert(me.data.length, records); + + return records; + }, + + /** + * Converts a literal to a model, if it's not a model already + * @private + * @param record {Ext.data.Model/Object} The record to create + * @return {Ext.data.Model} + */ + createModel: function(record) { + if (!record.isModel) { + record = Ext.ModelManager.create(record, this.model); + } + + return record; + }, + + /** + * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache. + * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed as the first parameter. + * Returning false aborts and exits the iteration. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. + * Defaults to the current {@link Ext.data.Model Record} in the iteration. + */ + each: function(fn, scope) { + this.data.each(fn, scope); + }, + + /** + * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single + * 'datachanged' event after removal. + * @param {Ext.data.Model/Array} records The Ext.data.Model instance or array of instances to remove + */ + remove: function(records, /* private */ isMove) { + if (!Ext.isArray(records)) { + records = [records]; + } + + /* + * Pass the isMove parameter if we know we're going to be re-inserting this record + */ + isMove = isMove === true; + var me = this, + sync = false, + i = 0, + length = records.length, + isPhantom, + index, + record; + + for (; i < length; i++) { + record = records[i]; + index = me.data.indexOf(record); + + if (me.snapshot) { + me.snapshot.remove(record); + } + + if (index > -1) { + isPhantom = record.phantom === true; + if (!isMove && !isPhantom) { + // don't push phantom records onto removed + me.removed.push(record); + } + + record.unjoin(me); + me.data.remove(record); + sync = sync || !isPhantom; + + me.fireEvent('remove', me, record, index); + } + } + + me.fireEvent('datachanged', me); + if (!isMove && me.autoSync && sync) { + me.sync(); + } + }, + + /** + * Removes the model instance at the given index + * @param {Number} index The record index + */ + removeAt: function(index) { + var record = this.getAt(index); + + if (record) { + this.remove(record); + } + }, + + /** + *

Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an + * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved + * instances into the Store and calling an optional callback if required. Example usage:

+ * +

+store.load({
+    scope   : this,
+    callback: function(records, operation, success) {
+        //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
+        console.log(records);
+    }
+});
+
+ * + *

If the callback scope does not need to be set, a function can simply be passed:

+ * +

+store.load(function(records, operation, success) {
+    console.log('loaded records');
+});
+
+ * + * @param {Object/Function} options Optional config object, passed into the Ext.data.Operation object before loading. + */ + load: function(options) { + var me = this; + + options = options || {}; + + if (Ext.isFunction(options)) { + options = { + callback: options + }; + } + + Ext.applyIf(options, { + groupers: me.groupers.items, + page: me.currentPage, + start: (me.currentPage - 1) * me.pageSize, + limit: me.pageSize, + addRecords: false + }); + + return me.callParent([options]); + }, + + /** + * @private + * Called internally when a Proxy has completed a load request + */ + onProxyLoad: function(operation) { + var me = this, + resultSet = operation.getResultSet(), + records = operation.getRecords(), + successful = operation.wasSuccessful(); + + if (resultSet) { + me.totalCount = resultSet.total; + } + + if (successful) { + me.loadRecords(records, operation); + } + + me.loading = false; + me.fireEvent('load', me, records, successful); + + //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not. + //People are definitely using this so can't deprecate safely until 2.x + me.fireEvent('read', me, records, operation.wasSuccessful()); + + //this is a callback that would have been passed to the 'read' function and is optional + Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]); + }, + + /** + * Create any new records when a write is returned from the server. + * @private + * @param {Array} records The array of new records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onCreateRecords: function(records, operation, success) { + if (success) { + var i = 0, + data = this.data, + snapshot = this.snapshot, + length = records.length, + originalRecords = operation.records, + record, + original, + index; + + /** + * Loop over each record returned from the server. Assume they are + * returned in order of how they were sent. If we find a matching + * record, replace it with the newly created one. + */ + for (; i < length; ++i) { + record = records[i]; + original = originalRecords[i]; + if (original) { + index = data.indexOf(original); + if (index > -1) { + data.removeAt(index); + data.insert(index, record); + } + if (snapshot) { + index = snapshot.indexOf(original); + if (index > -1) { + snapshot.removeAt(index); + snapshot.insert(index, record); + } + } + record.phantom = false; + record.join(this); + } + } + } + }, + + /** + * Update any records when a write is returned from the server. + * @private + * @param {Array} records The array of updated records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onUpdateRecords: function(records, operation, success){ + if (success) { + var i = 0, + length = records.length, + data = this.data, + snapshot = this.snapshot, + record; + + for (; i < length; ++i) { + record = records[i]; + data.replace(record); + if (snapshot) { + snapshot.replace(record); + } + record.join(this); + } + } + }, + + /** + * Remove any records when a write is returned from the server. + * @private + * @param {Array} records The array of removed records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onDestroyRecords: function(records, operation, success){ + if (success) { + var me = this, + i = 0, + length = records.length, + data = me.data, + snapshot = me.snapshot, + record; + + for (; i < length; ++i) { + record = records[i]; + record.unjoin(me); + data.remove(record); + if (snapshot) { + snapshot.remove(record); + } + } + me.removed = []; + } + }, + + //inherit docs + getNewRecords: function() { + return this.data.filterBy(this.filterNew).items; + }, + + //inherit docs + getUpdatedRecords: function() { + return this.data.filterBy(this.filterUpdated).items; + }, + + /** + * Filters the loaded set of records by a given set of filters. + * @param {Mixed} filters The set of filters to apply to the data. These are stored internally on the store, + * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See + * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively, + * pass in a property string + * @param {String} value Optional value to filter by (only if using a property string as the first argument) + */ + filter: function(filters, value) { + if (Ext.isString(filters)) { + filters = { + property: filters, + value: value + }; + } + + var me = this, + decoded = me.decodeFilters(filters), + i = 0, + doLocalSort = me.sortOnFilter && !me.remoteSort, + length = decoded.length; + + for (; i < length; i++) { + me.filters.replace(decoded[i]); + } + + if (me.remoteFilter) { + //the load function will pick up the new filters and request the filtered data from the proxy + me.load(); + } else { + /** + * A pristine (unfiltered) collection of the records in this store. This is used to reinstate + * records when a filter is removed or changed + * @property snapshot + * @type Ext.util.MixedCollection + */ + if (me.filters.getCount()) { + me.snapshot = me.snapshot || me.data.clone(); + me.data = me.data.filter(me.filters.items); + + if (doLocalSort) { + me.sort(); + } + // fire datachanged event if it hasn't already been fired by doSort + if (!doLocalSort || me.sorters.length < 1) { + me.fireEvent('datachanged', me); + } + } + } + }, + + /** + * Revert to a view of the Record cache with no filtering applied. + * @param {Boolean} suppressEvent If true the filter is cleared silently without firing the + * {@link #datachanged} event. + */ + clearFilter: function(suppressEvent) { + var me = this; + + me.filters.clear(); + + if (me.remoteFilter) { + me.load(); + } else if (me.isFiltered()) { + me.data = me.snapshot.clone(); + delete me.snapshot; + + if (suppressEvent !== true) { + me.fireEvent('datachanged', me); + } + } + }, + + /** + * Returns true if this store is currently filtered + * @return {Boolean} + */ + isFiltered: function() { + var snapshot = this.snapshot; + return !! snapshot && snapshot !== this.data; + }, + + /** + * Filter by a function. The specified function will be called for each + * Record in this Store. If the function returns true the Record is included, + * otherwise it is filtered out. + * @param {Function} fn The function to be called. It will be passed the following parameters:
    + *
  • record : Ext.data.Model

    The {@link Ext.data.Model record} + * to test for filtering. Access field values using {@link Ext.data.Model#get}.

  • + *
  • id : Object

    The ID of the Record passed.

  • + *
+ * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. + */ + filterBy: function(fn, scope) { + var me = this; + + me.snapshot = me.snapshot || me.data.clone(); + me.data = me.queryBy(fn, scope || me); + me.fireEvent('datachanged', me); + }, + + /** + * Query the cached records in this Store using a filtering function. The specified function + * will be called with each record in this Store. If the function returns true the record is + * included in the results. + * @param {Function} fn The function to be called. It will be passed the following parameters:
    + *
  • record : Ext.data.Model

    The {@link Ext.data.Model record} + * to test for filtering. Access field values using {@link Ext.data.Model#get}.

  • + *
  • id : Object

    The ID of the Record passed.

  • + *
+ * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. + * @return {MixedCollection} Returns an Ext.util.MixedCollection of the matched records + **/ + queryBy: function(fn, scope) { + var me = this, + data = me.snapshot || me.data; + return data.filterBy(fn, scope || me); + }, + + /** + * Loads an array of data straight into the Store + * @param {Array} data Array of data to load. Any non-model instances will be cast into model instances first + * @param {Boolean} append True to add the records to the existing records in the store, false to remove the old ones first + */ + loadData: function(data, append) { + var model = this.model, + length = data.length, + i, + record; + + //make sure each data element is an Ext.data.Model instance + for (i = 0; i < length; i++) { + record = data[i]; + + if (! (record instanceof Ext.data.Model)) { + data[i] = Ext.ModelManager.create(record, model); + } + } + + this.loadRecords(data, {addRecords: append}); + }, + + /** + * Loads an array of {@Ext.data.Model model} instances into the store, fires the datachanged event. This should only usually + * be called internally when loading from the {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #add} instead + * @param {Array} records The array of records to load + * @param {Object} options {addRecords: true} to add these records to the existing records, false to remove the Store's existing records first + */ + loadRecords: function(records, options) { + var me = this, + i = 0, + length = records.length; + + options = options || {}; + + + if (!options.addRecords) { + delete me.snapshot; + me.data.clear(); + } + + me.data.addAll(records); + + //FIXME: this is not a good solution. Ed Spencer is totally responsible for this and should be forced to fix it immediately. + for (; i < length; i++) { + if (options.start !== undefined) { + records[i].index = options.start + i; + + } + records[i].join(me); + } + + /* + * this rather inelegant suspension and resumption of events is required because both the filter and sort functions + * fire an additional datachanged event, which is not wanted. Ideally we would do this a different way. The first + * datachanged event is fired by the call to this.add, above. + */ + me.suspendEvents(); + + if (me.filterOnLoad && !me.remoteFilter) { + me.filter(); + } + + if (me.sortOnLoad && !me.remoteSort) { + me.sort(); + } + + me.resumeEvents(); + me.fireEvent('datachanged', me, records); + }, + + // PAGING METHODS + /** + * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal + * load operation, passing in calculated 'start' and 'limit' params + * @param {Number} page The number of the page to load + */ + loadPage: function(page) { + var me = this; + + me.currentPage = page; + + me.read({ + page: page, + start: (page - 1) * me.pageSize, + limit: me.pageSize, + addRecords: !me.clearOnPageLoad + }); + }, + + /** + * Loads the next 'page' in the current data set + */ + nextPage: function() { + this.loadPage(this.currentPage + 1); + }, + + /** + * Loads the previous 'page' in the current data set + */ + previousPage: function() { + this.loadPage(this.currentPage - 1); + }, + + // private + clearData: function() { + this.data.each(function(record) { + record.unjoin(); + }); + + this.data.clear(); + }, + + // Buffering + /** + * Prefetches data the Store using its configured {@link #proxy}. + * @param {Object} options Optional config object, passed into the Ext.data.Operation object before loading. + * See {@link #load} + */ + prefetch: function(options) { + var me = this, + operation, + requestId = me.getRequestId(); + + options = options || {}; + + Ext.applyIf(options, { + action : 'read', + filters: me.filters.items, + sorters: me.sorters.items, + requestId: requestId + }); + me.pendingRequests.push(requestId); + + operation = Ext.create('Ext.data.Operation', options); + + // HACK to implement loadMask support. + //if (operation.blocking) { + // me.fireEvent('beforeload', me, operation); + //} + if (me.fireEvent('beforeprefetch', me, operation) !== false) { + me.loading = true; + me.proxy.read(operation, me.onProxyPrefetch, me); + } + + return me; + }, + + /** + * Prefetches a page of data. + * @param {Number} page The page to prefetch + * @param {Object} options Optional config object, passed into the Ext.data.Operation object before loading. + * See {@link #load} + * @param + */ + prefetchPage: function(page, options) { + var me = this, + pageSize = me.pageSize, + start = (page - 1) * me.pageSize, + end = start + pageSize; + + // Currently not requesting this page and range isn't already satisified + if (Ext.Array.indexOf(me.pagesRequested, page) === -1 && !me.rangeSatisfied(start, end)) { + options = options || {}; + me.pagesRequested.push(page); + Ext.applyIf(options, { + page : page, + start: start, + limit: pageSize, + callback: me.onWaitForGuarantee, + scope: me + }); + + me.prefetch(options); + } + + }, + + /** + * Returns a unique requestId to track requests. + * @private + */ + getRequestId: function() { + this.requestSeed = this.requestSeed || 1; + return this.requestSeed++; + }, + + /** + * Handles a success pre-fetch + * @private + * @param {Ext.data.Operation} operation The operation that completed + */ + onProxyPrefetch: function(operation) { + var me = this, + resultSet = operation.getResultSet(), + records = operation.getRecords(), + + successful = operation.wasSuccessful(); + + if (resultSet) { + me.totalCount = resultSet.total; + me.fireEvent('totalcountchange', me.totalCount); + } + + if (successful) { + me.cacheRecords(records, operation); + } + Ext.Array.remove(me.pendingRequests, operation.requestId); + if (operation.page) { + Ext.Array.remove(me.pagesRequested, operation.page); + } + + me.loading = false; + me.fireEvent('prefetch', me, records, successful, operation); + + // HACK to support loadMask + if (operation.blocking) { + me.fireEvent('load', me, records, successful); + } + + //this is a callback that would have been passed to the 'read' function and is optional + Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]); + }, + + /** + * Caches the records in the prefetch and stripes them with their server-side + * index. + * @private + * @param {Array} records The records to cache + * @param {Ext.data.Operation} The associated operation + */ + cacheRecords: function(records, operation) { + var me = this, + i = 0, + length = records.length, + start = operation ? operation.start : 0; + + if (!Ext.isDefined(me.totalCount)) { + me.totalCount = records.length; + me.fireEvent('totalcountchange', me.totalCount); + } + + for (; i < length; i++) { + // this is the true index, not the viewIndex + records[i].index = start + i; + } + + me.prefetchData.addAll(records); + if (me.purgePageCount) { + me.purgeRecords(); + } + + }, + + + /** + * Purge the least recently used records in the prefetch if the purgeCount + * has been exceeded. + */ + purgeRecords: function() { + var me = this, + prefetchCount = me.prefetchData.getCount(), + purgeCount = me.purgePageCount * me.pageSize, + numRecordsToPurge = prefetchCount - purgeCount - 1, + i = 0; + + for (; i <= numRecordsToPurge; i++) { + me.prefetchData.removeAt(0); + } + }, + + /** + * Determines if the range has already been satisfied in the prefetchData. + * @private + * @param {Number} start The start index + * @param {Number} end The end index in the range + */ + rangeSatisfied: function(start, end) { + var me = this, + i = start, + satisfied = true; + + for (; i < end; i++) { + if (!me.prefetchData.getByKey(i)) { + satisfied = false; + // + if (end - i > me.pageSize) { + Ext.Error.raise("A single page prefetch could never satisfy this request."); + } + // + break; + } + } + return satisfied; + }, + + /** + * Determines the page from a record index + * @param {Number} index The record index + * @return {Number} The page the record belongs to + */ + getPageFromRecordIndex: function(index) { + return Math.floor(index / this.pageSize) + 1; + }, + + /** + * Handles a guaranteed range being loaded + * @private + */ + onGuaranteedRange: function() { + var me = this, + totalCount = me.getTotalCount(), + start = me.requestStart, + end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd, + range = [], + record, + i = start; + + // + if (start > end) { + Ext.Error.raise("Start (" + start + ") was greater than end (" + end + ")"); + } + // + + if (start !== me.guaranteedStart && end !== me.guaranteedEnd) { + me.guaranteedStart = start; + me.guaranteedEnd = end; + + for (; i <= end; i++) { + record = me.prefetchData.getByKey(i); + // + if (!record) { + Ext.Error.raise("Record was not found and store said it was guaranteed"); + } + // + range.push(record); + } + me.fireEvent('guaranteedrange', range, start, end); + if (me.cb) { + me.cb.call(me.scope || me, range); + } + } + + me.unmask(); + }, + + // hack to support loadmask + mask: function() { + this.masked = true; + this.fireEvent('beforeload'); + }, + + // hack to support loadmask + unmask: function() { + if (this.masked) { + this.fireEvent('load'); + } + }, + + /** + * Returns the number of pending requests out. + */ + hasPendingRequests: function() { + return this.pendingRequests.length; + }, + + + // wait until all requests finish, until guaranteeing the range. + onWaitForGuarantee: function() { + if (!this.hasPendingRequests()) { + this.onGuaranteedRange(); + } + }, + + /** + * Guarantee a specific range, this will load the store with a range (that + * must be the pageSize or smaller) and take care of any loading that may + * be necessary. + */ + guaranteeRange: function(start, end, cb, scope) { + // + if (start && end) { + if (end - start > this.pageSize) { + Ext.Error.raise({ + start: start, + end: end, + pageSize: this.pageSize, + msg: "Requested a bigger range than the specified pageSize" + }); + } + } + // + + end = (end > this.totalCount) ? this.totalCount - 1 : end; + + var me = this, + i = start, + prefetchData = me.prefetchData, + range = [], + startLoaded = !!prefetchData.getByKey(start), + endLoaded = !!prefetchData.getByKey(end), + startPage = me.getPageFromRecordIndex(start), + endPage = me.getPageFromRecordIndex(end); + + me.cb = cb; + me.scope = scope; + + me.requestStart = start; + me.requestEnd = end; + // neither beginning or end are loaded + if (!startLoaded || !endLoaded) { + // same page, lets load it + if (startPage === endPage) { + me.mask(); + me.prefetchPage(startPage, { + //blocking: true, + callback: me.onWaitForGuarantee, + scope: me + }); + // need to load two pages + } else { + me.mask(); + me.prefetchPage(startPage, { + //blocking: true, + callback: me.onWaitForGuarantee, + scope: me + }); + me.prefetchPage(endPage, { + //blocking: true, + callback: me.onWaitForGuarantee, + scope: me + }); + } + // Request was already satisfied via the prefetch + } else { + me.onGuaranteedRange(); + } + }, + + // because prefetchData is stored by index + // this invalidates all of the prefetchedData + sort: function() { + var me = this, + prefetchData = me.prefetchData, + sorters, + start, + end, + range; + + if (me.buffered) { + if (me.remoteSort) { + prefetchData.clear(); + me.callParent(arguments); + } else { + sorters = me.getSorters(); + start = me.guaranteedStart; + end = me.guaranteedEnd; + range; + + if (sorters.length) { + prefetchData.sort(sorters); + range = prefetchData.getRange(); + prefetchData.clear(); + me.cacheRecords(range); + delete me.guaranteedStart; + delete me.guaranteedEnd; + me.guaranteeRange(start, end); + } + me.callParent(arguments); + } + } else { + me.callParent(arguments); + } + }, + + // overriden to provide striping of the indexes as sorting occurs. + // this cannot be done inside of sort because datachanged has already + // fired and will trigger a repaint of the bound view. + doSort: function(sorterFn) { + var me = this; + if (me.remoteSort) { + //the load function will pick up the new sorters and request the sorted data from the proxy + me.load(); + } else { + me.data.sortBy(sorterFn); + if (!me.buffered) { + var range = me.getRange(), + ln = range.length, + i = 0; + for (; i < ln; i++) { + range[i].index = i; + } + } + me.fireEvent('datachanged', me); + } + }, + + /** + * Finds the index of the first matching Record in this store by a specific field value. + * @param {String} fieldName The name of the Record field to test. + * @param {String/RegExp} value Either a string that the field value + * should begin with, or a RegExp to test against the field. + * @param {Number} startIndex (optional) The index to start searching at + * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning + * @param {Boolean} caseSensitive (optional) True for case sensitive comparison + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. + * @return {Number} The matched index or -1 + */ + find: function(property, value, start, anyMatch, caseSensitive, exactMatch) { + var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch); + return fn ? this.data.findIndexBy(fn, null, start) : -1; + }, + + /** + * Finds the first matching Record in this store by a specific field value. + * @param {String} fieldName The name of the Record field to test. + * @param {String/RegExp} value Either a string that the field value + * should begin with, or a RegExp to test against the field. + * @param {Number} startIndex (optional) The index to start searching at + * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning + * @param {Boolean} caseSensitive (optional) True for case sensitive comparison + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. + * @return {Ext.data.Model} The matched record or null + */ + findRecord: function() { + var me = this, + index = me.find.apply(me, arguments); + return index !== -1 ? me.getAt(index) : null; + }, + + /** + * @private + * Returns a filter function used to test a the given property's value. Defers most of the work to + * Ext.util.MixedCollection's createValueMatcher function + * @param {String} property The property to create the filter function for + * @param {String/RegExp} value The string/regex to compare the property value to + * @param {Boolean} anyMatch True if we don't care if the filter value is not the full value (defaults to false) + * @param {Boolean} caseSensitive True to create a case-sensitive regex (defaults to false) + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. + * Ignored if anyMatch is true. + */ + createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) { + if (Ext.isEmpty(value)) { + return false; + } + value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch); + return function(r) { + return value.test(r.data[property]); + }; + }, + + /** + * Finds the index of the first matching Record in this store by a specific field value. + * @param {String} fieldName The name of the Record field to test. + * @param {Mixed} value The value to match the field against. + * @param {Number} startIndex (optional) The index to start searching at + * @return {Number} The matched index or -1 + */ + findExact: function(property, value, start) { + return this.data.findIndexBy(function(rec) { + return rec.get(property) === value; + }, + this, start); + }, + + /** + * Find the index of the first matching Record in this Store by a function. + * If the function returns true it is considered a match. + * @param {Function} fn The function to be called. It will be passed the following parameters:
    + *
  • record : Ext.data.Model

    The {@link Ext.data.Model record} + * to test for filtering. Access field values using {@link Ext.data.Model#get}.

  • + *
  • id : Object

    The ID of the Record passed.

  • + *
+ * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. + * @param {Number} startIndex (optional) The index to start searching at + * @return {Number} The matched index or -1 + */ + findBy: function(fn, scope, start) { + return this.data.findIndexBy(fn, scope, start); + }, + + /** + * Collects unique values for a particular dataIndex from this store. + * @param {String} dataIndex The property to collect + * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string values + * @param {Boolean} bypassFilter (optional) Pass true to collect from all records, even ones which are filtered + * @return {Array} An array of the unique values + **/ + collect: function(dataIndex, allowNull, bypassFilter) { + var me = this, + data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data; + + return data.collect(dataIndex, 'data', allowNull); + }, + + /** + * Gets the number of cached records. + *

If using paging, this may not be the total size of the dataset. If the data object + * used by the Reader contains the dataset size, then the {@link #getTotalCount} function returns + * the dataset size. Note: see the Important note in {@link #load}.

+ * @return {Number} The number of Records in the Store's cache. + */ + getCount: function() { + return this.data.length || 0; + }, + + /** + * Returns the total number of {@link Ext.data.Model Model} instances that the {@link Ext.data.proxy.Proxy Proxy} + * indicates exist. This will usually differ from {@link #getCount} when using paging - getCount returns the + * number of records loaded into the Store at the moment, getTotalCount returns the number of records that + * could be loaded into the Store if the Store contained all data + * @return {Number} The total number of Model instances available via the Proxy + */ + getTotalCount: function() { + return this.totalCount; + }, + + /** + * Get the Record at the specified index. + * @param {Number} index The index of the Record to find. + * @return {Ext.data.Model} The Record at the passed index. Returns undefined if not found. + */ + getAt: function(index) { + return this.data.getAt(index); + }, + + /** + * Returns a range of Records between specified indices. + * @param {Number} startIndex (optional) The starting index (defaults to 0) + * @param {Number} endIndex (optional) The ending index (defaults to the last Record in the Store) + * @return {Ext.data.Model[]} An array of Records + */ + getRange: function(start, end) { + return this.data.getRange(start, end); + }, + + /** + * Get the Record with the specified id. + * @param {String} id The id of the Record to find. + * @return {Ext.data.Model} The Record with the passed id. Returns undefined if not found. + */ + getById: function(id) { + return (this.snapshot || this.data).findBy(function(record) { + return record.getId() === id; + }); + }, + + /** + * Get the index within the cache of the passed Record. + * @param {Ext.data.Model} record The Ext.data.Model object to find. + * @return {Number} The index of the passed Record. Returns -1 if not found. + */ + indexOf: function(record) { + return this.data.indexOf(record); + }, + + + /** + * Get the index within the entire dataset. From 0 to the totalCount. + * @param {Ext.data.Model} record The Ext.data.Model object to find. + * @return {Number} The index of the passed Record. Returns -1 if not found. + */ + indexOfTotal: function(record) { + return record.index || this.indexOf(record); + }, + + /** + * Get the index within the cache of the Record with the passed id. + * @param {String} id The id of the Record to find. + * @return {Number} The index of the Record. Returns -1 if not found. + */ + indexOfId: function(id) { + return this.data.indexOfKey(id); + }, + + /** + * Remove all items from the store. + * @param {Boolean} silent Prevent the `clear` event from being fired. + */ + removeAll: function(silent) { + var me = this; + + me.clearData(); + if (me.snapshot) { + me.snapshot.clear(); + } + if (silent !== true) { + me.fireEvent('clear', me); + } + }, + + /* + * Aggregation methods + */ + + /** + * Convenience function for getting the first model instance in the store + * @param {Boolean} grouped (Optional) True to perform the operation for each group + * in the store. The value returned will be an object literal with the key being the group + * name and the first record being the value. The grouped parameter is only honored if + * the store has a groupField. + * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined + */ + first: function(grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(function(records) { + return records.length ? records[0] : undefined; + }, me, true); + } else { + return me.data.first(); + } + }, + + /** + * Convenience function for getting the last model instance in the store + * @param {Boolean} grouped (Optional) True to perform the operation for each group + * in the store. The value returned will be an object literal with the key being the group + * name and the last record being the value. The grouped parameter is only honored if + * the store has a groupField. + * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined + */ + last: function(grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(function(records) { + var len = records.length; + return len ? records[len - 1] : undefined; + }, me, true); + } else { + return me.data.last(); + } + }, + + /** + * Sums the value of property for each {@link Ext.data.Model record} between start + * and end and returns the result. + * @param {String} field A field in each record + * @param {Boolean} grouped (Optional) True to perform the operation for each group + * in the store. The value returned will be an object literal with the key being the group + * name and the sum for that group being the value. The grouped parameter is only honored if + * the store has a groupField. + * @return {Number} The sum + */ + sum: function(field, grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(me.getSum, me, true, [field]); + } else { + return me.getSum(me.data.items, field); + } + }, + + // @private, see sum + getSum: function(records, field) { + var total = 0, + i = 0, + len = records.length; + + for (; i < len; ++i) { + total += records[i].get(field); + } + + return total; + }, + + /** + * Gets the count of items in the store. + * @param {Boolean} grouped (Optional) True to perform the operation for each group + * in the store. The value returned will be an object literal with the key being the group + * name and the count for each group being the value. The grouped parameter is only honored if + * the store has a groupField. + * @return {Number} the count + */ + count: function(grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(function(records) { + return records.length; + }, me, true); + } else { + return me.getCount(); + } + }, + + /** + * Gets the minimum value in the store. + * @param {String} field The field in each record + * @param {Boolean} grouped (Optional) True to perform the operation for each group + * in the store. The value returned will be an object literal with the key being the group + * name and the minimum in the group being the value. The grouped parameter is only honored if + * the store has a groupField. + * @return {Mixed/undefined} The minimum value, if no items exist, undefined. + */ + min: function(field, grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(me.getMin, me, true, [field]); + } else { + return me.getMin(me.data.items, field); + } + }, + + // @private, see min + getMin: function(records, field){ + var i = 1, + len = records.length, + value, min; + + if (len > 0) { + min = records[0].get(field); + } + + for (; i < len; ++i) { + value = records[i].get(field); + if (value < min) { + min = value; + } + } + return min; + }, + + /** + * Gets the maximum value in the store. + * @param {String} field The field in each record + * @param {Boolean} grouped (Optional) True to perform the operation for each group + * in the store. The value returned will be an object literal with the key being the group + * name and the maximum in the group being the value. The grouped parameter is only honored if + * the store has a groupField. + * @return {Mixed/undefined} The maximum value, if no items exist, undefined. + */ + max: function(field, grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(me.getMax, me, true, [field]); + } else { + return me.getMax(me.data.items, field); + } + }, + + // @private, see max + getMax: function(records, field) { + var i = 1, + len = records.length, + value, + max; + + if (len > 0) { + max = records[0].get(field); + } + + for (; i < len; ++i) { + value = records[i].get(field); + if (value > max) { + max = value; + } + } + return max; + }, + + /** + * Gets the average value in the store. + * @param {String} field The field in each record + * @param {Boolean} grouped (Optional) True to perform the operation for each group + * in the store. The value returned will be an object literal with the key being the group + * name and the group average being the value. The grouped parameter is only honored if + * the store has a groupField. + * @return {Mixed/undefined} The average value, if no items exist, 0. + */ + average: function(field, grouped) { + var me = this; + if (grouped && me.isGrouped()) { + return me.aggregate(me.getAverage, me, true, [field]); + } else { + return me.getAverage(me.data.items, field); + } + }, + + // @private, see average + getAverage: function(records, field) { + var i = 0, + len = records.length, + sum = 0; + + if (records.length > 0) { + for (; i < len; ++i) { + sum += records[i].get(field); + } + return sum / len; + } + return 0; + }, + + /** + * Runs the aggregate function for all the records in the store. + * @param {Function} fn The function to execute. The function is called with a single parameter, + * an array of records for that group. + * @param {Object} scope (optional) The scope to execute the function in. Defaults to the store. + * @param {Boolean} grouped (Optional) True to perform the operation for each group + * in the store. The value returned will be an object literal with the key being the group + * name and the group average being the value. The grouped parameter is only honored if + * the store has a groupField. + * @param {Array} args (optional) Any arguments to append to the function call + * @return {Object} An object literal with the group names and their appropriate values. + */ + aggregate: function(fn, scope, grouped, args) { + args = args || []; + if (grouped && this.isGrouped()) { + var groups = this.getGroups(), + i = 0, + len = groups.length, + out = {}, + group; + + for (; i < len; ++i) { + group = groups[i]; + out[group.name] = fn.apply(scope || this, [group.children].concat(args)); + } + return out; + } else { + return fn.apply(scope || this, [this.data.items].concat(args)); + } + } +}); + +/** + * @author Ed Spencer + * @class Ext.data.JsonStore + * @extends Ext.data.Store + * @ignore + * + *

Small helper class to make creating {@link Ext.data.Store}s from JSON data easier. + * A JsonStore will be automatically configured with a {@link Ext.data.reader.Json}.

+ * + *

A store configuration would be something like:

+ * +

+var store = new Ext.data.JsonStore({
+    // store configs
+    autoDestroy: true,
+    storeId: 'myStore'
+
+    proxy: {
+        type: 'ajax',
+        url: 'get-images.php',
+        reader: {
+            type: 'json',
+            root: 'images',
+            idProperty: 'name'
+        }
+    },
+
+    //alternatively, a {@link Ext.data.Model} name can be given (see {@link Ext.data.Store} for an example)
+    fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
+});
+
+ * + *

This store is configured to consume a returned object of the form:


+{
+    images: [
+        {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
+        {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
+    ]
+}
+
+ * + *

An object literal of this form could also be used as the {@link #data} config option.

+ * + * @constructor + * @param {Object} config + * @xtype jsonstore + */ +Ext.define('Ext.data.JsonStore', { + extend: 'Ext.data.Store', + alias: 'store.json', + + /** + * @cfg {Ext.data.DataReader} reader @hide + */ + constructor: function(config) { + config = config || {}; + + Ext.applyIf(config, { + proxy: { + type : 'ajax', + reader: 'json', + writer: 'json' + } + }); + + this.callParent([config]); + } +}); + +/** + * @class Ext.chart.axis.Time + * @extends Ext.chart.axis.Axis + * + * 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. + * + * For example: + * +

+    axes: [{
+        type: 'Time',
+        position: 'bottom',
+        fields: 'date',
+        title: 'Day',
+        dateFormat: 'M d',
+        groupBy: 'year,month,day',
+        aggregateOp: 'sum',
+
+        constrain: true,
+        fromDate: new Date('1/1/11'),
+        toDate: new Date('1/7/11')
+    }]
+  
+ * + * In this example we're creating a time axis that has as title Day. + * The field the axis is bound to is date. + * The date format to use to display the text for the axis labels is M d + * which is a three letter month abbreviation followed by the day number. + * The time axis will show values for dates betwee fromDate and toDate. + * Since constrain is set to true all other values for other dates not between + * the fromDate and toDate will not be displayed. + * + * @constructor + */ +Ext.define('Ext.chart.axis.Time', { + + /* Begin Definitions */ + + extend: 'Ext.chart.axis.Category', + + alternateClassName: 'Ext.chart.TimeAxis', + + alias: 'axis.time', + + requires: ['Ext.data.Store', 'Ext.data.JsonStore'], + + /* End Definitions */ + + /** + * The minimum value drawn by the axis. If not set explicitly, the axis + * minimum will be calculated automatically. + * @property calculateByLabelSize + * @type Boolean + */ + calculateByLabelSize: true, + + /** + * Indicates the format the date will be rendered on. + * For example: 'M d' will render the dates as 'Jan 30', etc. + * + * @property dateFormat + * @type {String|Boolean} + */ + dateFormat: false, + + /** + * Indicates the time unit to use for each step. Can be 'day', 'month', 'year' or a comma-separated combination of all of them. + * Default's 'year,month,day'. + * + * @property timeUnit + * @type {String} + */ + groupBy: 'year,month,day', + + /** + * Aggregation operation when grouping. Possible options are 'sum', 'avg', 'max', 'min'. Default's 'sum'. + * + * @property aggregateOp + * @type {String} + */ + aggregateOp: 'sum', + + /** + * The starting date for the time axis. + * @property fromDate + * @type Date + */ + fromDate: false, + + /** + * The ending date for the time axis. + * @property toDate + * @type Date + */ + toDate: false, + + /** + * An array with two components: The first is the unit of the step (day, month, year, etc). The second one is the number of units for the step (1, 2, etc.). + * Default's [Ext.Date.DAY, 1]. + * + * @property step + * @type Array + */ + step: [Ext.Date.DAY, 1], + + /** + * If true, the values of the chart will be rendered only if they belong between the fromDate and toDate. + * If false, the time axis will adapt to the new values by adding/removing steps. + * Default's [Ext.Date.DAY, 1]. + * + * @property constrain + * @type Boolean + */ + constrain: false, + + // @private a wrapper for date methods. + dateMethods: { + 'year': function(date) { + return date.getFullYear(); + }, + 'month': function(date) { + return date.getMonth() + 1; + }, + 'day': function(date) { + return date.getDate(); + }, + 'hour': function(date) { + return date.getHours(); + }, + 'minute': function(date) { + return date.getMinutes(); + }, + 'second': function(date) { + return date.getSeconds(); + }, + 'millisecond': function(date) { + return date.getMilliseconds(); + } + }, + + // @private holds aggregate functions. + aggregateFn: (function() { + var etype = (function() { + var rgxp = /^\[object\s(.*)\]$/, + toString = Object.prototype.toString; + return function(e) { + return toString.call(e).match(rgxp)[1]; + }; + })(); + return { + 'sum': function(list) { + var i = 0, l = list.length, acum = 0; + if (!list.length || etype(list[0]) != 'Number') { + return list[0]; + } + for (; i < l; i++) { + acum += list[i]; + } + return acum; + }, + 'max': function(list) { + if (!list.length || etype(list[0]) != 'Number') { + return list[0]; + } + return Math.max.apply(Math, list); + }, + 'min': function(list) { + if (!list.length || etype(list[0]) != 'Number') { + return list[0]; + } + return Math.min.apply(Math, list); + }, + 'avg': function(list) { + var i = 0, l = list.length, acum = 0; + if (!list.length || etype(list[0]) != 'Number') { + return list[0]; + } + for (; i < l; i++) { + acum += list[i]; + } + return acum / l; + } + }; + })(), + + // @private normalized the store to fill date gaps in the time interval. + constrainDates: function() { + var fromDate = Ext.Date.clone(this.fromDate), + toDate = Ext.Date.clone(this.toDate), + step = this.step, + field = this.fields, + store = this.chart.store, + record, recObj, fieldNames = [], + newStore = Ext.create('Ext.data.Store', { + model: store.model + }); + + var getRecordByDate = (function() { + var index = 0, l = store.getCount(); + return function(date) { + var rec, recDate; + for (; index < l; index++) { + rec = store.getAt(index); + recDate = rec.get(field); + if (+recDate > +date) { + return false; + } else if (+recDate == +date) { + return rec; + } + } + return false; + }; + })(); + + if (!this.constrain) { + this.chart.filteredStore = this.chart.store; + return; + } + + while(+fromDate <= +toDate) { + record = getRecordByDate(fromDate); + recObj = {}; + if (record) { + newStore.add(record.data); + } else { + newStore.model.prototype.fields.each(function(f) { + recObj[f.name] = false; + }); + recObj.date = fromDate; + newStore.add(recObj); + } + fromDate = Ext.Date.add(fromDate, step[0], step[1]); + } + + this.chart.filteredStore = newStore; + }, + + // @private aggregates values if multiple store elements belong to the same time step. + aggregate: function() { + var aggStore = {}, + aggKeys = [], key, value, + op = this.aggregateOp, + field = this.fields, i, + fields = this.groupBy.split(','), + curField, + recFields = [], + recFieldsLen = 0, + obj, + dates = [], + json = [], + l = fields.length, + dateMethods = this.dateMethods, + aggregateFn = this.aggregateFn, + store = this.chart.filteredStore || this.chart.store; + + store.each(function(rec) { + //get all record field names in a simple array + if (!recFields.length) { + rec.fields.each(function(f) { + recFields.push(f.name); + }); + recFieldsLen = recFields.length; + } + //get record date value + value = rec.get(field); + //generate key for grouping records + for (i = 0; i < l; i++) { + if (i == 0) { + key = String(dateMethods[fields[i]](value)); + } else { + key += '||' + dateMethods[fields[i]](value); + } + } + //get aggregation record from hash + if (key in aggStore) { + obj = aggStore[key]; + } else { + obj = aggStore[key] = {}; + aggKeys.push(key); + dates.push(value); + } + //append record values to an aggregation record + for (i = 0; i < recFieldsLen; i++) { + curField = recFields[i]; + if (!obj[curField]) { + obj[curField] = []; + } + if (rec.get(curField) !== undefined) { + obj[curField].push(rec.get(curField)); + } + } + }); + //perform aggregation operations on fields + for (key in aggStore) { + obj = aggStore[key]; + for (i = 0; i < recFieldsLen; i++) { + curField = recFields[i]; + obj[curField] = aggregateFn[op](obj[curField]); + } + json.push(obj); + } + this.chart.substore = Ext.create('Ext.data.JsonStore', { + fields: recFields, + data: json + }); + + this.dates = dates; + }, + + // @private creates a label array to be used as the axis labels. + setLabels: function() { + var store = this.chart.substore, + fields = this.fields, + format = this.dateFormat, + labels, i, dates = this.dates, + formatFn = Ext.Date.format; + this.labels = labels = []; + store.each(function(record, i) { + if (!format) { + labels.push(record.get(fields)); + } else { + labels.push(formatFn(dates[i], format)); + } + }, this); + }, + + processView: function() { + //TODO(nico): fix this eventually... + if (this.constrain) { + this.constrainDates(); + this.aggregate(); + this.chart.substore = this.chart.filteredStore; + } else { + this.aggregate(); + } + }, + + // @private modifies the store and creates the labels for the axes. + applyData: function() { + this.setLabels(); + var count = this.chart.substore.getCount(); + return { + from: 0, + to: count, + steps: count - 1, + step: 1 + }; + } + }); + + +/** + * @class Ext.chart.series.Series + * + * 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 handling + * mouse events, 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 iteracts with a marker. + * - `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', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable', + labels: 'Ext.chart.Label', + highlights: 'Ext.chart.Highlight', + tips: 'Ext.chart.Tip', + callouts: 'Ext.chart.Callout' + }, + + /* End Definitions */ + + /** + * @cfg {Boolean|Object} highlight + * If set to `true` it will highlight the markers or the series when hovering + * with the mouse. This parameter can also be an object with the same style + * properties you would apply to a {@link Ext.draw.Sprite} to apply custom + * styles to markers and series. + */ + + /** + * @cfg {Object} tips + * Add tooltips to the visualization's markers. The options for the tips are the + * same configuration used with {@link Ext.tip.ToolTip}. For example: + * + * tips: { + * trackMouse: true, + * width: 140, + * height: 28, + * renderer: function(storeItem, item) { + * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views'); + * } + * }, + */ + + /** + * @cfg {String} type + * The type of series. Set in subclasses. + */ + type: null, + + /** + * @cfg {String} title + * The human-readable name of the series. + */ + title: null, + + /** + * @cfg {Boolean} showInLegend + * Whether to show this series in the legend. + */ + showInLegend: true, + + /** + * @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. + */ + renderer: function(sprite, record, attributes, index, store) { + return attributes; + }, + + /** + * @cfg {Array} shadowAttributes + * An array with shadow attributes + */ + shadowAttributes: null, + + //@private triggerdrawlistener flag + triggerAfterDraw: false, + + /** + * @cfg {Object} listeners + * An (optional) object with event callbacks. All event callbacks get the target *item* as first parameter. The callback functions are: + * + *
    + *
  • itemmouseover
  • + *
  • itemmouseout
  • + *
  • itemmousedown
  • + *
  • itemmouseup
  • + *
+ */ + + constructor: function(config) { + var me = this; + if (config) { + Ext.apply(me, config); + } + + me.shadowGroups = []; + + me.mixins.labels.constructor.call(me, config); + me.mixins.highlights.constructor.call(me, config); + me.mixins.tips.constructor.call(me, config); + me.mixins.callouts.constructor.call(me, config); + + me.addEvents({ + scope: me, + itemmouseover: true, + itemmouseout: true, + itemmousedown: true, + itemmouseup: true, + mouseleave: true, + afterdraw: true, + + /** + * @event titlechange + * Fires when the series title is changed via {@link #setTitle}. + * @param {String} title The new title value + * @param {Number} index The index in the collection of titles + */ + titlechange: true + }); + + me.mixins.observable.constructor.call(me, config); + + me.on({ + scope: me, + itemmouseover: me.onItemMouseOver, + itemmouseout: me.onItemMouseOut, + mouseleave: me.onMouseLeave + }); + }, + + // @private set the bbox and clipBox for the series + setBBox: function(noGutter) { + var me = this, + chart = me.chart, + chartBBox = chart.chartBBox, + gutterX = noGutter ? 0 : chart.maxGutter[0], + gutterY = noGutter ? 0 : chart.maxGutter[1], + clipBox, bbox; + + clipBox = { + x: chartBBox.x, + y: chartBBox.y, + width: chartBBox.width, + height: chartBBox.height + }; + me.clipBox = clipBox; + + bbox = { + x: (clipBox.x + gutterX) - (chart.zoom.x * chart.zoom.width), + y: (clipBox.y + gutterY) - (chart.zoom.y * chart.zoom.height), + width: (clipBox.width - (gutterX * 2)) * chart.zoom.width, + height: (clipBox.height - (gutterY * 2)) * chart.zoom.height + }; + me.bbox = bbox; + }, + + // @private set the animation for the sprite + onAnimate: function(sprite, attr) { + var me = this; + sprite.stopAnimation(); + if (me.triggerAfterDraw) { + return sprite.animate(Ext.applyIf(attr, me.chart.animate)); + } else { + me.triggerAfterDraw = true; + return sprite.animate(Ext.apply(Ext.applyIf(attr, me.chart.animate), { + listeners: { + 'afteranimate': function() { + me.triggerAfterDraw = false; + me.fireEvent('afterrender'); + } + } + })); + } + }, + + // @private return the gutter. + getGutters: function() { + return [0, 0]; + }, + + // @private wrapper for the itemmouseover event. + onItemMouseOver: function(item) { + var me = this; + if (item.series === me) { + if (me.highlight) { + me.highlightItem(item); + } + if (me.tooltip) { + me.showTip(item); + } + } + }, + + // @private wrapper for the itemmouseout event. + onItemMouseOut: function(item) { + var me = this; + if (item.series === me) { + me.unHighlightItem(); + if (me.tooltip) { + me.hideTip(item); + } + } + }, + + // @private wrapper for the mouseleave event. + onMouseLeave: function() { + var me = this; + me.unHighlightItem(); + if (me.tooltip) { + me.hideTip(); + } + }, + + /** + * For a given x/y point relative to the Surface, find a corresponding item from this + * series, if any. + * @param {Number} x + * @param {Number} y + * @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: + *
    + *
  • {Ext.chart.series.Series} series - the Series object to which the item belongs
  • + *
  • {Object} value - the value(s) of the item's data point
  • + *
  • {Array} 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.
  • + *
  • {Ext.draw.Sprite} sprite - the item's rendering Sprite. + *
+ */ + getItemForPoint: function(x, y) { + //if there are no items to query just return null. + if (!this.items || !this.items.length || this.seriesIsHidden) { + return null; + } + var me = this, + items = me.items, + bbox = me.bbox, + item, i, ln; + // Check bounds + if (!Ext.draw.Draw.withinBox(x, y, bbox)) { + return null; + } + for (i = 0, ln = items.length; i < ln; i++) { + if (items[i] && this.isItemInPoint(x, y, items[i], i)) { + return items[i]; + } + } + + return null; + }, + + isItemInPoint: function(x, y, item, i) { + return false; + }, + + /** + * Hides all the elements in the series. + */ + hideAll: function() { + var me = this, + items = me.items, + item, len, i, sprite; + + me.seriesIsHidden = true; + me._prevShowMarkers = me.showMarkers; + + me.showMarkers = false; + //hide all labels + me.hideLabels(0); + //hide all sprites + for (i = 0, len = items.length; i < len; i++) { + item = items[i]; + sprite = item.sprite; + if (sprite) { + sprite.setAttributes({ + hidden: true + }, true); + } + } + }, + + /** + * Shows all the elements in the series. + */ + showAll: function() { + var me = this, + prevAnimate = me.chart.animate; + me.chart.animate = false; + me.seriesIsHidden = false; + me.showMarkers = me._prevShowMarkers; + me.drawSeries(); + me.chart.animate = prevAnimate; + }, + + /** + * Returns a string with the color to be used for the series legend item. + */ + getLegendColor: function(index) { + var me = this, fill, stroke; + if (me.seriesStyle) { + fill = me.seriesStyle.fill; + stroke = me.seriesStyle.stroke; + if (fill && fill != 'none') { + return fill; + } + return stroke; + } + return '#000'; + }, + + /** + * Checks whether the data field should be visible in the legend + * @private + * @param {Number} index The index of the current item + */ + visibleInLegend: function(index){ + var excludes = this.__excludes; + if (excludes) { + return !excludes[index]; + } + return !this.seriesIsHidden; + }, + + /** + * Changes the value of the {@link #title} for the series. + * Arguments can take two forms: + *
    + *
  • A single String value: this will be used as the new single title for the series (applies + * to series with only one yField)
  • + *
  • A numeric index and a String value: this will set the title for a single indexed yField.
  • + *
+ * @param {Number} index + * @param {String} title + */ + setTitle: function(index, title) { + var me = this, + oldTitle = me.title; + + if (Ext.isString(index)) { + title = index; + index = 0; + } + + if (Ext.isArray(oldTitle)) { + oldTitle[index] = title; + } else { + me.title = title; + } + + me.fireEvent('titlechange', title, index); + } +}); + +/** + * @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', { + + /* Begin Definitions */ + + extend: 'Ext.chart.series.Series', + + alternateClassName: ['Ext.chart.CartesianSeries', 'Ext.chart.CartesianChart'], + + /* End Definitions */ + + /** + * The field used to access the x axis value from the items from the data + * source. + * + * @cfg xField + * @type String + */ + xField: null, + + /** + * The field used to access the y-axis value from the items from the data + * source. + * + * @cfg yField + * @type String + */ + yField: null, + + /** + * Indicates which axis the series will bind to + * + * @property axis + * @type String + */ + axis: 'left' +}); + +/** + * @class Ext.chart.series.Area + * @extends Ext.chart.series.Cartesian + * +

+ Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information. + As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart + documentation for more information. A typical configuration object for the area series could be: +

+{@img Ext.chart.series.Area/Ext.chart.series.Area.png Ext.chart.series.Area chart series} +

+   var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        store: store,
+        axes: [{
+            type: 'Numeric',
+            grid: true,
+            position: 'left',
+            fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+            title: 'Sample Values',
+            grid: {
+                odd: {
+                    opacity: 1,
+                    fill: '#ddd',
+                    stroke: '#bbb',
+                    'stroke-width': 1
+                }
+            },
+            minimum: 0,
+            adjustMinimumByMajorUnit: 0
+        }, {
+            type: 'Category',
+            position: 'bottom',
+            fields: ['name'],
+            title: 'Sample Metrics',
+            grid: true,
+            label: {
+                rotate: {
+                    degrees: 315
+                }
+            }
+        }],
+        series: [{
+            type: 'area',
+            highlight: false,
+            axis: 'left',
+            xField: 'name',
+            yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+            style: {
+                opacity: 0.93
+            }
+        }]
+    });
+   
+ + +

+ In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover, + take the left axis to measure the data in the area series, set as xField (x values) the name field of each element in the store, + and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity + to the style object. +

+ + * @xtype area + * + */ +Ext.define('Ext.chart.series.Area', { + + /* Begin Definitions */ + + extend: 'Ext.chart.series.Cartesian', + + alias: 'series.area', + + requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'], + + /* End Definitions */ + + type: 'area', + + // @private Area charts are alyways stacked + stacked: true, + + /** + * @cfg {Object} style + * Append styling properties to this object for it to override theme properties. + */ + style: {}, + + constructor: function(config) { + this.callParent(arguments); + var me = this, + surface = me.chart.surface, + i, l; + Ext.apply(me, config, { + __excludes: [], + highlightCfg: { + lineWidth: 3, + stroke: '#55c', + opacity: 0.8, + color: '#f00' + } + }); + if (me.highlight) { + me.highlightSprite = surface.add({ + type: 'path', + path: ['M', 0, 0], + zIndex: 1000, + opacity: 0.3, + lineWidth: 5, + hidden: true, + stroke: '#444' + }); + } + me.group = surface.getGroup(me.seriesId); + }, + + // @private Shrinks dataSets down to a smaller size + shrink: function(xValues, yValues, size) { + var len = xValues.length, + ratio = Math.floor(len / size), + i, j, + xSum = 0, + yCompLen = this.areas.length, + ySum = [], + xRes = [], + yRes = []; + //initialize array + for (j = 0; j < yCompLen; ++j) { + ySum[j] = 0; + } + for (i = 0; i < len; ++i) { + xSum += xValues[i]; + for (j = 0; j < yCompLen; ++j) { + ySum[j] += yValues[i][j]; + } + if (i % ratio == 0) { + //push averages + xRes.push(xSum/ratio); + for (j = 0; j < yCompLen; ++j) { + ySum[j] /= ratio; + } + yRes.push(ySum); + //reset sum accumulators + xSum = 0; + for (j = 0, ySum = []; j < yCompLen; ++j) { + ySum[j] = 0; + } + } + } + return { + x: xRes, + y: yRes + }; + }, + + // @private Get chart and data boundaries + getBounds: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + areas = [].concat(me.yField), + areasLen = areas.length, + xValues = [], + yValues = [], + infinity = Infinity, + minX = infinity, + minY = infinity, + maxX = -infinity, + maxY = -infinity, + math = Math, + mmin = math.min, + mmax = math.max, + bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem; + + me.setBBox(); + bbox = me.bbox; + + // Run through the axis + if (me.axis) { + axis = chart.axes.get(me.axis); + if (axis) { + out = axis.calcEnds(); + minY = out.from || axis.prevMin; + maxY = mmax(out.to || axis.prevMax, 0); + } + } + + if (me.yField && !Ext.isNumber(minY)) { + axis = Ext.create('Ext.chart.axis.Axis', { + chart: chart, + fields: [].concat(me.yField) + }); + out = axis.calcEnds(); + minY = out.from || axis.prevMin; + maxY = mmax(out.to || axis.prevMax, 0); + } + + if (!Ext.isNumber(minY)) { + minY = 0; + } + if (!Ext.isNumber(maxY)) { + maxY = 0; + } + + store.each(function(record, i) { + xValue = record.get(me.xField); + yValue = []; + if (typeof xValue != 'number') { + xValue = i; + } + xValues.push(xValue); + acumY = 0; + for (areaIndex = 0; areaIndex < areasLen; areaIndex++) { + areaElem = record.get(areas[areaIndex]); + if (typeof areaElem == 'number') { + minY = mmin(minY, areaElem); + yValue.push(areaElem); + acumY += areaElem; + } + } + minX = mmin(minX, xValue); + maxX = mmax(maxX, xValue); + maxY = mmax(maxY, acumY); + yValues.push(yValue); + }, me); + + xScale = bbox.width / (maxX - minX); + yScale = bbox.height / (maxY - minY); + + ln = xValues.length; + if ((ln > bbox.width) && me.areas) { + sumValues = me.shrink(xValues, yValues, bbox.width); + xValues = sumValues.x; + yValues = sumValues.y; + } + + return { + bbox: bbox, + minX: minX, + minY: minY, + xValues: xValues, + yValues: yValues, + xScale: xScale, + yScale: yScale, + areasLen: areasLen + }; + }, + + // @private Build an array of paths for the chart + getPaths: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + first = true, + bounds = me.getBounds(), + bbox = bounds.bbox, + items = me.items = [], + componentPaths = [], + componentPath, + paths = [], + i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path; + + ln = bounds.xValues.length; + // Start the path + for (i = 0; i < ln; i++) { + xValue = bounds.xValues[i]; + yValue = bounds.yValues[i]; + x = bbox.x + (xValue - bounds.minX) * bounds.xScale; + acumY = 0; + for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) { + // Excluded series + if (me.__excludes[areaIndex]) { + continue; + } + if (!componentPaths[areaIndex]) { + componentPaths[areaIndex] = []; + } + areaElem = yValue[areaIndex]; + acumY += areaElem; + y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale; + if (!paths[areaIndex]) { + paths[areaIndex] = ['M', x, y]; + componentPaths[areaIndex].push(['L', x, y]); + } else { + paths[areaIndex].push('L', x, y); + componentPaths[areaIndex].push(['L', x, y]); + } + if (!items[areaIndex]) { + items[areaIndex] = { + pointsUp: [], + pointsDown: [], + series: me + }; + } + items[areaIndex].pointsUp.push([x, y]); + } + } + + // Close the paths + for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) { + // Excluded series + if (me.__excludes[areaIndex]) { + continue; + } + path = paths[areaIndex]; + // Close bottom path to the axis + if (areaIndex == 0 || first) { + first = false; + path.push('L', x, bbox.y + bbox.height, + 'L', bbox.x, bbox.y + bbox.height, + 'Z'); + } + // Close other paths to the one before them + else { + componentPath = componentPaths[prevAreaIndex]; + componentPath.reverse(); + path.push('L', x, componentPath[0][2]); + for (i = 0; i < ln; i++) { + path.push(componentPath[i][0], + componentPath[i][1], + componentPath[i][2]); + items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]]; + } + path.push('L', bbox.x, path[2], 'Z'); + } + prevAreaIndex = areaIndex; + } + return { + paths: paths, + areasLen: bounds.areasLen + }; + }, + + /** + * Draws the series for the current chart. + */ + drawSeries: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + surface = chart.surface, + animate = chart.animate, + group = me.group, + endLineStyle = Ext.apply(me.seriesStyle, me.style), + colorArrayStyle = me.colorArrayStyle, + colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, + areaIndex, areaElem, paths, path, rendererAttributes; + + me.unHighlightItem(); + me.cleanHighlights(); + + if (!store || !store.getCount()) { + return; + } + + paths = me.getPaths(); + + if (!me.areas) { + me.areas = []; + } + + for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) { + // Excluded series + if (me.__excludes[areaIndex]) { + continue; + } + if (!me.areas[areaIndex]) { + me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, { + type: 'path', + group: group, + // 'clip-rect': me.clipBox, + path: paths.paths[areaIndex], + stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength], + fill: colorArrayStyle[areaIndex % colorArrayLength] + }, endLineStyle || {})); + } + areaElem = me.areas[areaIndex]; + path = paths.paths[areaIndex]; + if (animate) { + //Add renderer to line. There is not a unique record associated with this. + rendererAttributes = me.renderer(areaElem, false, { + path: path, + // 'clip-rect': me.clipBox, + fill: colorArrayStyle[areaIndex % colorArrayLength], + stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength] + }, areaIndex, store); + //fill should not be used here but when drawing the special fill path object + me.animation = me.onAnimate(areaElem, { + to: rendererAttributes + }); + } else { + rendererAttributes = me.renderer(areaElem, false, { + path: path, + // 'clip-rect': me.clipBox, + hidden: false, + fill: colorArrayStyle[areaIndex % colorArrayLength], + stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength] + }, areaIndex, store); + me.areas[areaIndex].setAttributes(rendererAttributes, true); + } + } + me.renderLabels(); + me.renderCallouts(); + }, + + // @private + onAnimate: function(sprite, attr) { + sprite.show(); + return this.callParent(arguments); + }, + + // @private + onCreateLabel: function(storeItem, item, i, display) { + var me = this, + group = me.labelsGroup, + config = me.label, + bbox = me.bbox, + endLabelStyle = Ext.apply(config, me.seriesLabelStyle); + + return me.chart.surface.add(Ext.apply({ + 'type': 'text', + 'text-anchor': 'middle', + 'group': group, + 'x': item.point[0], + 'y': bbox.y + bbox.height / 2 + }, endLabelStyle || {})); + }, + + // @private + onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { + var me = this, + chart = me.chart, + resizing = chart.resizing, + config = me.label, + format = config.renderer, + field = config.field, + bbox = me.bbox, + x = item.point[0], + y = item.point[1], + bb, width, height; + + label.setAttributes({ + text: format(storeItem.get(field[index])), + hidden: true + }, true); + + bb = label.getBBox(); + width = bb.width / 2; + height = bb.height / 2; + + x = x - width < bbox.x? bbox.x + width : x; + x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x; + y = y - height < bbox.y? bbox.y + height : y; + y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y; + + if (me.chart.animate && !me.chart.resizing) { + label.show(true); + me.onAnimate(label, { + to: { + x: x, + y: y + } + }); + } else { + label.setAttributes({ + x: x, + y: y + }, true); + if (resizing) { + me.animation.on('afteranimate', function() { + label.show(true); + }); + } else { + label.show(true); + } + } + }, + + // @private + onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) { + var me = this, + chart = me.chart, + surface = chart.surface, + resizing = chart.resizing, + config = me.callouts, + items = me.items, + prev = (i == 0) ? false : items[i -1].point, + next = (i == items.length -1) ? false : items[i +1].point, + cur = item.point, + dir, norm, normal, a, aprev, anext, + bbox = callout.label.getBBox(), + offsetFromViz = 30, + offsetToSide = 10, + offsetBox = 3, + boxx, boxy, boxw, boxh, + p, clipRect = me.clipRect, + x, y; + + //get the right two points + if (!prev) { + prev = cur; + } + if (!next) { + next = cur; + } + a = (next[1] - prev[1]) / (next[0] - prev[0]); + aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]); + anext = (next[1] - cur[1]) / (next[0] - cur[0]); + + norm = Math.sqrt(1 + a * a); + dir = [1 / norm, a / norm]; + normal = [-dir[1], dir[0]]; + + //keep the label always on the outer part of the "elbow" + if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) { + normal[0] *= -1; + normal[1] *= -1; + } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) { + normal[0] *= -1; + normal[1] *= -1; + } + + //position + x = cur[0] + normal[0] * offsetFromViz; + y = cur[1] + normal[1] * offsetFromViz; + + //box position and dimensions + boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); + boxy = y - bbox.height /2 - offsetBox; + boxw = bbox.width + 2 * offsetBox; + boxh = bbox.height + 2 * offsetBox; + + //now check if we're out of bounds and invert the normal vector correspondingly + //this may add new overlaps between labels (but labels won't be out of bounds). + if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) { + normal[0] *= -1; + } + if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) { + normal[1] *= -1; + } + + //update positions + x = cur[0] + normal[0] * offsetFromViz; + y = cur[1] + normal[1] * offsetFromViz; + + //update box position and dimensions + boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); + boxy = y - bbox.height /2 - offsetBox; + boxw = bbox.width + 2 * offsetBox; + boxh = bbox.height + 2 * offsetBox; + + //set the line from the middle of the pie to the box. + callout.lines.setAttributes({ + path: ["M", cur[0], cur[1], "L", x, y, "Z"] + }, true); + //set box position + callout.box.setAttributes({ + x: boxx, + y: boxy, + width: boxw, + height: boxh + }, true); + //set text position + callout.label.setAttributes({ + x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)), + y: y + }, true); + for (p in callout) { + callout[p].show(true); + } + }, + + isItemInPoint: function(x, y, item, i) { + var me = this, + pointsUp = item.pointsUp, + pointsDown = item.pointsDown, + abs = Math.abs, + dist = Infinity, p, pln, point; + + for (p = 0, pln = pointsUp.length; p < pln; p++) { + point = [pointsUp[p][0], pointsUp[p][1]]; + if (dist > abs(x - point[0])) { + dist = abs(x - point[0]); + } else { + point = pointsUp[p -1]; + if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) { + item.storeIndex = p -1; + item.storeField = me.yField[i]; + item.storeItem = me.chart.store.getAt(p -1); + item._points = pointsDown.length? [point, pointsDown[p -1]] : [point]; + return true; + } else { + break; + } + } + } + return false; + }, + + /** + * Highlight this entire series. + * @param {Object} item Info about the item; same format as returned by #getItemForPoint. + */ + highlightSeries: function() { + var area, to, fillColor; + if (this._index !== undefined) { + area = this.areas[this._index]; + if (area.__highlightAnim) { + area.__highlightAnim.paused = true; + } + area.__highlighted = true; + area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1; + area.__prevFill = area.__prevFill || area.attr.fill; + area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth; + fillColor = Ext.draw.Color.fromString(area.__prevFill); + to = { + lineWidth: (area.__prevLineWidth || 0) + 2 + }; + if (fillColor) { + to.fill = fillColor.getLighter(0.2).toString(); + } + else { + to.opacity = Math.max(area.__prevOpacity - 0.3, 0); + } + if (this.chart.animate) { + area.__highlightAnim = Ext.create('Ext.fx.Anim', Ext.apply({ + target: area, + to: to + }, this.chart.animate)); + } + else { + area.setAttributes(to, true); + } + } + }, + + /** + * UnHighlight this entire series. + * @param {Object} item Info about the item; same format as returned by #getItemForPoint. + */ + unHighlightSeries: function() { + var area; + if (this._index !== undefined) { + area = this.areas[this._index]; + if (area.__highlightAnim) { + area.__highlightAnim.paused = true; + } + if (area.__highlighted) { + area.__highlighted = false; + area.__highlightAnim = Ext.create('Ext.fx.Anim', { + target: area, + to: { + fill: area.__prevFill, + opacity: area.__prevOpacity, + lineWidth: area.__prevLineWidth + } + }); + } + } + }, + + /** + * Highlight the specified item. If no item is provided the whole series will be highlighted. + * @param item {Object} Info about the item; same format as returned by #getItemForPoint + */ + highlightItem: function(item) { + var me = this, + points, path; + if (!item) { + this.highlightSeries(); + return; + } + points = item._points; + path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]] + : ['M', points[0][0], points[0][1], 'L', points[0][0], me.bbox.y + me.bbox.height]; + me.highlightSprite.setAttributes({ + path: path, + hidden: false + }, true); + }, + + /** + * un-highlights the specified item. If no item is provided it will un-highlight the entire series. + * @param item {Object} Info about the item; same format as returned by #getItemForPoint + */ + unHighlightItem: function(item) { + if (!item) { + this.unHighlightSeries(); + } + + if (this.highlightSprite) { + this.highlightSprite.hide(true); + } + }, + + // @private + hideAll: function() { + if (!isNaN(this._index)) { + this.__excludes[this._index] = true; + this.areas[this._index].hide(true); + this.drawSeries(); + } + }, + + // @private + showAll: function() { + if (!isNaN(this._index)) { + this.__excludes[this._index] = false; + this.areas[this._index].show(true); + this.drawSeries(); + } + }, + + /** + * Returns the color of the series (to be displayed as color for the series legend item). + * @param item {Object} Info about the item; same format as returned by #getItemForPoint + */ + getLegendColor: function(index) { + var me = this; + return me.colorArrayStyle[index % me.colorArrayStyle.length]; + } +}); + +/** + * Creates a Bar Chart. A Bar Chart is a useful visualization technique to display quantitative information for + * different categories that can show some progression (or regression) in the dataset. As with all other series, the Bar + * Series must be appended in the *series* Chart array configuration. See the Chart documentation for more information. + * A typical configuration object for the bar series could be: + * + * {@img Ext.chart.series.Bar/Ext.chart.series.Bar.png Ext.chart.series.Bar chart series} + * + * var store = Ext.create('Ext.data.JsonStore', { + * 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} + * ] + * }); + * + * Ext.create('Ext.chart.Chart', { + * renderTo: Ext.getBody(), + * width: 500, + * height: 300, + * animate: true, + * store: store, + * axes: [{ + * type: 'Numeric', + * position: 'bottom', + * fields: ['data1'], + * label: { + * renderer: Ext.util.Format.numberRenderer('0,0') + * }, + * title: 'Sample Values', + * grid: true, + * minimum: 0 + * }, { + * type: 'Category', + * position: 'left', + * fields: ['name'], + * title: 'Sample Metrics' + * }], + * series: [{ + * type: 'bar', + * axis: 'bottom', + * highlight: true, + * tips: { + * trackMouse: true, + * width: 140, + * height: 28, + * renderer: function(storeItem, item) { + * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views'); + * } + * }, + * label: { + * display: 'insideEnd', + * field: 'data1', + * renderer: Ext.util.Format.numberRenderer('0'), + * orientation: 'horizontal', + * color: '#333', + * 'text-anchor': 'middle' + * }, + * xField: 'name', + * yField: ['data1'] + * }] + * }); + * + * In this configuration we set `bar` as the series type, bind the values of the bar to the bottom axis and set the + * xField or category field to the `name` parameter of the store. We also set `highlight` to true which enables smooth + * animations when bars are hovered. We also set some configuration for the bar labels to be displayed inside the bar, + * to display the information found in the `data1` property of each element store, to render a formated text with the + * `Ext.util.Format` we pass in, to have an `horizontal` orientation (as opposed to a vertical one) and we also set + * other styles like `color`, `text-anchor`, etc. + */ +Ext.define('Ext.chart.series.Bar', { + + /* Begin Definitions */ + + extend: 'Ext.chart.series.Cartesian', + + alternateClassName: ['Ext.chart.BarSeries', 'Ext.chart.BarChart', 'Ext.chart.StackedBarChart'], + + requires: ['Ext.chart.axis.Axis', 'Ext.fx.Anim'], + + /* End Definitions */ + + type: 'bar', + + alias: 'series.bar', + /** + * @cfg {Boolean} column Whether to set the visualization as column chart or horizontal bar chart. + */ + column: false, + + /** + * @cfg style Style properties that will override the theming series styles. + */ + style: {}, + + /** + * @cfg {Number} gutter The gutter space between single bars, as a percentage of the bar width + */ + gutter: 38.2, + + /** + * @cfg {Number} groupGutter The gutter space between groups of bars, as a percentage of the bar width + */ + groupGutter: 38.2, + + /** + * @cfg {Number} xPadding Padding between the left/right axes and the bars + */ + xPadding: 0, + + /** + * @cfg {Number} yPadding Padding between the top/bottom axes and the bars + */ + yPadding: 10, + + constructor: function(config) { + this.callParent(arguments); + var me = this, + surface = me.chart.surface, + shadow = me.chart.shadow, + i, l; + Ext.apply(me, config, { + highlightCfg: { + lineWidth: 3, + stroke: '#55c', + opacity: 0.8, + color: '#f00' + }, + + shadowAttributes: [{ + "stroke-width": 6, + "stroke-opacity": 0.05, + stroke: 'rgb(200, 200, 200)', + translate: { + x: 1.2, + y: 1.2 + } + }, { + "stroke-width": 4, + "stroke-opacity": 0.1, + stroke: 'rgb(150, 150, 150)', + translate: { + x: 0.9, + y: 0.9 + } + }, { + "stroke-width": 2, + "stroke-opacity": 0.15, + stroke: 'rgb(100, 100, 100)', + translate: { + x: 0.6, + y: 0.6 + } + }] + }); + me.group = surface.getGroup(me.seriesId + '-bars'); + if (shadow) { + for (i = 0, l = me.shadowAttributes.length; i < l; i++) { + me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); + } + } + }, + + // @private sets the bar girth. + getBarGirth: function() { + var me = this, + store = me.chart.store, + column = me.column, + ln = store.getCount(), + gutter = me.gutter / 100; + + return (me.chart.chartBBox[column ? 'width' : 'height'] - me[column ? 'xPadding' : 'yPadding'] * 2) / (ln * (gutter + 1) - gutter); + }, + + // @private returns the gutters. + getGutters: function() { + var me = this, + column = me.column, + gutter = Math.ceil(me[column ? 'xPadding' : 'yPadding'] + me.getBarGirth() / 2); + return me.column ? [gutter, 0] : [0, gutter]; + }, + + // @private Get chart and data boundaries + getBounds: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + bars = [].concat(me.yField), + barsLen = bars.length, + groupBarsLen = barsLen, + groupGutter = me.groupGutter / 100, + column = me.column, + xPadding = me.xPadding, + yPadding = me.yPadding, + stacked = me.stacked, + barWidth = me.getBarGirth(), + math = Math, + mmax = math.max, + mabs = math.abs, + groupBarWidth, bbox, minY, maxY, axis, out, + scale, zero, total, rec, j, plus, minus; + + me.setBBox(true); + bbox = me.bbox; + + //Skip excluded series + if (me.__excludes) { + for (j = 0, total = me.__excludes.length; j < total; j++) { + if (me.__excludes[j]) { + groupBarsLen--; + } + } + } + + if (me.axis) { + axis = chart.axes.get(me.axis); + if (axis) { + out = axis.calcEnds(); + minY = out.from || axis.prevMin; + maxY = mmax(out.to || axis.prevMax, 0); + } + } + + if (me.yField && !Ext.isNumber(minY)) { + axis = Ext.create('Ext.chart.axis.Axis', { + chart: chart, + fields: [].concat(me.yField) + }); + out = axis.calcEnds(); + minY = out.from || axis.prevMin; + maxY = mmax(out.to || axis.prevMax, 0); + } + + if (!Ext.isNumber(minY)) { + minY = 0; + } + if (!Ext.isNumber(maxY)) { + maxY = 0; + } + scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (maxY - minY); + groupBarWidth = barWidth / ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter); + zero = (column) ? bbox.y + bbox.height - yPadding : bbox.x + xPadding; + + if (stacked) { + total = [[], []]; + store.each(function(record, i) { + total[0][i] = total[0][i] || 0; + total[1][i] = total[1][i] || 0; + for (j = 0; j < barsLen; j++) { + if (me.__excludes && me.__excludes[j]) { + continue; + } + rec = record.get(bars[j]); + total[+(rec > 0)][i] += mabs(rec); + } + }); + total[+(maxY > 0)].push(mabs(maxY)); + total[+(minY > 0)].push(mabs(minY)); + minus = mmax.apply(math, total[0]); + plus = mmax.apply(math, total[1]); + scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (plus + minus); + zero = zero + minus * scale * (column ? -1 : 1); + } + else if (minY / maxY < 0) { + zero = zero - minY * scale * (column ? -1 : 1); + } + return { + bars: bars, + bbox: bbox, + barsLen: barsLen, + groupBarsLen: groupBarsLen, + barWidth: barWidth, + groupBarWidth: groupBarWidth, + scale: scale, + zero: zero, + xPadding: xPadding, + yPadding: yPadding, + signed: minY / maxY < 0, + minY: minY, + maxY: maxY + }; + }, + + // @private Build an array of paths for the chart + getPaths: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + bounds = me.bounds = me.getBounds(), + items = me.items = [], + gutter = me.gutter / 100, + groupGutter = me.groupGutter / 100, + animate = chart.animate, + column = me.column, + group = me.group, + enableShadows = chart.shadow, + shadowGroups = me.shadowGroups, + shadowAttributes = me.shadowAttributes, + shadowGroupsLn = shadowGroups.length, + bbox = bounds.bbox, + xPadding = me.xPadding, + yPadding = me.yPadding, + stacked = me.stacked, + barsLen = bounds.barsLen, + colors = me.colorArrayStyle, + colorLength = colors && colors.length || 0, + math = Math, + mmax = math.max, + mmin = math.min, + mabs = math.abs, + j, yValue, height, totalDim, totalNegDim, bottom, top, hasShadow, barAttr, attrs, counter, + shadowIndex, shadow, sprite, offset, floorY; + + store.each(function(record, i, total) { + bottom = bounds.zero; + top = bounds.zero; + totalDim = 0; + totalNegDim = 0; + hasShadow = false; + for (j = 0, counter = 0; j < barsLen; j++) { + // Excluded series + if (me.__excludes && me.__excludes[j]) { + continue; + } + yValue = record.get(bounds.bars[j]); + height = Math.round((yValue - ((bounds.minY < 0) ? 0 : bounds.minY)) * bounds.scale); + barAttr = { + fill: colors[(barsLen > 1 ? j : 0) % colorLength] + }; + if (column) { + Ext.apply(barAttr, { + height: height, + width: mmax(bounds.groupBarWidth, 0), + x: (bbox.x + xPadding + i * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked), + y: bottom - height + }); + } + else { + // draw in reverse order + offset = (total - 1) - i; + Ext.apply(barAttr, { + height: mmax(bounds.groupBarWidth, 0), + width: height + (bottom == bounds.zero), + x: bottom + (bottom != bounds.zero), + y: (bbox.y + yPadding + offset * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked + 1) + }); + } + if (height < 0) { + if (column) { + barAttr.y = top; + barAttr.height = mabs(height); + } else { + barAttr.x = top + height; + barAttr.width = mabs(height); + } + } + if (stacked) { + if (height < 0) { + top += height * (column ? -1 : 1); + } else { + bottom += height * (column ? -1 : 1); + } + totalDim += mabs(height); + if (height < 0) { + totalNegDim += mabs(height); + } + } + barAttr.x = Math.floor(barAttr.x) + 1; + floorY = Math.floor(barAttr.y); + if (!Ext.isIE9 && barAttr.y > floorY) { + floorY--; + } + barAttr.y = floorY; + barAttr.width = Math.floor(barAttr.width); + barAttr.height = Math.floor(barAttr.height); + items.push({ + series: me, + storeItem: record, + value: [record.get(me.xField), yValue], + attr: barAttr, + point: column ? [barAttr.x + barAttr.width / 2, yValue >= 0 ? barAttr.y : barAttr.y + barAttr.height] : + [yValue >= 0 ? barAttr.x + barAttr.width : barAttr.x, barAttr.y + barAttr.height / 2] + }); + // When resizing, reset before animating + if (animate && chart.resizing) { + attrs = column ? { + x: barAttr.x, + y: bounds.zero, + width: barAttr.width, + height: 0 + } : { + x: bounds.zero, + y: barAttr.y, + width: 0, + height: barAttr.height + }; + if (enableShadows && (stacked && !hasShadow || !stacked)) { + hasShadow = true; + //update shadows + for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { + shadow = shadowGroups[shadowIndex].getAt(stacked ? i : (i * barsLen + j)); + if (shadow) { + shadow.setAttributes(attrs, true); + } + } + } + //update sprite position and width/height + sprite = group.getAt(i * barsLen + j); + if (sprite) { + sprite.setAttributes(attrs, true); + } + } + counter++; + } + if (stacked && items.length) { + items[i * counter].totalDim = totalDim; + items[i * counter].totalNegDim = totalNegDim; + } + }, me); + }, + + // @private render/setAttributes on the shadows + renderShadows: function(i, barAttr, baseAttrs, bounds) { + var me = this, + chart = me.chart, + surface = chart.surface, + animate = chart.animate, + stacked = me.stacked, + shadowGroups = me.shadowGroups, + shadowAttributes = me.shadowAttributes, + shadowGroupsLn = shadowGroups.length, + store = chart.substore || chart.store, + column = me.column, + items = me.items, + shadows = [], + zero = bounds.zero, + shadowIndex, shadowBarAttr, shadow, totalDim, totalNegDim, j, rendererAttributes; + + if ((stacked && (i % bounds.groupBarsLen === 0)) || !stacked) { + j = i / bounds.groupBarsLen; + //create shadows + for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { + shadowBarAttr = Ext.apply({}, shadowAttributes[shadowIndex]); + shadow = shadowGroups[shadowIndex].getAt(stacked ? j : i); + Ext.copyTo(shadowBarAttr, barAttr, 'x,y,width,height'); + if (!shadow) { + shadow = surface.add(Ext.apply({ + type: 'rect', + group: shadowGroups[shadowIndex] + }, Ext.apply({}, baseAttrs, shadowBarAttr))); + } + if (stacked) { + totalDim = items[i].totalDim; + totalNegDim = items[i].totalNegDim; + if (column) { + shadowBarAttr.y = zero - totalNegDim; + shadowBarAttr.height = totalDim; + } + else { + shadowBarAttr.x = zero - totalNegDim; + shadowBarAttr.width = totalDim; + } + } + if (animate) { + if (!stacked) { + rendererAttributes = me.renderer(shadow, store.getAt(j), shadowBarAttr, i, store); + me.onAnimate(shadow, { to: rendererAttributes }); + } + else { + rendererAttributes = me.renderer(shadow, store.getAt(j), Ext.apply(shadowBarAttr, { hidden: true }), i, store); + shadow.setAttributes(rendererAttributes, true); + } + } + else { + rendererAttributes = me.renderer(shadow, store.getAt(j), Ext.apply(shadowBarAttr, { hidden: false }), i, store); + shadow.setAttributes(rendererAttributes, true); + } + shadows.push(shadow); + } + } + return shadows; + }, + + /** + * Draws the series for the current chart. + */ + drawSeries: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + surface = chart.surface, + animate = chart.animate, + stacked = me.stacked, + column = me.column, + enableShadows = chart.shadow, + shadowGroups = me.shadowGroups, + shadowGroupsLn = shadowGroups.length, + group = me.group, + seriesStyle = me.seriesStyle, + items, ln, i, j, baseAttrs, sprite, rendererAttributes, shadowIndex, shadowGroup, + bounds, endSeriesStyle, barAttr, attrs, anim; + + if (!store || !store.getCount()) { + return; + } + + //fill colors are taken from the colors array. + delete seriesStyle.fill; + endSeriesStyle = Ext.apply(seriesStyle, this.style); + me.unHighlightItem(); + me.cleanHighlights(); + + me.getPaths(); + bounds = me.bounds; + items = me.items; + + baseAttrs = column ? { + y: bounds.zero, + height: 0 + } : { + x: bounds.zero, + width: 0 + }; + ln = items.length; + // Create new or reuse sprites and animate/display + for (i = 0; i < ln; i++) { + sprite = group.getAt(i); + barAttr = items[i].attr; + + if (enableShadows) { + items[i].shadows = me.renderShadows(i, barAttr, baseAttrs, bounds); + } + + // Create a new sprite if needed (no height) + if (!sprite) { + attrs = Ext.apply({}, baseAttrs, barAttr); + attrs = Ext.apply(attrs, endSeriesStyle || {}); + sprite = surface.add(Ext.apply({}, { + type: 'rect', + group: group + }, attrs)); + } + if (animate) { + rendererAttributes = me.renderer(sprite, store.getAt(i), barAttr, i, store); + sprite._to = rendererAttributes; + anim = me.onAnimate(sprite, { to: Ext.apply(rendererAttributes, endSeriesStyle) }); + if (enableShadows && stacked && (i % bounds.barsLen === 0)) { + j = i / bounds.barsLen; + for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { + anim.on('afteranimate', function() { + this.show(true); + }, shadowGroups[shadowIndex].getAt(j)); + } + } + } + else { + rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(barAttr, { hidden: false }), i, store); + sprite.setAttributes(Ext.apply(rendererAttributes, endSeriesStyle), true); + } + items[i].sprite = sprite; + } + + // Hide unused sprites + ln = group.getCount(); + for (j = i; j < ln; j++) { + group.getAt(j).hide(true); + } + // Hide unused shadows + if (enableShadows) { + for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { + shadowGroup = shadowGroups[shadowIndex]; + ln = shadowGroup.getCount(); + for (j = i; j < ln; j++) { + shadowGroup.getAt(j).hide(true); + } + } + } + me.renderLabels(); + }, + + // @private handled when creating a label. + onCreateLabel: function(storeItem, item, i, display) { + var me = this, + surface = me.chart.surface, + group = me.labelsGroup, + config = me.label, + endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}), + sprite; + return surface.add(Ext.apply({ + type: 'text', + group: group + }, endLabelStyle || {})); + }, + + // @private callback used when placing a label. + onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { + // Determine the label's final position. Starts with the configured preferred value but + // may get flipped from inside to outside or vice-versa depending on space. + var me = this, + opt = me.bounds, + groupBarWidth = opt.groupBarWidth, + column = me.column, + chart = me.chart, + chartBBox = chart.chartBBox, + resizing = chart.resizing, + xValue = item.value[0], + yValue = item.value[1], + attr = item.attr, + config = me.label, + rotate = config.orientation == 'vertical', + field = [].concat(config.field), + format = config.renderer, + text = format(storeItem.get(field[index])), + size = me.getLabelSize(text), + width = size.width, + height = size.height, + zero = opt.zero, + outside = 'outside', + insideStart = 'insideStart', + insideEnd = 'insideEnd', + offsetX = 10, + offsetY = 6, + signed = opt.signed, + x, y, finalAttr; + + label.setAttributes({ + text: text + }); + + if (column) { + if (display == outside) { + if (height + offsetY + attr.height > (yValue >= 0 ? zero - chartBBox.y : chartBBox.y + chartBBox.height - zero)) { + display = insideEnd; + } + } else { + if (height + offsetY > attr.height) { + display = outside; + } + } + x = attr.x + groupBarWidth / 2; + y = display == insideStart ? + (zero + ((height / 2 + 3) * (yValue >= 0 ? -1 : 1))) : + (yValue >= 0 ? (attr.y + ((height / 2 + 3) * (display == outside ? -1 : 1))) : + (attr.y + attr.height + ((height / 2 + 3) * (display === outside ? 1 : -1)))); + } + else { + if (display == outside) { + if (width + offsetX + attr.width > (yValue >= 0 ? chartBBox.x + chartBBox.width - zero : zero - chartBBox.x)) { + display = insideEnd; + } + } + else { + if (width + offsetX > attr.width) { + display = outside; + } + } + x = display == insideStart ? + (zero + ((width / 2 + 5) * (yValue >= 0 ? 1 : -1))) : + (yValue >= 0 ? (attr.x + attr.width + ((width / 2 + 5) * (display === outside ? 1 : -1))) : + (attr.x + ((width / 2 + 5) * (display === outside ? -1 : 1)))); + y = attr.y + groupBarWidth / 2; + } + //set position + finalAttr = { + x: x, + y: y + }; + //rotate + if (rotate) { + finalAttr.rotate = { + x: x, + y: y, + degrees: 270 + }; + } + //check for resizing + if (animate && resizing) { + if (column) { + x = attr.x + attr.width / 2; + y = zero; + } else { + x = zero; + y = attr.y + attr.height / 2; + } + label.setAttributes({ + x: x, + y: y + }, true); + if (rotate) { + label.setAttributes({ + rotate: { + x: x, + y: y, + degrees: 270 + } + }, true); + } + } + //handle animation + if (animate) { + me.onAnimate(label, { to: finalAttr }); + } + else { + label.setAttributes(Ext.apply(finalAttr, { + hidden: false + }), true); + } + }, + + /* @private + * Gets the dimensions of a given bar label. Uses a single hidden sprite to avoid + * changing visible sprites. + * @param value + */ + getLabelSize: function(value) { + var tester = this.testerLabel, + config = this.label, + endLabelStyle = Ext.apply({}, config, this.seriesLabelStyle || {}), + rotated = config.orientation === 'vertical', + bbox, w, h, + undef; + if (!tester) { + tester = this.testerLabel = this.chart.surface.add(Ext.apply({ + type: 'text', + opacity: 0 + }, endLabelStyle)); + } + tester.setAttributes({ + text: value + }, true); + + // Flip the width/height if rotated, as getBBox returns the pre-rotated dimensions + bbox = tester.getBBox(); + w = bbox.width; + h = bbox.height; + return { + width: rotated ? h : w, + height: rotated ? w : h + }; + }, + + // @private used to animate label, markers and other sprites. + onAnimate: function(sprite, attr) { + sprite.show(); + return this.callParent(arguments); + }, + + isItemInPoint: function(x, y, item) { + var bbox = item.sprite.getBBox(); + return bbox.x <= x && bbox.y <= y + && (bbox.x + bbox.width) >= x + && (bbox.y + bbox.height) >= y; + }, + + // @private hide all markers + hideAll: function() { + var axes = this.chart.axes; + if (!isNaN(this._index)) { + if (!this.__excludes) { + this.__excludes = []; + } + this.__excludes[this._index] = true; + this.drawSeries(); + axes.each(function(axis) { + axis.drawAxis(); + }); + } + }, + + // @private show all markers + showAll: function() { + var axes = this.chart.axes; + if (!isNaN(this._index)) { + if (!this.__excludes) { + this.__excludes = []; + } + this.__excludes[this._index] = false; + this.drawSeries(); + axes.each(function(axis) { + axis.drawAxis(); + }); + } + }, + + /** + * Returns a string with the color to be used for the series legend item. + * @param index + */ + getLegendColor: function(index) { + var me = this; + return me.colorArrayStyle[index % me.colorArrayStyle.length]; + } +}); +/** + * @class Ext.chart.series.Column + * @extends Ext.chart.series.Bar + * +

+ Creates a Column Chart. Much of the methods are inherited from Bar. A Column Chart is a useful visualization technique to display quantitative information for different + categories that can show some progression (or regression) in the data set. + As with all other series, the Column Series must be appended in the *series* Chart array configuration. See the Chart + documentation for more information. A typical configuration object for the column series could be: +

+{@img Ext.chart.series.Column/Ext.chart.series.Column.png Ext.chart.series.Column chart series +

+    var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        animate: true,
+        store: store,
+        axes: [{
+            type: 'Numeric',
+            position: 'bottom',
+            fields: ['data1'],
+            label: {
+                renderer: Ext.util.Format.numberRenderer('0,0')
+            },
+            title: 'Sample Values',
+            grid: true,
+            minimum: 0
+        }, {
+            type: 'Category',
+            position: 'left',
+            fields: ['name'],
+            title: 'Sample Metrics'
+        }],
+            axes: [{
+                type: 'Numeric',
+                position: 'left',
+                fields: ['data1'],
+                label: {
+                    renderer: Ext.util.Format.numberRenderer('0,0')
+                },
+                title: 'Sample Values',
+                grid: true,
+                minimum: 0
+            }, {
+                type: 'Category',
+                position: 'bottom',
+                fields: ['name'],
+                title: 'Sample Metrics'
+            }],
+            series: [{
+                type: 'column',
+                axis: 'left',
+                highlight: true,
+                tips: {
+                  trackMouse: true,
+                  width: 140,
+                  height: 28,
+                  renderer: function(storeItem, item) {
+                    this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' $');
+                  }
+                },
+                label: {
+                  display: 'insideEnd',
+                  'text-anchor': 'middle',
+                    field: 'data1',
+                    renderer: Ext.util.Format.numberRenderer('0'),
+                    orientation: 'vertical',
+                    color: '#333'
+                },
+                xField: 'name',
+                yField: 'data1'
+            }]
+    });
+   
+ +

+ In this configuration we set `column` as the series type, bind the values of the bars to the bottom axis, set `highlight` to true so that bars are smoothly highlighted + when hovered and bind the `xField` or category field to the data store `name` property and the `yField` as the data1 property of a store element. +

+ */ + +Ext.define('Ext.chart.series.Column', { + + /* Begin Definitions */ + + alternateClassName: ['Ext.chart.ColumnSeries', 'Ext.chart.ColumnChart', 'Ext.chart.StackedColumnChart'], + + extend: 'Ext.chart.series.Bar', + + /* End Definitions */ + + type: 'column', + alias: 'series.column', + + column: true, + + /** + * @cfg {Number} xPadding + * Padding between the left/right axes and the bars + */ + xPadding: 10, + + /** + * @cfg {Number} yPadding + * Padding between the top/bottom axes and the bars + */ + yPadding: 0 +}); +/** + * @class Ext.chart.series.Gauge + * @extends Ext.chart.series.Series + * + * Creates a Gauge Chart. Gauge Charts are used to show progress in a certain variable. There are two ways of using the Gauge chart. + * One is setting a store element into the Gauge and selecting the field to be used from that store. Another one is instanciating the + * visualization and using the `setValue` method to adjust the value you want. + * + * A chart/series configuration for the Gauge visualization could look like this: + * + * { + * xtype: 'chart', + * store: store, + * axes: [{ + * type: 'gauge', + * position: 'gauge', + * minimum: 0, + * maximum: 100, + * steps: 10, + * margin: -10 + * }], + * series: [{ + * type: 'gauge', + * field: 'data1', + * donut: false, + * colorSet: ['#F49D10', '#ddd'] + * }] + * } + * + * In this configuration we create a special Gauge axis to be used with the gauge visualization (describing half-circle markers), and also we're + * setting a maximum, minimum and steps configuration options into the axis. The Gauge series configuration contains the store field to be bound to + * the visual display and the color set to be used with the visualization. + * + * @xtype gauge + */ +Ext.define('Ext.chart.series.Gauge', { + + /* Begin Definitions */ + + extend: 'Ext.chart.series.Series', + + /* End Definitions */ + + type: "gauge", + alias: 'series.gauge', + + rad: Math.PI / 180, + + /** + * @cfg {Number} highlightDuration + * The duration for the pie slice highlight effect. + */ + highlightDuration: 150, + + /** + * @cfg {String} angleField + * The store record field name to be used for the pie angles. + * The values bound to this field name must be positive real numbers. + * This parameter is required. + */ + angleField: false, + + /** + * @cfg {Boolean} needle + * Use the Gauge Series as an area series or add a needle to it. Default's false. + */ + needle: false, + + /** + * @cfg {Boolean|Number} donut + * Use the entire disk or just a fraction of it for the gauge. Default's false. + */ + donut: false, + + /** + * @cfg {Boolean} showInLegend + * Whether to add the pie chart elements as legend items. Default's false. + */ + showInLegend: false, + + /** + * @cfg {Object} style + * An object containing styles for overriding series styles from Theming. + */ + style: {}, + + constructor: function(config) { + this.callParent(arguments); + var me = this, + chart = me.chart, + surface = chart.surface, + store = chart.store, + shadow = chart.shadow, i, l, cfg; + Ext.apply(me, config, { + shadowAttributes: [{ + "stroke-width": 6, + "stroke-opacity": 1, + stroke: 'rgb(200, 200, 200)', + translate: { + x: 1.2, + y: 2 + } + }, + { + "stroke-width": 4, + "stroke-opacity": 1, + stroke: 'rgb(150, 150, 150)', + translate: { + x: 0.9, + y: 1.5 + } + }, + { + "stroke-width": 2, + "stroke-opacity": 1, + stroke: 'rgb(100, 100, 100)', + translate: { + x: 0.6, + y: 1 + } + }] + }); + me.group = surface.getGroup(me.seriesId); + if (shadow) { + for (i = 0, l = me.shadowAttributes.length; i < l; i++) { + me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); + } + } + surface.customAttributes.segment = function(opt) { + return me.getSegment(opt); + }; + }, + + //@private updates some onbefore render parameters. + initialize: function() { + var me = this, + store = me.chart.substore || me.chart.store; + //Add yFields to be used in Legend.js + me.yField = []; + if (me.label.field) { + store.each(function(rec) { + me.yField.push(rec.get(me.label.field)); + }); + } + }, + + // @private returns an object with properties for a Slice + getSegment: function(opt) { + var me = this, + rad = me.rad, + cos = Math.cos, + sin = Math.sin, + abs = Math.abs, + x = me.centerX, + y = me.centerY, + x1 = 0, x2 = 0, x3 = 0, x4 = 0, + y1 = 0, y2 = 0, y3 = 0, y4 = 0, + delta = 1e-2, + r = opt.endRho - opt.startRho, + startAngle = opt.startAngle, + endAngle = opt.endAngle, + midAngle = (startAngle + endAngle) / 2 * rad, + margin = opt.margin || 0, + flag = abs(endAngle - startAngle) > 180, + a1 = Math.min(startAngle, endAngle) * rad, + a2 = Math.max(startAngle, endAngle) * rad, + singleSlice = false; + + x += margin * cos(midAngle); + y += margin * sin(midAngle); + + x1 = x + opt.startRho * cos(a1); + y1 = y + opt.startRho * sin(a1); + + x2 = x + opt.endRho * cos(a1); + y2 = y + opt.endRho * sin(a1); + + x3 = x + opt.startRho * cos(a2); + y3 = y + opt.startRho * sin(a2); + + x4 = x + opt.endRho * cos(a2); + y4 = y + opt.endRho * sin(a2); + + if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) { + singleSlice = true; + } + //Solves mysterious clipping bug with IE + if (singleSlice) { + return { + path: [ + ["M", x1, y1], + ["L", x2, y2], + ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4], + ["Z"]] + }; + } else { + return { + path: [ + ["M", x1, y1], + ["L", x2, y2], + ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4], + ["L", x3, y3], + ["A", opt.startRho, opt.startRho, 0, +flag, 0, x1, y1], + ["Z"]] + }; + } + }, + + // @private utility function to calculate the middle point of a pie slice. + calcMiddle: function(item) { + var me = this, + rad = me.rad, + slice = item.slice, + x = me.centerX, + y = me.centerY, + startAngle = slice.startAngle, + endAngle = slice.endAngle, + radius = Math.max(('rho' in slice) ? slice.rho: me.radius, me.label.minMargin), + donut = +me.donut, + a1 = Math.min(startAngle, endAngle) * rad, + a2 = Math.max(startAngle, endAngle) * rad, + midAngle = -(a1 + (a2 - a1) / 2), + xm = x + (item.endRho + item.startRho) / 2 * Math.cos(midAngle), + ym = y - (item.endRho + item.startRho) / 2 * Math.sin(midAngle); + + item.middle = { + x: xm, + y: ym + }; + }, + + /** + * Draws the series for the current chart. + */ + drawSeries: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + group = me.group, + animate = me.chart.animate, + axis = me.chart.axes.get(0), + minimum = axis && axis.minimum || me.minimum || 0, + maximum = axis && axis.maximum || me.maximum || 0, + field = me.angleField || me.field || me.xField, + surface = chart.surface, + chartBBox = chart.chartBBox, + rad = me.rad, + donut = +me.donut, + values = {}, + items = [], + seriesStyle = me.seriesStyle, + seriesLabelStyle = me.seriesLabelStyle, + colorArrayStyle = me.colorArrayStyle, + colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, + gutterX = chart.maxGutter[0], + gutterY = chart.maxGutter[1], + cos = Math.cos, + sin = Math.sin, + rendererAttributes, centerX, centerY, slice, slices, sprite, value, + item, ln, record, i, j, startAngle, endAngle, middleAngle, sliceLength, path, + p, spriteOptions, bbox, splitAngle, sliceA, sliceB; + + Ext.apply(seriesStyle, me.style || {}); + + me.setBBox(); + bbox = me.bbox; + + //override theme colors + if (me.colorSet) { + colorArrayStyle = me.colorSet; + colorArrayLength = colorArrayStyle.length; + } + + //if not store or store is empty then there's nothing to draw + if (!store || !store.getCount()) { + return; + } + + centerX = me.centerX = chartBBox.x + (chartBBox.width / 2); + centerY = me.centerY = chartBBox.y + chartBBox.height; + me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y); + me.slices = slices = []; + me.items = items = []; + + if (!me.value) { + record = store.getAt(0); + me.value = record.get(field); + } + + value = me.value; + if (me.needle) { + sliceA = { + series: me, + value: value, + startAngle: -180, + endAngle: 0, + rho: me.radius + }; + splitAngle = -180 * (1 - (value - minimum) / (maximum - minimum)); + slices.push(sliceA); + } else { + splitAngle = -180 * (1 - (value - minimum) / (maximum - minimum)); + sliceA = { + series: me, + value: value, + startAngle: -180, + endAngle: splitAngle, + rho: me.radius + }; + sliceB = { + series: me, + value: me.maximum - value, + startAngle: splitAngle, + endAngle: 0, + rho: me.radius + }; + slices.push(sliceA, sliceB); + } + + //do pie slices after. + for (i = 0, ln = slices.length; i < ln; i++) { + slice = slices[i]; + sprite = group.getAt(i); + //set pie slice properties + rendererAttributes = Ext.apply({ + segment: { + startAngle: slice.startAngle, + endAngle: slice.endAngle, + margin: 0, + rho: slice.rho, + startRho: slice.rho * +donut / 100, + endRho: slice.rho + } + }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[i % colorArrayLength] } || {})); + + item = Ext.apply({}, + rendererAttributes.segment, { + slice: slice, + series: me, + storeItem: record, + index: i + }); + items[i] = item; + // Create a new sprite if needed (no height) + if (!sprite) { + spriteOptions = Ext.apply({ + type: "path", + group: group + }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[i % colorArrayLength] } || {})); + sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes)); + } + slice.sprite = slice.sprite || []; + item.sprite = sprite; + slice.sprite.push(sprite); + if (animate) { + rendererAttributes = me.renderer(sprite, record, rendererAttributes, i, store); + sprite._to = rendererAttributes; + me.onAnimate(sprite, { + to: rendererAttributes + }); + } else { + rendererAttributes = me.renderer(sprite, record, Ext.apply(rendererAttributes, { + hidden: false + }), i, store); + sprite.setAttributes(rendererAttributes, true); + } + } + + if (me.needle) { + splitAngle = splitAngle * Math.PI / 180; + + if (!me.needleSprite) { + me.needleSprite = me.chart.surface.add({ + type: 'path', + path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle), + centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)), + 'L', centerX + me.radius * cos(splitAngle), + centerY + -Math.abs(me.radius * sin(splitAngle))], + 'stroke-width': 4, + 'stroke': '#222' + }); + } else { + if (animate) { + me.onAnimate(me.needleSprite, { + to: { + path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle), + centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)), + 'L', centerX + me.radius * cos(splitAngle), + centerY + -Math.abs(me.radius * sin(splitAngle))] + } + }); + } else { + me.needleSprite.setAttributes({ + type: 'path', + path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle), + centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)), + 'L', centerX + me.radius * cos(splitAngle), + centerY + -Math.abs(me.radius * sin(splitAngle))] + }); + } + } + me.needleSprite.setAttributes({ + hidden: false + }, true); + } + + delete me.value; + }, + + /** + * Sets the Gauge chart to the current specified value. + */ + setValue: function (value) { + this.value = value; + this.drawSeries(); + }, + + // @private callback for when creating a label sprite. + onCreateLabel: function(storeItem, item, i, display) {}, + + // @private callback for when placing a label sprite. + onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {}, + + // @private callback for when placing a callout. + onPlaceCallout: function() {}, + + // @private handles sprite animation for the series. + onAnimate: function(sprite, attr) { + sprite.show(); + return this.callParent(arguments); + }, + + isItemInPoint: function(x, y, item, i) { + return false; + }, + + // @private shows all elements in the series. + showAll: function() { + if (!isNaN(this._index)) { + this.__excludes[this._index] = false; + this.drawSeries(); + } + }, + + /** + * Returns the color of the series (to be displayed as color for the series legend item). + * @param item {Object} Info about the item; same format as returned by #getItemForPoint + */ + getLegendColor: function(index) { + var me = this; + return me.colorArrayStyle[index % me.colorArrayStyle.length]; + } +}); + + +/** + * @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: +

+{@img Ext.chart.series.Line/Ext.chart.series.Line.png Ext.chart.series.Line chart series} +

+    var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        animate: true,
+        store: store,
+        axes: [{
+            type: 'Numeric',
+            position: 'bottom',
+            fields: ['data1'],
+            label: {
+                renderer: Ext.util.Format.numberRenderer('0,0')
+            },
+            title: 'Sample Values',
+            grid: true,
+            minimum: 0
+        }, {
+            type: 'Category',
+            position: 'left',
+            fields: ['name'],
+            title: 'Sample Metrics'
+        }],
+        series: [{
+            type: 'line',
+            highlight: {
+                size: 7,
+                radius: 7
+            },
+            axis: 'left',
+            xField: 'name',
+            yField: 'data1',
+            markerCfg: {
+                type: 'cross',
+                size: 4,
+                radius: 4,
+                'stroke-width': 0
+            }
+        }, {
+            type: 'line',
+            highlight: {
+                size: 7,
+                radius: 7
+            },
+            axis: 'left',
+            fill: true,
+            xField: 'name',
+            yField: 'data3',
+            markerCfg: {
+                type: 'circle',
+                size: 4,
+                radius: 4,
+                'stroke-width': 0
+            }
+        }]
+    });
+   
+ +

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

+ */ + +Ext.define('Ext.chart.series.Line', { + + /* Begin Definitions */ + + extend: 'Ext.chart.series.Cartesian', + + alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'], + + requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.draw.Draw', 'Ext.fx.Anim'], + + /* End Definitions */ + + type: 'line', + + alias: 'series.line', + + /** + * @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 {Boolean} showMarkers + * Whether markers should be displayed at the data points along the line. If true, + * then the {@link #markerConfig} config item will determine the markers' styling. + */ + showMarkers: true, + + /** + * @cfg {Object} markerConfig + * The display style for the markers. Only used if {@link #showMarkers} is true. + * The markerConfig is a configuration object containing the same set of properties defined in + * the Sprite class. For example, if we were to set red circles as markers to the line series we could + * pass the object: + * +

+        markerConfig: {
+            type: 'circle',
+            radius: 4,
+            'fill': '#f00'
+        }
+     
+ + */ + markerConfig: {}, + + /** + * @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. + */ + style: {}, + + /** + * @cfg {Boolean} smooth + * If true, the line will be smoothed/rounded around its points, otherwise straight line + * segments will be drawn. Defaults to false. + */ + smooth: false, + + /** + * @cfg {Boolean} fill + * If true, the area below the line will be filled in using the {@link #style.eefill} and + * {@link #style.opacity} config properties. Defaults to false. + */ + fill: false, + + constructor: function(config) { + this.callParent(arguments); + var me = this, + surface = me.chart.surface, + shadow = me.chart.shadow, + i, l; + Ext.apply(me, config, { + highlightCfg: { + 'stroke-width': 3 + }, + shadowAttributes: [{ + "stroke-width": 6, + "stroke-opacity": 0.05, + stroke: 'rgb(0, 0, 0)', + translate: { + x: 1, + y: 1 + } + }, { + "stroke-width": 4, + "stroke-opacity": 0.1, + stroke: 'rgb(0, 0, 0)', + translate: { + x: 1, + y: 1 + } + }, { + "stroke-width": 2, + "stroke-opacity": 0.15, + stroke: 'rgb(0, 0, 0)', + translate: { + x: 1, + y: 1 + } + }] + }); + me.group = surface.getGroup(me.seriesId); + if (me.showMarkers) { + me.markerGroup = surface.getGroup(me.seriesId + '-markers'); + } + if (shadow) { + for (i = 0, l = this.shadowAttributes.length; i < l; i++) { + me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); + } + } + }, + + // @private makes an average of points when there are more data points than pixels to be rendered. + shrink: function(xValues, yValues, size) { + // Start at the 2nd point... + var len = xValues.length, + ratio = Math.floor(len / size), + i = 1, + xSum = 0, + ySum = 0, + xRes = [xValues[0]], + yRes = [yValues[0]]; + + for (; i < len; ++i) { + xSum += xValues[i] || 0; + ySum += yValues[i] || 0; + if (i % ratio == 0) { + xRes.push(xSum/ratio); + yRes.push(ySum/ratio); + xSum = 0; + ySum = 0; + } + } + return { + x: xRes, + y: yRes + }; + }, + + /** + * Draws the series for the current chart. + */ + drawSeries: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + surface = chart.surface, + chartBBox = chart.chartBBox, + bbox = {}, + group = me.group, + gutterX = chart.maxGutter[0], + gutterY = chart.maxGutter[1], + showMarkers = me.showMarkers, + markerGroup = me.markerGroup, + enableShadows = chart.shadow, + shadowGroups = me.shadowGroups, + shadowAttributes = this.shadowAttributes, + lnsh = shadowGroups.length, + dummyPath = ["M"], + path = ["M"], + markerIndex = chart.markerIndex, + axes = [].concat(me.axis), + shadowGroup, + shadowBarAttr, + xValues = [], + yValues = [], + onbreak = false, + markerStyle = me.markerStyle, + seriesStyle = me.seriesStyle, + seriesLabelStyle = me.seriesLabelStyle, + colorArrayStyle = me.colorArrayStyle, + colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, + seriesIdx = me.seriesIdx, shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes, + x, y, prevX, prevY, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue, + yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle, + endLineStyle, type, props, firstMarker; + + //if store is empty then there's nothing to draw. + if (!store || !store.getCount()) { + return; + } + + //prepare style objects for line and markers + endMarkerStyle = Ext.apply(markerStyle, me.markerConfig); + type = endMarkerStyle.type; + delete endMarkerStyle.type; + endLineStyle = Ext.apply(seriesStyle, me.style); + //if no stroke with is specified force it to 0.5 because this is + //about making *lines* + if (!endLineStyle['stroke-width']) { + endLineStyle['stroke-width'] = 0.5; + } + //If we're using a time axis and we need to translate the points, + //then reuse the first markers as the last markers. + if (markerIndex && markerGroup && markerGroup.getCount()) { + for (i = 0; i < markerIndex; i++) { + marker = markerGroup.getAt(i); + markerGroup.remove(marker); + markerGroup.add(marker); + markerAux = markerGroup.getAt(markerGroup.getCount() - 2); + marker.setAttributes({ + x: 0, + y: 0, + translate: { + x: markerAux.attr.translation.x, + y: markerAux.attr.translation.y + } + }, true); + } + } + + me.unHighlightItem(); + me.cleanHighlights(); + + me.setBBox(); + bbox = me.bbox; + + me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height]; + + for (i = 0, ln = axes.length; i < ln; i++) { + axis = chart.axes.get(axes[i]); + if (axis) { + ends = axis.calcEnds(); + if (axis.position == 'top' || axis.position == 'bottom') { + minX = ends.from; + maxX = ends.to; + } + else { + minY = ends.from; + maxY = ends.to; + } + } + } + // If a field was specified without a corresponding axis, create one to get bounds + //only do this for the axis where real values are bound (that's why we check for + //me.axis) + if (me.xField && !Ext.isNumber(minX) + && (me.axis == 'bottom' || me.axis == 'top')) { + axis = Ext.create('Ext.chart.axis.Axis', { + chart: chart, + fields: [].concat(me.xField) + }).calcEnds(); + minX = axis.from; + maxX = axis.to; + } + if (me.yField && !Ext.isNumber(minY) + && (me.axis == 'right' || me.axis == 'left')) { + axis = Ext.create('Ext.chart.axis.Axis', { + chart: chart, + fields: [].concat(me.yField) + }).calcEnds(); + minY = axis.from; + maxY = axis.to; + } + + if (isNaN(minX)) { + minX = 0; + xScale = bbox.width / (store.getCount() - 1); + } + else { + xScale = bbox.width / (maxX - minX); + } + + if (isNaN(minY)) { + minY = 0; + yScale = bbox.height / (store.getCount() - 1); + } + else { + yScale = bbox.height / (maxY - minY); + } + + store.each(function(record, i) { + xValue = record.get(me.xField); + yValue = record.get(me.yField); + //skip undefined values + if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) { + // + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn("[Ext.chart.series.Line] Skipping a store element with an undefined value at ", record, xValue, yValue); + } + // + return; + } + // Ensure a value + if (typeof xValue == 'string' || typeof xValue == 'object' + //set as uniform distribution if the axis is a category axis. + || (me.axis != 'top' && me.axis != 'bottom')) { + xValue = i; + } + if (typeof yValue == 'string' || typeof yValue == 'object' + //set as uniform distribution if the axis is a category axis. + || (me.axis != 'left' && me.axis != 'right')) { + yValue = i; + } + xValues.push(xValue); + yValues.push(yValue); + }, me); + + ln = xValues.length; + if (ln > bbox.width) { + coords = me.shrink(xValues, yValues, bbox.width); + xValues = coords.x; + yValues = coords.y; + } + + me.items = []; + + ln = xValues.length; + for (i = 0; i < ln; i++) { + xValue = xValues[i]; + yValue = yValues[i]; + if (yValue === false) { + if (path.length == 1) { + path = []; + } + onbreak = true; + me.items.push(false); + continue; + } else { + x = (bbox.x + (xValue - minX) * xScale).toFixed(2); + y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2); + if (onbreak) { + onbreak = false; + path.push('M'); + } + path = path.concat([x, y]); + } + if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) { + firstY = y; + } + // If this is the first line, create a dummypath to animate in from. + if (!me.line || chart.resizing) { + dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]); + } + + // When resizing, reset before animating + if (chart.animate && chart.resizing && me.line) { + me.line.setAttributes({ + path: dummyPath + }, true); + if (me.fillPath) { + me.fillPath.setAttributes({ + path: dummyPath, + opacity: 0.2 + }, true); + } + if (me.line.shadows) { + shadows = me.line.shadows; + for (j = 0, lnsh = shadows.length; j < lnsh; j++) { + shadow = shadows[j]; + shadow.setAttributes({ + path: dummyPath + }, true); + } + } + } + if (showMarkers) { + marker = markerGroup.getAt(i); + if (!marker) { + marker = Ext.chart.Shape[type](surface, Ext.apply({ + group: [group, markerGroup], + x: 0, y: 0, + translate: { + x: prevX || x, + y: prevY || (bbox.y + bbox.height / 2) + }, + value: '"' + xValue + ', ' + yValue + '"' + }, endMarkerStyle)); + marker._to = { + translate: { + x: x, + y: y + } + }; + } else { + marker.setAttributes({ + value: '"' + xValue + ', ' + yValue + '"', + x: 0, y: 0, + hidden: false + }, true); + marker._to = { + translate: { + x: x, y: y + } + }; + } + } + me.items.push({ + series: me, + value: [xValue, yValue], + point: [x, y], + sprite: marker, + storeItem: store.getAt(i) + }); + prevX = x; + prevY = y; + } + + if (path.length <= 1) { + //nothing to be rendered + return; + } + + if (me.smooth) { + path = Ext.draw.Draw.smooth(path, 6); + } + + //Correct path if we're animating timeAxis intervals + if (chart.markerIndex && me.previousPath) { + fromPath = me.previousPath; + fromPath.splice(1, 2); + } else { + fromPath = path; + } + + // Only create a line if one doesn't exist. + if (!me.line) { + me.line = surface.add(Ext.apply({ + type: 'path', + group: group, + path: dummyPath, + stroke: endLineStyle.stroke || endLineStyle.fill + }, endLineStyle || {})); + //unset fill here (there's always a default fill withing the themes). + me.line.setAttributes({ + fill: 'none' + }); + if (!endLineStyle.stroke && colorArrayLength) { + me.line.setAttributes({ + stroke: colorArrayStyle[seriesIdx % colorArrayLength] + }, true); + } + if (enableShadows) { + //create shadows + shadows = me.line.shadows = []; + for (shindex = 0; shindex < lnsh; shindex++) { + shadowBarAttr = shadowAttributes[shindex]; + shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath }); + shadow = chart.surface.add(Ext.apply({}, { + type: 'path', + group: shadowGroups[shindex] + }, shadowBarAttr)); + shadows.push(shadow); + } + } + } + if (me.fill) { + fillPath = path.concat([ + ["L", x, bbox.y + bbox.height], + ["L", bbox.x, bbox.y + bbox.height], + ["L", bbox.x, firstY] + ]); + if (!me.fillPath) { + me.fillPath = surface.add({ + group: group, + type: 'path', + opacity: endLineStyle.opacity || 0.3, + fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill, + path: dummyPath + }); + } + } + markerCount = showMarkers && markerGroup.getCount(); + if (chart.animate) { + fill = me.fill; + line = me.line; + //Add renderer to line. There is not unique record associated with this. + rendererAttributes = me.renderer(line, false, { path: path }, i, store); + Ext.apply(rendererAttributes, endLineStyle || {}, { + stroke: endLineStyle.stroke || endLineStyle.fill + }); + //fill should not be used here but when drawing the special fill path object + delete rendererAttributes.fill; + if (chart.markerIndex && me.previousPath) { + me.animation = animation = me.onAnimate(line, { + to: rendererAttributes, + from: { + path: fromPath + } + }); + } else { + me.animation = animation = me.onAnimate(line, { + to: rendererAttributes + }); + } + //animate shadows + if (enableShadows) { + shadows = line.shadows; + for(j = 0; j < lnsh; j++) { + if (chart.markerIndex && me.previousPath) { + me.onAnimate(shadows[j], { + to: { path: path }, + from: { path: fromPath } + }); + } else { + me.onAnimate(shadows[j], { + to: { path: path } + }); + } + } + } + //animate fill path + if (fill) { + me.onAnimate(me.fillPath, { + to: Ext.apply({}, { + path: fillPath, + fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill + }, endLineStyle || {}) + }); + } + //animate markers + if (showMarkers) { + for(i = 0; i < ln; i++) { + item = markerGroup.getAt(i); + if (item) { + if (me.items[i]) { + rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); + me.onAnimate(item, { + to: Ext.apply(rendererAttributes, endMarkerStyle || {}) + }); + } else { + item.setAttributes(Ext.apply({ + hidden: true + }, item._to), true); + } + } + } + for(; i < markerCount; i++) { + item = markerGroup.getAt(i); + item.hide(true); + } +// for(i = 0; i < (chart.markerIndex || 0)-1; i++) { +// item = markerGroup.getAt(i); +// item.hide(true); +// } + } + } else { + rendererAttributes = me.renderer(me.line, false, { path: path, hidden: false }, i, store); + Ext.apply(rendererAttributes, endLineStyle || {}, { + stroke: endLineStyle.stroke || endLineStyle.fill + }); + //fill should not be used here but when drawing the special fill path object + delete rendererAttributes.fill; + me.line.setAttributes(rendererAttributes, true); + //set path for shadows + if (enableShadows) { + shadows = me.line.shadows; + for(j = 0; j < lnsh; j++) { + shadows[j].setAttributes({ + path: path + }, true); + } + } + if (me.fill) { + me.fillPath.setAttributes({ + path: fillPath + }, true); + } + if (showMarkers) { + for(i = 0; i < ln; i++) { + item = markerGroup.getAt(i); + if (item) { + if (me.items[i]) { + rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); + item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true); + } else { + item.hide(true); + } + } + } + for(; i < markerCount; i++) { + item = markerGroup.getAt(i); + item.hide(true); + } + } + } + + if (chart.markerIndex) { + path.splice(1, 0, path[1], path[2]); + me.previousPath = path; + } + me.renderLabels(); + me.renderCallouts(); + }, + + // @private called when a label is to be created. + onCreateLabel: function(storeItem, item, i, display) { + var me = this, + group = me.labelsGroup, + config = me.label, + bbox = me.bbox, + endLabelStyle = Ext.apply(config, me.seriesLabelStyle); + + return me.chart.surface.add(Ext.apply({ + 'type': 'text', + 'text-anchor': 'middle', + 'group': group, + 'x': item.point[0], + 'y': bbox.y + bbox.height / 2 + }, endLabelStyle || {})); + }, + + // @private called when a label is to be created. + onPlaceLabel: function(label, storeItem, item, i, display, animate) { + var me = this, + chart = me.chart, + resizing = chart.resizing, + config = me.label, + format = config.renderer, + field = config.field, + bbox = me.bbox, + x = item.point[0], + y = item.point[1], + radius = item.sprite.attr.radius, + bb, width, height; + + label.setAttributes({ + text: format(storeItem.get(field)), + hidden: true + }, true); + + if (display == 'rotate') { + label.setAttributes({ + 'text-anchor': 'start', + 'rotation': { + x: x, + y: y, + degrees: -45 + } + }, true); + //correct label position to fit into the box + bb = label.getBBox(); + width = bb.width; + height = bb.height; + x = x < bbox.x? bbox.x : x; + x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x; + y = (y - height < bbox.y)? bbox.y + height : y; + + } else if (display == 'under' || display == 'over') { + //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined. + bb = item.sprite.getBBox(); + bb.width = bb.width || (radius * 2); + bb.height = bb.height || (radius * 2); + y = y + (display == 'over'? -bb.height : bb.height); + //correct label position to fit into the box + bb = label.getBBox(); + width = bb.width/2; + height = bb.height/2; + x = x - width < bbox.x? bbox.x + width : x; + x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x; + y = y - height < bbox.y? bbox.y + height : y; + y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y; + } + + if (me.chart.animate && !me.chart.resizing) { + label.show(true); + me.onAnimate(label, { + to: { + x: x, + y: y + } + }); + } else { + label.setAttributes({ + x: x, + y: y + }, true); + if (resizing) { + me.animation.on('afteranimate', function() { + label.show(true); + }); + } else { + label.show(true); + } + } + }, + + //@private Overriding highlights.js highlightItem method. + highlightItem: function() { + var me = this; + me.callParent(arguments); + if (this.line && !this.highlighted) { + if (!('__strokeWidth' in this.line)) { + this.line.__strokeWidth = this.line.attr['stroke-width'] || 0; + } + if (this.line.__anim) { + this.line.__anim.paused = true; + } + this.line.__anim = Ext.create('Ext.fx.Anim', { + target: this.line, + to: { + 'stroke-width': this.line.__strokeWidth + 3 + } + }); + this.highlighted = true; + } + }, + + //@private Overriding highlights.js unHighlightItem method. + unHighlightItem: function() { + var me = this; + me.callParent(arguments); + if (this.line && this.highlighted) { + this.line.__anim = Ext.create('Ext.fx.Anim', { + target: this.line, + to: { + 'stroke-width': this.line.__strokeWidth + } + }); + this.highlighted = false; + } + }, + + //@private called when a callout needs to be placed. + onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) { + if (!display) { + return; + } + + var me = this, + chart = me.chart, + surface = chart.surface, + resizing = chart.resizing, + config = me.callouts, + items = me.items, + prev = i == 0? false : items[i -1].point, + next = (i == items.length -1)? false : items[i +1].point, + cur = [+item.point[0], +item.point[1]], + dir, norm, normal, a, aprev, anext, + offsetFromViz = config.offsetFromViz || 30, + offsetToSide = config.offsetToSide || 10, + offsetBox = config.offsetBox || 3, + boxx, boxy, boxw, boxh, + p, clipRect = me.clipRect, + bbox = { + width: config.styles.width || 10, + height: config.styles.height || 10 + }, + x, y; + + //get the right two points + if (!prev) { + prev = cur; + } + if (!next) { + next = cur; + } + a = (next[1] - prev[1]) / (next[0] - prev[0]); + aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]); + anext = (next[1] - cur[1]) / (next[0] - cur[0]); + + norm = Math.sqrt(1 + a * a); + dir = [1 / norm, a / norm]; + normal = [-dir[1], dir[0]]; + + //keep the label always on the outer part of the "elbow" + if (aprev > 0 && anext < 0 && normal[1] < 0 + || aprev < 0 && anext > 0 && normal[1] > 0) { + normal[0] *= -1; + normal[1] *= -1; + } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 + || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) { + normal[0] *= -1; + normal[1] *= -1; + } + //position + x = cur[0] + normal[0] * offsetFromViz; + y = cur[1] + normal[1] * offsetFromViz; + + //box position and dimensions + boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); + boxy = y - bbox.height /2 - offsetBox; + boxw = bbox.width + 2 * offsetBox; + boxh = bbox.height + 2 * offsetBox; + + //now check if we're out of bounds and invert the normal vector correspondingly + //this may add new overlaps between labels (but labels won't be out of bounds). + if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) { + normal[0] *= -1; + } + if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) { + normal[1] *= -1; + } + + //update positions + x = cur[0] + normal[0] * offsetFromViz; + y = cur[1] + normal[1] * offsetFromViz; + + //update box position and dimensions + boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); + boxy = y - bbox.height /2 - offsetBox; + boxw = bbox.width + 2 * offsetBox; + boxh = bbox.height + 2 * offsetBox; + + if (chart.animate) { + //set the line from the middle of the pie to the box. + me.onAnimate(callout.lines, { + to: { + path: ["M", cur[0], cur[1], "L", x, y, "Z"] + } + }); + //set component position + if (callout.panel) { + callout.panel.setPosition(boxx, boxy, true); + } + } + else { + //set the line from the middle of the pie to the box. + callout.lines.setAttributes({ + path: ["M", cur[0], cur[1], "L", x, y, "Z"] + }, true); + //set component position + if (callout.panel) { + callout.panel.setPosition(boxx, boxy); + } + } + for (p in callout) { + callout[p].show(true); + } + }, + + isItemInPoint: function(x, y, item, i) { + var me = this, + items = me.items, + tolerance = me.selectionTolerance, + result = null, + prevItem, + nextItem, + prevPoint, + nextPoint, + ln, + x1, + y1, + x2, + y2, + xIntersect, + yIntersect, + dist1, dist2, dist, midx, midy, + sqrt = Math.sqrt, abs = Math.abs; + + nextItem = items[i]; + prevItem = i && items[i - 1]; + + if (i >= ln) { + prevItem = items[ln - 1]; + } + prevPoint = prevItem && prevItem.point; + nextPoint = nextItem && nextItem.point; + x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance; + y1 = prevItem ? prevPoint[1] : nextPoint[1]; + x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance; + y2 = nextItem ? nextPoint[1] : prevPoint[1]; + dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1)); + dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); + dist = Math.min(dist1, dist2); + + if (dist <= tolerance) { + return dist == dist1? prevItem : nextItem; + } + return false; + }, + + // @private toggle visibility of all series elements (markers, sprites). + toggleAll: function(show) { + var me = this, + i, ln, shadow, shadows; + if (!show) { + Ext.chart.series.Line.superclass.hideAll.call(me); + } + else { + Ext.chart.series.Line.superclass.showAll.call(me); + } + if (me.line) { + me.line.setAttributes({ + hidden: !show + }, true); + //hide shadows too + if (me.line.shadows) { + for (i = 0, shadows = me.line.shadows, ln = shadows.length; i < ln; i++) { + shadow = shadows[i]; + shadow.setAttributes({ + hidden: !show + }, true); + } + } + } + if (me.fillPath) { + me.fillPath.setAttributes({ + hidden: !show + }, true); + } + }, + + // @private hide all series elements (markers, sprites). + hideAll: function() { + this.toggleAll(false); + }, + + // @private hide all series elements (markers, sprites). + showAll: function() { + this.toggleAll(true); + } +}); +/** + * @class Ext.chart.series.Pie + * @extends Ext.chart.series.Series + * + * 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: + * +{@img Ext.chart.series.Pie/Ext.chart.series.Pie.png Ext.chart.series.Pie chart series} +

+    var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        animate: true,
+        store: store,
+        theme: 'Base:gradients',
+        series: [{
+            type: 'pie',
+            field: 'data1',
+            showInLegend: true,
+            tips: {
+              trackMouse: true,
+              width: 140,
+              height: 28,
+              renderer: function(storeItem, item) {
+                //calculate and display percentage on hover
+                var total = 0;
+                store.each(function(rec) {
+                    total += rec.get('data1');
+                });
+                this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
+              }
+            },
+            highlight: {
+              segment: {
+                margin: 20
+              }
+            },
+            label: {
+                field: 'name',
+                display: 'rotate',
+                contrast: true,
+                font: '18px Arial'
+            }
+        }]    
+    });
+   
+ + * + * 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. + * + * @xtype pie + * + */ +Ext.define('Ext.chart.series.Pie', { + + /* Begin Definitions */ + + alternateClassName: ['Ext.chart.PieSeries', 'Ext.chart.PieChart'], + + extend: 'Ext.chart.series.Series', + + /* End Definitions */ + + type: "pie", + + alias: 'series.pie', + + rad: Math.PI / 180, + + /** + * @cfg {Number} highlightDuration + * The duration for the pie slice highlight effect. + */ + highlightDuration: 150, + + /** + * @cfg {String} angleField + * The store record field name to be used for the pie angles. + * The values bound to this field name must be positive real numbers. + * This parameter is required. + */ + angleField: false, + + /** + * @cfg {String} lengthField + * The store record field name to be used for the pie slice lengths. + * The values bound to this field name must be positive real numbers. + * This parameter is optional. + */ + lengthField: false, + + /** + * @cfg {Boolean|Number} donut + * Whether to set the pie chart as donut chart. + * Default's false. Can be set to a particular percentage to set the radius + * of the donut chart. + */ + donut: false, + + /** + * @cfg {Boolean} showInLegend + * Whether to add the pie chart elements as legend items. Default's false. + */ + showInLegend: false, + + /** + * @cfg {Array} colorSet + * An array of color values which will be used, in order, as the pie slice fill colors. + */ + + /** + * @cfg {Object} style + * An object containing styles for overriding series styles from Theming. + */ + style: {}, + + constructor: function(config) { + this.callParent(arguments); + var me = this, + chart = me.chart, + surface = chart.surface, + store = chart.store, + shadow = chart.shadow, i, l, cfg; + Ext.applyIf(me, { + highlightCfg: { + segment: { + margin: 20 + } + } + }); + Ext.apply(me, config, { + shadowAttributes: [{ + "stroke-width": 6, + "stroke-opacity": 1, + stroke: 'rgb(200, 200, 200)', + translate: { + x: 1.2, + y: 2 + } + }, + { + "stroke-width": 4, + "stroke-opacity": 1, + stroke: 'rgb(150, 150, 150)', + translate: { + x: 0.9, + y: 1.5 + } + }, + { + "stroke-width": 2, + "stroke-opacity": 1, + stroke: 'rgb(100, 100, 100)', + translate: { + x: 0.6, + y: 1 + } + }] + }); + me.group = surface.getGroup(me.seriesId); + if (shadow) { + for (i = 0, l = me.shadowAttributes.length; i < l; i++) { + me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); + } + } + surface.customAttributes.segment = function(opt) { + return me.getSegment(opt); + }; + }, + + //@private updates some onbefore render parameters. + initialize: function() { + var me = this, + store = me.chart.substore || me.chart.store; + //Add yFields to be used in Legend.js + me.yField = []; + if (me.label.field) { + store.each(function(rec) { + me.yField.push(rec.get(me.label.field)); + }); + } + }, + + // @private returns an object with properties for a PieSlice. + getSegment: function(opt) { + var me = this, + rad = me.rad, + cos = Math.cos, + sin = Math.sin, + abs = Math.abs, + x = me.centerX, + y = me.centerY, + x1 = 0, x2 = 0, x3 = 0, x4 = 0, + y1 = 0, y2 = 0, y3 = 0, y4 = 0, + delta = 1e-2, + r = opt.endRho - opt.startRho, + startAngle = opt.startAngle, + endAngle = opt.endAngle, + midAngle = (startAngle + endAngle) / 2 * rad, + margin = opt.margin || 0, + flag = abs(endAngle - startAngle) > 180, + a1 = Math.min(startAngle, endAngle) * rad, + a2 = Math.max(startAngle, endAngle) * rad, + singleSlice = false; + + x += margin * cos(midAngle); + y += margin * sin(midAngle); + + x1 = x + opt.startRho * cos(a1); + y1 = y + opt.startRho * sin(a1); + + x2 = x + opt.endRho * cos(a1); + y2 = y + opt.endRho * sin(a1); + + x3 = x + opt.startRho * cos(a2); + y3 = y + opt.startRho * sin(a2); + + x4 = x + opt.endRho * cos(a2); + y4 = y + opt.endRho * sin(a2); + + if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) { + singleSlice = true; + } + //Solves mysterious clipping bug with IE + if (singleSlice) { + return { + path: [ + ["M", x1, y1], + ["L", x2, y2], + ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4], + ["Z"]] + }; + } else { + return { + path: [ + ["M", x1, y1], + ["L", x2, y2], + ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4], + ["L", x3, y3], + ["A", opt.startRho, opt.startRho, 0, +flag, 0, x1, y1], + ["Z"]] + }; + } + }, + + // @private utility function to calculate the middle point of a pie slice. + calcMiddle: function(item) { + var me = this, + rad = me.rad, + slice = item.slice, + x = me.centerX, + y = me.centerY, + startAngle = slice.startAngle, + endAngle = slice.endAngle, + donut = +me.donut, + a1 = Math.min(startAngle, endAngle) * rad, + a2 = Math.max(startAngle, endAngle) * rad, + midAngle = -(a1 + (a2 - a1) / 2), + xm = x + (item.endRho + item.startRho) / 2 * Math.cos(midAngle), + ym = y - (item.endRho + item.startRho) / 2 * Math.sin(midAngle); + + item.middle = { + x: xm, + y: ym + }; + }, + + /** + * Draws the series for the current chart. + */ + drawSeries: function() { + var me = this, + store = me.chart.substore || me.chart.store, + group = me.group, + animate = me.chart.animate, + field = me.angleField || me.field || me.xField, + lenField = [].concat(me.lengthField), + totalLenField = 0, + colors = me.colorSet, + chart = me.chart, + surface = chart.surface, + chartBBox = chart.chartBBox, + enableShadows = chart.shadow, + shadowGroups = me.shadowGroups, + shadowAttributes = me.shadowAttributes, + lnsh = shadowGroups.length, + rad = me.rad, + layers = lenField.length, + rhoAcum = 0, + donut = +me.donut, + layerTotals = [], + values = {}, + fieldLength, + items = [], + passed = false, + totalField = 0, + maxLenField = 0, + cut = 9, + defcut = true, + angle = 0, + seriesStyle = me.seriesStyle, + seriesLabelStyle = me.seriesLabelStyle, + colorArrayStyle = me.colorArrayStyle, + colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, + gutterX = chart.maxGutter[0], + gutterY = chart.maxGutter[1], + rendererAttributes, + shadowGroup, + shadowAttr, + shadows, + shadow, + shindex, + centerX, + centerY, + deltaRho, + first = 0, + slice, + slices, + sprite, + value, + item, + lenValue, + ln, + record, + i, + j, + startAngle, + endAngle, + middleAngle, + sliceLength, + path, + p, + spriteOptions, bbox; + + Ext.apply(seriesStyle, me.style || {}); + + me.setBBox(); + bbox = me.bbox; + + //override theme colors + if (me.colorSet) { + colorArrayStyle = me.colorSet; + colorArrayLength = colorArrayStyle.length; + } + + //if not store or store is empty then there's nothing to draw + if (!store || !store.getCount()) { + return; + } + + me.unHighlightItem(); + me.cleanHighlights(); + + centerX = me.centerX = chartBBox.x + (chartBBox.width / 2); + centerY = me.centerY = chartBBox.y + (chartBBox.height / 2); + me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y); + me.slices = slices = []; + me.items = items = []; + + store.each(function(record, i) { + if (this.__excludes && this.__excludes[i]) { + //hidden series + return; + } + totalField += +record.get(field); + if (lenField[0]) { + for (j = 0, totalLenField = 0; j < layers; j++) { + totalLenField += +record.get(lenField[j]); + } + layerTotals[i] = totalLenField; + maxLenField = Math.max(maxLenField, totalLenField); + } + }, this); + + store.each(function(record, i) { + if (this.__excludes && this.__excludes[i]) { + //hidden series + return; + } + value = record.get(field); + middleAngle = angle - 360 * value / totalField / 2; + // TODO - Put up an empty circle + if (isNaN(middleAngle)) { + middleAngle = 360; + value = 1; + totalField = 1; + } + // First slice + if (!i || first == 0) { + angle = 360 - middleAngle; + me.firstAngle = angle; + middleAngle = angle - 360 * value / totalField / 2; + } + endAngle = angle - 360 * value / totalField; + slice = { + series: me, + value: value, + startAngle: angle, + endAngle: endAngle, + storeItem: record + }; + if (lenField[0]) { + lenValue = layerTotals[i]; + slice.rho = me.radius * (lenValue / maxLenField); + } else { + slice.rho = me.radius; + } + slices[i] = slice; + if((slice.startAngle % 360) == (slice.endAngle % 360)) { + slice.startAngle -= 0.0001; + } + angle = endAngle; + first++; + }, me); + + //do all shadows first. + if (enableShadows) { + for (i = 0, ln = slices.length; i < ln; i++) { + if (this.__excludes && this.__excludes[i]) { + //hidden series + continue; + } + slice = slices[i]; + slice.shadowAttrs = []; + for (j = 0, rhoAcum = 0, shadows = []; j < layers; j++) { + sprite = group.getAt(i * layers + j); + deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho; + //set pie slice properties + rendererAttributes = { + segment: { + startAngle: slice.startAngle, + endAngle: slice.endAngle, + margin: 0, + rho: slice.rho, + startRho: rhoAcum + (deltaRho * donut / 100), + endRho: rhoAcum + deltaRho + } + }; + //create shadows + for (shindex = 0, shadows = []; shindex < lnsh; shindex++) { + shadowAttr = shadowAttributes[shindex]; + shadow = shadowGroups[shindex].getAt(i); + if (!shadow) { + shadow = chart.surface.add(Ext.apply({}, + { + type: 'path', + group: shadowGroups[shindex], + strokeLinejoin: "round" + }, + rendererAttributes, shadowAttr)); + } + if (animate) { + rendererAttributes = me.renderer(shadow, store.getAt(i), Ext.apply({}, + rendererAttributes, shadowAttr), i, store); + me.onAnimate(shadow, { + to: rendererAttributes + }); + } else { + rendererAttributes = me.renderer(shadow, store.getAt(i), Ext.apply(shadowAttr, { + hidden: false + }), i, store); + shadow.setAttributes(rendererAttributes, true); + } + shadows.push(shadow); + } + slice.shadowAttrs[j] = shadows; + } + } + } + //do pie slices after. + for (i = 0, ln = slices.length; i < ln; i++) { + if (this.__excludes && this.__excludes[i]) { + //hidden series + continue; + } + slice = slices[i]; + for (j = 0, rhoAcum = 0; j < layers; j++) { + sprite = group.getAt(i * layers + j); + deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho; + //set pie slice properties + rendererAttributes = Ext.apply({ + segment: { + startAngle: slice.startAngle, + endAngle: slice.endAngle, + margin: 0, + rho: slice.rho, + startRho: rhoAcum + (deltaRho * donut / 100), + endRho: rhoAcum + deltaRho + } + }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {})); + item = Ext.apply({}, + rendererAttributes.segment, { + slice: slice, + series: me, + storeItem: slice.storeItem, + index: i + }); + me.calcMiddle(item); + if (enableShadows) { + item.shadows = slice.shadowAttrs[j]; + } + items[i] = item; + // Create a new sprite if needed (no height) + if (!sprite) { + spriteOptions = Ext.apply({ + type: "path", + group: group, + middle: item.middle + }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {})); + sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes)); + } + slice.sprite = slice.sprite || []; + item.sprite = sprite; + slice.sprite.push(sprite); + slice.point = [item.middle.x, item.middle.y]; + if (animate) { + rendererAttributes = me.renderer(sprite, store.getAt(i), rendererAttributes, i, store); + sprite._to = rendererAttributes; + sprite._animating = true; + me.onAnimate(sprite, { + to: rendererAttributes, + listeners: { + afteranimate: { + fn: function() { + this._animating = false; + }, + scope: sprite + } + } + }); + } else { + rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(rendererAttributes, { + hidden: false + }), i, store); + sprite.setAttributes(rendererAttributes, true); + } + rhoAcum += deltaRho; + } + } + + // Hide unused bars + ln = group.getCount(); + for (i = 0; i < ln; i++) { + if (!slices[(i / layers) >> 0] && group.getAt(i)) { + group.getAt(i).hide(true); + } + } + if (enableShadows) { + lnsh = shadowGroups.length; + for (shindex = 0; shindex < ln; shindex++) { + if (!slices[(shindex / layers) >> 0]) { + for (j = 0; j < lnsh; j++) { + if (shadowGroups[j].getAt(shindex)) { + shadowGroups[j].getAt(shindex).hide(true); + } + } + } + } + } + me.renderLabels(); + me.renderCallouts(); + }, + + // @private callback for when creating a label sprite. + onCreateLabel: function(storeItem, item, i, display) { + var me = this, + group = me.labelsGroup, + config = me.label, + centerX = me.centerX, + centerY = me.centerY, + middle = item.middle, + endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {}); + + return me.chart.surface.add(Ext.apply({ + 'type': 'text', + 'text-anchor': 'middle', + 'group': group, + 'x': middle.x, + 'y': middle.y + }, endLabelStyle)); + }, + + // @private callback for when placing a label sprite. + onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { + var me = this, + chart = me.chart, + resizing = chart.resizing, + config = me.label, + format = config.renderer, + field = [].concat(config.field), + centerX = me.centerX, + centerY = me.centerY, + middle = item.middle, + opt = { + x: middle.x, + y: middle.y + }, + x = middle.x - centerX, + y = middle.y - centerY, + from = {}, + rho = 1, + theta = Math.atan2(y, x || 1), + dg = theta * 180 / Math.PI, + prevDg; + + function fixAngle(a) { + if (a < 0) a += 360; + return a % 360; + } + + label.setAttributes({ + text: format(storeItem.get(field[index])) + }, true); + + switch (display) { + case 'outside': + rho = Math.sqrt(x * x + y * y) * 2; + //update positions + opt.x = rho * Math.cos(theta) + centerX; + opt.y = rho * Math.sin(theta) + centerY; + break; + + case 'rotate': + dg = fixAngle(dg); + dg = (dg > 90 && dg < 270) ? dg + 180: dg; + + prevDg = label.attr.rotation.degrees; + if (prevDg != null && Math.abs(prevDg - dg) > 180) { + if (dg > prevDg) { + dg -= 360; + } else { + dg += 360; + } + dg = dg % 360; + } else { + dg = fixAngle(dg); + } + //update rotation angle + opt.rotate = { + degrees: dg, + x: opt.x, + y: opt.y + }; + break; + + default: + break; + } + //ensure the object has zero translation + opt.translate = { + x: 0, y: 0 + }; + if (animate && !resizing && (display != 'rotate' || prevDg != null)) { + me.onAnimate(label, { + to: opt + }); + } else { + label.setAttributes(opt, true); + } + label._from = from; + }, + + // @private callback for when placing a callout sprite. + onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) { + var me = this, + chart = me.chart, + resizing = chart.resizing, + config = me.callouts, + centerX = me.centerX, + centerY = me.centerY, + middle = item.middle, + opt = { + x: middle.x, + y: middle.y + }, + x = middle.x - centerX, + y = middle.y - centerY, + rho = 1, + rhoCenter, + theta = Math.atan2(y, x || 1), + bbox = callout.label.getBBox(), + offsetFromViz = 20, + offsetToSide = 10, + offsetBox = 10, + p; + + //should be able to config this. + rho = item.endRho + offsetFromViz; + rhoCenter = (item.endRho + item.startRho) / 2 + (item.endRho - item.startRho) / 3; + //update positions + opt.x = rho * Math.cos(theta) + centerX; + opt.y = rho * Math.sin(theta) + centerY; + + x = rhoCenter * Math.cos(theta); + y = rhoCenter * Math.sin(theta); + + if (chart.animate) { + //set the line from the middle of the pie to the box. + me.onAnimate(callout.lines, { + to: { + path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"] + } + }); + //set box position + me.onAnimate(callout.box, { + to: { + x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)), + y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)), + width: bbox.width + 2 * offsetBox, + height: bbox.height + 2 * offsetBox + } + }); + //set text position + me.onAnimate(callout.label, { + to: { + x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)), + y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4) + } + }); + } else { + //set the line from the middle of the pie to the box. + callout.lines.setAttributes({ + path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"] + }, + true); + //set box position + callout.box.setAttributes({ + x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)), + y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)), + width: bbox.width + 2 * offsetBox, + height: bbox.height + 2 * offsetBox + }, + true); + //set text position + callout.label.setAttributes({ + x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)), + y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4) + }, + true); + } + for (p in callout) { + callout[p].show(true); + } + }, + + // @private handles sprite animation for the series. + onAnimate: function(sprite, attr) { + sprite.show(); + return this.callParent(arguments); + }, + + isItemInPoint: function(x, y, item, i) { + var me = this, + cx = me.centerX, + cy = me.centerY, + abs = Math.abs, + dx = abs(x - cx), + dy = abs(y - cy), + startAngle = item.startAngle, + endAngle = item.endAngle, + rho = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(y - cy, x - cx) / me.rad + 360; + + // normalize to the same range of angles created by drawSeries + if (angle > me.firstAngle) { + angle -= 360; + } + return (angle <= startAngle && angle > endAngle + && rho >= item.startRho && rho <= item.endRho); + }, + + // @private hides all elements in the series. + hideAll: function() { + var i, l, shadow, shadows, sh, lsh, sprite; + if (!isNaN(this._index)) { + this.__excludes = this.__excludes || []; + this.__excludes[this._index] = true; + sprite = this.slices[this._index].sprite; + for (sh = 0, lsh = sprite.length; sh < lsh; sh++) { + sprite[sh].setAttributes({ + hidden: true + }, true); + } + if (this.slices[this._index].shadowAttrs) { + for (i = 0, shadows = this.slices[this._index].shadowAttrs, l = shadows.length; i < l; i++) { + shadow = shadows[i]; + for (sh = 0, lsh = shadow.length; sh < lsh; sh++) { + shadow[sh].setAttributes({ + hidden: true + }, true); + } + } + } + this.drawSeries(); + } + }, + + // @private shows all elements in the series. + showAll: function() { + if (!isNaN(this._index)) { + this.__excludes[this._index] = false; + this.drawSeries(); + } + }, + + /** + * Highlight the specified item. If no item is provided the whole series will be highlighted. + * @param item {Object} Info about the item; same format as returned by #getItemForPoint + */ + highlightItem: function(item) { + var me = this, + rad = me.rad; + item = item || this.items[this._index]; + + //TODO(nico): sometimes in IE itemmouseover is triggered + //twice without triggering itemmouseout in between. This + //fixes the highlighting bug. Eventually, events should be + //changed to trigger one itemmouseout between two itemmouseovers. + this.unHighlightItem(); + + if (!item || item.sprite && item.sprite._animating) { + return; + } + me.callParent([item]); + if (!me.highlight) { + return; + } + if ('segment' in me.highlightCfg) { + var highlightSegment = me.highlightCfg.segment, + animate = me.chart.animate, + attrs, i, shadows, shadow, ln, to, itemHighlightSegment, prop; + //animate labels + if (me.labelsGroup) { + var group = me.labelsGroup, + display = me.label.display, + label = group.getAt(item.index), + middle = (item.startAngle + item.endAngle) / 2 * rad, + r = highlightSegment.margin || 0, + x = r * Math.cos(middle), + y = r * Math.sin(middle); + + //TODO(nico): rounding to 1e-10 + //gives the right translation. Translation + //was buggy for very small numbers. In this + //case we're not looking to translate to very small + //numbers but not to translate at all. + if (Math.abs(x) < 1e-10) { + x = 0; + } + if (Math.abs(y) < 1e-10) { + y = 0; + } + + if (animate) { + label.stopAnimation(); + label.animate({ + to: { + translate: { + x: x, + y: y + } + }, + duration: me.highlightDuration + }); + } + else { + label.setAttributes({ + translate: { + x: x, + y: y + } + }, true); + } + } + //animate shadows + if (me.chart.shadow && item.shadows) { + i = 0; + shadows = item.shadows; + ln = shadows.length; + for (; i < ln; i++) { + shadow = shadows[i]; + to = {}; + itemHighlightSegment = item.sprite._from.segment; + for (prop in itemHighlightSegment) { + if (! (prop in highlightSegment)) { + to[prop] = itemHighlightSegment[prop]; + } + } + attrs = { + segment: Ext.applyIf(to, me.highlightCfg.segment) + }; + if (animate) { + shadow.stopAnimation(); + shadow.animate({ + to: attrs, + duration: me.highlightDuration + }); + } + else { + shadow.setAttributes(attrs, true); + } + } + } + } + }, + + /** + * un-highlights the specified item. If no item is provided it will un-highlight the entire series. + * @param item {Object} Info about the item; same format as returned by #getItemForPoint + */ + unHighlightItem: function() { + var me = this; + if (!me.highlight) { + return; + } + + if (('segment' in me.highlightCfg) && me.items) { + var items = me.items, + animate = me.chart.animate, + shadowsEnabled = !!me.chart.shadow, + group = me.labelsGroup, + len = items.length, + i = 0, + j = 0, + display = me.label.display, + shadowLen, p, to, ihs, hs, sprite, shadows, shadow, item, label, attrs; + + for (; i < len; i++) { + item = items[i]; + if (!item) { + continue; + } + sprite = item.sprite; + if (sprite && sprite._highlighted) { + //animate labels + if (group) { + label = group.getAt(item.index); + attrs = Ext.apply({ + translate: { + x: 0, + y: 0 + } + }, + display == 'rotate' ? { + rotate: { + x: label.attr.x, + y: label.attr.y, + degrees: label.attr.rotation.degrees + } + }: {}); + if (animate) { + label.stopAnimation(); + label.animate({ + to: attrs, + duration: me.highlightDuration + }); + } + else { + label.setAttributes(attrs, true); + } + } + if (shadowsEnabled) { + shadows = item.shadows; + shadowLen = shadows.length; + for (; j < shadowLen; j++) { + to = {}; + ihs = item.sprite._to.segment; + hs = item.sprite._from.segment; + Ext.apply(to, hs); + for (p in ihs) { + if (! (p in hs)) { + to[p] = ihs[p]; + } + } + shadow = shadows[j]; + if (animate) { + shadow.stopAnimation(); + shadow.animate({ + to: { + segment: to + }, + duration: me.highlightDuration + }); + } + else { + shadow.setAttributes({ segment: to }, true); + } + } + } + } + } + } + me.callParent(arguments); + }, + + /** + * Returns the color of the series (to be displayed as color for the series legend item). + * @param item {Object} Info about the item; same format as returned by #getItemForPoint + */ + getLegendColor: function(index) { + var me = this; + return me.colorArrayStyle[index % me.colorArrayStyle.length]; + } +}); + + +/** + * @class Ext.chart.series.Radar + * @extends Ext.chart.series.Series + * + * 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: + * + {@img Ext.chart.series.Radar/Ext.chart.series.Radar.png Ext.chart.series.Radar chart series} +

+    var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        animate: true,
+        theme:'Category2',
+        store: store,
+        axes: [{
+            type: 'Radial',
+            position: 'radial',
+            label: {
+                display: true
+            }
+        }],
+        series: [{
+            type: 'radar',
+            xField: 'name',
+            yField: 'data3',
+            showInLegend: true,
+            showMarkers: true,
+            markerConfig: {
+                radius: 5,
+                size: 5           
+            },
+            style: {
+                'stroke-width': 2,
+                fill: 'none'
+            }
+        },{
+            type: 'radar',
+            xField: 'name',
+            yField: 'data2',
+            showMarkers: true,
+            showInLegend: true,
+            markerConfig: {
+                radius: 5,
+                size: 5
+            },
+            style: {
+                'stroke-width': 2,
+                fill: 'none'
+            }
+        },{
+            type: 'radar',
+            xField: 'name',
+            yField: 'data5',
+            showMarkers: true,
+            showInLegend: true,
+            markerConfig: {
+                radius: 5,
+                size: 5
+            },
+            style: {
+                'stroke-width': 2,
+                fill: 'none'
+            }
+        }]    
+    });
+   
+ * + * In this configuration we add three series to the chart. Each of these series is bound to the same categories field, `name` but bound to different properties for each category, + * `data1`, `data2` and `data3` respectively. All series display markers by having `showMarkers` enabled. The configuration for the markers of each series can be set by adding properties onto + * the markerConfig object. Finally we override some theme styling properties by adding properties to the `style` object. + * + * @xtype radar + * + */ +Ext.define('Ext.chart.series.Radar', { + + /* Begin Definitions */ + + extend: 'Ext.chart.series.Series', + + requires: ['Ext.chart.Shape', 'Ext.fx.Anim'], + + /* End Definitions */ + + type: "radar", + alias: 'series.radar', + + + rad: Math.PI / 180, + + showInLegend: false, + + /** + * @cfg {Object} style + * An object containing styles for overriding series styles from Theming. + */ + style: {}, + + constructor: function(config) { + this.callParent(arguments); + var me = this, + surface = me.chart.surface, i, l; + me.group = surface.getGroup(me.seriesId); + if (me.showMarkers) { + me.markerGroup = surface.getGroup(me.seriesId + '-markers'); + } + }, + + /** + * Draws the series for the current chart. + */ + drawSeries: function() { + var me = this, + store = me.chart.substore || me.chart.store, + group = me.group, + sprite, + chart = me.chart, + animate = chart.animate, + field = me.field || me.yField, + surface = chart.surface, + chartBBox = chart.chartBBox, + rendererAttributes, + centerX, centerY, + items, + radius, + maxValue = 0, + fields = [], + max = Math.max, + cos = Math.cos, + sin = Math.sin, + pi2 = Math.PI * 2, + l = store.getCount(), + startPath, path, x, y, rho, + i, nfields, + seriesStyle = me.seriesStyle, + seriesLabelStyle = me.seriesLabelStyle, + first = chart.resizing || !me.radar, + axis = chart.axes && chart.axes.get(0), + aggregate = !(axis && axis.maximum); + + me.setBBox(); + + maxValue = aggregate? 0 : (axis.maximum || 0); + + Ext.apply(seriesStyle, me.style || {}); + + //if the store is empty then there's nothing to draw + if (!store || !store.getCount()) { + return; + } + + me.unHighlightItem(); + me.cleanHighlights(); + + centerX = me.centerX = chartBBox.x + (chartBBox.width / 2); + centerY = me.centerY = chartBBox.y + (chartBBox.height / 2); + me.radius = radius = Math.min(chartBBox.width, chartBBox.height) /2; + me.items = items = []; + + if (aggregate) { + //get all renderer fields + chart.series.each(function(series) { + fields.push(series.yField); + }); + //get maxValue to interpolate + store.each(function(record, i) { + for (i = 0, nfields = fields.length; i < nfields; i++) { + maxValue = max(+record.get(fields[i]), maxValue); + } + }); + } + //ensure non-zero value. + maxValue = maxValue || 1; + //create path and items + startPath = []; path = []; + store.each(function(record, i) { + rho = radius * record.get(field) / maxValue; + x = rho * cos(i / l * pi2); + y = rho * sin(i / l * pi2); + if (i == 0) { + path.push('M', x + centerX, y + centerY); + startPath.push('M', 0.01 * x + centerX, 0.01 * y + centerY); + } else { + path.push('L', x + centerX, y + centerY); + startPath.push('L', 0.01 * x + centerX, 0.01 * y + centerY); + } + items.push({ + sprite: false, //TODO(nico): add markers + point: [centerX + x, centerY + y], + series: me + }); + }); + path.push('Z'); + //create path sprite + if (!me.radar) { + me.radar = surface.add(Ext.apply({ + type: 'path', + group: group, + path: startPath + }, seriesStyle || {})); + } + //reset on resizing + if (chart.resizing) { + me.radar.setAttributes({ + path: startPath + }, true); + } + //render/animate + if (chart.animate) { + me.onAnimate(me.radar, { + to: Ext.apply({ + path: path + }, seriesStyle || {}) + }); + } else { + me.radar.setAttributes(Ext.apply({ + path: path + }, seriesStyle || {}), true); + } + //render markers, labels and callouts + if (me.showMarkers) { + me.drawMarkers(); + } + me.renderLabels(); + me.renderCallouts(); + }, + + // @private draws the markers for the lines (if any). + drawMarkers: function() { + var me = this, + chart = me.chart, + surface = chart.surface, + markerStyle = Ext.apply({}, me.markerStyle || {}), + endMarkerStyle = Ext.apply(markerStyle, me.markerConfig), + items = me.items, + type = endMarkerStyle.type, + markerGroup = me.markerGroup, + centerX = me.centerX, + centerY = me.centerY, + item, i, l, marker; + + delete endMarkerStyle.type; + + for (i = 0, l = items.length; i < l; i++) { + item = items[i]; + marker = markerGroup.getAt(i); + if (!marker) { + marker = Ext.chart.Shape[type](surface, Ext.apply({ + group: markerGroup, + x: 0, + y: 0, + translate: { + x: centerX, + y: centerY + } + }, endMarkerStyle)); + } + else { + marker.show(); + } + if (chart.resizing) { + marker.setAttributes({ + x: 0, + y: 0, + translate: { + x: centerX, + y: centerY + } + }, true); + } + marker._to = { + translate: { + x: item.point[0], + y: item.point[1] + } + }; + //render/animate + if (chart.animate) { + me.onAnimate(marker, { + to: marker._to + }); + } + else { + marker.setAttributes(Ext.apply(marker._to, endMarkerStyle || {}), true); + } + } + }, + + isItemInPoint: function(x, y, item) { + var point, + tolerance = 10, + abs = Math.abs; + point = item.point; + return (abs(point[0] - x) <= tolerance && + abs(point[1] - y) <= tolerance); + }, + + // @private callback for when creating a label sprite. + onCreateLabel: function(storeItem, item, i, display) { + var me = this, + group = me.labelsGroup, + config = me.label, + centerX = me.centerX, + centerY = me.centerY, + point = item.point, + endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config); + + return me.chart.surface.add(Ext.apply({ + 'type': 'text', + 'text-anchor': 'middle', + 'group': group, + 'x': centerX, + 'y': centerY + }, config || {})); + }, + + // @private callback for when placing a label sprite. + onPlaceLabel: function(label, storeItem, item, i, display, animate) { + var me = this, + chart = me.chart, + resizing = chart.resizing, + config = me.label, + format = config.renderer, + field = config.field, + centerX = me.centerX, + centerY = me.centerY, + opt = { + x: item.point[0], + y: item.point[1] + }, + x = opt.x - centerX, + y = opt.y - centerY; + + label.setAttributes({ + text: format(storeItem.get(field)), + hidden: true + }, + true); + + if (resizing) { + label.setAttributes({ + x: centerX, + y: centerY + }, true); + } + + if (animate) { + label.show(true); + me.onAnimate(label, { + to: opt + }); + } else { + label.setAttributes(opt, true); + label.show(true); + } + }, + + // @private for toggling (show/hide) series. + toggleAll: function(show) { + var me = this, + i, ln, shadow, shadows; + if (!show) { + Ext.chart.series.Radar.superclass.hideAll.call(me); + } + else { + Ext.chart.series.Radar.superclass.showAll.call(me); + } + if (me.radar) { + me.radar.setAttributes({ + hidden: !show + }, true); + //hide shadows too + if (me.radar.shadows) { + for (i = 0, shadows = me.radar.shadows, ln = shadows.length; i < ln; i++) { + shadow = shadows[i]; + shadow.setAttributes({ + hidden: !show + }, true); + } + } + } + }, + + // @private hide all elements in the series. + hideAll: function() { + this.toggleAll(false); + this.hideMarkers(0); + }, + + // @private show all elements in the series. + showAll: function() { + this.toggleAll(true); + }, + + // @private hide all markers that belong to `markerGroup` + hideMarkers: function(index) { + var me = this, + count = me.markerGroup && me.markerGroup.getCount() || 0, + i = index || 0; + for (; i < count; i++) { + me.markerGroup.getAt(i).hide(true); + } + } +}); + + +/** + * @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: + * +{@img Ext.chart.series.Scatter/Ext.chart.series.Scatter.png Ext.chart.series.Scatter chart series} +

+    var store = Ext.create('Ext.data.JsonStore', {
+        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}                                                
+        ]
+    });
+    
+    Ext.create('Ext.chart.Chart', {
+        renderTo: Ext.getBody(),
+        width: 500,
+        height: 300,
+        animate: true,
+        theme:'Category2',
+        store: store,
+        axes: [{
+            type: 'Numeric',
+            position: 'bottom',
+            fields: ['data1', 'data2', 'data3'],
+            title: 'Sample Values',
+            grid: true,
+            minimum: 0
+        }, {
+            type: 'Category',
+            position: 'left',
+            fields: ['name'],
+            title: 'Sample Metrics'
+        }],
+        series: [{
+            type: 'scatter',
+            markerConfig: {
+                radius: 5,
+                size: 5
+            },
+            axis: 'left',
+            xField: 'name',
+            yField: 'data2'
+        }, {
+            type: 'scatter',
+            markerConfig: {
+                radius: 5,
+                size: 5
+            },
+            axis: 'left',
+            xField: 'name',
+            yField: 'data3'
+        }]   
+    });
+   
+ + * + * 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 `markerConfig` object. Finally we set the left axis as + * axis to show the current values of the elements. + * + * @xtype scatter + * + */ +Ext.define('Ext.chart.series.Scatter', { + + /* Begin Definitions */ + + extend: 'Ext.chart.series.Cartesian', + + requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.fx.Anim'], + + /* End Definitions */ + + type: 'scatter', + alias: 'series.scatter', + + /** + * @cfg {Object} markerConfig + * The display style for the scatter series markers. + */ + + /** + * @cfg {Object} style + * Append styling properties to this object for it to override theme properties. + */ + + constructor: function(config) { + this.callParent(arguments); + var me = this, + shadow = me.chart.shadow, + surface = me.chart.surface, i, l; + Ext.apply(me, config, { + style: {}, + markerConfig: {}, + shadowAttributes: [{ + "stroke-width": 6, + "stroke-opacity": 0.05, + stroke: 'rgb(0, 0, 0)' + }, { + "stroke-width": 4, + "stroke-opacity": 0.1, + stroke: 'rgb(0, 0, 0)' + }, { + "stroke-width": 2, + "stroke-opacity": 0.15, + stroke: 'rgb(0, 0, 0)' + }] + }); + me.group = surface.getGroup(me.seriesId); + if (shadow) { + for (i = 0, l = me.shadowAttributes.length; i < l; i++) { + me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); + } + } + }, + + // @private Get chart and data boundaries + getBounds: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + axes = [].concat(me.axis), + bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends; + + me.setBBox(); + bbox = me.bbox; + + for (i = 0, ln = axes.length; i < ln; i++) { + axis = chart.axes.get(axes[i]); + if (axis) { + ends = axis.calcEnds(); + if (axis.position == 'top' || axis.position == 'bottom') { + minX = ends.from; + maxX = ends.to; + } + else { + minY = ends.from; + maxY = ends.to; + } + } + } + // If a field was specified without a corresponding axis, create one to get bounds + if (me.xField && !Ext.isNumber(minX)) { + axis = Ext.create('Ext.chart.axis.Axis', { + chart: chart, + fields: [].concat(me.xField) + }).calcEnds(); + minX = axis.from; + maxX = axis.to; + } + if (me.yField && !Ext.isNumber(minY)) { + axis = Ext.create('Ext.chart.axis.Axis', { + chart: chart, + fields: [].concat(me.yField) + }).calcEnds(); + minY = axis.from; + maxY = axis.to; + } + + if (isNaN(minX)) { + minX = 0; + maxX = store.getCount() - 1; + xScale = bbox.width / (store.getCount() - 1); + } + else { + xScale = bbox.width / (maxX - minX); + } + + if (isNaN(minY)) { + minY = 0; + maxY = store.getCount() - 1; + yScale = bbox.height / (store.getCount() - 1); + } + else { + yScale = bbox.height / (maxY - minY); + } + + return { + bbox: bbox, + minX: minX, + minY: minY, + xScale: xScale, + yScale: yScale + }; + }, + + // @private Build an array of paths for the chart + getPaths: function() { + var me = this, + chart = me.chart, + enableShadows = chart.shadow, + store = chart.substore || chart.store, + group = me.group, + bounds = me.bounds = me.getBounds(), + bbox = me.bbox, + xScale = bounds.xScale, + yScale = bounds.yScale, + minX = bounds.minX, + minY = bounds.minY, + boxX = bbox.x, + boxY = bbox.y, + boxHeight = bbox.height, + items = me.items = [], + attrs = [], + x, y, xValue, yValue, sprite; + + store.each(function(record, i) { + xValue = record.get(me.xField); + yValue = record.get(me.yField); + //skip undefined values + if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) { + // + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn("[Ext.chart.series.Scatter] Skipping a store element with an undefined value at ", record, xValue, yValue); + } + // + return; + } + // Ensure a value + if (typeof xValue == 'string' || typeof xValue == 'object') { + xValue = i; + } + if (typeof yValue == 'string' || typeof yValue == 'object') { + yValue = i; + } + x = boxX + (xValue - minX) * xScale; + y = boxY + boxHeight - (yValue - minY) * yScale; + attrs.push({ + x: x, + y: y + }); + + me.items.push({ + series: me, + value: [xValue, yValue], + point: [x, y], + storeItem: record + }); + + // When resizing, reset before animating + if (chart.animate && chart.resizing) { + sprite = group.getAt(i); + if (sprite) { + me.resetPoint(sprite); + if (enableShadows) { + me.resetShadow(sprite); + } + } + } + }); + return attrs; + }, + + // @private translate point to the center + resetPoint: function(sprite) { + var bbox = this.bbox; + sprite.setAttributes({ + translate: { + x: (bbox.x + bbox.width) / 2, + y: (bbox.y + bbox.height) / 2 + } + }, true); + }, + + // @private translate shadows of a sprite to the center + resetShadow: function(sprite) { + var me = this, + shadows = sprite.shadows, + shadowAttributes = me.shadowAttributes, + ln = me.shadowGroups.length, + bbox = me.bbox, + i, attr; + for (i = 0; i < ln; i++) { + attr = Ext.apply({}, shadowAttributes[i]); + if (attr.translate) { + attr.translate.x += (bbox.x + bbox.width) / 2; + attr.translate.y += (bbox.y + bbox.height) / 2; + } + else { + attr.translate = { + x: (bbox.x + bbox.width) / 2, + y: (bbox.y + bbox.height) / 2 + }; + } + shadows[i].setAttributes(attr, true); + } + }, + + // @private create a new point + createPoint: function(attr, type) { + var me = this, + chart = me.chart, + group = me.group, + bbox = me.bbox; + + return Ext.chart.Shape[type](chart.surface, Ext.apply({}, { + x: 0, + y: 0, + group: group, + translate: { + x: (bbox.x + bbox.width) / 2, + y: (bbox.y + bbox.height) / 2 + } + }, attr)); + }, + + // @private create a new set of shadows for a sprite + createShadow: function(sprite, endMarkerStyle, type) { + var me = this, + chart = me.chart, + shadowGroups = me.shadowGroups, + shadowAttributes = me.shadowAttributes, + lnsh = shadowGroups.length, + bbox = me.bbox, + i, shadow, shadows, attr; + + sprite.shadows = shadows = []; + + for (i = 0; i < lnsh; i++) { + attr = Ext.apply({}, shadowAttributes[i]); + if (attr.translate) { + attr.translate.x += (bbox.x + bbox.width) / 2; + attr.translate.y += (bbox.y + bbox.height) / 2; + } + else { + Ext.apply(attr, { + translate: { + x: (bbox.x + bbox.width) / 2, + y: (bbox.y + bbox.height) / 2 + } + }); + } + Ext.apply(attr, endMarkerStyle); + shadow = Ext.chart.Shape[type](chart.surface, Ext.apply({}, { + x: 0, + y: 0, + group: shadowGroups[i] + }, attr)); + shadows.push(shadow); + } + }, + + /** + * Draws the series for the current chart. + */ + drawSeries: function() { + var me = this, + chart = me.chart, + store = chart.substore || chart.store, + group = me.group, + enableShadows = chart.shadow, + shadowGroups = me.shadowGroups, + shadowAttributes = me.shadowAttributes, + lnsh = shadowGroups.length, + sprite, attrs, attr, ln, i, endMarkerStyle, shindex, type, shadows, + rendererAttributes, shadowAttribute; + + endMarkerStyle = Ext.apply(me.markerStyle, me.markerConfig); + type = endMarkerStyle.type; + delete endMarkerStyle.type; + + //if the store is empty then there's nothing to be rendered + if (!store || !store.getCount()) { + return; + } + + me.unHighlightItem(); + me.cleanHighlights(); + + attrs = me.getPaths(); + ln = attrs.length; + for (i = 0; i < ln; i++) { + attr = attrs[i]; + sprite = group.getAt(i); + Ext.apply(attr, endMarkerStyle); + + // Create a new sprite if needed (no height) + if (!sprite) { + sprite = me.createPoint(attr, type); + if (enableShadows) { + me.createShadow(sprite, endMarkerStyle, type); + } + } + + shadows = sprite.shadows; + if (chart.animate) { + rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store); + sprite._to = rendererAttributes; + me.onAnimate(sprite, { + to: rendererAttributes + }); + //animate shadows + for (shindex = 0; shindex < lnsh; shindex++) { + shadowAttribute = Ext.apply({}, shadowAttributes[shindex]); + rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, { + translate: { + x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0), + y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0) + } + }, shadowAttribute), i, store); + me.onAnimate(shadows[shindex], { to: rendererAttributes }); + } + } + else { + rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply({ translate: attr }, { hidden: false }), i, store); + sprite.setAttributes(rendererAttributes, true); + //update shadows + for (shindex = 0; shindex < lnsh; shindex++) { + shadowAttribute = shadowAttributes[shindex]; + rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({ + x: attr.x, + y: attr.y + }, shadowAttribute), i, store); + shadows[shindex].setAttributes(rendererAttributes, true); + } + } + me.items[i].sprite = sprite; + } + + // Hide unused sprites + ln = group.getCount(); + for (i = attrs.length; i < ln; i++) { + group.getAt(i).hide(true); + } + me.renderLabels(); + me.renderCallouts(); + }, + + // @private callback for when creating a label sprite. + onCreateLabel: function(storeItem, item, i, display) { + var me = this, + group = me.labelsGroup, + config = me.label, + endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle), + bbox = me.bbox; + + return me.chart.surface.add(Ext.apply({ + type: 'text', + group: group, + x: item.point[0], + y: bbox.y + bbox.height / 2 + }, endLabelStyle)); + }, + + // @private callback for when placing a label sprite. + onPlaceLabel: function(label, storeItem, item, i, display, animate) { + var me = this, + chart = me.chart, + resizing = chart.resizing, + config = me.label, + format = config.renderer, + field = config.field, + bbox = me.bbox, + x = item.point[0], + y = item.point[1], + radius = item.sprite.attr.radius, + bb, width, height, anim; + + label.setAttributes({ + text: format(storeItem.get(field)), + hidden: true + }, true); + + if (display == 'rotate') { + label.setAttributes({ + 'text-anchor': 'start', + 'rotation': { + x: x, + y: y, + degrees: -45 + } + }, true); + //correct label position to fit into the box + bb = label.getBBox(); + width = bb.width; + height = bb.height; + x = x < bbox.x? bbox.x : x; + x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x; + y = (y - height < bbox.y)? bbox.y + height : y; + + } else if (display == 'under' || display == 'over') { + //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined. + bb = item.sprite.getBBox(); + bb.width = bb.width || (radius * 2); + bb.height = bb.height || (radius * 2); + y = y + (display == 'over'? -bb.height : bb.height); + //correct label position to fit into the box + bb = label.getBBox(); + width = bb.width/2; + height = bb.height/2; + x = x - width < bbox.x ? bbox.x + width : x; + x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x; + y = y - height < bbox.y? bbox.y + height : y; + y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y; + } + + if (!chart.animate) { + label.setAttributes({ + x: x, + y: y + }, true); + label.show(true); + } + else { + if (resizing) { + anim = item.sprite.getActiveAnimation(); + if (anim) { + anim.on('afteranimate', function() { + label.setAttributes({ + x: x, + y: y + }, true); + label.show(true); + }); + } + else { + label.show(true); + } + } + else { + me.onAnimate(label, { + to: { + x: x, + y: y + } + }); + } + } + }, + + // @private callback for when placing a callout sprite. + onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) { + var me = this, + chart = me.chart, + surface = chart.surface, + resizing = chart.resizing, + config = me.callouts, + items = me.items, + cur = item.point, + normal, + bbox = callout.label.getBBox(), + offsetFromViz = 30, + offsetToSide = 10, + offsetBox = 3, + boxx, boxy, boxw, boxh, + p, clipRect = me.bbox, + x, y; + + //position + normal = [Math.cos(Math.PI /4), -Math.sin(Math.PI /4)]; + x = cur[0] + normal[0] * offsetFromViz; + y = cur[1] + normal[1] * offsetFromViz; + + //box position and dimensions + boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); + boxy = y - bbox.height /2 - offsetBox; + boxw = bbox.width + 2 * offsetBox; + boxh = bbox.height + 2 * offsetBox; + + //now check if we're out of bounds and invert the normal vector correspondingly + //this may add new overlaps between labels (but labels won't be out of bounds). + if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) { + normal[0] *= -1; + } + if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) { + normal[1] *= -1; + } + + //update positions + x = cur[0] + normal[0] * offsetFromViz; + y = cur[1] + normal[1] * offsetFromViz; + + //update box position and dimensions + boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox)); + boxy = y - bbox.height /2 - offsetBox; + boxw = bbox.width + 2 * offsetBox; + boxh = bbox.height + 2 * offsetBox; + + if (chart.animate) { + //set the line from the middle of the pie to the box. + me.onAnimate(callout.lines, { + to: { + path: ["M", cur[0], cur[1], "L", x, y, "Z"] + } + }, true); + //set box position + me.onAnimate(callout.box, { + to: { + x: boxx, + y: boxy, + width: boxw, + height: boxh + } + }, true); + //set text position + me.onAnimate(callout.label, { + to: { + x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)), + y: y + } + }, true); + } else { + //set the line from the middle of the pie to the box. + callout.lines.setAttributes({ + path: ["M", cur[0], cur[1], "L", x, y, "Z"] + }, true); + //set box position + callout.box.setAttributes({ + x: boxx, + y: boxy, + width: boxw, + height: boxh + }, true); + //set text position + callout.label.setAttributes({ + x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)), + y: y + }, true); + } + for (p in callout) { + callout[p].show(true); + } + }, + + // @private handles sprite animation for the series. + onAnimate: function(sprite, attr) { + sprite.show(); + return this.callParent(arguments); + }, + + isItemInPoint: function(x, y, item) { + var point, + tolerance = 10, + abs = Math.abs; + + function dist(point) { + var dx = abs(point[0] - x), + dy = abs(point[1] - y); + return Math.sqrt(dx * dx + dy * dy); + } + point = item.point; + return (point[0] - tolerance <= x && point[0] + tolerance >= x && + point[1] - tolerance <= y && point[1] + tolerance >= y); + } +}); + + +/** + * @class Ext.chart.theme.Base + * Provides default colors for non-specified things. Should be sub-classed when creating new themes. + * @ignore + */ +Ext.define('Ext.chart.theme.Base', { + + /* Begin Definitions */ + + requires: ['Ext.chart.theme.Theme'], + + /* End Definitions */ + + constructor: function(config) { + Ext.chart.theme.call(this, config, { + background: false, + axis: { + stroke: '#444', + 'stroke-width': 1 + }, + axisLabelTop: { + fill: '#444', + font: '12px Arial, Helvetica, sans-serif', + spacing: 2, + padding: 5, + renderer: function(v) { return v; } + }, + axisLabelRight: { + fill: '#444', + font: '12px Arial, Helvetica, sans-serif', + spacing: 2, + padding: 5, + renderer: function(v) { return v; } + }, + axisLabelBottom: { + fill: '#444', + font: '12px Arial, Helvetica, sans-serif', + spacing: 2, + padding: 5, + renderer: function(v) { return v; } + }, + axisLabelLeft: { + fill: '#444', + font: '12px Arial, Helvetica, sans-serif', + spacing: 2, + padding: 5, + renderer: function(v) { return v; } + }, + axisTitleTop: { + font: 'bold 18px Arial', + fill: '#444' + }, + axisTitleRight: { + font: 'bold 18px Arial', + fill: '#444', + rotate: { + x:0, y:0, + degrees: 270 + } + }, + axisTitleBottom: { + font: 'bold 18px Arial', + fill: '#444' + }, + axisTitleLeft: { + font: 'bold 18px Arial', + fill: '#444', + rotate: { + x:0, y:0, + degrees: 270 + } + }, + series: { + 'stroke-width': 0 + }, + seriesLabel: { + font: '12px Arial', + fill: '#333' + }, + marker: { + stroke: '#555', + fill: '#000', + radius: 3, + size: 3 + }, + colors: [ "#94ae0a", "#115fa6","#a61120", "#ff8809", "#ffd13e", "#a61187", "#24ad9a", "#7c7474", "#a66111"], + seriesThemes: [{ + fill: "#115fa6" + }, { + fill: "#94ae0a" + }, { + fill: "#a61120" + }, { + fill: "#ff8809" + }, { + fill: "#ffd13e" + }, { + fill: "#a61187" + }, { + fill: "#24ad9a" + }, { + fill: "#7c7474" + }, { + fill: "#a66111" + }], + markerThemes: [{ + fill: "#115fa6", + type: 'circle' + }, { + fill: "#94ae0a", + type: 'cross' + }, { + fill: "#a61120", + type: 'plus' + }] + }); + } +}, function() { + var palette = ['#b1da5a', '#4ce0e7', '#e84b67', '#da5abd', '#4d7fe6', '#fec935'], + names = ['Green', 'Sky', 'Red', 'Purple', 'Blue', 'Yellow'], + i = 0, j = 0, l = palette.length, themes = Ext.chart.theme, + categories = [['#f0a50a', '#c20024', '#2044ba', '#810065', '#7eae29'], + ['#6d9824', '#87146e', '#2a9196', '#d39006', '#1e40ac'], + ['#fbbc29', '#ce2e4e', '#7e0062', '#158b90', '#57880e'], + ['#ef5773', '#fcbd2a', '#4f770d', '#1d3eaa', '#9b001f'], + ['#7eae29', '#fdbe2a', '#910019', '#27b4bc', '#d74dbc'], + ['#44dce1', '#0b2592', '#996e05', '#7fb325', '#b821a1']], + cats = categories.length; + + //Create themes from base colors + for (; i < l; i++) { + themes[names[i]] = (function(color) { + return Ext.extend(themes.Base, { + constructor: function(config) { + themes.Base.prototype.constructor.call(this, Ext.apply({ + baseColor: color + }, config)); + } + }); + })(palette[i]); + } + + //Create theme from color array + for (i = 0; i < cats; i++) { + themes['Category' + (i + 1)] = (function(category) { + return Ext.extend(themes.Base, { + constructor: function(config) { + themes.Base.prototype.constructor.call(this, Ext.apply({ + colors: category + }, config)); + } + }); + })(categories[i]); + } +}); + +/** + * @author Ed Spencer + * @class Ext.data.ArrayStore + * @extends Ext.data.Store + * @ignore + * + *

Small helper class to make creating {@link Ext.data.Store}s from Array data easier. + * An ArrayStore will be automatically configured with a {@link Ext.data.reader.Array}.

+ * + *

A store configuration would be something like:

+

+var store = new Ext.data.ArrayStore({
+    // store configs
+    autoDestroy: true,
+    storeId: 'myStore',
+    // reader configs
+    idIndex: 0,
+    fields: [
+       'company',
+       {name: 'price', type: 'float'},
+       {name: 'change', type: 'float'},
+       {name: 'pctChange', type: 'float'},
+       {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
+    ]
+});
+
+ *

This store is configured to consume a returned object of the form: +


+var myData = [
+    ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
+    ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
+    ['Boeing Co.',75.43,0.53,0.71,'9/1 12:00am'],
+    ['Hewlett-Packard Co.',36.53,-0.03,-0.08,'9/1 12:00am'],
+    ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
+];
+
+* + *

An object literal of this form could also be used as the {@link #data} config option.

+ * + *

*Note: Although not listed here, this class accepts all of the configuration options of + * {@link Ext.data.reader.Array ArrayReader}.

+ * + * @constructor + * @param {Object} config + * @xtype arraystore + */ +Ext.define('Ext.data.ArrayStore', { + extend: 'Ext.data.Store', + alias: 'store.array', + uses: ['Ext.data.reader.Array'], + + /** + * @cfg {Ext.data.DataReader} reader @hide + */ + constructor: function(config) { + config = config || {}; + + Ext.applyIf(config, { + proxy: { + type: 'memory', + reader: 'array' + } + }); + + this.callParent([config]); + }, + + loadData: function(data, append) { + if (this.expandData === true) { + var r = [], + i = 0, + ln = data.length; + + for (; i < ln; i++) { + r[r.length] = [data[i]]; + } + + data = r; + } + + this.callParent([data, append]); + } +}, function() { + // backwards compat + Ext.data.SimpleStore = Ext.data.ArrayStore; + // Ext.reg('simplestore', Ext.data.SimpleStore); +}); + +/** + * @author Ed Spencer + * @class Ext.data.Batch + * + *

Provides a mechanism to run one or more {@link Ext.data.Operation operations} in a given order. Fires the 'operationcomplete' event + * after the completion of each Operation, and the 'complete' event when all Operations have been successfully executed. Fires an 'exception' + * event if any of the Operations encounter an exception.

+ * + *

Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes

+ * + * @constructor + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.Batch', { + mixins: { + observable: 'Ext.util.Observable' + }, + + /** + * True to immediately start processing the batch as soon as it is constructed (defaults to false) + * @property autoStart + * @type Boolean + */ + autoStart: false, + + /** + * The index of the current operation being executed + * @property current + * @type Number + */ + current: -1, + + /** + * The total number of operations in this batch. Read only + * @property total + * @type Number + */ + total: 0, + + /** + * True if the batch is currently running + * @property isRunning + * @type Boolean + */ + isRunning: false, + + /** + * True if this batch has been executed completely + * @property isComplete + * @type Boolean + */ + isComplete: false, + + /** + * True if this batch has encountered an exception. This is cleared at the start of each operation + * @property hasException + * @type Boolean + */ + hasException: false, + + /** + * True to automatically pause the execution of the batch if any operation encounters an exception (defaults to true) + * @property pauseOnException + * @type Boolean + */ + pauseOnException: true, + + constructor: function(config) { + var me = this; + + me.addEvents( + /** + * @event complete + * Fired when all operations of this batch have been completed + * @param {Ext.data.Batch} batch The batch object + * @param {Object} operation The last operation that was executed + */ + 'complete', + + /** + * @event exception + * Fired when a operation encountered an exception + * @param {Ext.data.Batch} batch The batch object + * @param {Object} operation The operation that encountered the exception + */ + 'exception', + + /** + * @event operationcomplete + * Fired when each operation of the batch completes + * @param {Ext.data.Batch} batch The batch object + * @param {Object} operation The operation that just completed + */ + 'operationcomplete' + ); + + me.mixins.observable.constructor.call(me, config); + + /** + * Ordered array of operations that will be executed by this batch + * @property operations + * @type Array + */ + me.operations = []; + }, + + /** + * Adds a new operation to this batch + * @param {Object} operation The {@link Ext.data.Operation Operation} object + */ + add: function(operation) { + this.total++; + + operation.setBatch(this); + + this.operations.push(operation); + }, + + /** + * Kicks off the execution of the batch, continuing from the next operation if the previous + * operation encountered an exception, or if execution was paused + */ + start: function() { + this.hasException = false; + this.isRunning = true; + + this.runNextOperation(); + }, + + /** + * @private + * Runs the next operation, relative to this.current. + */ + runNextOperation: function() { + this.runOperation(this.current + 1); + }, + + /** + * Pauses execution of the batch, but does not cancel the current operation + */ + pause: function() { + this.isRunning = false; + }, + + /** + * Executes a operation by its numeric index + * @param {Number} index The operation index to run + */ + runOperation: function(index) { + var me = this, + operations = me.operations, + operation = operations[index], + onProxyReturn; + + if (operation === undefined) { + me.isRunning = false; + me.isComplete = true; + me.fireEvent('complete', me, operations[operations.length - 1]); + } else { + me.current = index; + + onProxyReturn = function(operation) { + var hasException = operation.hasException(); + + if (hasException) { + me.hasException = true; + me.fireEvent('exception', me, operation); + } else { + me.fireEvent('operationcomplete', me, operation); + } + + if (hasException && me.pauseOnException) { + me.pause(); + } else { + operation.setCompleted(); + me.runNextOperation(); + } + }; + + operation.setStarted(); + + me.proxy[operation.action](operation, onProxyReturn, me); + } + } +}); +/** + * @author Ed Spencer + * @class Ext.data.BelongsToAssociation + * @extends Ext.data.Association + * + *

Represents a many to one association with another model. The owner model is expected to have + * a foreign key which references the primary key of the associated model:

+ * +

+Ext.define('Category', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'id',   type: 'int'},
+        {name: 'name', type: 'string'}
+    ]
+});
+
+Ext.define('Product', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'id',          type: 'int'},
+        {name: 'category_id', type: 'int'},
+        {name: 'name',        type: 'string'}
+    ],
+    // we can use the belongsTo shortcut on the model to create a belongsTo association
+    belongsTo: {type: 'belongsTo', model: 'Category'}
+});
+
+ *

In the example above we have created models for Products and Categories, and linked them together + * by saying that each Product belongs to a Category. This automatically links each Product to a Category + * based on the Product's category_id, and provides new functions on the Product model:

+ * + *

Generated getter function

+ * + *

The first function that is added to the owner model is a getter function:

+ * +

+var product = new Product({
+    id: 100,
+    category_id: 20,
+    name: 'Sneakers'
+});
+
+product.getCategory(function(category, operation) {
+    //do something with the category object
+    alert(category.get('id')); //alerts 20
+}, this);
+
+* + *

The getCategory function was created on the Product model when we defined the association. This uses the + * Category's configured {@link Ext.data.proxy.Proxy proxy} to load the Category asynchronously, calling the provided + * callback when it has loaded.

+ * + *

The new getCategory function will also accept an object containing success, failure and callback properties + * - callback will always be called, success will only be called if the associated model was loaded successfully + * and failure will only be called if the associatied model could not be loaded:

+ * +

+product.getCategory({
+    callback: function(category, operation) {}, //a function that will always be called
+    success : function(category, operation) {}, //a function that will only be called if the load succeeded
+    failure : function(category, operation) {}, //a function that will only be called if the load did not succeed
+    scope   : this //optionally pass in a scope object to execute the callbacks in
+});
+
+ * + *

In each case above the callbacks are called with two arguments - the associated model instance and the + * {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is + * useful when the instance could not be loaded.

+ * + *

Generated setter function

+ * + *

The second generated function sets the associated model instance - if only a single argument is passed to + * the setter then the following two calls are identical:

+ * +

+//this call
+product.setCategory(10);
+
+//is equivalent to this call:
+product.set('category_id', 10);
+
+ *

If we pass in a second argument, the model will be automatically saved and the second argument passed to + * the owner model's {@link Ext.data.Model#save save} method:

+

+product.setCategory(10, function(product, operation) {
+    //the product has been saved
+    alert(product.get('category_id')); //now alerts 10
+});
+
+//alternative syntax:
+product.setCategory(10, {
+    callback: function(product, operation), //a function that will always be called
+    success : function(product, operation), //a function that will only be called if the load succeeded
+    failure : function(product, operation), //a function that will only be called if the load did not succeed
+    scope   : this //optionally pass in a scope object to execute the callbacks in
+})
+
+* + *

Customisation

+ * + *

Associations reflect on the models they are linking to automatically set up properties such as the + * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:

+ * +

+Ext.define('Product', {
+    fields: [...],
+
+    associations: [
+        {type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id'}
+    ]
+});
+ 
+ * + *

Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id') + * with our own settings. Usually this will not be needed.

+ */ +Ext.define('Ext.data.BelongsToAssociation', { + extend: 'Ext.data.Association', + + alias: 'association.belongsto', + + /** + * @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated + * model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a + * model called Product would set up a product_id foreign key. + *

+Ext.define('Order', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'date'],
+    hasMany: 'Product'
+});
+
+Ext.define('Product', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to
+    belongsTo: 'Group'
+});
+var product = new Product({
+    id: 1,
+    name: 'Product 1',
+    order_id: 22
+}, 1);
+product.getOrder(); // Will make a call to the server asking for order_id 22
+
+     * 
+ */ + + /** + * @cfg {String} getterName The name of the getter function that will be added to the local model's prototype. + * Defaults to 'get' + the name of the foreign model, e.g. getCategory + */ + + /** + * @cfg {String} setterName The name of the setter function that will be added to the local model's prototype. + * Defaults to 'set' + the name of the foreign model, e.g. setCategory + */ + + /** + * @cfg {String} type The type configuration can be used when creating associations using a configuration object. + * Use 'belongsTo' to create a HasManyAssocation + *

+associations: [{
+    type: 'belongsTo',
+    model: 'User'
+}]
+     * 
+ */ + + constructor: function(config) { + this.callParent(arguments); + + var me = this, + ownerProto = me.ownerModel.prototype, + associatedName = me.associatedName, + getterName = me.getterName || 'get' + associatedName, + setterName = me.setterName || 'set' + associatedName; + + Ext.applyIf(me, { + name : associatedName, + foreignKey : associatedName.toLowerCase() + "_id", + instanceName: associatedName + 'BelongsToInstance', + associationKey: associatedName.toLowerCase() + }); + + ownerProto[getterName] = me.createGetter(); + ownerProto[setterName] = me.createSetter(); + }, + + /** + * @private + * Returns a setter function to be placed on the owner model's prototype + * @return {Function} The setter function + */ + createSetter: function() { + var me = this, + ownerModel = me.ownerModel, + associatedModel = me.associatedModel, + foreignKey = me.foreignKey, + primaryKey = me.primaryKey; + + //'this' refers to the Model instance inside this function + return function(value, options, scope) { + this.set(foreignKey, value); + + if (typeof options == 'function') { + options = { + callback: options, + scope: scope || this + }; + } + + if (Ext.isObject(options)) { + return this.save(options); + } + }; + }, + + /** + * @private + * Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance + * the first time it is loaded so that subsequent calls to the getter always receive the same reference. + * @return {Function} The getter function + */ + createGetter: function() { + var me = this, + ownerModel = me.ownerModel, + associatedName = me.associatedName, + associatedModel = me.associatedModel, + foreignKey = me.foreignKey, + primaryKey = me.primaryKey, + instanceName = me.instanceName; + + //'this' refers to the Model instance inside this function + return function(options, scope) { + options = options || {}; + + var foreignKeyId = this.get(foreignKey), + instance, callbackFn; + + if (this[instanceName] === undefined) { + instance = Ext.ModelManager.create({}, associatedName); + instance.set(primaryKey, foreignKeyId); + + if (typeof options == 'function') { + options = { + callback: options, + scope: scope || this + }; + } + + associatedModel.load(foreignKeyId, options); + } else { + instance = this[instanceName]; + + //TODO: We're duplicating the callback invokation code that the instance.load() call above + //makes here - ought to be able to normalize this - perhaps by caching at the Model.load layer + //instead of the association layer. + if (typeof options == 'function') { + options.call(scope || this, instance); + } + + if (options.success) { + options.success.call(scope || this, instance); + } + + if (options.callback) { + options.callback.call(scope || this, instance); + } + + return instance; + } + }; + }, + + /** + * Read associated data + * @private + * @param {Ext.data.Model} record The record we're writing to + * @param {Ext.data.reader.Reader} reader The reader for the associated model + * @param {Object} associationData The raw associated data + */ + read: function(record, reader, associationData){ + record[this.instanceName] = reader.read([associationData]).records[0]; + } +}); + +/** + * @class Ext.data.BufferStore + * @extends Ext.data.Store + * @ignore + */ +Ext.define('Ext.data.BufferStore', { + extend: 'Ext.data.Store', + alias: 'store.buffer', + sortOnLoad: false, + filterOnLoad: false, + + constructor: function() { + Ext.Error.raise('The BufferStore class has been deprecated. Instead, specify the buffered config option on Ext.data.Store'); + } +}); +/** + * @class Ext.direct.Manager + *

Overview

+ * + *

Ext.Direct aims to streamline communication between the client and server + * by providing a single interface that reduces the amount of common code + * typically required to validate data and handle returned data packets + * (reading data, error conditions, etc).

+ * + *

The Ext.direct namespace includes several classes for a closer integration + * with the server-side. The Ext.data namespace also includes classes for working + * with Ext.data.Stores which are backed by data from an Ext.Direct method.

+ * + *

Specification

+ * + *

For additional information consult the + * Ext.Direct Specification.

+ * + *

Providers

+ * + *

Ext.Direct uses a provider architecture, where one or more providers are + * used to transport data to and from the server. There are several providers + * that exist in the core at the moment:

    + * + *
  • {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
  • + *
  • {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
  • + *
  • {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side + * on the client.
  • + *
+ * + *

A provider does not need to be invoked directly, providers are added via + * {@link Ext.direct.Manager}.{@link Ext.direct.Manager#add add}.

+ * + *

Router

+ * + *

Ext.Direct utilizes a "router" on the server to direct requests from the client + * to the appropriate server-side method. Because the Ext.Direct API is completely + * platform-agnostic, you could completely swap out a Java based server solution + * and replace it with one that uses C# without changing the client side JavaScript + * at all.

+ * + *

Server side events

+ * + *

Custom events from the server may be handled by the client by adding + * listeners, for example:

+ *

+{"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
+
+// add a handler for a 'message' event sent by the server
+Ext.direct.Manager.on('message', function(e){
+    out.append(String.format('<p><i>{0}</i></p>', e.data));
+            out.el.scrollTo('t', 100000, true);
+});
+ * 
+ * @singleton + */ + +Ext.define('Ext.direct.Manager', { + + /* Begin Definitions */ + singleton: true, + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: ['Ext.util.MixedCollection'], + + statics: { + exceptions: { + TRANSPORT: 'xhr', + PARSE: 'parse', + LOGIN: 'login', + SERVER: 'exception' + } + }, + + /* End Definitions */ + + constructor: function(){ + var me = this; + + me.addEvents( + /** + * @event event + * Fires after an event. + * @param {event} e The Ext.direct.Event type that occurred. + * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}. + */ + 'event', + /** + * @event exception + * Fires after an event exception. + * @param {event} e The Ext.direct.Event type that occurred. + */ + 'exception' + ); + me.transactions = Ext.create('Ext.util.MixedCollection'); + me.providers = Ext.create('Ext.util.MixedCollection'); + + me.mixins.observable.constructor.call(me); + }, + + /** + * Adds an Ext.Direct Provider and creates the proxy or stub methods to execute server-side methods. + * If the provider is not already connected, it will auto-connect. + *

+var pollProv = new Ext.direct.PollingProvider({
+    url: 'php/poll2.php'
+});
+
+Ext.direct.Manager.addProvider({
+    "type":"remoting",       // create a {@link Ext.direct.RemotingProvider}
+    "url":"php\/router.php", // url to connect to the Ext.Direct server-side router.
+    "actions":{              // each property within the actions object represents a Class
+        "TestAction":[       // array of methods within each server side Class
+        {
+            "name":"doEcho", // name of method
+            "len":1
+        },{
+            "name":"multiply",
+            "len":1
+        },{
+            "name":"doForm",
+            "formHandler":true, // handle form on server with Ext.Direct.Transaction
+            "len":1
+        }]
+    },
+    "namespace":"myApplication",// namespace to create the Remoting Provider in
+},{
+    type: 'polling', // create a {@link Ext.direct.PollingProvider}
+    url:  'php/poll.php'
+}, pollProv); // reference to previously created instance
+     * 
+ * @param {Object/Array} provider Accepts either an Array of Provider descriptions (an instance + * or config object for a Provider) or any number of Provider descriptions as arguments. Each + * Provider description instructs Ext.Direct how to create client-side stub methods. + */ + addProvider : function(provider){ + var me = this, + args = arguments, + i = 0, + len; + + if (args.length > 1) { + for (len = args.length; i < len; ++i) { + me.addProvider(args[i]); + } + return; + } + + // if provider has not already been instantiated + if (!provider.isProvider) { + provider = Ext.create('direct.' + provider.type + 'provider', provider); + } + me.providers.add(provider); + provider.on('data', me.onProviderData, me); + + + if (!provider.isConnected()) { + provider.connect(); + } + + return provider; + }, + + /** + * Retrieve a {@link Ext.direct.Provider provider} by the + * {@link Ext.direct.Provider#id id} specified when the provider is + * {@link #addProvider added}. + * @param {String/Ext.data.Provider} id The id of the provider, or the provider instance. + */ + getProvider : function(id){ + return id.isProvider ? id : this.providers.get(id); + }, + + /** + * Removes the provider. + * @param {String/Ext.direct.Provider} provider The provider instance or the id of the provider. + * @return {Ext.direct.Provider} The provider, null if not found. + */ + removeProvider : function(provider){ + var me = this, + providers = me.providers, + provider = provider.isProvider ? provider : providers.get(provider); + + if (provider) { + provider.un('data', me.onProviderData, me); + providers.remove(provider); + return provider; + } + return null; + }, + + /** + * Add a transaction to the manager. + * @private + * @param {Ext.direct.Transaction} transaction The transaction to add + * @return {Ext.direct.Transaction} transaction + */ + addTransaction: function(transaction){ + this.transactions.add(transaction); + return transaction; + }, + + /** + * Remove a transaction from the manager. + * @private + * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to remove + * @return {Ext.direct.Transaction} transaction + */ + removeTransaction: function(transaction){ + transaction = this.getTransaction(transaction); + this.transactions.remove(transaction); + return transaction; + }, + + /** + * Gets a transaction + * @private + * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to get + * @return {Ext.direct.Transaction} + */ + getTransaction: function(transaction){ + return transaction.isTransaction ? transaction : this.transactions.get(transaction); + }, + + onProviderData : function(provider, event){ + var me = this, + i = 0, + len; + + if (Ext.isArray(event)) { + for (len = event.length; i < len; ++i) { + me.onProviderData(provider, event[i]); + } + return; + } + if (event.name && event.name != 'event' && event.name != 'exception') { + me.fireEvent(event.name, event); + } else if (event.type == 'exception') { + me.fireEvent('exception', event); + } + me.fireEvent('event', event, provider); + } +}, function(){ + // Backwards compatibility + Ext.Direct = Ext.direct.Manager; +}); + +/** + * @class Ext.data.proxy.Direct + * @extends Ext.data.proxy.Server + * + * This class is used to send requests to the server using {@link Ext.direct}. When a request is made, + * the transport mechanism is handed off to the appropriate {@link Ext.direct.RemotingProvider Provider} + * to complete the call. + * + * ## Specifying the function + * This proxy expects a Direct remoting method to be passed in order to be able to complete requests. + * This can be done by specifying the {@link #directFn} configuration. This will use the same direct + * method for all requests. Alternatively, you can provide an {@link #api} configuration. This + * allows you to specify a different remoting method for each CRUD action. + * + * ## Paramaters + * This proxy provides options to help configure which parameters will be sent to the server. + * By specifying the {@link #paramsAsHash} option, it will send an object literal containing each + * of the passed parameters. The {@link #paramOrder} option can be used to specify the order in which + * the remoting method parameters are passed. + * + * ## Example Usage + * + * Ext.define('User', { + * extend: 'Ext.data.Model', + * fields: ['firstName', 'lastName'], + * proxy: { + * type: 'direct', + * directFn: MyApp.getUsers, + * paramOrder: 'id' // Tells the proxy to pass the id as the first parameter to the remoting method. + * } + * }); + * User.load(1); + */ +Ext.define('Ext.data.proxy.Direct', { + /* Begin Definitions */ + + extend: 'Ext.data.proxy.Server', + alternateClassName: 'Ext.data.DirectProxy', + + alias: 'proxy.direct', + + requires: ['Ext.direct.Manager'], + + /* End Definitions */ + + /** + * @cfg {Array/String} paramOrder Defaults to undefined. A list of params to be executed + * server side. Specify the params in the order in which they must be executed on the server-side + * as either (1) an Array of String values, or (2) a String of params delimited by either whitespace, + * comma, or pipe. For example, + * any of the following would be acceptable:

+paramOrder: ['param1','param2','param3']
+paramOrder: 'param1 param2 param3'
+paramOrder: 'param1,param2,param3'
+paramOrder: 'param1|param2|param'
+     
+ */ + paramOrder: undefined, + + /** + * @cfg {Boolean} paramsAsHash + * Send parameters as a collection of named arguments (defaults to true). Providing a + * {@link #paramOrder} nullifies this configuration. + */ + paramsAsHash: true, + + /** + * @cfg {Function} directFn + * Function to call when executing a request. directFn is a simple alternative to defining the api configuration-parameter + * for Store's which will not implement a full CRUD api. + */ + directFn : undefined, + + /** + * @cfg {Object} api The same as {@link Ext.data.proxy.Server#api}, however instead of providing urls, you should provide a direct + * function call. + */ + + /** + * @cfg {Object} extraParams Extra parameters that will be included on every read request. Individual requests with params + * of the same name will override these params when they are in conflict. + */ + + // private + paramOrderRe: /[\s,|]/, + + constructor: function(config){ + var me = this; + + Ext.apply(me, config); + if (Ext.isString(me.paramOrder)) { + me.paramOrder = me.paramOrder.split(me.paramOrderRe); + } + me.callParent(arguments); + }, + + doRequest: function(operation, callback, scope) { + var me = this, + writer = me.getWriter(), + request = me.buildRequest(operation, callback, scope), + fn = me.api[request.action] || me.directFn, + args = [], + params = request.params, + paramOrder = me.paramOrder, + method, + i = 0, + len; + + // + if (!fn) { + Ext.Error.raise('No direct function specified for this proxy'); + } + // + + if (operation.allowWrite()) { + request = writer.write(request); + } + + if (operation.action == 'read') { + // We need to pass params + method = fn.directCfg.method; + + if (method.ordered) { + if (method.len > 0) { + if (paramOrder) { + for (len = paramOrder.length; i < len; ++i) { + args.push(params[paramOrder[i]]); + } + } else if (me.paramsAsHash) { + args.push(params); + } + } + } else { + args.push(params); + } + } else { + args.push(request.jsonData); + } + + Ext.apply(request, { + args: args, + directFn: fn + }); + args.push(me.createRequestCallback(request, operation, callback, scope), me); + fn.apply(window, args); + }, + + /* + * Inherit docs. We don't apply any encoding here because + * all of the direct requests go out as jsonData + */ + applyEncoding: function(value){ + return value; + }, + + createRequestCallback: function(request, operation, callback, scope){ + var me = this; + + return function(data, event){ + me.processResponse(event.status, operation, request, event, callback, scope); + }; + }, + + // inherit docs + extractResponseData: function(response){ + return Ext.isDefined(response.result) ? response.result : response.data; + }, + + // inherit docs + setException: function(operation, response) { + operation.setException(response.message); + }, + + // inherit docs + buildUrl: function(){ + return ''; + } +}); + +/** + * @class Ext.data.DirectStore + * @extends Ext.data.Store + *

Small helper class to create an {@link Ext.data.Store} configured with an + * {@link Ext.data.proxy.Direct} and {@link Ext.data.reader.Json} to make interacting + * with an {@link Ext.Direct} Server-side {@link Ext.direct.Provider Provider} easier. + * To create a different proxy/reader combination create a basic {@link Ext.data.Store} + * configured as needed.

+ * + *

*Note: Although they are not listed, this class inherits all of the config options of:

+ *
    + *
  • {@link Ext.data.Store Store}
  • + *
      + * + *
    + *
  • {@link Ext.data.reader.Json JsonReader}
  • + *
      + *
    • {@link Ext.data.reader.Json#root root}
    • + *
    • {@link Ext.data.reader.Json#idProperty idProperty}
    • + *
    • {@link Ext.data.reader.Json#totalProperty totalProperty}
    • + *
    + * + *
  • {@link Ext.data.proxy.Direct DirectProxy}
  • + *
      + *
    • {@link Ext.data.proxy.Direct#directFn directFn}
    • + *
    • {@link Ext.data.proxy.Direct#paramOrder paramOrder}
    • + *
    • {@link Ext.data.proxy.Direct#paramsAsHash paramsAsHash}
    • + *
    + *
+ * + * @constructor + * @param {Object} config + */ + +Ext.define('Ext.data.DirectStore', { + /* Begin Definitions */ + + extend: 'Ext.data.Store', + + alias: 'store.direct', + + requires: ['Ext.data.proxy.Direct'], + + /* End Definitions */ + + constructor : function(config){ + config = Ext.apply({}, config); + if (!config.proxy) { + var proxy = { + type: 'direct', + reader: { + type: 'json' + } + }; + Ext.copyTo(proxy, config, 'paramOrder,paramsAsHash,directFn,api,simpleSortMode'); + Ext.copyTo(proxy.reader, config, 'totalProperty,root,idProperty'); + config.proxy = proxy; + } + this.callParent([config]); + } +}); + +/** + * @class Ext.util.Inflector + * @extends Object + *

General purpose inflector class that {@link #pluralize pluralizes}, {@link #singularize singularizes} and + * {@link #ordinalize ordinalizes} words. Sample usage:

+ * +

+//turning singular words into plurals
+Ext.util.Inflector.pluralize('word'); //'words'
+Ext.util.Inflector.pluralize('person'); //'people'
+Ext.util.Inflector.pluralize('sheep'); //'sheep'
+
+//turning plurals into singulars
+Ext.util.Inflector.singularize('words'); //'word'
+Ext.util.Inflector.singularize('people'); //'person'
+Ext.util.Inflector.singularize('sheep'); //'sheep'
+
+//ordinalizing numbers
+Ext.util.Inflector.ordinalize(11); //"11th"
+Ext.util.Inflector.ordinalize(21); //"21th"
+Ext.util.Inflector.ordinalize(1043); //"1043rd"
+
+ * + *

Customization

+ * + *

The Inflector comes with a default set of US English pluralization rules. These can be augmented with additional + * rules if the default rules do not meet your application's requirements, or swapped out entirely for other languages. + * Here is how we might add a rule that pluralizes "ox" to "oxen":

+ * +

+Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
+
+ * + *

Each rule consists of two items - a regular expression that matches one or more rules, and a replacement string. + * In this case, the regular expression will only match the string "ox", and will replace that match with "oxen". + * Here's how we could add the inverse rule:

+ * +

+Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
+
+ * + *

Note that the ox/oxen rules are present by default.

+ * + * @singleton + */ + +Ext.define('Ext.util.Inflector', { + + /* Begin Definitions */ + + singleton: true, + + /* End Definitions */ + + /** + * @private + * The registered plural tuples. Each item in the array should contain two items - the first must be a regular + * expression that matchers the singular form of a word, the second must be a String that replaces the matched + * part of the regular expression. This is managed by the {@link #plural} method. + * @property plurals + * @type Array + */ + plurals: [ + [(/(quiz)$/i), "$1zes" ], + [(/^(ox)$/i), "$1en" ], + [(/([m|l])ouse$/i), "$1ice" ], + [(/(matr|vert|ind)ix|ex$/i), "$1ices" ], + [(/(x|ch|ss|sh)$/i), "$1es" ], + [(/([^aeiouy]|qu)y$/i), "$1ies" ], + [(/(hive)$/i), "$1s" ], + [(/(?:([^f])fe|([lr])f)$/i), "$1$2ves"], + [(/sis$/i), "ses" ], + [(/([ti])um$/i), "$1a" ], + [(/(buffal|tomat|potat)o$/i), "$1oes" ], + [(/(bu)s$/i), "$1ses" ], + [(/(alias|status|sex)$/i), "$1es" ], + [(/(octop|vir)us$/i), "$1i" ], + [(/(ax|test)is$/i), "$1es" ], + [(/^person$/), "people" ], + [(/^man$/), "men" ], + [(/^(child)$/), "$1ren" ], + [(/s$/i), "s" ], + [(/$/), "s" ] + ], + + /** + * @private + * The set of registered singular matchers. Each item in the array should contain two items - the first must be a + * regular expression that matches the plural form of a word, the second must be a String that replaces the + * matched part of the regular expression. This is managed by the {@link #singular} method. + * @property singulars + * @type Array + */ + singulars: [ + [(/(quiz)zes$/i), "$1" ], + [(/(matr)ices$/i), "$1ix" ], + [(/(vert|ind)ices$/i), "$1ex" ], + [(/^(ox)en/i), "$1" ], + [(/(alias|status)es$/i), "$1" ], + [(/(octop|vir)i$/i), "$1us" ], + [(/(cris|ax|test)es$/i), "$1is" ], + [(/(shoe)s$/i), "$1" ], + [(/(o)es$/i), "$1" ], + [(/(bus)es$/i), "$1" ], + [(/([m|l])ice$/i), "$1ouse" ], + [(/(x|ch|ss|sh)es$/i), "$1" ], + [(/(m)ovies$/i), "$1ovie" ], + [(/(s)eries$/i), "$1eries"], + [(/([^aeiouy]|qu)ies$/i), "$1y" ], + [(/([lr])ves$/i), "$1f" ], + [(/(tive)s$/i), "$1" ], + [(/(hive)s$/i), "$1" ], + [(/([^f])ves$/i), "$1fe" ], + [(/(^analy)ses$/i), "$1sis" ], + [(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i), "$1$2sis"], + [(/([ti])a$/i), "$1um" ], + [(/(n)ews$/i), "$1ews" ], + [(/people$/i), "person" ], + [(/s$/i), "" ] + ], + + /** + * @private + * The registered uncountable words + * @property uncountable + * @type Array + */ + uncountable: [ + "sheep", + "fish", + "series", + "species", + "money", + "rice", + "information", + "equipment", + "grass", + "mud", + "offspring", + "deer", + "means" + ], + + /** + * Adds a new singularization rule to the Inflector. See the intro docs for more information + * @param {RegExp} matcher The matcher regex + * @param {String} replacer The replacement string, which can reference matches from the matcher argument + */ + singular: function(matcher, replacer) { + this.singulars.unshift([matcher, replacer]); + }, + + /** + * Adds a new pluralization rule to the Inflector. See the intro docs for more information + * @param {RegExp} matcher The matcher regex + * @param {String} replacer The replacement string, which can reference matches from the matcher argument + */ + plural: function(matcher, replacer) { + this.plurals.unshift([matcher, replacer]); + }, + + /** + * Removes all registered singularization rules + */ + clearSingulars: function() { + this.singulars = []; + }, + + /** + * Removes all registered pluralization rules + */ + clearPlurals: function() { + this.plurals = []; + }, + + /** + * Returns true if the given word is transnumeral (the word is its own singular and plural form - e.g. sheep, fish) + * @param {String} word The word to test + * @return {Boolean} True if the word is transnumeral + */ + isTransnumeral: function(word) { + return Ext.Array.indexOf(this.uncountable, word) != -1; + }, + + /** + * Returns the pluralized form of a word (e.g. Ext.util.Inflector.pluralize('word') returns 'words') + * @param {String} word The word to pluralize + * @return {String} The pluralized form of the word + */ + pluralize: function(word) { + if (this.isTransnumeral(word)) { + return word; + } + + var plurals = this.plurals, + length = plurals.length, + tuple, regex, i; + + for (i = 0; i < length; i++) { + tuple = plurals[i]; + regex = tuple[0]; + + if (regex == word || (regex.test && regex.test(word))) { + return word.replace(regex, tuple[1]); + } + } + + return word; + }, + + /** + * Returns the singularized form of a word (e.g. Ext.util.Inflector.singularize('words') returns 'word') + * @param {String} word The word to singularize + * @return {String} The singularized form of the word + */ + singularize: function(word) { + if (this.isTransnumeral(word)) { + return word; + } + + var singulars = this.singulars, + length = singulars.length, + tuple, regex, i; + + for (i = 0; i < length; i++) { + tuple = singulars[i]; + regex = tuple[0]; + + if (regex == word || (regex.test && regex.test(word))) { + return word.replace(regex, tuple[1]); + } + } + + return word; + }, + + /** + * Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used internally by the data + * package + * @param {String} word The word to classify + * @return {String} The classified version of the word + */ + classify: function(word) { + return Ext.String.capitalize(this.singularize(word)); + }, + + /** + * Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on the last digit of the + * number. 21 -> 21st, 22 -> 22nd, 23 -> 23rd, 24 -> 24th etc + * @param {Number} number The number to ordinalize + * @return {String} The ordinalized number + */ + ordinalize: function(number) { + var parsed = parseInt(number, 10), + mod10 = parsed % 10, + mod100 = parsed % 100; + + //11 through 13 are a special case + if (11 <= mod100 && mod100 <= 13) { + return number + "th"; + } else { + switch(mod10) { + case 1 : return number + "st"; + case 2 : return number + "nd"; + case 3 : return number + "rd"; + default: return number + "th"; + } + } + } +}, function() { + //aside from the rules above, there are a number of words that have irregular pluralization so we add them here + var irregulars = { + alumnus: 'alumni', + cactus : 'cacti', + focus : 'foci', + nucleus: 'nuclei', + radius: 'radii', + stimulus: 'stimuli', + ellipsis: 'ellipses', + paralysis: 'paralyses', + oasis: 'oases', + appendix: 'appendices', + index: 'indexes', + beau: 'beaux', + bureau: 'bureaux', + tableau: 'tableaux', + woman: 'women', + child: 'children', + man: 'men', + corpus: 'corpora', + criterion: 'criteria', + curriculum: 'curricula', + genus: 'genera', + memorandum: 'memoranda', + phenomenon: 'phenomena', + foot: 'feet', + goose: 'geese', + tooth: 'teeth', + antenna: 'antennae', + formula: 'formulae', + nebula: 'nebulae', + vertebra: 'vertebrae', + vita: 'vitae' + }, + singular; + + for (singular in irregulars) { + this.plural(singular, irregulars[singular]); + this.singular(irregulars[singular], singular); + } +}); +/** + * @author Ed Spencer + * @class Ext.data.HasManyAssociation + * @extends Ext.data.Association + * + *

Represents a one-to-many relationship between two models. Usually created indirectly via a model definition:

+ * +

+Ext.define('Product', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'id',      type: 'int'},
+        {name: 'user_id', type: 'int'},
+        {name: 'name',    type: 'string'}
+    ]
+});
+
+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'id',   type: 'int'},
+        {name: 'name', type: 'string'}
+    ],
+    // we can use the hasMany shortcut on the model to create a hasMany association
+    hasMany: {model: 'Product', name: 'products'}
+});
+
+* + *

Above we created Product and User models, and linked them by saying that a User hasMany Products. This gives + * us a new function on every User instance, in this case the function is called 'products' because that is the name + * we specified in the association configuration above.

+ * + *

This new function returns a specialized {@link Ext.data.Store Store} which is automatically filtered to load + * only Products for the given model instance:

+ * +

+//first, we load up a User with id of 1
+var user = Ext.ModelManager.create({id: 1, name: 'Ed'}, 'User');
+
+//the user.products function was created automatically by the association and returns a {@link Ext.data.Store Store}
+//the created store is automatically scoped to the set of Products for the User with id of 1
+var products = user.products();
+
+//we still have all of the usual Store functions, for example it's easy to add a Product for this User
+products.add({
+    name: 'Another Product'
+});
+
+//saves the changes to the store - this automatically sets the new Product's user_id to 1 before saving
+products.sync();
+
+ * + *

The new Store is only instantiated the first time you call products() to conserve memory and processing time, + * though calling products() a second time returns the same store instance.

+ * + *

Custom filtering

+ * + *

The Store is automatically furnished with a filter - by default this filter tells the store to only return + * records where the associated model's foreign key matches the owner model's primary key. For example, if a User + * with ID = 100 hasMany Products, the filter loads only Products with user_id == 100.

+ * + *

Sometimes we want to filter by another field - for example in the case of a Twitter search application we may + * have models for Search and Tweet:

+ * +

+Ext.define('Search', {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'query'
+    ],
+
+    hasMany: {
+        model: 'Tweet',
+        name : 'tweets',
+        filterProperty: 'query'
+    }
+});
+
+Ext.define('Tweet', {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'text', 'from_user'
+    ]
+});
+
+//returns a Store filtered by the filterProperty
+var store = new Search({query: 'Sencha Touch'}).tweets();
+
+ * + *

The tweets association above is filtered by the query property by setting the {@link #filterProperty}, and is + * equivalent to this:

+ * +

+var store = new Ext.data.Store({
+    model: 'Tweet',
+    filters: [
+        {
+            property: 'query',
+            value   : 'Sencha Touch'
+        }
+    ]
+});
+
+ */ +Ext.define('Ext.data.HasManyAssociation', { + extend: 'Ext.data.Association', + requires: ['Ext.util.Inflector'], + + alias: 'association.hasmany', + + /** + * @cfg {String} foreignKey The name of the foreign key on the associated model that links it to the owner + * model. Defaults to the lowercased name of the owner model plus "_id", e.g. an association with a where a + * model called Group hasMany Users would create 'group_id' as the foreign key. When the remote store is loaded, + * the store is automatically filtered so that only records with a matching foreign key are included in the + * resulting child store. This can be overridden by specifying the {@link #filterProperty}. + *

+Ext.define('Group', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name'],
+    hasMany: 'User'
+});
+
+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name', 'group_id'], // refers to the id of the group that this user belongs to
+    belongsTo: 'Group'
+});
+     * 
+ */ + + /** + * @cfg {String} name The name of the function to create on the owner model to retrieve the child store. + * If not specified, the pluralized name of the child model is used. + *

+// This will create a users() method on any Group model instance
+Ext.define('Group', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name'],
+    hasMany: 'User'
+});
+var group = new Group();
+console.log(group.users());
+
+// The method to retrieve the users will now be getUserList
+Ext.define('Group', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name'],
+    hasMany: {model: 'User', name: 'getUserList'}
+});
+var group = new Group();
+console.log(group.getUserList());
+     * 
+ */ + + /** + * @cfg {Object} storeConfig Optional configuration object that will be passed to the generated Store. Defaults to + * undefined. + */ + + /** + * @cfg {String} filterProperty Optionally overrides the default filter that is set up on the associated Store. If + * this is not set, a filter is automatically created which filters the association based on the configured + * {@link #foreignKey}. See intro docs for more details. Defaults to undefined + */ + + /** + * @cfg {Boolean} autoLoad True to automatically load the related store from a remote source when instantiated. + * Defaults to false. + */ + + /** + * @cfg {String} type The type configuration can be used when creating associations using a configuration object. + * Use 'hasMany' to create a HasManyAssocation + *

+associations: [{
+    type: 'hasMany',
+    model: 'User'
+}]
+     * 
+ */ + + constructor: function(config) { + var me = this, + ownerProto, + name; + + me.callParent(arguments); + + me.name = me.name || Ext.util.Inflector.pluralize(me.associatedName.toLowerCase()); + + ownerProto = me.ownerModel.prototype; + name = me.name; + + Ext.applyIf(me, { + storeName : name + "Store", + foreignKey: me.ownerName.toLowerCase() + "_id" + }); + + ownerProto[name] = me.createStore(); + }, + + /** + * @private + * Creates a function that returns an Ext.data.Store which is configured to load a set of data filtered + * by the owner model's primary key - e.g. in a hasMany association where Group hasMany Users, this function + * returns a Store configured to return the filtered set of a single Group's Users. + * @return {Function} The store-generating function + */ + createStore: function() { + var that = this, + associatedModel = that.associatedModel, + storeName = that.storeName, + foreignKey = that.foreignKey, + primaryKey = that.primaryKey, + filterProperty = that.filterProperty, + autoLoad = that.autoLoad, + storeConfig = that.storeConfig || {}; + + return function() { + var me = this, + config, filter, + modelDefaults = {}; + + if (me[storeName] === undefined) { + if (filterProperty) { + filter = { + property : filterProperty, + value : me.get(filterProperty), + exactMatch: true + }; + } else { + filter = { + property : foreignKey, + value : me.get(primaryKey), + exactMatch: true + }; + } + + modelDefaults[foreignKey] = me.get(primaryKey); + + config = Ext.apply({}, storeConfig, { + model : associatedModel, + filters : [filter], + remoteFilter : false, + modelDefaults: modelDefaults + }); + + me[storeName] = Ext.create('Ext.data.Store', config); + if (autoLoad) { + me[storeName].load(); + } + } + + return me[storeName]; + }; + }, + + /** + * Read associated data + * @private + * @param {Ext.data.Model} record The record we're writing to + * @param {Ext.data.reader.Reader} reader The reader for the associated model + * @param {Object} associationData The raw associated data + */ + read: function(record, reader, associationData){ + var store = record[this.name](), + inverse; + + store.add(reader.read(associationData).records); + + //now that we've added the related records to the hasMany association, set the inverse belongsTo + //association on each of them if it exists + inverse = this.associatedModel.prototype.associations.findBy(function(assoc){ + return assoc.type === 'belongsTo' && assoc.associatedName === record.$className; + }); + + //if the inverse association was found, set it now on each record we've just created + if (inverse) { + store.data.each(function(associatedRecord){ + associatedRecord[inverse.instanceName] = record; + }); + } + } +}); +/** + * @class Ext.data.JsonP + * @singleton + * This class is used to create JSONP requests. JSONP is a mechanism that allows for making + * requests for data cross domain. More information is available here: + * http://en.wikipedia.org/wiki/JSONP + */ +Ext.define('Ext.data.JsonP', { + + /* Begin Definitions */ + + singleton: true, + + statics: { + requestCount: 0, + requests: {} + }, + + /* End Definitions */ + + /** + * @property timeout + * @type Number + * A default timeout for any JsonP requests. If the request has not completed in this time the + * failure callback will be fired. The timeout is in ms. Defaults to 30000. + */ + timeout: 30000, + + /** + * @property disableCaching + * @type Boolean + * True to add a unique cache-buster param to requests. Defaults to true. + */ + disableCaching: true, + + /** + * @property disableCachingParam + * @type String + * Change the parameter which is sent went disabling caching through a cache buster. Defaults to '_dc'. + */ + disableCachingParam: '_dc', + + /** + * @property callbackKey + * @type String + * Specifies the GET parameter that will be sent to the server containing the function name to be executed when + * the request completes. Defaults to callback. Thus, a common request will be in the form of + * url?callback=Ext.data.JsonP.callback1 + */ + callbackKey: 'callback', + + /** + * Makes a JSONP request. + * @param {Object} options An object which may contain the following properties. Note that options will + * take priority over any defaults that are specified in the class. + *
    + *
  • url : String
    The URL to request.
  • + *
  • params : Object (Optional)
    An object containing a series of + * key value pairs that will be sent along with the request.
  • + *
  • timeout : Number (Optional)
    See {@link #timeout}
  • + *
  • callbackKey : String (Optional)
    See {@link #callbackKey}
  • + *
  • disableCaching : Boolean (Optional)
    See {@link #disableCaching}
  • + *
  • disableCachingParam : String (Optional)
    See {@link #disableCachingParam}
  • + *
  • success : Function (Optional)
    A function to execute if the request succeeds.
  • + *
  • failure : Function (Optional)
    A function to execute if the request fails.
  • + *
  • callback : Function (Optional)
    A function to execute when the request + * completes, whether it is a success or failure.
  • + *
  • scope : Object (Optional)
    The scope in + * which to execute the callbacks: The "this" object for the callback function. Defaults to the browser window.
  • + *
+ * @return {Object} request An object containing the request details. + */ + request: function(options){ + options = Ext.apply({}, options); + + // + if (!options.url) { + Ext.Error.raise('A url must be specified for a JSONP request.'); + } + // + + var me = this, + disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching, + cacheParam = options.disableCachingParam || me.disableCachingParam, + id = ++me.statics().requestCount, + callbackName = 'callback' + id, + callbackKey = options.callbackKey || me.callbackKey, + timeout = Ext.isDefined(options.timeout) ? options.timeout : me.timeout, + params = Ext.apply({}, options.params), + url = options.url, + request, + script; + + params[callbackKey] = 'Ext.data.JsonP.' + callbackName; + if (disableCaching) { + params[cacheParam] = new Date().getTime(); + } + + script = me.createScript(url, params); + + me.statics().requests[id] = request = { + url: url, + params: params, + script: script, + id: id, + scope: options.scope, + success: options.success, + failure: options.failure, + callback: options.callback, + callbackName: callbackName + }; + + if (timeout > 0) { + request.timeout = setTimeout(Ext.bind(me.handleTimeout, me, [request]), timeout); + } + + me.setupErrorHandling(request); + me[callbackName] = Ext.bind(me.handleResponse, me, [request], true); + Ext.getHead().appendChild(script); + return request; + }, + + /** + * Abort a request. If the request parameter is not specified all open requests will + * be aborted. + * @param {Object/String} request (Optional) The request to abort + */ + abort: function(request){ + var requests = this.statics().requests, + key; + + if (request) { + if (!request.id) { + request = requests[request]; + } + this.abort(request); + } else { + for (key in requests) { + if (requests.hasOwnProperty(key)) { + this.abort(requests[key]); + } + } + } + }, + + /** + * Sets up error handling for the script + * @private + * @param {Object} request The request + */ + setupErrorHandling: function(request){ + request.script.onerror = Ext.bind(this.handleError, this, [request]); + }, + + /** + * Handles any aborts when loading the script + * @private + * @param {Object} request The request + */ + handleAbort: function(request){ + request.errorType = 'abort'; + this.handleResponse(null, request); + }, + + /** + * Handles any script errors when loading the script + * @private + * @param {Object} request The request + */ + handleError: function(request){ + request.errorType = 'error'; + this.handleResponse(null, request); + }, + + /** + * Cleans up anu script handling errors + * @private + * @param {Object} request The request + */ + cleanupErrorHandling: function(request){ + request.script.onerror = null; + }, + + /** + * Handle any script timeouts + * @private + * @param {Object} request The request + */ + handleTimeout: function(request){ + request.errorType = 'timeout'; + this.handleResponse(null, request); + }, + + /** + * Handle a successful response + * @private + * @param {Object} result The result from the request + * @param {Object} request The request + */ + handleResponse: function(result, request){ + + var success = true; + + if (request.timeout) { + clearTimeout(request.timeout); + } + delete this[request.callbackName]; + delete this.statics()[request.id]; + this.cleanupErrorHandling(request); + Ext.fly(request.script).remove(); + + if (request.errorType) { + success = false; + Ext.callback(request.failure, request.scope, [request.errorType]); + } else { + Ext.callback(request.success, request.scope, [result]); + } + Ext.callback(request.callback, request.scope, [success, result, request.errorType]); + }, + + /** + * Create the script tag + * @private + * @param {String} url The url of the request + * @param {Object} params Any extra params to be sent + */ + createScript: function(url, params) { + var script = document.createElement('script'); + script.setAttribute("src", Ext.urlAppend(url, Ext.Object.toQueryString(params))); + script.setAttribute("async", true); + script.setAttribute("type", "text/javascript"); + return script; + } +}); + +/** + * @class Ext.data.JsonPStore + * @extends Ext.data.Store + * @ignore + * @private + *

NOTE: This class is in need of migration to the new API.

+ *

Small helper class to make creating {@link Ext.data.Store}s from different domain JSON data easier. + * A JsonPStore will be automatically configured with a {@link Ext.data.reader.Json} and a {@link Ext.data.proxy.JsonP JsonPProxy}.

+ *

A store configuration would be something like:


+var store = new Ext.data.JsonPStore({
+    // store configs
+    autoDestroy: true,
+    storeId: 'myStore',
+
+    // proxy configs
+    url: 'get-images.php',
+
+    // reader configs
+    root: 'images',
+    idProperty: 'name',
+    fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
+});
+ * 

+ *

This store is configured to consume a returned object of the form:


+stcCallback({
+    images: [
+        {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
+        {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
+    ]
+})
+ * 
+ *

Where stcCallback is the callback name passed in the request to the remote domain. See {@link Ext.data.proxy.JsonP JsonPProxy} + * for details of how this works.

+ * An object literal of this form could also be used as the {@link #data} config option.

+ *

*Note: Although not listed here, this class accepts all of the configuration options of + * {@link Ext.data.reader.Json JsonReader} and {@link Ext.data.proxy.JsonP JsonPProxy}.

+ * @constructor + * @param {Object} config + * @xtype jsonpstore + */ +Ext.define('Ext.data.JsonPStore', { + extend: 'Ext.data.Store', + alias : 'store.jsonp', + + /** + * @cfg {Ext.data.DataReader} reader @hide + */ + constructor: function(config) { + this.callParent(Ext.apply(config, { + reader: Ext.create('Ext.data.reader.Json', config), + proxy : Ext.create('Ext.data.proxy.JsonP', config) + })); + } +}); + +/** + * @class Ext.data.NodeInterface + * This class is meant to be used as a set of methods that are applied to the prototype of a + * Record to decorate it with a Node API. This means that models used in conjunction with a tree + * will have all of the tree related methods available on the model. In general this class will + * not be used directly by the developer. + */ +Ext.define('Ext.data.NodeInterface', { + requires: ['Ext.data.Field'], + + statics: { + /** + * This method allows you to decorate a Record's prototype to implement the NodeInterface. + * This adds a set of methods, new events, new properties and new fields on every Record + * with the same Model as the passed Record. + * @param {Ext.data.Record} record The Record you want to decorate the prototype of. + * @static + */ + decorate: function(record) { + if (!record.isNode) { + // Apply the methods and fields to the prototype + // @TODO: clean this up to use proper class system stuff + var mgr = Ext.ModelManager, + modelName = record.modelName, + modelClass = mgr.getModel(modelName), + idName = modelClass.prototype.idProperty, + instances = Ext.Array.filter(mgr.all.getArray(), function(item) { + return item.modelName == modelName; + }), + iln = instances.length, + newFields = [], + i, instance, jln, j, newField; + + // Start by adding the NodeInterface methods to the Model's prototype + modelClass.override(this.getPrototypeBody()); + newFields = this.applyFields(modelClass, [ + {name: idName, type: 'string', defaultValue: null}, + {name: 'parentId', type: 'string', defaultValue: null}, + {name: 'index', type: 'int', defaultValue: null}, + {name: 'depth', type: 'int', defaultValue: 0}, + {name: 'expanded', type: 'bool', defaultValue: false, persist: false}, + {name: 'checked', type: 'auto', defaultValue: null}, + {name: 'leaf', type: 'bool', defaultValue: false, persist: false}, + {name: 'cls', type: 'string', defaultValue: null, persist: false}, + {name: 'iconCls', type: 'string', defaultValue: null, persist: false}, + {name: 'root', type: 'boolean', defaultValue: false, persist: false}, + {name: 'isLast', type: 'boolean', defaultValue: false, persist: false}, + {name: 'isFirst', type: 'boolean', defaultValue: false, persist: false}, + {name: 'allowDrop', type: 'boolean', defaultValue: true, persist: false}, + {name: 'allowDrag', type: 'boolean', defaultValue: true, persist: false}, + {name: 'loaded', type: 'boolean', defaultValue: false, persist: false}, + {name: 'loading', type: 'boolean', defaultValue: false, persist: false}, + {name: 'href', type: 'string', defaultValue: null, persist: false}, + {name: 'hrefTarget',type: 'string', defaultValue: null, persist: false}, + {name: 'qtip', type: 'string', defaultValue: null, persist: false}, + {name: 'qtitle', type: 'string', defaultValue: null, persist: false} + ]); + + jln = newFields.length; + // Set default values to all instances already out there + for (i = 0; i < iln; i++) { + instance = instances[i]; + for (j = 0; j < jln; j++) { + newField = newFields[j]; + if (instance.get(newField.name) === undefined) { + instance.data[newField.name] = newField.defaultValue; + } + } + } + } + + Ext.applyIf(record, { + firstChild: null, + lastChild: null, + parentNode: null, + previousSibling: null, + nextSibling: null, + childNodes: [] + }); + // Commit any fields so the record doesn't show as dirty initially + record.commit(true); + + record.enableBubble([ + /** + * @event append + * Fires when a new child node is appended + * @param {Node} this This node + * @param {Node} node The newly appended node + * @param {Number} index The index of the newly appended node + */ + "append", + + /** + * @event remove + * Fires when a child node is removed + * @param {Node} this This node + * @param {Node} node The removed node + */ + "remove", + + /** + * @event move + * Fires when this node is moved to a new location in the tree + * @param {Node} this This node + * @param {Node} oldParent The old parent of this node + * @param {Node} newParent The new parent of this node + * @param {Number} index The index it was moved to + */ + "move", + + /** + * @event insert + * Fires when a new child node is inserted. + * @param {Node} this This node + * @param {Node} node The child node inserted + * @param {Node} refNode The child node the node was inserted before + */ + "insert", + + /** + * @event beforeappend + * Fires before a new child is appended, return false to cancel the append. + * @param {Node} this This node + * @param {Node} node The child node to be appended + */ + "beforeappend", + + /** + * @event beforeremove + * Fires before a child is removed, return false to cancel the remove. + * @param {Node} this This node + * @param {Node} node The child node to be removed + */ + "beforeremove", + + /** + * @event beforemove + * Fires before this node is moved to a new location in the tree. Return false to cancel the move. + * @param {Node} this This node + * @param {Node} oldParent The parent of this node + * @param {Node} newParent The new parent this node is moving to + * @param {Number} index The index it is being moved to + */ + "beforemove", + + /** + * @event beforeinsert + * Fires before a new child is inserted, return false to cancel the insert. + * @param {Node} this This node + * @param {Node} node The child node to be inserted + * @param {Node} refNode The child node the node is being inserted before + */ + "beforeinsert", + + /** + * @event expand + * Fires when this node is expanded. + * @param {Node} this The expanding node + */ + "expand", + + /** + * @event collapse + * Fires when this node is collapsed. + * @param {Node} this The collapsing node + */ + "collapse", + + /** + * @event beforeexpand + * Fires before this node is expanded. + * @param {Node} this The expanding node + */ + "beforeexpand", + + /** + * @event beforecollapse + * Fires before this node is collapsed. + * @param {Node} this The collapsing node + */ + "beforecollapse", + + /** + * @event beforecollapse + * Fires before this node is collapsed. + * @param {Node} this The collapsing node + */ + "sort" + ]); + + return record; + }, + + applyFields: function(modelClass, addFields) { + var modelPrototype = modelClass.prototype, + fields = modelPrototype.fields, + keys = fields.keys, + ln = addFields.length, + addField, i, name, + newFields = []; + + for (i = 0; i < ln; i++) { + addField = addFields[i]; + if (!Ext.Array.contains(keys, addField.name)) { + addField = Ext.create('data.field', addField); + + newFields.push(addField); + fields.add(addField); + } + } + + return newFields; + }, + + getPrototypeBody: function() { + return { + isNode: true, + + /** + * Ensures that the passed object is an instance of a Record with the NodeInterface applied + * @return {Boolean} + */ + createNode: function(node) { + if (Ext.isObject(node) && !node.isModel) { + node = Ext.ModelManager.create(node, this.modelName); + } + // Make sure the node implements the node interface + return Ext.data.NodeInterface.decorate(node); + }, + + /** + * Returns true if this node is a leaf + * @return {Boolean} + */ + isLeaf : function() { + return this.get('leaf') === true; + }, + + /** + * Sets the first child of this node + * @private + * @param {Ext.data.NodeInterface} node + */ + setFirstChild : function(node) { + this.firstChild = node; + }, + + /** + * Sets the last child of this node + * @private + * @param {Ext.data.NodeInterface} node + */ + setLastChild : function(node) { + this.lastChild = node; + }, + + /** + * Updates general data of this node like isFirst, isLast, depth. This + * method is internally called after a node is moved. This shouldn't + * have to be called by the developer unless they are creating custom + * Tree plugins. + * @return {Boolean} + */ + updateInfo: function(silent) { + var me = this, + isRoot = me.isRoot(), + parentNode = me.parentNode, + isFirst = (!parentNode ? true : parentNode.firstChild == me), + isLast = (!parentNode ? true : parentNode.lastChild == me), + depth = 0, + parent = me, + children = me.childNodes, + len = children.length, + i = 0; + + while (parent.parentNode) { + ++depth; + parent = parent.parentNode; + } + + me.beginEdit(); + me.set({ + isFirst: isFirst, + isLast: isLast, + depth: depth, + index: parentNode ? parentNode.indexOf(me) : 0, + parentId: parentNode ? parentNode.getId() : null + }); + me.endEdit(silent); + if (silent) { + me.commit(); + } + + for (i = 0; i < len; i++) { + children[i].updateInfo(silent); + } + }, + + /** + * Returns true if this node is the last child of its parent + * @return {Boolean} + */ + isLast : function() { + return this.get('isLast'); + }, + + /** + * Returns true if this node is the first child of its parent + * @return {Boolean} + */ + isFirst : function() { + return this.get('isFirst'); + }, + + /** + * Returns true if this node has one or more child nodes, else false. + * @return {Boolean} + */ + hasChildNodes : function() { + return !this.isLeaf() && this.childNodes.length > 0; + }, + + /** + * Returns true if this node has one or more child nodes, or if the expandable + * node attribute is explicitly specified as true (see {@link #attributes}), otherwise returns false. + * @return {Boolean} + */ + isExpandable : function() { + return this.get('expandable') || this.hasChildNodes(); + }, + + /** + *

Insert node(s) as the last child node of this node.

+ *

If the node was previously a child node of another parent node, it will be removed from that node first.

+ * @param {Node/Array} node The node or Array of nodes to append + * @return {Node} The appended node if single append, or null if an array was passed + */ + appendChild : function(node, suppressEvents, suppressNodeUpdate) { + var me = this, + i, ln, + index, + oldParent, + ps; + + // if passed an array or multiple args do them one by one + if (Ext.isArray(node)) { + for (i = 0, ln = node.length; i < ln; i++) { + me.appendChild(node[i]); + } + } else { + // Make sure it is a record + node = me.createNode(node); + + if (suppressEvents !== true && me.fireEvent("beforeappend", me, node) === false) { + return false; + } + + index = me.childNodes.length; + oldParent = node.parentNode; + + // it's a move, make sure we move it cleanly + if (oldParent) { + if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index) === false) { + return false; + } + oldParent.removeChild(node, null, false, true); + } + + index = me.childNodes.length; + if (index === 0) { + me.setFirstChild(node); + } + + me.childNodes.push(node); + node.parentNode = me; + node.nextSibling = null; + + me.setLastChild(node); + + ps = me.childNodes[index - 1]; + if (ps) { + node.previousSibling = ps; + ps.nextSibling = node; + ps.updateInfo(suppressNodeUpdate); + } else { + node.previousSibling = null; + } + + node.updateInfo(suppressNodeUpdate); + + // As soon as we append a child to this node, we are loaded + if (!me.isLoaded()) { + me.set('loaded', true); + } + // If this node didnt have any childnodes before, update myself + else if (me.childNodes.length === 1) { + me.set('loaded', me.isLoaded()); + } + + if (suppressEvents !== true) { + me.fireEvent("append", me, node, index); + + if (oldParent) { + node.fireEvent("move", node, oldParent, me, index); + } + } + + return node; + } + }, + + /** + * Returns the bubble target for this node + * @private + * @return {Object} The bubble target + */ + getBubbleTarget: function() { + return this.parentNode; + }, + + /** + * Removes a child node from this node. + * @param {Node} node The node to remove + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. + * @return {Node} The removed node + */ + removeChild : function(node, destroy, suppressEvents, suppressNodeUpdate) { + var me = this, + index = me.indexOf(node); + + if (index == -1 || (suppressEvents !== true && me.fireEvent("beforeremove", me, node) === false)) { + return false; + } + + // remove it from childNodes collection + me.childNodes.splice(index, 1); + + // update child refs + if (me.firstChild == node) { + me.setFirstChild(node.nextSibling); + } + if (me.lastChild == node) { + me.setLastChild(node.previousSibling); + } + + // update siblings + if (node.previousSibling) { + node.previousSibling.nextSibling = node.nextSibling; + node.previousSibling.updateInfo(suppressNodeUpdate); + } + if (node.nextSibling) { + node.nextSibling.previousSibling = node.previousSibling; + node.nextSibling.updateInfo(suppressNodeUpdate); + } + + if (suppressEvents !== true) { + me.fireEvent("remove", me, node); + } + + + // If this node suddenly doesnt have childnodes anymore, update myself + if (!me.childNodes.length) { + me.set('loaded', me.isLoaded()); + } + + if (destroy) { + node.destroy(true); + } else { + node.clear(); + } + + return node; + }, + + /** + * Creates a copy (clone) of this Node. + * @param {String} id (optional) A new id, defaults to this Node's id. See {@link #id}. + * @param {Boolean} deep (optional)

If passed as true, all child Nodes are recursively copied into the new Node.

+ *

If omitted or false, the copy will have no child Nodes.

+ * @return {Node} A copy of this Node. + */ + copy: function(newId, deep) { + var me = this, + result = me.callOverridden(arguments), + len = me.childNodes ? me.childNodes.length : 0, + i; + + // Move child nodes across to the copy if required + if (deep) { + for (i = 0; i < len; i++) { + result.appendChild(me.childNodes[i].copy(true)); + } + } + return result; + }, + + /** + * Clear the node. + * @private + * @param {Boolean} destroy True to destroy the node. + */ + clear : function(destroy) { + var me = this; + + // clear any references from the node + me.parentNode = me.previousSibling = me.nextSibling = null; + if (destroy) { + me.firstChild = me.lastChild = null; + } + }, + + /** + * Destroys the node. + */ + destroy : function(silent) { + /* + * Silent is to be used in a number of cases + * 1) When setRoot is called. + * 2) When destroy on the tree is called + * 3) For destroying child nodes on a node + */ + var me = this; + + if (silent === true) { + me.clear(true); + Ext.each(me.childNodes, function(n) { + n.destroy(true); + }); + me.childNodes = null; + } else { + me.remove(true); + } + + me.callOverridden(); + }, + + /** + * Inserts the first node before the second node in this nodes childNodes collection. + * @param {Node} node The node to insert + * @param {Node} refNode The node to insert before (if null the node is appended) + * @return {Node} The inserted node + */ + insertBefore : function(node, refNode, suppressEvents) { + var me = this, + index = me.indexOf(refNode), + oldParent = node.parentNode, + refIndex = index, + ps; + + if (!refNode) { // like standard Dom, refNode can be null for append + return me.appendChild(node); + } + + // nothing to do + if (node == refNode) { + return false; + } + + // Make sure it is a record with the NodeInterface + node = me.createNode(node); + + if (suppressEvents !== true && me.fireEvent("beforeinsert", me, node, refNode) === false) { + return false; + } + + // when moving internally, indexes will change after remove + if (oldParent == me && me.indexOf(node) < index) { + refIndex--; + } + + // it's a move, make sure we move it cleanly + if (oldParent) { + if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index, refNode) === false) { + return false; + } + oldParent.removeChild(node); + } + + if (refIndex === 0) { + me.setFirstChild(node); + } + + me.childNodes.splice(refIndex, 0, node); + node.parentNode = me; + + node.nextSibling = refNode; + refNode.previousSibling = node; + + ps = me.childNodes[refIndex - 1]; + if (ps) { + node.previousSibling = ps; + ps.nextSibling = node; + ps.updateInfo(); + } else { + node.previousSibling = null; + } + + node.updateInfo(); + + if (!me.isLoaded()) { + me.set('loaded', true); + } + // If this node didnt have any childnodes before, update myself + else if (me.childNodes.length === 1) { + me.set('loaded', me.isLoaded()); + } + + if (suppressEvents !== true) { + me.fireEvent("insert", me, node, refNode); + + if (oldParent) { + node.fireEvent("move", node, oldParent, me, refIndex, refNode); + } + } + + return node; + }, + + /** + * Insert a node into this node + * @param {Number} index The zero-based index to insert the node at + * @param {Ext.data.Model} node The node to insert + * @return {Ext.data.Record} The record you just inserted + */ + insertChild: function(index, node) { + var sibling = this.childNodes[index]; + if (sibling) { + return this.insertBefore(node, sibling); + } + else { + return this.appendChild(node); + } + }, + + /** + * Removes this node from its parent + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. + * @return {Node} this + */ + remove : function(destroy, suppressEvents) { + var parentNode = this.parentNode; + + if (parentNode) { + parentNode.removeChild(this, destroy, suppressEvents, true); + } + return this; + }, + + /** + * Removes all child nodes from this node. + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. + * @return {Node} this + */ + removeAll : function(destroy, suppressEvents) { + var cn = this.childNodes, + n; + + while ((n = cn[0])) { + this.removeChild(n, destroy, suppressEvents); + } + return this; + }, + + /** + * Returns the child node at the specified index. + * @param {Number} index + * @return {Node} + */ + getChildAt : function(index) { + return this.childNodes[index]; + }, + + /** + * Replaces one child node in this node with another. + * @param {Node} newChild The replacement node + * @param {Node} oldChild The node to replace + * @return {Node} The replaced node + */ + replaceChild : function(newChild, oldChild, suppressEvents) { + var s = oldChild ? oldChild.nextSibling : null; + + this.removeChild(oldChild, suppressEvents); + this.insertBefore(newChild, s, suppressEvents); + return oldChild; + }, + + /** + * Returns the index of a child node + * @param {Node} node + * @return {Number} The index of the node or -1 if it was not found + */ + indexOf : function(child) { + return Ext.Array.indexOf(this.childNodes, child); + }, + + /** + * Returns depth of this node (the root node has a depth of 0) + * @return {Number} + */ + getDepth : function() { + return this.get('depth'); + }, + + /** + * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function + * will be the args provided or the current node. If the function returns false at any point, + * the bubble is stopped. + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) + */ + bubble : function(fn, scope, args) { + var p = this; + while (p) { + if (fn.apply(scope || p, args || [p]) === false) { + break; + } + p = p.parentNode; + } + }, + + // + cascade: function() { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.data.Node: cascade has been deprecated. Please use cascadeBy instead.'); + } + return this.cascadeBy.apply(this, arguments); + }, + // + + /** + * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function + * will be the args provided or the current node. If the function returns false at any point, + * the cascade is stopped on that branch. + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) + */ + cascadeBy : function(fn, scope, args) { + if (fn.apply(scope || this, args || [this]) !== false) { + var childNodes = this.childNodes, + length = childNodes.length, + i; + + for (i = 0; i < length; i++) { + childNodes[i].cascadeBy(fn, scope, args); + } + } + }, + + /** + * Interates the child nodes of this node, calling the specified function with each node. The arguments to the function + * will be the args provided or the current node. If the function returns false at any point, + * the iteration stops. + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node in the iteration. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) + */ + eachChild : function(fn, scope, args) { + var childNodes = this.childNodes, + length = childNodes.length, + i; + + for (i = 0; i < length; i++) { + if (fn.apply(scope || this, args || [childNodes[i]]) === false) { + break; + } + } + }, + + /** + * Finds the first child that has the attribute with the specified value. + * @param {String} attribute The attribute name + * @param {Mixed} value The value to search for + * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children + * @return {Node} The found child or null if none was found + */ + findChild : function(attribute, value, deep) { + return this.findChildBy(function() { + return this.get(attribute) == value; + }, null, deep); + }, + + /** + * Finds the first child by a custom function. The child matches if the function passed returns true. + * @param {Function} fn A function which must return true if the passed Node is the required Node. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the Node being tested. + * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children + * @return {Node} The found child or null if none was found + */ + findChildBy : function(fn, scope, deep) { + var cs = this.childNodes, + len = cs.length, + i = 0, n, res; + + for (; i < len; i++) { + n = cs[i]; + if (fn.call(scope || n, n) === true) { + return n; + } + else if (deep) { + res = n.findChildBy(fn, scope, deep); + if (res !== null) { + return res; + } + } + } + + return null; + }, + + /** + * Returns true if this node is an ancestor (at any point) of the passed node. + * @param {Node} node + * @return {Boolean} + */ + contains : function(node) { + return node.isAncestor(this); + }, + + /** + * Returns true if the passed node is an ancestor (at any point) of this node. + * @param {Node} node + * @return {Boolean} + */ + isAncestor : function(node) { + var p = this.parentNode; + while (p) { + if (p == node) { + return true; + } + p = p.parentNode; + } + return false; + }, + + /** + * Sorts this nodes children using the supplied sort function. + * @param {Function} fn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order. + * @param {Boolean} recursive Whether or not to apply this sort recursively + * @param {Boolean} suppressEvent Set to true to not fire a sort event. + */ + sort : function(sortFn, recursive, suppressEvent) { + var cs = this.childNodes, + ln = cs.length, + i, n; + + if (ln > 0) { + Ext.Array.sort(cs, sortFn); + for (i = 0; i < ln; i++) { + n = cs[i]; + n.previousSibling = cs[i-1]; + n.nextSibling = cs[i+1]; + + if (i === 0) { + this.setFirstChild(n); + n.updateInfo(); + } + if (i == ln - 1) { + this.setLastChild(n); + n.updateInfo(); + } + if (recursive && !n.isLeaf()) { + n.sort(sortFn, true, true); + } + } + + if (suppressEvent !== true) { + this.fireEvent('sort', this, cs); + } + } + }, + + /** + * Returns true if this node is expaned + * @return {Boolean} + */ + isExpanded: function() { + return this.get('expanded'); + }, + + /** + * Returns true if this node is loaded + * @return {Boolean} + */ + isLoaded: function() { + return this.get('loaded'); + }, + + /** + * Returns true if this node is loading + * @return {Boolean} + */ + isLoading: function() { + return this.get('loading'); + }, + + /** + * Returns true if this node is the root node + * @return {Boolean} + */ + isRoot: function() { + return !this.parentNode; + }, + + /** + * Returns true if this node is visible + * @return {Boolean} + */ + isVisible: function() { + var parent = this.parentNode; + while (parent) { + if (!parent.isExpanded()) { + return false; + } + parent = parent.parentNode; + } + return true; + }, + + /** + * Expand this node. + * @param {Function} recursive (Optional) True to recursively expand all the children + * @param {Function} callback (Optional) The function to execute once the expand completes + * @param {Object} scope (Optional) The scope to run the callback in + */ + expand: function(recursive, callback, scope) { + var me = this; + + // all paths must call the callback (eventually) or things like + // selectPath fail + + // First we start by checking if this node is a parent + if (!me.isLeaf()) { + // Now we check if this record is already expanding or expanded + if (!me.isLoading() && !me.isExpanded()) { + // The TreeStore actually listens for the beforeexpand method and checks + // whether we have to asynchronously load the children from the server + // first. Thats why we pass a callback function to the event that the + // store can call once it has loaded and parsed all the children. + me.fireEvent('beforeexpand', me, function(records) { + me.set('expanded', true); + me.fireEvent('expand', me, me.childNodes, false); + + // Call the expandChildren method if recursive was set to true + if (recursive) { + me.expandChildren(true, callback, scope); + } + else { + Ext.callback(callback, scope || me, [me.childNodes]); + } + }, me); + } + // If it is is already expanded but we want to recursively expand then call expandChildren + else if (recursive) { + me.expandChildren(true, callback, scope); + } + else { + Ext.callback(callback, scope || me, [me.childNodes]); + } + + // TODO - if the node isLoading, we probably need to defer the + // callback until it is loaded (e.g., selectPath would need us + // to not make the callback until the childNodes exist). + } + // If it's not then we fire the callback right away + else { + Ext.callback(callback, scope || me); // leaf = no childNodes + } + }, + + /** + * Expand all the children of this node. + * @param {Function} recursive (Optional) True to recursively expand all the children + * @param {Function} callback (Optional) The function to execute once all the children are expanded + * @param {Object} scope (Optional) The scope to run the callback in + */ + expandChildren: function(recursive, callback, scope) { + var me = this, + i = 0, + nodes = me.childNodes, + ln = nodes.length, + node, + expanding = 0; + + for (; i < ln; ++i) { + node = nodes[i]; + if (!node.isLeaf() && !node.isExpanded()) { + expanding++; + nodes[i].expand(recursive, function () { + expanding--; + if (callback && !expanding) { + Ext.callback(callback, scope || me, me.childNodes); + } + }); + } + } + + if (!expanding && callback) { + Ext.callback(callback, scope || me, me.childNodes); + } + }, + + /** + * Collapse this node. + * @param {Function} recursive (Optional) True to recursively collapse all the children + * @param {Function} callback (Optional) The function to execute once the collapse completes + * @param {Object} scope (Optional) The scope to run the callback in + */ + collapse: function(recursive, callback, scope) { + var me = this; + + // First we start by checking if this node is a parent + if (!me.isLeaf()) { + // Now we check if this record is already collapsing or collapsed + if (!me.collapsing && me.isExpanded()) { + me.fireEvent('beforecollapse', me, function(records) { + me.set('expanded', false); + me.fireEvent('collapse', me, me.childNodes, false); + + // Call the collapseChildren method if recursive was set to true + if (recursive) { + me.collapseChildren(true, callback, scope); + } + else { + Ext.callback(callback, scope || me, [me.childNodes]); + } + }, me); + } + // If it is is already collapsed but we want to recursively collapse then call collapseChildren + else if (recursive) { + me.collapseChildren(true, callback, scope); + } + } + // If it's not then we fire the callback right away + else { + Ext.callback(callback, scope || me, me.childNodes); + } + }, + + /** + * Collapse all the children of this node. + * @param {Function} recursive (Optional) True to recursively collapse all the children + * @param {Function} callback (Optional) The function to execute once all the children are collapsed + * @param {Object} scope (Optional) The scope to run the callback in + */ + collapseChildren: function(recursive, callback, scope) { + var me = this, + i = 0, + nodes = me.childNodes, + ln = nodes.length, + node, + collapsing = 0; + + for (; i < ln; ++i) { + node = nodes[i]; + if (!node.isLeaf() && node.isExpanded()) { + collapsing++; + nodes[i].collapse(recursive, function () { + collapsing--; + if (callback && !collapsing) { + Ext.callback(callback, scope || me, me.childNodes); + } + }); + } + } + + if (!collapsing && callback) { + Ext.callback(callback, scope || me, me.childNodes); + } + } + }; + } + } +}); +/** + * @class Ext.data.NodeStore + * @extends Ext.data.AbstractStore + * Node Store + * @ignore + */ +Ext.define('Ext.data.NodeStore', { + extend: 'Ext.data.Store', + alias: 'store.node', + requires: ['Ext.data.NodeInterface'], + + /** + * @cfg {Ext.data.Record} node The Record you want to bind this Store to. Note that + * this record will be decorated with the Ext.data.NodeInterface if this is not the + * case yet. + */ + node: null, + + /** + * @cfg {Boolean} recursive Set this to true if you want this NodeStore to represent + * all the descendents of the node in its flat data collection. This is useful for + * rendering a tree structure to a DataView and is being used internally by + * the TreeView. Any records that are moved, removed, inserted or appended to the + * node at any depth below the node this store is bound to will be automatically + * updated in this Store's internal flat data structure. + */ + recursive: false, + + /** + * @cfg {Boolean} rootVisible false to not include the root node in this Stores collection (defaults to true) + */ + rootVisible: false, + + constructor: function(config) { + var me = this, + node; + + config = config || {}; + Ext.apply(me, config); + + // + if (Ext.isDefined(me.proxy)) { + Ext.Error.raise("A NodeStore cannot be bound to a proxy. Instead bind it to a record " + + "decorated with the NodeInterface by setting the node config."); + } + // + + config.proxy = {type: 'proxy'}; + me.callParent([config]); + + me.addEvents('expand', 'collapse', 'beforeexpand', 'beforecollapse'); + + node = me.node; + if (node) { + me.node = null; + me.setNode(node); + } + }, + + setNode: function(node) { + var me = this; + + if (me.node && me.node != node) { + // We want to unbind our listeners on the old node + me.mun(me.node, { + expand: me.onNodeExpand, + collapse: me.onNodeCollapse, + append: me.onNodeAppend, + insert: me.onNodeInsert, + remove: me.onNodeRemove, + sort: me.onNodeSort, + scope: me + }); + me.node = null; + } + + if (node) { + Ext.data.NodeInterface.decorate(node); + me.removeAll(); + if (me.rootVisible) { + me.add(node); + } + me.mon(node, { + expand: me.onNodeExpand, + collapse: me.onNodeCollapse, + append: me.onNodeAppend, + insert: me.onNodeInsert, + remove: me.onNodeRemove, + sort: me.onNodeSort, + scope: me + }); + me.node = node; + if (node.isExpanded() && node.isLoaded()) { + me.onNodeExpand(node, node.childNodes, true); + } + } + }, + + onNodeSort: function(node, childNodes) { + var me = this; + + if ((me.indexOf(node) !== -1 || (node === me.node && !me.rootVisible) && node.isExpanded())) { + me.onNodeCollapse(node, childNodes, true); + me.onNodeExpand(node, childNodes, true); + } + }, + + onNodeExpand: function(parent, records, suppressEvent) { + var me = this, + insertIndex = me.indexOf(parent) + 1, + ln = records ? records.length : 0, + i, record; + + if (!me.recursive && parent !== me.node) { + return; + } + + if (!me.isVisible(parent)) { + return; + } + + if (!suppressEvent && me.fireEvent('beforeexpand', parent, records, insertIndex) === false) { + return; + } + + if (ln) { + me.insert(insertIndex, records); + for (i = 0; i < ln; i++) { + record = records[i]; + if (record.isExpanded()) { + if (record.isLoaded()) { + // Take a shortcut + me.onNodeExpand(record, record.childNodes, true); + } + else { + record.set('expanded', false); + record.expand(); + } + } + } + } + + if (!suppressEvent) { + me.fireEvent('expand', parent, records); + } + }, + + onNodeCollapse: function(parent, records, suppressEvent) { + var me = this, + ln = records.length, + collapseIndex = me.indexOf(parent) + 1, + i, record; + + if (!me.recursive && parent !== me.node) { + return; + } + + if (!suppressEvent && me.fireEvent('beforecollapse', parent, records, collapseIndex) === false) { + return; + } + + for (i = 0; i < ln; i++) { + record = records[i]; + me.remove(record); + if (record.isExpanded()) { + me.onNodeCollapse(record, record.childNodes, true); + } + } + + if (!suppressEvent) { + me.fireEvent('collapse', parent, records, collapseIndex); + } + }, + + onNodeAppend: function(parent, node, index) { + var me = this, + refNode, sibling; + + if (me.isVisible(node)) { + if (index === 0) { + refNode = parent; + } else { + sibling = node.previousSibling; + while (sibling.isExpanded() && sibling.lastChild) { + sibling = sibling.lastChild; + } + refNode = sibling; + } + me.insert(me.indexOf(refNode) + 1, node); + if (!node.isLeaf() && node.isExpanded()) { + if (node.isLoaded()) { + // Take a shortcut + me.onNodeExpand(node, node.childNodes, true); + } + else { + node.set('expanded', false); + node.expand(); + } + } + } + }, + + onNodeInsert: function(parent, node, refNode) { + var me = this, + index = this.indexOf(refNode); + + if (index != -1 && me.isVisible(node)) { + me.insert(index, node); + if (!node.isLeaf() && node.isExpanded()) { + if (node.isLoaded()) { + // Take a shortcut + me.onNodeExpand(node, node.childNodes, true); + } + else { + node.set('expanded', false); + node.expand(); + } + } + } + }, + + onNodeRemove: function(parent, node, index) { + var me = this; + if (me.indexOf(node) != -1) { + if (!node.isLeaf() && node.isExpanded()) { + me.onNodeCollapse(node, node.childNodes, true); + } + me.remove(node); + } + }, + + isVisible: function(node) { + var parent = node.parentNode; + while (parent) { + if (parent === this.node && !this.rootVisible && parent.isExpanded()) { + return true; + } + + if (this.indexOf(parent) === -1 || !parent.isExpanded()) { + return false; + } + + parent = parent.parentNode; + } + return true; + } +}); +/** + * @author Ed Spencer + * @class Ext.data.Request + * @extends Object + * + *

Simple class that represents a Request that will be made by any {@link Ext.data.proxy.Server} subclass. + * All this class does is standardize the representation of a Request as used by any ServerProxy subclass, + * it does not contain any actual logic or perform the request itself.

+ * + * @constructor + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.Request', { + /** + * @cfg {String} action The name of the action this Request represents. Usually one of 'create', 'read', 'update' or 'destroy' + */ + action: undefined, + + /** + * @cfg {Object} params HTTP request params. The Proxy and its Writer have access to and can modify this object. + */ + params: undefined, + + /** + * @cfg {String} method The HTTP method to use on this Request (defaults to 'GET'). Should be one of 'GET', 'POST', 'PUT' or 'DELETE' + */ + method: 'GET', + + /** + * @cfg {String} url The url to access on this Request + */ + url: undefined, + + constructor: function(config) { + Ext.apply(this, config); + } +}); +/** + * @class Ext.data.Tree + * + * This class is used as a container for a series of nodes. The nodes themselves maintain + * the relationship between parent/child. The tree itself acts as a manager. It gives functionality + * to retrieve a node by its identifier: {@link #getNodeById}. + * + * The tree also relays events from any of it's child nodes, allowing them to be handled in a + * centralized fashion. In general this class is not used directly, rather used internally + * by other parts of the framework. + * + * @constructor + * @param {Node} root (optional) The root node + */ +Ext.define('Ext.data.Tree', { + alias: 'data.tree', + + mixins: { + observable: "Ext.util.Observable" + }, + + /** + * The root node for this tree + * @type Node + */ + root: null, + + constructor: function(root) { + var me = this; + + me.nodeHash = {}; + + me.mixins.observable.constructor.call(me); + + if (root) { + me.setRootNode(root); + } + }, + + /** + * Returns the root node for this tree. + * @return {Ext.data.NodeInterface} + */ + getRootNode : function() { + return this.root; + }, + + /** + * Sets the root node for this tree. + * @param {Ext.data.NodeInterface} node + * @return {Ext.data.NodeInterface} The root node + */ + setRootNode : function(node) { + var me = this; + + me.root = node; + Ext.data.NodeInterface.decorate(node); + + if (me.fireEvent('beforeappend', null, node) !== false) { + node.set('root', true); + node.updateInfo(); + + me.relayEvents(node, [ + /** + * @event append + * Fires when a new child node is appended to a node in this tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The newly appended node + * @param {Number} index The index of the newly appended node + */ + "append", + + /** + * @event remove + * Fires when a child node is removed from a node in this tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node removed + */ + "remove", + + /** + * @event move + * Fires when a node is moved to a new location in the tree + * @param {Tree} tree The owner tree + * @param {Node} node The node moved + * @param {Node} oldParent The old parent of this node + * @param {Node} newParent The new parent of this node + * @param {Number} index The index it was moved to + */ + "move", + + /** + * @event insert + * Fires when a new child node is inserted in a node in this tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node inserted + * @param {Node} refNode The child node the node was inserted before + */ + "insert", + + /** + * @event beforeappend + * Fires before a new child is appended to a node in this tree, return false to cancel the append. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be appended + */ + "beforeappend", + + /** + * @event beforeremove + * Fires before a child is removed from a node in this tree, return false to cancel the remove. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be removed + */ + "beforeremove", + + /** + * @event beforemove + * Fires before a node is moved to a new location in the tree. Return false to cancel the move. + * @param {Tree} tree The owner tree + * @param {Node} node The node being moved + * @param {Node} oldParent The parent of the node + * @param {Node} newParent The new parent the node is moving to + * @param {Number} index The index it is being moved to + */ + "beforemove", + + /** + * @event beforeinsert + * Fires before a new child is inserted in a node in this tree, return false to cancel the insert. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be inserted + * @param {Node} refNode The child node the node is being inserted before + */ + "beforeinsert", + + /** + * @event expand + * Fires when this node is expanded. + * @param {Node} this The expanding node + */ + "expand", + + /** + * @event collapse + * Fires when this node is collapsed. + * @param {Node} this The collapsing node + */ + "collapse", + + /** + * @event beforeexpand + * Fires before this node is expanded. + * @param {Node} this The expanding node + */ + "beforeexpand", + + /** + * @event beforecollapse + * Fires before this node is collapsed. + * @param {Node} this The collapsing node + */ + "beforecollapse" , + + /** + * @event rootchange + * Fires whenever the root node is changed in the tree. + * @param {Ext.data.Model} root The new root + */ + "rootchange" + ]); + + node.on({ + scope: me, + insert: me.onNodeInsert, + append: me.onNodeAppend, + remove: me.onNodeRemove + }); + + me.registerNode(node); + me.fireEvent('append', null, node); + me.fireEvent('rootchange', node); + } + + return node; + }, + + /** + * Flattens all the nodes in the tree into an array. + * @private + * @return {Array} The flattened nodes. + */ + flatten: function(){ + var nodes = [], + hash = this.nodeHash, + key; + + for (key in hash) { + if (hash.hasOwnProperty(key)) { + nodes.push(hash[key]); + } + } + return nodes; + }, + + /** + * Fired when a node is inserted into the root or one of it's children + * @private + * @param {Ext.data.NodeInterface} parent The parent node + * @param {Ext.data.NodeInterface} node The inserted node + */ + onNodeInsert: function(parent, node) { + this.registerNode(node); + }, + + /** + * Fired when a node is appended into the root or one of it's children + * @private + * @param {Ext.data.NodeInterface} parent The parent node + * @param {Ext.data.NodeInterface} node The appended node + */ + onNodeAppend: function(parent, node) { + this.registerNode(node); + }, + + /** + * Fired when a node is removed from the root or one of it's children + * @private + * @param {Ext.data.NodeInterface} parent The parent node + * @param {Ext.data.NodeInterface} node The removed node + */ + onNodeRemove: function(parent, node) { + this.unregisterNode(node); + }, + + /** + * Gets a node in this tree by its id. + * @param {String} id + * @return {Ext.data.NodeInterface} The match node. + */ + getNodeById : function(id) { + return this.nodeHash[id]; + }, + + /** + * Registers a node with the tree + * @private + * @param {Ext.data.NodeInterface} The node to register + */ + registerNode : function(node) { + this.nodeHash[node.getId() || node.internalId] = node; + }, + + /** + * Unregisters a node with the tree + * @private + * @param {Ext.data.NodeInterface} The node to unregister + */ + unregisterNode : function(node) { + delete this.nodeHash[node.getId() || node.internalId]; + }, + + /** + * Sorts this tree + * @private + * @param {Function} sorterFn The function to use for sorting + * @param {Boolean} recursive True to perform recursive sorting + */ + sort: function(sorterFn, recursive) { + this.getRootNode().sort(sorterFn, recursive); + }, + + /** + * Filters this tree + * @private + * @param {Function} sorterFn The function to use for filtering + * @param {Boolean} recursive True to perform recursive filtering + */ + filter: function(filters, recursive) { + this.getRootNode().filter(filters, recursive); + } +}); +/** + * @class Ext.data.TreeStore + * @extends Ext.data.AbstractStore + * + * The TreeStore is a store implementation that is backed by by an {@link Ext.data.Tree}. + * It provides convenience methods for loading nodes, as well as the ability to use + * the hierarchical tree structure combined with a store. This class is generally used + * in conjunction with {@link Ext.tree.Panel}. This class also relays many events from + * the Tree for convenience. + * + * ## Using Models + * If no Model is specified, an implicit model will be created that implements {@link Ext.data.NodeInterface}. + * The standard Tree fields will also be copied onto the Model for maintaining their state. + * + * ## Reading Nested Data + * For the tree to read nested data, the {@link Ext.data.Reader} must be configured with a root property, + * so the reader can find nested data for each node. If a root is not specified, it will default to + * 'children'. + */ +Ext.define('Ext.data.TreeStore', { + extend: 'Ext.data.AbstractStore', + alias: 'store.tree', + requires: ['Ext.data.Tree', 'Ext.data.NodeInterface', 'Ext.data.NodeStore'], + + /** + * @cfg {Boolean} clearOnLoad (optional) Default to true. Remove previously existing + * child nodes before loading. + */ + clearOnLoad : true, + + /** + * @cfg {String} nodeParam The name of the parameter sent to the server which contains + * the identifier of the node. Defaults to 'node'. + */ + nodeParam: 'node', + + /** + * @cfg {String} defaultRootId + * The default root id. Defaults to 'root' + */ + defaultRootId: 'root', + + /** + * @cfg {String} defaultRootProperty + * The root property to specify on the reader if one is not explicitly defined. + */ + defaultRootProperty: 'children', + + /** + * @cfg {Boolean} folderSort Set to true to automatically prepend a leaf sorter (defaults to undefined) + */ + folderSort: false, + + constructor: function(config) { + var me = this, + root, + fields; + + + config = Ext.apply({}, config); + + /** + * If we have no fields declare for the store, add some defaults. + * These will be ignored if a model is explicitly specified. + */ + fields = config.fields || me.fields; + if (!fields) { + config.fields = [{name: 'text', type: 'string'}]; + } + + me.callParent([config]); + + // We create our data tree. + me.tree = Ext.create('Ext.data.Tree'); + + me.tree.on({ + scope: me, + remove: me.onNodeRemove, + beforeexpand: me.onBeforeNodeExpand, + beforecollapse: me.onBeforeNodeCollapse, + append: me.onNodeAdded, + insert: me.onNodeAdded + }); + + me.onBeforeSort(); + + root = me.root; + if (root) { + delete me.root; + me.setRootNode(root); + } + + me.relayEvents(me.tree, [ + /** + * @event append + * Fires when a new child node is appended to a node in this store's tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The newly appended node + * @param {Number} index The index of the newly appended node + */ + "append", + + /** + * @event remove + * Fires when a child node is removed from a node in this store's tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node removed + */ + "remove", + + /** + * @event move + * Fires when a node is moved to a new location in the store's tree + * @param {Tree} tree The owner tree + * @param {Node} node The node moved + * @param {Node} oldParent The old parent of this node + * @param {Node} newParent The new parent of this node + * @param {Number} index The index it was moved to + */ + "move", + + /** + * @event insert + * Fires when a new child node is inserted in a node in this store's tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node inserted + * @param {Node} refNode The child node the node was inserted before + */ + "insert", + + /** + * @event beforeappend + * Fires before a new child is appended to a node in this store's tree, return false to cancel the append. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be appended + */ + "beforeappend", + + /** + * @event beforeremove + * Fires before a child is removed from a node in this store's tree, return false to cancel the remove. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be removed + */ + "beforeremove", + + /** + * @event beforemove + * Fires before a node is moved to a new location in the store's tree. Return false to cancel the move. + * @param {Tree} tree The owner tree + * @param {Node} node The node being moved + * @param {Node} oldParent The parent of the node + * @param {Node} newParent The new parent the node is moving to + * @param {Number} index The index it is being moved to + */ + "beforemove", + + /** + * @event beforeinsert + * Fires before a new child is inserted in a node in this store's tree, return false to cancel the insert. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be inserted + * @param {Node} refNode The child node the node is being inserted before + */ + "beforeinsert", + + /** + * @event expand + * Fires when this node is expanded. + * @param {Node} this The expanding node + */ + "expand", + + /** + * @event collapse + * Fires when this node is collapsed. + * @param {Node} this The collapsing node + */ + "collapse", + + /** + * @event beforeexpand + * Fires before this node is expanded. + * @param {Node} this The expanding node + */ + "beforeexpand", + + /** + * @event beforecollapse + * Fires before this node is collapsed. + * @param {Node} this The collapsing node + */ + "beforecollapse", + + /** + * @event sort + * Fires when this TreeStore is sorted. + * @param {Node} node The node that is sorted. + */ + "sort", + + /** + * @event rootchange + * Fires whenever the root node is changed in the tree. + * @param {Ext.data.Model} root The new root + */ + "rootchange" + ]); + + me.addEvents( + /** + * @event rootchange + * Fires when the root node on this TreeStore is changed. + * @param {Ext.data.TreeStore} store This TreeStore + * @param {Node} The new root node. + */ + 'rootchange' + ); + + // + if (Ext.isDefined(me.nodeParameter)) { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.data.TreeStore: nodeParameter has been deprecated. Please use nodeParam instead.'); + } + me.nodeParam = me.nodeParameter; + delete me.nodeParameter; + } + // + }, + + // inherit docs + setProxy: function(proxy) { + var reader, + needsRoot; + + if (proxy instanceof Ext.data.proxy.Proxy) { + // proxy instance, check if a root was set + needsRoot = Ext.isEmpty(proxy.getReader().root); + } else if (Ext.isString(proxy)) { + // string type, means a reader can't be set + needsRoot = true; + } else { + // object, check if a reader and a root were specified. + reader = proxy.reader; + needsRoot = !(reader && !Ext.isEmpty(reader.root)); + } + proxy = this.callParent(arguments); + if (needsRoot) { + reader = proxy.getReader(); + reader.root = this.defaultRootProperty; + // force rebuild + reader.buildExtractors(true); + } + }, + + // inherit docs + onBeforeSort: function() { + if (this.folderSort) { + this.sort({ + property: 'leaf', + direction: 'ASC' + }, 'prepend', false); + } + }, + + /** + * Called before a node is expanded. + * @private + * @param {Ext.data.NodeInterface} node The node being expanded. + * @param {Function} callback The function to run after the expand finishes + * @param {Object} scope The scope in which to run the callback function + */ + onBeforeNodeExpand: function(node, callback, scope) { + if (node.isLoaded()) { + Ext.callback(callback, scope || node, [node.childNodes]); + } + else if (node.isLoading()) { + this.on('load', function() { + Ext.callback(callback, scope || node, [node.childNodes]); + }, this, {single: true}); + } + else { + this.read({ + node: node, + callback: function() { + Ext.callback(callback, scope || node, [node.childNodes]); + } + }); + } + }, + + //inherit docs + getNewRecords: function() { + return Ext.Array.filter(this.tree.flatten(), this.filterNew); + }, + + //inherit docs + getUpdatedRecords: function() { + return Ext.Array.filter(this.tree.flatten(), this.filterUpdated); + }, + + /** + * Called before a node is collapsed. + * @private + * @param {Ext.data.NodeInterface} node The node being collapsed. + * @param {Function} callback The function to run after the collapse finishes + * @param {Object} scope The scope in which to run the callback function + */ + onBeforeNodeCollapse: function(node, callback, scope) { + callback.call(scope || node, node.childNodes); + }, + + onNodeRemove: function(parent, node) { + var removed = this.removed; + + if (!node.isReplace && Ext.Array.indexOf(removed, node) == -1) { + removed.push(node); + } + }, + + onNodeAdded: function(parent, node) { + var proxy = this.getProxy(), + reader = proxy.getReader(), + data = node.raw || node.data, + dataRoot, children; + + Ext.Array.remove(this.removed, node); + + if (!node.isLeaf() && !node.isLoaded()) { + dataRoot = reader.getRoot(data); + if (dataRoot) { + this.fillNode(node, reader.extractData(dataRoot)); + delete data[reader.root]; + } + } + }, + + /** + * Sets the root node for this store + * @param {Ext.data.Model/Ext.data.NodeInterface} root + * @return {Ext.data.NodeInterface} The new root + */ + setRootNode: function(root) { + var me = this; + + root = root || {}; + if (!root.isNode) { + // create a default rootNode and create internal data struct. + Ext.applyIf(root, { + id: me.defaultRootId, + text: 'Root', + allowDrag: false + }); + root = Ext.ModelManager.create(root, me.model); + } + Ext.data.NodeInterface.decorate(root); + + // Because we have decorated the model with new fields, + // we need to build new extactor functions on the reader. + me.getProxy().getReader().buildExtractors(true); + + // When we add the root to the tree, it will automaticaly get the NodeInterface + me.tree.setRootNode(root); + + // If the user has set expanded: true on the root, we want to call the expand function + if (!root.isLoaded() && root.isExpanded()) { + me.load({ + node: root + }); + } + + return root; + }, + + /** + * Returns the root node for this tree. + * @return {Ext.data.NodeInterface} + */ + getRootNode: function() { + return this.tree.getRootNode(); + }, + + /** + * Returns the record node by id + * @return {Ext.data.NodeInterface} + */ + getNodeById: function(id) { + return this.tree.getNodeById(id); + }, + + /** + * Loads the Store using its configured {@link #proxy}. + * @param {Object} options Optional config object. This is passed into the {@link Ext.data.Operation Operation} + * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function. + * The options can also contain a node, which indicates which node is to be loaded. If not specified, it will + * default to the root node. + */ + load: function(options) { + options = options || {}; + options.params = options.params || {}; + + var me = this, + node = options.node || me.tree.getRootNode(), + root; + + // If there is not a node it means the user hasnt defined a rootnode yet. In this case lets just + // create one for them. + if (!node) { + node = me.setRootNode({ + expanded: true + }); + } + + if (me.clearOnLoad) { + node.removeAll(); + } + + Ext.applyIf(options, { + node: node + }); + options.params[me.nodeParam] = node ? node.getId() : 'root'; + + if (node) { + node.set('loading', true); + } + + return me.callParent([options]); + }, + + + /** + * Fills a node with a series of child records. + * @private + * @param {Ext.data.NodeInterface} node The node to fill + * @param {Array} records The records to add + */ + fillNode: function(node, records) { + var me = this, + ln = records ? records.length : 0, + i = 0, sortCollection; + + if (ln && me.sortOnLoad && !me.remoteSort && me.sorters && me.sorters.items) { + sortCollection = Ext.create('Ext.util.MixedCollection'); + sortCollection.addAll(records); + sortCollection.sort(me.sorters.items); + records = sortCollection.items; + } + + node.set('loaded', true); + for (; i < ln; i++) { + node.appendChild(records[i], undefined, true); + } + + return records; + }, + + // inherit docs + onProxyLoad: function(operation) { + var me = this, + successful = operation.wasSuccessful(), + records = operation.getRecords(), + node = operation.node; + + node.set('loading', false); + if (successful) { + records = me.fillNode(node, records); + } + // deprecate read? + me.fireEvent('read', me, operation.node, records, successful); + me.fireEvent('load', me, operation.node, records, successful); + //this is a callback that would have been passed to the 'read' function and is optional + Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]); + }, + + /** + * Create any new records when a write is returned from the server. + * @private + * @param {Array} records The array of new records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onCreateRecords: function(records, operation, success) { + if (success) { + var i = 0, + length = records.length, + originalRecords = operation.records, + parentNode, + record, + original, + index; + + /** + * Loop over each record returned from the server. Assume they are + * returned in order of how they were sent. If we find a matching + * record, replace it with the newly created one. + */ + for (; i < length; ++i) { + record = records[i]; + original = originalRecords[i]; + if (original) { + parentNode = original.parentNode; + if (parentNode) { + // prevent being added to the removed cache + original.isReplace = true; + parentNode.replaceChild(record, original); + delete original.isReplace; + } + record.phantom = false; + } + } + } + }, + + /** + * Update any records when a write is returned from the server. + * @private + * @param {Array} records The array of updated records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onUpdateRecords: function(records, operation, success){ + if (success) { + var me = this, + i = 0, + length = records.length, + data = me.data, + original, + parentNode, + record; + + for (; i < length; ++i) { + record = records[i]; + original = me.tree.getNodeById(record.getId()); + parentNode = original.parentNode; + if (parentNode) { + // prevent being added to the removed cache + original.isReplace = true; + parentNode.replaceChild(record, original); + original.isReplace = false; + } + } + } + }, + + /** + * Remove any records when a write is returned from the server. + * @private + * @param {Array} records The array of removed records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onDestroyRecords: function(records, operation, success){ + if (success) { + this.removed = []; + } + }, + + // inherit docs + removeAll: function() { + this.getRootNode().destroy(); + this.fireEvent('clear', this); + }, + + // inherit docs + doSort: function(sorterFn) { + var me = this; + if (me.remoteSort) { + //the load function will pick up the new sorters and request the sorted data from the proxy + me.load(); + } else { + me.tree.sort(sorterFn, true); + me.fireEvent('datachanged', me); + } + me.fireEvent('sort', me); + } +}); +/** + * @author Ed Spencer + * @class Ext.data.XmlStore + * @extends Ext.data.Store + * @private + * @ignore + *

Small helper class to make creating {@link Ext.data.Store}s from XML data easier. + * A XmlStore will be automatically configured with a {@link Ext.data.reader.Xml}.

+ *

A store configuration would be something like:


+var store = new Ext.data.XmlStore({
+    // store configs
+    autoDestroy: true,
+    storeId: 'myStore',
+    url: 'sheldon.xml', // automatically configures a HttpProxy
+    // reader configs
+    record: 'Item', // records will have an "Item" tag
+    idPath: 'ASIN',
+    totalRecords: '@TotalResults'
+    fields: [
+        // set up the fields mapping into the xml doc
+        // The first needs mapping, the others are very basic
+        {name: 'Author', mapping: 'ItemAttributes > Author'},
+        'Title', 'Manufacturer', 'ProductGroup'
+    ]
+});
+ * 

+ *

This store is configured to consume a returned object of the form:


+<?xml version="1.0" encoding="UTF-8"?>
+<ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2009-05-15">
+    <Items>
+        <Request>
+            <IsValid>True</IsValid>
+            <ItemSearchRequest>
+                <Author>Sidney Sheldon</Author>
+                <SearchIndex>Books</SearchIndex>
+            </ItemSearchRequest>
+        </Request>
+        <TotalResults>203</TotalResults>
+        <TotalPages>21</TotalPages>
+        <Item>
+            <ASIN>0446355453</ASIN>
+            <DetailPageURL>
+                http://www.amazon.com/
+            </DetailPageURL>
+            <ItemAttributes>
+                <Author>Sidney Sheldon</Author>
+                <Manufacturer>Warner Books</Manufacturer>
+                <ProductGroup>Book</ProductGroup>
+                <Title>Master of the Game</Title>
+            </ItemAttributes>
+        </Item>
+    </Items>
+</ItemSearchResponse>
+ * 
+ * An object literal of this form could also be used as the {@link #data} config option.

+ *

Note: Although not listed here, this class accepts all of the configuration options of + * {@link Ext.data.reader.Xml XmlReader}.

+ * @constructor + * @param {Object} config + * @xtype xmlstore + */ +Ext.define('Ext.data.XmlStore', { + extend: 'Ext.data.Store', + alternateClassName: 'Ext.data.XmlStore', + alias: 'store.xml', + + /** + * @cfg {Ext.data.DataReader} reader @hide + */ + constructor: function(config){ + config = config || {}; + config = config || {}; + + Ext.applyIf(config, { + proxy: { + type: 'ajax', + reader: 'xml', + writer: 'xml' + } + }); + + this.callParent([config]); + } +}); + +/** + * @author Ed Spencer + * @class Ext.data.proxy.Client + * @extends Ext.data.proxy.Proxy + * + *

Base class for any client-side storage. Used as a superclass for {@link Ext.data.proxy.Memory Memory} and + * {@link Ext.data.proxy.WebStorage Web Storage} proxies. Do not use directly, use one of the subclasses instead.

+ */ +Ext.define('Ext.data.proxy.Client', { + extend: 'Ext.data.proxy.Proxy', + alternateClassName: 'Ext.data.ClientProxy', + + /** + * Abstract function that must be implemented by each ClientProxy subclass. This should purge all record data + * from the client side storage, as well as removing any supporting data (such as lists of record IDs) + */ + clear: function() { + // + Ext.Error.raise("The Ext.data.proxy.Client subclass that you are using has not defined a 'clear' function. See src/data/ClientProxy.js for details."); + // + } +}); +/** + * @author Ed Spencer + * @class Ext.data.proxy.JsonP + * @extends Ext.data.proxy.Server + * + *

JsonPProxy is useful when you need to load data from a domain other than the one your application is running + * on. If your application is running on http://domainA.com it cannot use {@link Ext.data.proxy.Ajax Ajax} to load its + * data from http://domainB.com because cross-domain ajax requests are prohibited by the browser.

+ * + *

We can get around this using a JsonPProxy. JsonPProxy injects a <script> tag into the DOM whenever + * an AJAX request would usually be made. Let's say we want to load data from http://domainB.com/users - the script tag + * that would be injected might look like this:

+ * +

+<script src="http://domainB.com/users?callback=someCallback"></script>
+
+ * + *

When we inject the tag above, the browser makes a request to that url and includes the response as if it was any + * other type of JavaScript include. By passing a callback in the url above, we're telling domainB's server that we + * want to be notified when the result comes in and that it should call our callback function with the data it sends + * back. So long as the server formats the response to look like this, everything will work:

+ * +

+someCallback({
+    users: [
+        {
+            id: 1,
+            name: "Ed Spencer",
+            email: "ed@sencha.com"
+        }
+    ]
+});
+
+ * + *

As soon as the script finishes loading, the 'someCallback' function that we passed in the url is called with the + * JSON object that the server returned.

+ * + *

JsonPProxy takes care of all of this automatically. It formats the url you pass, adding the callback + * parameter automatically. It even creates a temporary callback function, waits for it to be called and then puts + * the data into the Proxy making it look just like you loaded it through a normal {@link Ext.data.proxy.Ajax AjaxProxy}. + * Here's how we might set that up:

+ * +

+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'name', 'email']
+});
+
+var store = new Ext.data.Store({
+    model: 'User',
+    proxy: {
+        type: 'jsonp',
+        url : 'http://domainB.com/users'
+    }
+});
+
+store.load();
+
+ * + *

That's all we need to do - JsonPProxy takes care of the rest. In this case the Proxy will have injected a + * script tag like this: + * +


+<script src="http://domainB.com/users?callback=stcCallback001" id="stcScript001"></script>
+
+ * + *

Customization

+ * + *

Most parts of this script tag can be customized using the {@link #callbackParam}, {@link #callbackPrefix} and + * {@link #scriptIdPrefix} configurations. For example: + * +


+var store = new Ext.data.Store({
+    model: 'User',
+    proxy: {
+        type: 'jsonp',
+        url : 'http://domainB.com/users',
+        callbackParam: 'theCallbackFunction',
+        callbackPrefix: 'ABC',
+        scriptIdPrefix: 'injectedScript'
+    }
+});
+
+store.load();
+
+ * + *

Would inject a script tag like this:

+ * +

+<script src="http://domainB.com/users?theCallbackFunction=ABC001" id="injectedScript001"></script>
+
+ * + *

Implementing on the server side

+ * + *

The remote server side needs to be configured to return data in this format. Here are suggestions for how you + * might achieve this using Java, PHP and ASP.net:

+ * + *

Java:

+ * +

+boolean jsonP = false;
+String cb = request.getParameter("callback");
+if (cb != null) {
+    jsonP = true;
+    response.setContentType("text/javascript");
+} else {
+    response.setContentType("application/x-json");
+}
+Writer out = response.getWriter();
+if (jsonP) {
+    out.write(cb + "(");
+}
+out.print(dataBlock.toJsonString());
+if (jsonP) {
+    out.write(");");
+}
+
+ * + *

PHP:

+ * +

+$callback = $_REQUEST['callback'];
+
+// Create the output object.
+$output = array('a' => 'Apple', 'b' => 'Banana');
+
+//start output
+if ($callback) {
+    header('Content-Type: text/javascript');
+    echo $callback . '(' . json_encode($output) . ');';
+} else {
+    header('Content-Type: application/x-json');
+    echo json_encode($output);
+}
+
+ * + *

ASP.net:

+ * +

+String jsonString = "{success: true}";
+String cb = Request.Params.Get("callback");
+String responseString = "";
+if (!String.IsNullOrEmpty(cb)) {
+    responseString = cb + "(" + jsonString + ")";
+} else {
+    responseString = jsonString;
+}
+Response.Write(responseString);
+
+ * + */ +Ext.define('Ext.data.proxy.JsonP', { + extend: 'Ext.data.proxy.Server', + alternateClassName: 'Ext.data.ScriptTagProxy', + alias: ['proxy.jsonp', 'proxy.scripttag'], + requires: ['Ext.data.JsonP'], + + defaultWriterType: 'base', + + /** + * @cfg {String} callbackKey (Optional) See {@link Ext.data.JsonP#callbackKey}. + */ + callbackKey : 'callback', + + /** + * @cfg {String} recordParam + * The param name to use when passing records to the server (e.g. 'records=someEncodedRecordString'). + * Defaults to 'records' + */ + recordParam: 'records', + + /** + * @cfg {Boolean} autoAppendParams True to automatically append the request's params to the generated url. Defaults to true + */ + autoAppendParams: true, + + constructor: function(){ + this.addEvents( + /** + * @event exception + * Fires when the server returns an exception + * @param {Ext.data.proxy.Proxy} this + * @param {Ext.data.Request} request The request that was sent + * @param {Ext.data.Operation} operation The operation that triggered the request + */ + 'exception' + ); + this.callParent(arguments); + }, + + /** + * @private + * Performs the read request to the remote domain. JsonPProxy does not actually create an Ajax request, + * instead we write out a + * + * ## Configuration + * + * This component allows several options for configuring how the target Flash movie is embedded. The most + * important is the required {@link #url} which points to the location of the Flash movie to load. Other + * configurations include: + * + * - {@link #backgroundColor} + * - {@link #wmode} + * - {@link #flashVars} + * - {@link #flashParams} + * - {@link #flashAttributes} + * + * ## Example usage: + * + * var win = Ext.widget('window', { + * title: "It's a tiger!", + * layout: 'fit', + * width: 300, + * height: 300, + * x: 20, + * y: 20, + * resizable: true, + * items: { + * xtype: 'flash', + * url: 'tiger.swf' + * } + * }); + * win.show(); + * + * ## Express Install + * + * Adobe provides a tool called [Express Install](http://www.adobe.com/devnet/flashplayer/articles/express_install.html) + * that offers users an easy way to upgrade their Flash player. If you wish to make use of this, you should set + * the static EXPRESS\_INSTALL\_URL property to the location of your Express Install SWF file: + * + * Ext.flash.Component.EXPRESS_INSTALL_URL = 'path/to/local/expressInstall.swf'; + * + * @constructor + * Creates a new Ext.flash.Component instance. + * @param {Object} config The component configuration. + * + * @xtype flash + * @docauthor Jason Johnston + */ +Ext.define('Ext.flash.Component', { + extend: 'Ext.Component', + alternateClassName: 'Ext.FlashComponent', + alias: 'widget.flash', + + /** + * @cfg {String} flashVersion + * Indicates the version the flash content was published for. Defaults to '9.0.115'. + */ + flashVersion : '9.0.115', + + /** + * @cfg {String} backgroundColor + * The background color of the SWF movie. Defaults to '#ffffff'. + */ + backgroundColor: '#ffffff', + + /** + * @cfg {String} wmode + * The wmode of the flash object. This can be used to control layering. Defaults to 'opaque'. + * Set to 'transparent' to ignore the {@link #backgroundColor} and make the background of the Flash + * movie transparent. + */ + wmode: 'opaque', + + /** + * @cfg {Object} flashVars + * A set of key value pairs to be passed to the flash object as flash variables. Defaults to undefined. + */ + + /** + * @cfg {Object} flashParams + * A set of key value pairs to be passed to the flash object as parameters. Possible parameters can be found here: + * http://kb2.adobe.com/cps/127/tn_12701.html Defaults to undefined. + */ + + /** + * @cfg {Object} flashAttributes + * A set of key value pairs to be passed to the flash object as attributes. Defaults to undefined. + */ + + /** + * @cfg {String} url + * The URL of the SWF file to include. Required. + */ + + /** + * @cfg {String/Number} swfWidth The width of the embedded SWF movie inside the component. Defaults to "100%" + * so that the movie matches the width of the component. + */ + swfWidth: '100%', + + /** + * @cfg {String/Number} swfHeight The height of the embedded SWF movie inside the component. Defaults to "100%" + * so that the movie matches the height of the component. + */ + swfHeight: '100%', + + /** + * @cfg {Boolean} expressInstall + * True to prompt the user to install flash if not installed. Note that this uses + * Ext.FlashComponent.EXPRESS_INSTALL_URL, which should be set to the local resource. Defaults to false. + */ + expressInstall: false, + + /** + * @property swf + * @type {Ext.core.Element} + * A reference to the object or embed element into which the SWF file is loaded. Only + * populated after the component is rendered and the SWF has been successfully embedded. + */ + + // Have to create a placeholder div with the swfId, which SWFObject will replace with the object/embed element. + renderTpl: ['
'], + + initComponent: function() { + // + if (!('swfobject' in window)) { + Ext.Error.raise('The SWFObject library is not loaded. Ext.flash.Component requires SWFObject version 2.2 or later: http://code.google.com/p/swfobject/'); + } + if (!this.url) { + Ext.Error.raise('The "url" config is required for Ext.flash.Component'); + } + // + + this.callParent(); + this.addEvents( + /** + * @event success + * Fired when the Flash movie has been successfully embedded + * @param {Ext.flash.Component} this + */ + 'success', + + /** + * @event failure + * Fired when the Flash movie embedding fails + * @param {Ext.flash.Component} this + */ + 'failure' + ); + }, + + onRender: function() { + var me = this, + params, vars, undef, + swfId = me.getSwfId(); + + me.renderData.swfId = swfId; + + me.callParent(arguments); + + params = Ext.apply({ + allowScriptAccess: 'always', + bgcolor: me.backgroundColor, + wmode: me.wmode + }, me.flashParams); + + vars = Ext.apply({ + allowedDomain: document.location.hostname + }, me.flashVars); + + new swfobject.embedSWF( + me.url, + swfId, + me.swfWidth, + me.swfHeight, + me.flashVersion, + me.expressInstall ? me.statics.EXPRESS_INSTALL_URL : undef, + vars, + params, + me.flashAttributes, + Ext.bind(me.swfCallback, me) + ); + }, + + /** + * @private + * The callback method for handling an embedding success or failure by SWFObject + * @param {Object} e The event object passed by SWFObject - see http://code.google.com/p/swfobject/wiki/api + */ + swfCallback: function(e) { + var me = this; + if (e.success) { + me.swf = Ext.get(e.ref); + me.onSuccess(); + me.fireEvent('success', me); + } else { + me.onFailure(); + me.fireEvent('failure', me); + } + }, + + /** + * Retrieve the id of the SWF object/embed element + */ + getSwfId: function() { + return this.swfId || (this.swfId = "extswf" + this.getAutoId()); + }, + + onSuccess: function() { + // swfobject forces visiblity:visible on the swf element, which prevents it + // from getting hidden when an ancestor is given visibility:hidden. + this.swf.setStyle('visibility', 'inherit'); + }, + + onFailure: Ext.emptyFn, + + beforeDestroy: function() { + var me = this, + swf = me.swf; + if (swf) { + swfobject.removeSWF(me.getSwfId()); + Ext.destroy(swf); + delete me.swf; + } + me.callParent(); + }, + + statics: { + /** + * Sets the url for installing flash if it doesn't exist. This should be set to a local resource. + * See http://www.adobe.com/devnet/flashplayer/articles/express_install.html for details. + * @static + * @type String + */ + EXPRESS_INSTALL_URL: 'http:/' + '/swfobject.googlecode.com/svn/trunk/swfobject/expressInstall.swf' + } +}); + +/** + * @class Ext.form.action.Action + * @extends Ext.Base + *

The subclasses of this class provide actions to perform upon {@link Ext.form.Basic Form}s.

+ *

Instances of this class are only created by a {@link Ext.form.Basic Form} when + * the Form needs to perform an action such as submit or load. The Configuration options + * listed for this class are set through the Form's action methods: {@link Ext.form.Basic#submit submit}, + * {@link Ext.form.Basic#load load} and {@link Ext.form.Basic#doAction doAction}

+ *

The instance of Action which performed the action is passed to the success + * and failure callbacks of the Form's action methods ({@link Ext.form.Basic#submit submit}, + * {@link Ext.form.Basic#load load} and {@link Ext.form.Basic#doAction doAction}), + * and to the {@link Ext.form.Basic#actioncomplete actioncomplete} and + * {@link Ext.form.Basic#actionfailed actionfailed} event handlers.

+ * @constructor + * @param {Object} config The configuration for this instance. + */ +Ext.define('Ext.form.action.Action', { + alternateClassName: 'Ext.form.Action', + + /** + * @cfg {Ext.form.Basic} form The {@link Ext.form.Basic BasicForm} instance that + * is invoking this Action. Required. + */ + + /** + * @cfg {String} url The URL that the Action is to invoke. Will default to the {@link Ext.form.Basic#url url} + * configured on the {@link #form}. + */ + + /** + * @cfg {Boolean} reset When set to true, causes the Form to be + * {@link Ext.form.Basic#reset reset} on Action success. If specified, this happens + * before the {@link #success} callback is called and before the Form's + * {@link Ext.form.Basic#actioncomplete actioncomplete} event fires. + */ + + /** + * @cfg {String} method The HTTP method to use to access the requested URL. Defaults to the + * {@link Ext.form.Basic#method BasicForm's method}, or 'POST' if not specified. + */ + + /** + * @cfg {Object/String} params

Extra parameter values to pass. These are added to the Form's + * {@link Ext.form.Basic#baseParams} and passed to the specified URL along with the Form's + * input fields.

+ *

Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode Ext.Object.toQueryString}.

+ */ + + /** + * @cfg {Object} headers

Extra headers to be sent in the AJAX request for submit and load actions. See + * {@link Ext.data.Connection#headers}.

+ */ + + /** + * @cfg {Number} timeout The number of seconds to wait for a server response before + * failing with the {@link #failureType} as {@link Ext.form.action.Action#CONNECT_FAILURE}. If not specified, + * defaults to the configured {@link Ext.form.Basic#timeout timeout} of the + * {@link #form}. + */ + + /** + * @cfg {Function} success The function to call when a valid success return packet is received. + * The function is passed the following parameters:
    + *
  • form : Ext.form.Basic
    The form that requested the action
  • + *
  • action : Ext.form.action.Action
    The Action class. The {@link #result} + * property of this object may be examined to perform custom postprocessing.
  • + *
+ */ + + /** + * @cfg {Function} failure The function to call when a failure packet was received, or when an + * error ocurred in the Ajax communication. + * The function is passed the following parameters:
    + *
  • form : Ext.form.Basic
    The form that requested the action
  • + *
  • action : Ext.form.action.Action
    The Action class. If an Ajax + * error ocurred, the failure type will be in {@link #failureType}. The {@link #result} + * property of this object may be examined to perform custom postprocessing.
  • + *
+ */ + + /** + * @cfg {Object} scope The scope in which to call the configured success and failure + * callback functions (the this reference for the callback functions). + */ + + /** + * @cfg {String} waitMsg The message to be displayed by a call to {@link Ext.window.MessageBox#wait} + * during the time the action is being processed. + */ + + /** + * @cfg {String} waitTitle The title to be displayed by a call to {@link Ext.window.MessageBox#wait} + * during the time the action is being processed. + */ + + /** + * @cfg {Boolean} submitEmptyText If set to true, the emptyText value will be sent with the form + * when it is submitted. Defaults to true. + */ + + /** + * @property type + * The type of action this Action instance performs. + * Currently only "submit" and "load" are supported. + * @type {String} + */ + + /** + * The type of failure detected will be one of these: {@link Ext.form.action.Action#CLIENT_INVALID}, + * {@link Ext.form.action.Action#SERVER_INVALID}, {@link Ext.form.action.Action#CONNECT_FAILURE}, or + * {@link Ext.form.action.Action#LOAD_FAILURE}. Usage: + *

+var fp = new Ext.form.Panel({
+...
+buttons: [{
+    text: 'Save',
+    formBind: true,
+    handler: function(){
+        if(fp.getForm().isValid()){
+            fp.getForm().submit({
+                url: 'form-submit.php',
+                waitMsg: 'Submitting your data...',
+                success: function(form, action){
+                    // server responded with success = true
+                    var result = action.{@link #result};
+                },
+                failure: function(form, action){
+                    if (action.{@link #failureType} === {@link Ext.form.action.Action#CONNECT_FAILURE}) {
+                        Ext.Msg.alert('Error',
+                            'Status:'+action.{@link #response}.status+': '+
+                            action.{@link #response}.statusText);
+                    }
+                    if (action.failureType === {@link Ext.form.action.Action#SERVER_INVALID}){
+                        // server responded with success = false
+                        Ext.Msg.alert('Invalid', action.{@link #result}.errormsg);
+                    }
+                }
+            });
+        }
+    }
+},{
+    text: 'Reset',
+    handler: function(){
+        fp.getForm().reset();
+    }
+}]
+     * 
+ * @property failureType + * @type {String} + */ + + /** + * The raw XMLHttpRequest object used to perform the action. + * @property response + * @type {Object} + */ + + /** + * The decoded response object containing a boolean success property and + * other, action-specific properties. + * @property result + * @type {Object} + */ + + + + constructor: function(config) { + if (config) { + Ext.apply(this, config); + } + + // Normalize the params option to an Object + var params = config.params; + if (Ext.isString(params)) { + this.params = Ext.Object.fromQueryString(params); + } + }, + + /** + * Invokes this action using the current configuration. + */ + run: Ext.emptyFn, + + /** + * @private + * @method onSuccess + * Callback method that gets invoked when the action completes successfully. Must be implemented by subclasses. + * @param {Object} response + */ + + /** + * @private + * @method handleResponse + * Handles the raw response and builds a result object from it. Must be implemented by subclasses. + * @param {Object} response + */ + + /** + * @private + * Handles a failure response. + * @param {Object} response + */ + onFailure : function(response){ + this.response = response; + this.failureType = Ext.form.action.Action.CONNECT_FAILURE; + this.form.afterAction(this, false); + }, + + /** + * @private + * Validates that a response contains either responseText or responseXML and invokes + * {@link #handleResponse} to build the result object. + * @param {Object} response The raw response object. + * @return {Object/Boolean} result The result object as built by handleResponse, or true if + * the response had empty responseText and responseXML. + */ + processResponse : function(response){ + this.response = response; + if (!response.responseText && !response.responseXML) { + return true; + } + return (this.result = this.handleResponse(response)); + }, + + /** + * @private + * Build the URL for the AJAX request. Used by the standard AJAX submit and load actions. + * @return {String} The URL. + */ + getUrl: function() { + return this.url || this.form.url; + }, + + /** + * @private + * Determine the HTTP method to be used for the request. + * @return {String} The HTTP method + */ + getMethod: function() { + return (this.method || this.form.method || 'POST').toUpperCase(); + }, + + /** + * @private + * Get the set of parameters specified in the BasicForm's baseParams and/or the params option. + * Items in params override items of the same name in baseParams. + * @return {Object} the full set of parameters + */ + getParams: function() { + return Ext.apply({}, this.params, this.form.baseParams); + }, + + /** + * @private + * Creates a callback object. + */ + createCallback: function() { + var me = this, + undef, + form = me.form; + return { + success: me.onSuccess, + failure: me.onFailure, + scope: me, + timeout: (this.timeout * 1000) || (form.timeout * 1000), + upload: form.fileUpload ? me.onSuccess : undef + }; + }, + + statics: { + /** + * @property CLIENT_INVALID + * Failure type returned when client side validation of the Form fails + * thus aborting a submit action. Client side validation is performed unless + * {@link Ext.form.action.Submit#clientValidation} is explicitly set to false. + * @type {String} + * @static + */ + CLIENT_INVALID: 'client', + + /** + * @property SERVER_INVALID + *

Failure type returned when server side processing fails and the {@link #result}'s + * success property is set to false.

+ *

In the case of a form submission, field-specific error messages may be returned in the + * {@link #result}'s errors property.

+ * @type {String} + * @static + */ + SERVER_INVALID: 'server', + + /** + * @property CONNECT_FAILURE + * Failure type returned when a communication error happens when attempting + * to send a request to the remote server. The {@link #response} may be examined to + * provide further information. + * @type {String} + * @static + */ + CONNECT_FAILURE: 'connect', + + /** + * @property LOAD_FAILURE + * Failure type returned when the response's success + * property is set to false, or no field values are returned in the response's + * data property. + * @type {String} + * @static + */ + LOAD_FAILURE: 'load' + + + } +}); + +/** + * @class Ext.form.action.Submit + * @extends Ext.form.action.Action + *

A class which handles submission of data from {@link Ext.form.Basic Form}s + * and processes the returned response.

+ *

Instances of this class are only created by a {@link Ext.form.Basic Form} when + * {@link Ext.form.Basic#submit submit}ting.

+ *

Response Packet Criteria

+ *

A response packet may contain: + *

    + *
  • success property : Boolean + *
    The success property is required.
  • + *
  • errors property : Object + *
    The errors property, + * which is optional, contains error messages for invalid fields.
  • + *
+ *

JSON Packets

+ *

By default, response packets are assumed to be JSON, so a typical response + * packet may look like this:


+{
+    success: false,
+    errors: {
+        clientCode: "Client not found",
+        portOfLoading: "This field must not be null"
+    }
+}
+ *

Other data may be placed into the response for processing by the {@link Ext.form.Basic}'s callback + * or event handler methods. The object decoded from this JSON is available in the + * {@link Ext.form.action.Action#result result} property.

+ *

Alternatively, if an {@link #errorReader} is specified as an {@link Ext.data.reader.Xml XmlReader}:


+    errorReader: new Ext.data.reader.Xml({
+            record : 'field',
+            success: '@success'
+        }, [
+            'id', 'msg'
+        ]
+    )
+
+ *

then the results may be sent back in XML format:


+<?xml version="1.0" encoding="UTF-8"?>
+<message success="false">
+<errors>
+    <field>
+        <id>clientCode</id>
+        <msg><![CDATA[Code not found. <br /><i>This is a test validation message from the server </i>]]></msg>
+    </field>
+    <field>
+        <id>portOfLoading</id>
+        <msg><![CDATA[Port not found. <br /><i>This is a test validation message from the server </i>]]></msg>
+    </field>
+</errors>
+</message>
+
+ *

Other elements may be placed into the response XML for processing by the {@link Ext.form.Basic}'s callback + * or event handler methods. The XML document is available in the {@link #errorReader}'s {@link Ext.data.reader.Xml#xmlData xmlData} property.

+ */ +Ext.define('Ext.form.action.Submit', { + extend:'Ext.form.action.Action', + alternateClassName: 'Ext.form.Action.Submit', + alias: 'formaction.submit', + + type: 'submit', + + /** + * @cfg {boolean} clientValidation Determines whether a Form's fields are validated + * in a final call to {@link Ext.form.Basic#isValid isValid} prior to submission. + * Pass false in the Form's submit options to prevent this. Defaults to true. + */ + + // inherit docs + run : function(){ + var form = this.form; + if (this.clientValidation === false || form.isValid()) { + this.doSubmit(); + } else { + // client validation failed + this.failureType = Ext.form.action.Action.CLIENT_INVALID; + form.afterAction(this, false); + } + }, + + /** + * @private + * Perform the submit of the form data. + */ + doSubmit: function() { + var formEl, + ajaxOptions = Ext.apply(this.createCallback(), { + url: this.getUrl(), + method: this.getMethod(), + headers: this.headers + }); + + // For uploads we need to create an actual form that contains the file upload fields, + // and pass that to the ajax call so it can do its iframe-based submit method. + if (this.form.hasUpload()) { + formEl = ajaxOptions.form = this.buildForm(); + ajaxOptions.isUpload = true; + } else { + ajaxOptions.params = this.getParams(); + } + + Ext.Ajax.request(ajaxOptions); + + if (formEl) { + Ext.removeNode(formEl); + } + }, + + /** + * @private + * Build the full set of parameters from the field values plus any additional configured params. + */ + getParams: function() { + var nope = false, + configParams = this.callParent(), + fieldParams = this.form.getValues(nope, nope, this.submitEmptyText !== nope); + return Ext.apply({}, fieldParams, configParams); + }, + + /** + * @private + * Build a form element containing fields corresponding to all the parameters to be + * submitted (everything returned by {@link #getParams}. + * NOTE: the form element is automatically added to the DOM, so any code that uses + * it must remove it from the DOM after finishing with it. + * @return HTMLFormElement + */ + buildForm: function() { + var fieldsSpec = [], + formSpec, + formEl, + basicForm = this.form, + params = this.getParams(), + uploadFields = []; + + basicForm.getFields().each(function(field) { + if (field.isFileUpload()) { + uploadFields.push(field); + } + }); + + function addField(name, val) { + fieldsSpec.push({ + tag: 'input', + type: 'hidden', + name: name, + value: val + }); + } + + // Add the form field values + Ext.iterate(params, function(key, val) { + if (Ext.isArray(val)) { + Ext.each(val, function(v) { + addField(key, v); + }); + } else { + addField(key, val); + } + }); + + formSpec = { + tag: 'form', + action: this.getUrl(), + method: this.getMethod(), + target: this.target || '_self', + style: 'display:none', + cn: fieldsSpec + }; + + // Set the proper encoding for file uploads + if (uploadFields.length) { + formSpec.encoding = formSpec.enctype = 'multipart/form-data'; + } + + // Create the form + formEl = Ext.core.DomHelper.append(Ext.getBody(), formSpec); + + // Special handling for file upload fields: since browser security measures prevent setting + // their values programatically, and prevent carrying their selected values over when cloning, + // we have to move the actual field instances out of their components and into the form. + Ext.Array.each(uploadFields, function(field) { + if (field.rendered) { // can only have a selected file value after being rendered + formEl.appendChild(field.extractFileInput()); + } + }); + + return formEl; + }, + + + + /** + * @private + */ + onSuccess: function(response) { + var form = this.form, + success = true, + result = this.processResponse(response); + if (result !== true && !result.success) { + if (result.errors) { + form.markInvalid(result.errors); + } + this.failureType = Ext.form.action.Action.SERVER_INVALID; + success = false; + } + form.afterAction(this, success); + }, + + /** + * @private + */ + handleResponse: function(response) { + var form = this.form, + errorReader = form.errorReader, + rs, errors, i, len, records; + if (errorReader) { + rs = errorReader.read(response); + records = rs.records; + errors = []; + if (records) { + for(i = 0, len = records.length; i < len; i++) { + errors[i] = records[i].data; + } + } + if (errors.length < 1) { + errors = null; + } + return { + success : rs.success, + errors : errors + }; + } + return Ext.decode(response.responseText); + } +}); + +/** + * @class Ext.util.ComponentDragger + * @extends Ext.dd.DragTracker + *

A subclass of Ext.dd.DragTracker which handles dragging any Component.

+ *

This is configured with a Component to be made draggable, and a config object for the + * {@link Ext.dd.DragTracker} class.

+ *

A {@link #} delegate may be provided which may be either the element to use as the mousedown target + * or a {@link Ext.DomQuery} selector to activate multiple mousedown targets.

+ * @constructor Create a new ComponentTracker + * @param {object} comp The Component to provide dragging for. + * @param {object} config The config object + */ +Ext.define('Ext.util.ComponentDragger', { + + /** + * @cfg {Boolean} constrain + * Specify as true to constrain the Component to within the bounds of the {@link #constrainTo} region. + */ + + /** + * @cfg {String/Element} delegate + * Optional.

A {@link Ext.DomQuery DomQuery} selector which identifies child elements within the Component's encapsulating + * Element which are the drag handles. This limits dragging to only begin when the matching elements are mousedowned.

+ *

This may also be a specific child element within the Component's encapsulating element to use as the drag handle.

+ */ + + /** + * @cfg {Boolean} constrainDelegate + * Specify as true to constrain the drag handles within the {@link constrainTo} region. + */ + + extend: 'Ext.dd.DragTracker', + + autoStart: 500, + + constructor: function(comp, config) { + this.comp = comp; + this.initialConstrainTo = config.constrainTo; + this.callParent([ config ]); + }, + + onStart: function(e) { + var me = this, + comp = me.comp; + + // Cache the start [X, Y] array + this.startPosition = comp.getPosition(); + + // If client Component has a ghost method to show a lightweight version of itself + // then use that as a drag proxy unless configured to liveDrag. + if (comp.ghost && !comp.liveDrag) { + me.proxy = comp.ghost(); + me.dragTarget = me.proxy.header.el; + } + + // Set the constrainTo Region before we start dragging. + if (me.constrain || me.constrainDelegate) { + me.constrainTo = me.calculateConstrainRegion(); + } + }, + + calculateConstrainRegion: function() { + var me = this, + comp = me.comp, + c = me.initialConstrainTo, + delegateRegion, + elRegion, + shadowSize = comp.el.shadow ? comp.el.shadow.offset : 0; + + // The configured constrainTo might be a Region or an element + if (!(c instanceof Ext.util.Region)) { + c = Ext.fly(c).getViewRegion(); + } + + // Reduce the constrain region to allow for shadow + if (shadowSize) { + c.adjust(0, -shadowSize, -shadowSize, shadowSize); + } + + // If they only want to constrain the *delegate* to within the constrain region, + // adjust the region to be larger based on the insets of the delegate from the outer + // edges of the Component. + if (!me.constrainDelegate) { + delegateRegion = Ext.fly(me.dragTarget).getRegion(); + elRegion = me.proxy ? me.proxy.el.getRegion() : comp.el.getRegion(); + + c.adjust( + delegateRegion.top - elRegion.top, + delegateRegion.right - elRegion.right, + delegateRegion.bottom - elRegion.bottom, + delegateRegion.left - elRegion.left + ); + } + return c; + }, + + // Move either the ghost Component or the target Component to its new position on drag + onDrag: function(e) { + var me = this, + comp = (me.proxy && !me.comp.liveDrag) ? me.proxy : me.comp, + offset = me.getOffset(me.constrain || me.constrainDelegate ? 'dragTarget' : null); + + comp.setPosition.apply(comp, [me.startPosition[0] + offset[0], me.startPosition[1] + offset[1]]); + }, + + onEnd: function(e) { + if (this.proxy && !this.comp.liveDrag) { + this.comp.unghost(); + } + } +}); +/** + * @class Ext.form.Labelable + +A mixin which allows a component to be configured and decorated with a label and/or error message as is +common for form fields. This is used by e.g. {@link Ext.form.field.Base} and {@link Ext.form.FieldContainer} +to let them be managed by the Field layout. + +**NOTE**: This mixin is mainly for internal library use and most users should not need to use it directly. It +is more likely you will want to use one of the component classes that import this mixin, such as +{@link Ext.form.field.Base} or {@link Ext.form.FieldContainer}. + +Use of this mixin does not make a component a field in the logical sense, meaning it does not provide any +logic or state related to values or validation; that is handled by the related {@link Ext.form.field.Field} +mixin. These two mixins may be used separately (for example {@link Ext.form.FieldContainer} is Labelable but not a +Field), or in combination (for example {@link Ext.form.field.Base} implements both and has logic for connecting the +two.) + +Component classes which use this mixin should use the Field layout +or a derivation thereof to properly size and position the label and message according to the component config. +They must also call the {@link #initLabelable} method during component initialization to ensure the mixin gets +set up correctly. + + * @markdown + * @docauthor Jason Johnston + */ +Ext.define("Ext.form.Labelable", { + requires: ['Ext.XTemplate'], + + /** + * @cfg {Array/String/Ext.XTemplate} labelableRenderTpl + * The rendering template for the field decorations. Component classes using this mixin should include + * logic to use this as their {@link Ext.AbstractComponent#renderTpl renderTpl}, and implement the + * {@link #getSubTplMarkup} method to generate the field body content. + */ + labelableRenderTpl: [ + '', + ' for="{inputId}" class="{labelCls}" style="{labelStyle}">', + '{fieldLabel}{labelSeparator}', + '', + '', + '
id="{baseBodyCls}-{inputId}" role="presentation">{subTplMarkup}
', + '', + '', + { + compiled: true, + disableFormats: true + } + ], + + /** + * @cfg {Ext.XTemplate} activeErrorsTpl + * The template used to format the Array of error messages passed to {@link #setActiveErrors} + * into a single HTML string. By default this renders each message as an item in an unordered list. + */ + activeErrorsTpl: [ + '', + '
    class="last">{.}
', + '
' + ], + + /** + * @property isFieldLabelable + * @type Boolean + * Flag denoting that this object is labelable as a field. Always true. + */ + isFieldLabelable: true, + + /** + * @cfg {String} formItemCls + * A CSS class to be applied to the outermost element to denote that it is participating in the form + * field layout. Defaults to 'x-form-item'. + */ + formItemCls: Ext.baseCSSPrefix + 'form-item', + + /** + * @cfg {String} labelCls + * The CSS class to be applied to the label element. Defaults to 'x-form-item-label'. + */ + labelCls: Ext.baseCSSPrefix + 'form-item-label', + + /** + * @cfg {String} errorMsgCls + * The CSS class to be applied to the error message element. Defaults to 'x-form-error-msg'. + */ + errorMsgCls: Ext.baseCSSPrefix + 'form-error-msg', + + /** + * @cfg {String} baseBodyCls + * The CSS class to be applied to the body content element. Defaults to 'x-form-item-body'. + */ + baseBodyCls: Ext.baseCSSPrefix + 'form-item-body', + + /** + * @cfg {String} fieldBodyCls + * An extra CSS class to be applied to the body content element in addition to {@link #fieldBodyCls}. + * Defaults to empty. + */ + fieldBodyCls: '', + + /** + * @cfg {String} clearCls + * The CSS class to be applied to the special clearing div rendered directly after the field + * contents wrapper to provide field clearing (defaults to 'x-clear'). + */ + clearCls: Ext.baseCSSPrefix + 'clear', + + /** + * @cfg {String} invalidCls + * The CSS class to use when marking the component invalid (defaults to 'x-form-invalid') + */ + invalidCls : Ext.baseCSSPrefix + 'form-invalid', + + /** + * @cfg {String} fieldLabel + * The label for the field. It gets appended with the {@link #labelSeparator}, and its position + * and sizing is determined by the {@link #labelAlign}, {@link #labelWidth}, and {@link #labelPad} + * configs. Defaults to undefined. + */ + fieldLabel: undefined, + + /** + * @cfg {String} labelAlign + *

Controls the position and alignment of the {@link #fieldLabel}. Valid values are:

+ *
    + *
  • "left" (the default) - The label is positioned to the left of the field, with its text + * aligned to the left. Its width is determined by the {@link #labelWidth} config.
  • + *
  • "top" - The label is positioned above the field.
  • + *
  • "right" - The label is positioned to the left of the field, with its text aligned + * to the right. Its width is determined by the {@link #labelWidth} config.
  • + *
+ */ + labelAlign : 'left', + + /** + * @cfg {Number} labelWidth + * The width of the {@link #fieldLabel} in pixels. Only applicable if the {@link #labelAlign} is set + * to "left" or "right". Defaults to 100. + */ + labelWidth: 100, + + /** + * @cfg {Number} labelPad + * The amount of space in pixels between the {@link #fieldLabel} and the input field. Defaults to 5. + */ + labelPad : 5, + + /** + * @cfg {String} labelSeparator + * Character(s) to be inserted at the end of the {@link #fieldLabel label text}. + */ + labelSeparator : ':', + + /** + * @cfg {String} labelStyle + *

A CSS style specification string to apply directly to this field's label. Defaults to undefined.

+ */ + + /** + * @cfg {Boolean} hideLabel + *

Set to true to completely hide the label element ({@link #fieldLabel} and {@link #labelSeparator}). + * Defaults to false.

+ *

Also see {@link #hideEmptyLabel}, which controls whether space will be reserved for an empty fieldLabel.

+ */ + hideLabel: false, + + /** + * @cfg {Boolean} hideEmptyLabel + *

When set to true, the label element ({@link #fieldLabel} and {@link #labelSeparator}) will be + * automatically hidden if the {@link #fieldLabel} is empty. Setting this to false will cause the empty + * label element to be rendered and space to be reserved for it; this is useful if you want a field without a label + * to line up with other labeled fields in the same form. Defaults to true.

+ *

If you wish to unconditionall hide the label even if a non-empty fieldLabel is configured, then set + * the {@link #hideLabel} config to true.

+ */ + hideEmptyLabel: true, + + /** + * @cfg {Boolean} preventMark + * true to disable displaying any {@link #setActiveError error message} set on this object. + * Defaults to false. + */ + preventMark: false, + + /** + * @cfg {Boolean} autoFitErrors + * Whether to adjust the component's body area to make room for 'side' or 'under' + * {@link #msgTarget error messages}. Defaults to true. + */ + autoFitErrors: true, + + /** + * @cfg {String} msgTarget

The location where the error message text should display. + * Must be one of the following values:

+ *
    + *
  • qtip Display a quick tip containing the message when the user hovers over the field. This is the default. + *
    {@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager.init} must have been called for this setting to work.
  • + *
  • title Display the message in a default browser title attribute popup.
  • + *
  • under Add a block div beneath the field containing the error message.
  • + *
  • side Add an error icon to the right of the field, displaying the message in a popup on hover.
  • + *
  • none Don't display any error message. This might be useful if you are implementing custom error display.
  • + *
  • [element id] Add the error message directly to the innerHTML of the specified element.
  • + *
+ */ + msgTarget: 'qtip', + + /** + * @cfg {String} activeError + * If specified, then the component will be displayed with this value as its active error when + * first rendered. Defaults to undefined. Use {@link #setActiveError} or {@link #unsetActiveError} to + * change it after component creation. + */ + + + /** + * Performs initialization of this mixin. Component classes using this mixin should call this method + * during their own initialization. + */ + initLabelable: function() { + this.addCls(this.formItemCls); + + this.addEvents( + /** + * @event errorchange + * Fires when the active error message is changed via {@link #setActiveError}. + * @param {Ext.form.Labelable} this + * @param {String} error The active error message + */ + 'errorchange' + ); + }, + + /** + * Returns the label for the field. Defaults to simply returning the {@link #fieldLabel} config. Can be + * overridden to provide + * @return {String} The configured field label, or empty string if not defined + */ + getFieldLabel: function() { + return this.fieldLabel || ''; + }, + + /** + * @protected + * Generates the arguments for the field decorations {@link #labelableRenderTpl rendering template}. + * @return {Object} The template arguments + */ + getLabelableRenderData: function() { + var me = this, + labelAlign = me.labelAlign, + labelPad = me.labelPad, + labelStyle; + + // Calculate label styles up front rather than in the Field layout for speed; this + // is safe because label alignment/width/pad are not expected to change. + if (labelAlign === 'top') { + labelStyle = 'margin-bottom:' + labelPad + 'px;'; + } else { + labelStyle = 'margin-right:' + labelPad + 'px;'; + // Add the width for border-box browsers; will be set by the Field layout for content-box + if (Ext.isBorderBox) { + labelStyle += 'width:' + me.labelWidth + 'px;'; + } + } + + return Ext.copyTo( + { + inputId: me.getInputId(), + fieldLabel: me.getFieldLabel(), + labelStyle: labelStyle + (me.labelStyle || ''), + subTplMarkup: me.getSubTplMarkup() + }, + me, + 'hideLabel,hideEmptyLabel,labelCls,fieldBodyCls,baseBodyCls,errorMsgCls,clearCls,labelSeparator', + true + ); + }, + + /** + * @protected + * Returns the additional {@link Ext.AbstractComponent#renderSelectors} for selecting the field + * decoration elements from the rendered {@link #labelableRenderTpl}. Component classes using this mixin should + * be sure and merge this method's result into the component's {@link Ext.AbstractComponent#renderSelectors} + * before rendering. + */ + getLabelableSelectors: function() { + return { + /** + * @property labelEl + * @type Ext.core.Element + * The label Element for this component. Only available after the component has been rendered. + */ + labelEl: 'label.' + this.labelCls, + + /** + * @property bodyEl + * @type Ext.core.Element + * The div Element wrapping the component's contents. Only available after the component has been rendered. + */ + bodyEl: '.' + this.baseBodyCls, + + /** + * @property errorEl + * @type Ext.core.Element + * The div Element that will contain the component's error message(s). Note that depending on the + * configured {@link #msgTarget}, this element may be hidden in favor of some other form of + * presentation, but will always be present in the DOM for use by assistive technologies. + */ + errorEl: '.' + this.errorMsgCls + }; + }, + + /** + * @protected + * Gets the markup to be inserted into the outer template's bodyEl. Defaults to empty string, should + * be implemented by classes including this mixin as needed. + * @return {String} The markup to be inserted + */ + getSubTplMarkup: function() { + return ''; + }, + + /** + * Get the input id, if any, for this component. This is used as the "for" attribute on the label element. + * Implementing subclasses may also use this as e.g. the id for their own input element. + * @return {String} The input id + */ + getInputId: function() { + return ''; + }, + + /** + * Gets the active error message for this component, if any. This does not trigger + * validation on its own, it merely returns any message that the component may already hold. + * @return {String} The active error message on the component; if there is no error, an empty string is returned. + */ + getActiveError : function() { + return this.activeError || ''; + }, + + /** + * Tells whether the field currently has an active error message. This does not trigger + * validation on its own, it merely looks for any message that the component may already hold. + * @return {Boolean} + */ + hasActiveError: function() { + return !!this.getActiveError(); + }, + + /** + * Sets the active error message to the given string. This replaces the entire error message + * contents with the given string. Also see {@link #setActiveErrors} which accepts an Array of + * messages and formats them according to the {@link #activeErrorsTpl}. + * @param {String} msg The error message + */ + setActiveError: function(msg) { + this.activeError = msg; + this.activeErrors = [msg]; + this.renderActiveError(); + }, + + /** + * Gets an Array of any active error messages currently applied to the field. This does not trigger + * validation on its own, it merely returns any messages that the component may already hold. + * @return {Array} The active error messages on the component; if there are no errors, an empty Array is returned. + */ + getActiveErrors: function() { + return this.activeErrors || []; + }, + + /** + * Set the active error message to an Array of error messages. The messages are formatted into + * a single message string using the {@link #activeErrorsTpl}. Also see {@link #setActiveError} + * which allows setting the entire error contents with a single string. + * @param {Array} errors The error messages + */ + setActiveErrors: function(errors) { + this.activeErrors = errors; + this.activeError = this.getTpl('activeErrorsTpl').apply({errors: errors}); + this.renderActiveError(); + }, + + /** + * Clears the active error. + */ + unsetActiveError: function() { + delete this.activeError; + delete this.activeErrors; + this.renderActiveError(); + }, + + /** + * @private + * Updates the rendered DOM to match the current activeError. This only updates the content and + * attributes, you'll have to call doComponentLayout to actually update the display. + */ + renderActiveError: function() { + var me = this, + activeError = me.getActiveError(), + hasError = !!activeError; + + if (activeError !== me.lastActiveError) { + me.fireEvent('errorchange', me, activeError); + me.lastActiveError = activeError; + } + + if (me.rendered && !me.isDestroyed && !me.preventMark) { + // Add/remove invalid class + me.el[hasError ? 'addCls' : 'removeCls'](me.invalidCls); + + // Update the aria-invalid attribute + me.getActionEl().dom.setAttribute('aria-invalid', hasError); + + // Update the errorEl with the error message text + me.errorEl.dom.innerHTML = activeError; + } + }, + + /** + * Applies a set of default configuration values to this Labelable instance. For each of the + * properties in the given object, check if this component hasOwnProperty that config; if not + * then it's inheriting a default value from its prototype and we should apply the default value. + * @param {Object} defaults The defaults to apply to the object. + */ + setFieldDefaults: function(defaults) { + var me = this; + Ext.iterate(defaults, function(key, val) { + if (!me.hasOwnProperty(key)) { + me[key] = val; + } + }); + }, + + /** + * @protected Calculate and return the natural width of the bodyEl. Override to provide custom logic. + * Note for implementors: if at all possible this method should be overridden with a custom implementation + * that can avoid anything that would cause the browser to reflow, e.g. querying offsetWidth. + */ + getBodyNaturalWidth: function() { + return this.bodyEl.getWidth(); + } + +}); + +/** + * @class Ext.form.field.Field + +This mixin provides a common interface for the logical behavior and state of form fields, including: + +- Getter and setter methods for field values +- Events and methods for tracking value and validity changes +- Methods for triggering validation + +**NOTE**: When implementing custom fields, it is most likely that you will want to extend the {@link Ext.form.field.Base} +component class rather than using this mixin directly, as BaseField contains additional logic for generating an +actual DOM complete with {@link Ext.form.Labelable label and error message} display and a form input field, +plus methods that bind the Field value getters and setters to the input field's value. + +If you do want to implement this mixin directly and don't want to extend {@link Ext.form.field.Base}, then +you will most likely want to override the following methods with custom implementations: {@link #getValue}, +{@link #setValue}, and {@link #getErrors}. Other methods may be overridden as needed but their base +implementations should be sufficient for common cases. You will also need to make sure that {@link #initField} +is called during the component's initialization. + + * @markdown + * @docauthor Jason Johnston + */ +Ext.define('Ext.form.field.Field', { + + /** + * @property isFormField + * @type {Boolean} + * Flag denoting that this component is a Field. Always true. + */ + isFormField : true, + + /** + * @cfg {Mixed} value A value to initialize this field with (defaults to undefined). + */ + + /** + * @cfg {String} name The name of the field (defaults to undefined). By default this is used as the parameter + * name when including the {@link #getSubmitData field value} in a {@link Ext.form.Basic#submit form submit()}. + * To prevent the field from being included in the form submit, set {@link #submitValue} to false. + */ + + /** + * @cfg {Boolean} disabled True to disable the field (defaults to false). Disabled Fields will not be + * {@link Ext.form.Basic#submit submitted}.

+ */ + disabled : false, + + /** + * @cfg {Boolean} submitValue Setting this to false will prevent the field from being + * {@link Ext.form.Basic#submit submitted} even when it is not disabled. Defaults to true. + */ + submitValue: true, + + /** + * @cfg {Boolean} validateOnChange + *

Specifies whether this field should be validated immediately whenever a change in its value is detected. + * Defaults to true. If the validation results in a change in the field's validity, a + * {@link #validitychange} event will be fired. This allows the field to show feedback about the + * validity of its contents immediately as the user is typing.

+ *

When set to false, feedback will not be immediate. However the form will still be validated + * before submitting if the clientValidation option to {@link Ext.form.Basic#doAction} is + * enabled, or if the field or form are validated manually.

+ *

See also {@link Ext.form.field.Base#checkChangeEvents}for controlling how changes to the field's value are detected.

+ */ + validateOnChange: true, + + /** + * @private + */ + suspendCheckChange: 0, + + /** + * Initializes this Field mixin on the current instance. Components using this mixin should call + * this method during their own initialization process. + */ + initField: function() { + this.addEvents( + /** + * @event change + * Fires when a user-initiated change is detected in the value of the field. + * @param {Ext.form.field.Field} this + * @param {Mixed} newValue The new value + * @param {Mixed} oldValue The original value + */ + 'change', + /** + * @event validitychange + * Fires when a change in the field's validity is detected. + * @param {Ext.form.field.Field} this + * @param {Boolean} isValid Whether or not the field is now valid + */ + 'validitychange', + /** + * @event dirtychange + * Fires when a change in the field's {@link #isDirty} state is detected. + * @param {Ext.form.field.Field} this + * @param {Boolean} isDirty Whether or not the field is now dirty + */ + 'dirtychange' + ); + + this.initValue(); + }, + + /** + * @protected + * Initializes the field's value based on the initial config. + */ + initValue: function() { + var me = this; + + /** + * @property originalValue + * @type Mixed + * The original value of the field as configured in the {@link #value} configuration, or as loaded by + * the last form load operation if the form's {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} + * setting is true. + */ + me.originalValue = me.lastValue = me.value; + + // Set the initial value - prevent validation on initial set + me.suspendCheckChange++; + me.setValue(me.value); + me.suspendCheckChange--; + }, + + /** + * Returns the {@link Ext.form.field.Field#name name} attribute of the field. This is used as the parameter + * name when including the field value in a {@link Ext.form.Basic#submit form submit()}. + * @return {String} name The field {@link Ext.form.field.Field#name name} + */ + getName: function() { + return this.name; + }, + + /** + * Returns the current data value of the field. The type of value returned is particular to the type of the + * particular field (e.g. a Date object for {@link Ext.form.field.Date}). + * @return {Mixed} value The field value + */ + getValue: function() { + return this.value; + }, + + /** + * Sets a data value into the field and runs the change detection and validation. + * @param {Mixed} value The value to set + * @return {Ext.form.field.Field} this + */ + setValue: function(value) { + var me = this; + me.value = value; + me.checkChange(); + return me; + }, + + /** + * Returns whether two field {@link #getValue values} are logically equal. Field implementations may override + * this to provide custom comparison logic appropriate for the particular field's data type. + * @param {Mixed} value1 The first value to compare + * @param {Mixed} value2 The second value to compare + * @return {Boolean} True if the values are equal, false if inequal. + */ + isEqual: function(value1, value2) { + return String(value1) === String(value2); + }, + + /** + *

Returns the parameter(s) that would be included in a standard form submit for this field. Typically this + * will be an object with a single name-value pair, the name being this field's {@link #getName name} and the + * value being its current stringified value. More advanced field implementations may return more than one + * name-value pair.

+ *

Note that the values returned from this method are not guaranteed to have been successfully + * {@link #validate validated}.

+ * @return {Object} A mapping of submit parameter names to values; each value should be a string, or an array + * of strings if that particular name has multiple values. It can also return null if there are no + * parameters to be submitted. + */ + getSubmitData: function() { + var me = this, + data = null; + if (!me.disabled && me.submitValue && !me.isFileUpload()) { + data = {}; + data[me.getName()] = '' + me.getValue(); + } + return data; + }, + + /** + *

Returns the value(s) that should be saved to the {@link Ext.data.Model} instance for this field, when + * {@link Ext.form.Basic#updateRecord} is called. Typically this will be an object with a single name-value + * pair, the name being this field's {@link #getName name} and the value being its current data value. More + * advanced field implementations may return more than one name-value pair. The returned values will be + * saved to the corresponding field names in the Model.

+ *

Note that the values returned from this method are not guaranteed to have been successfully + * {@link #validate validated}.

+ * @return {Object} A mapping of submit parameter names to values; each value should be a string, or an array + * of strings if that particular name has multiple values. It can also return null if there are no + * parameters to be submitted. + */ + getModelData: function() { + var me = this, + data = null; + if (!me.disabled && !me.isFileUpload()) { + data = {}; + data[me.getName()] = me.getValue(); + } + return data; + }, + + /** + * Resets the current field value to the originally loaded value and clears any validation messages. + * See {@link Ext.form.Basic}.{@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} + */ + reset : function(){ + var me = this; + + me.setValue(me.originalValue); + me.clearInvalid(); + // delete here so we reset back to the original state + delete me.wasValid; + }, + + /** + * Resets the field's {@link #originalValue} property so it matches the current {@link #getValue value}. + * This is called by {@link Ext.form.Basic}.{@link Ext.form.Basic#setValues setValues} if the form's + * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} property is set to true. + */ + resetOriginalValue: function() { + this.originalValue = this.getValue(); + this.checkDirty(); + }, + + /** + *

Checks whether the value of the field has changed since the last time it was checked. If the value + * has changed, it:

+ *
    + *
  1. Fires the {@link #change change event},
  2. + *
  3. Performs validation if the {@link #validateOnChange} config is enabled, firing the + * {@link #validationchange validationchange event} if the validity has changed, and
  4. + *
  5. Checks the {@link #isDirty dirty state} of the field and fires the {@link #dirtychange dirtychange event} + * if it has changed.
  6. + *
+ */ + checkChange: function() { + if (!this.suspendCheckChange) { + var me = this, + newVal = me.getValue(), + oldVal = me.lastValue; + if (!me.isEqual(newVal, oldVal) && !me.isDestroyed) { + me.lastValue = newVal; + me.fireEvent('change', me, newVal, oldVal); + me.onChange(newVal, oldVal); + } + } + }, + + /** + * @private + * Called when the field's value changes. Performs validation if the {@link #validateOnChange} + * config is enabled, and invokes the dirty check. + */ + onChange: function(newVal, oldVal) { + if (this.validateOnChange) { + this.validate(); + } + this.checkDirty(); + }, + + /** + *

Returns true if the value of this Field has been changed from its {@link #originalValue}. + * Will always return false if the field is disabled.

+ *

Note that if the owning {@link Ext.form.Basic form} was configured with + * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} + * then the {@link #originalValue} is updated when the values are loaded by + * {@link Ext.form.Basic}.{@link Ext.form.Basic#setValues setValues}.

+ * @return {Boolean} True if this field has been changed from its original value (and + * is not disabled), false otherwise. + */ + isDirty : function() { + var me = this; + return !me.disabled && !me.isEqual(me.getValue(), me.originalValue); + }, + + /** + * Checks the {@link #isDirty} state of the field and if it has changed since the last time + * it was checked, fires the {@link #dirtychange} event. + */ + checkDirty: function() { + var me = this, + isDirty = me.isDirty(); + if (isDirty !== me.wasDirty) { + me.fireEvent('dirtychange', me, isDirty); + me.onDirtyChange(isDirty); + me.wasDirty = isDirty; + } + }, + + /** + * @private Called when the field's dirty state changes. + * @param {Boolean} isDirty + */ + onDirtyChange: Ext.emptyFn, + + /** + *

Runs this field's validators and returns an array of error messages for any validation failures. + * This is called internally during validation and would not usually need to be used manually.

+ *

Each subclass should override or augment the return value to provide their own errors.

+ * @param {Mixed} value The value to get errors for (defaults to the current field value) + * @return {Array} All error messages for this field; an empty Array if none. + */ + getErrors: function(value) { + return []; + }, + + /** + *

Returns whether or not the field value is currently valid by {@link #getErrors validating} the + * field's current value. The {@link #validitychange} event will not be fired; use {@link #validate} + * instead if you want the event to fire. Note: {@link #disabled} fields are always treated as valid.

+ *

Implementations are encouraged to ensure that this method does not have side-effects such as + * triggering error message display.

+ * @return {Boolean} True if the value is valid, else false + */ + isValid : function() { + var me = this; + return me.disabled || Ext.isEmpty(me.getErrors()); + }, + + /** + *

Returns whether or not the field value is currently valid by {@link #getErrors validating} the + * field's current value, and fires the {@link #validitychange} event if the field's validity has + * changed since the last validation. Note: {@link #disabled} fields are always treated as valid.

+ *

Custom implementations of this method are allowed to have side-effects such as triggering error + * message display. To validate without side-effects, use {@link #isValid}.

+ * @return {Boolean} True if the value is valid, else false + */ + validate : function() { + var me = this, + isValid = me.isValid(); + if (isValid !== me.wasValid) { + me.wasValid = isValid; + me.fireEvent('validitychange', me, isValid); + } + return isValid; + }, + + /** + * A utility for grouping a set of modifications which may trigger value changes into a single + * transaction, to prevent excessive firing of {@link #change} events. This is useful for instance + * if the field has sub-fields which are being updated as a group; you don't want the container + * field to check its own changed state for each subfield change. + * @param fn A function containing the transaction code + */ + batchChanges: function(fn) { + this.suspendCheckChange++; + fn(); + this.suspendCheckChange--; + this.checkChange(); + }, + + /** + * Returns whether this Field is a file upload field; if it returns true, forms will use + * special techniques for {@link Ext.form.Basic#submit submitting the form} via AJAX. See + * {@link Ext.form.Basic#hasUpload} for details. If this returns true, the {@link #extractFileInput} + * method must also be implemented to return the corresponding file input element. + * @return {Boolean} + */ + isFileUpload: function() { + return false; + }, + + /** + * Only relevant if the instance's {@link #isFileUpload} method returns true. Returns a reference + * to the file input DOM element holding the user's selected file. The input will be appended into + * the submission form and will not be returned, so this method should also create a replacement. + * @return {HTMLInputElement} + */ + extractFileInput: function() { + return null; + }, + + /** + *

Associate one or more error messages with this field. Components using this mixin should implement + * this method to update the component's rendering to display the messages.

+ *

Note: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to + * return false if the value does pass validation. So simply marking a Field as invalid + * will not prevent submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} + * option set.

+ * @param {String/Array} errors The error message(s) for the field. + */ + markInvalid: Ext.emptyFn, + + /** + *

Clear any invalid styles/messages for this field. Components using this mixin should implement + * this method to update the components rendering to clear any existing messages.

+ *

Note: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to + * return true if the value does not pass validation. So simply clearing a field's errors + * will not necessarily allow submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} + * option set.

+ */ + clearInvalid: Ext.emptyFn + +}); + +/** + * @class Ext.layout.component.field.Field + * @extends Ext.layout.component.Component + * Layout class for components with {@link Ext.form.Labelable field labeling}, handling the sizing and alignment of + * the form control, label, and error message treatment. + * @private + */ +Ext.define('Ext.layout.component.field.Field', { + + /* Begin Definitions */ + + alias: ['layout.field'], + + extend: 'Ext.layout.component.Component', + + uses: ['Ext.tip.QuickTip', 'Ext.util.TextMetrics'], + + /* End Definitions */ + + type: 'field', + + beforeLayout: function(width, height) { + var me = this; + return me.callParent(arguments) || (!me.owner.preventMark && me.activeError !== me.owner.getActiveError()); + }, + + onLayout: function(width, height) { + var me = this, + owner = me.owner, + labelStrategy = me.getLabelStrategy(), + errorStrategy = me.getErrorStrategy(), + isDefined = Ext.isDefined, + isNumber = Ext.isNumber, + lastSize, autoWidth, autoHeight, info, undef; + + lastSize = me.lastComponentSize || {}; + if (!isDefined(width)) { + width = lastSize.width; + if (width < 0) { //first pass lastComponentSize.width is -Infinity + width = undef; + } + } + if (!isDefined(height)) { + height = lastSize.height; + if (height < 0) { //first pass lastComponentSize.height is -Infinity + height = undef; + } + } + autoWidth = !isNumber(width); + autoHeight = !isNumber(height); + + info = { + autoWidth: autoWidth, + autoHeight: autoHeight, + width: autoWidth ? owner.getBodyNaturalWidth() : width, //always give a pixel width + height: height, + + // insets for the bodyEl from each side of the component layout area + insets: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + }; + + // NOTE the order of calculating insets and setting styles here is very important; we must first + // calculate and set horizontal layout alone, as the horizontal sizing of elements can have an impact + // on the vertical sizes due to wrapping, then calculate and set the vertical layout. + + // perform preparation on the label and error (setting css classes, qtips, etc.) + labelStrategy.prepare(owner, info); + errorStrategy.prepare(owner, info); + + // calculate the horizontal insets for the label and error + labelStrategy.adjustHorizInsets(owner, info); + errorStrategy.adjustHorizInsets(owner, info); + + // set horizontal styles for label and error based on the current insets + labelStrategy.layoutHoriz(owner, info); + errorStrategy.layoutHoriz(owner, info); + + // calculate the vertical insets for the label and error + labelStrategy.adjustVertInsets(owner, info); + errorStrategy.adjustVertInsets(owner, info); + + // set vertical styles for label and error based on the current insets + labelStrategy.layoutVert(owner, info); + errorStrategy.layoutVert(owner, info); + + // perform sizing of the elements based on the final dimensions and insets + if (autoWidth && autoHeight) { + // Don't use setTargetSize if auto-sized, so the calculated size is not reused next time + me.setElementSize(owner.el, info.width, info.height); + } else { + me.setTargetSize(info.width, info.height); + } + me.sizeBody(info); + + me.activeError = owner.getActiveError(); + }, + + + /** + * Perform sizing and alignment of the bodyEl (and children) to match the calculated insets. + */ + sizeBody: function(info) { + var me = this, + owner = me.owner, + insets = info.insets, + totalWidth = info.width, + totalHeight = info.height, + width = Ext.isNumber(totalWidth) ? totalWidth - insets.left - insets.right : totalWidth, + height = Ext.isNumber(totalHeight) ? totalHeight - insets.top - insets.bottom : totalHeight; + + // size the bodyEl + me.setElementSize(owner.bodyEl, width, height); + + // size the bodyEl's inner contents if necessary + me.sizeBodyContents(width, height); + }, + + /** + * Size the contents of the field body, given the full dimensions of the bodyEl. Does nothing by + * default, subclasses can override to handle their specific contents. + * @param {Number} width The bodyEl width + * @param {Number} height The bodyEl height + */ + sizeBodyContents: Ext.emptyFn, + + + /** + * Return the set of strategy functions from the {@link #labelStrategies labelStrategies collection} + * that is appropriate for the field's {@link Ext.form.field.Field#labelAlign labelAlign} config. + */ + getLabelStrategy: function() { + var me = this, + strategies = me.labelStrategies, + labelAlign = me.owner.labelAlign; + return strategies[labelAlign] || strategies.base; + }, + + /** + * Return the set of strategy functions from the {@link #errorStrategies errorStrategies collection} + * that is appropriate for the field's {@link Ext.form.field.Field#msgTarget msgTarget} config. + */ + getErrorStrategy: function() { + var me = this, + owner = me.owner, + strategies = me.errorStrategies, + msgTarget = owner.msgTarget; + return !owner.preventMark && Ext.isString(msgTarget) ? + (strategies[msgTarget] || strategies.elementId) : + strategies.none; + }, + + + + /** + * Collection of named strategies for laying out and adjusting labels to accommodate error messages. + * An appropriate one will be chosen based on the owner field's {@link Ext.form.field.Field#labelAlign} config. + */ + labelStrategies: (function() { + var applyIf = Ext.applyIf, + emptyFn = Ext.emptyFn, + base = { + prepare: function(owner, info) { + var cls = owner.labelCls + '-' + owner.labelAlign, + labelEl = owner.labelEl; + if (labelEl && !labelEl.hasCls(cls)) { + labelEl.addCls(cls); + } + }, + adjustHorizInsets: emptyFn, + adjustVertInsets: emptyFn, + layoutHoriz: emptyFn, + layoutVert: emptyFn + }, + left = applyIf({ + prepare: function(owner, info) { + base.prepare(owner, info); + // If auto width, add the label width to the body's natural width. + if (info.autoWidth) { + info.width += (!owner.labelEl ? 0 : owner.labelWidth + owner.labelPad); + } + }, + adjustHorizInsets: function(owner, info) { + if (owner.labelEl) { + info.insets.left += owner.labelWidth + owner.labelPad; + } + }, + layoutHoriz: function(owner, info) { + // For content-box browsers we can't rely on Labelable.js#getLabelableRenderData + // setting the width style because it needs to account for the final calculated + // padding/border styles for the label. So we set the width programmatically here to + // normalize content-box sizing, while letting border-box browsers use the original + // width style. + var labelEl = owner.labelEl; + if (labelEl && !owner.isLabelSized && !Ext.isBorderBox) { + labelEl.setWidth(owner.labelWidth); + owner.isLabelSized = true; + } + } + }, base); + + + return { + base: base, + + /** + * Label displayed above the bodyEl + */ + top: applyIf({ + adjustVertInsets: function(owner, info) { + var labelEl = owner.labelEl; + if (labelEl) { + info.insets.top += Ext.util.TextMetrics.measure(labelEl, owner.fieldLabel, info.width).height + + labelEl.getFrameWidth('tb') + owner.labelPad; + } + } + }, base), + + /** + * Label displayed to the left of the bodyEl + */ + left: left, + + /** + * Same as left, only difference is text-align in CSS + */ + right: left + }; + })(), + + + + /** + * Collection of named strategies for laying out and adjusting insets to accommodate error messages. + * An appropriate one will be chosen based on the owner field's {@link Ext.form.field.Field#msgTarget} config. + */ + errorStrategies: (function() { + function setDisplayed(el, displayed) { + var wasDisplayed = el.getStyle('display') !== 'none'; + if (displayed !== wasDisplayed) { + el.setDisplayed(displayed); + } + } + + function setStyle(el, name, value) { + if (el.getStyle(name) !== value) { + el.setStyle(name, value); + } + } + + var applyIf = Ext.applyIf, + emptyFn = Ext.emptyFn, + base = { + prepare: function(owner) { + setDisplayed(owner.errorEl, false); + }, + adjustHorizInsets: emptyFn, + adjustVertInsets: emptyFn, + layoutHoriz: emptyFn, + layoutVert: emptyFn + }; + + return { + none: base, + + /** + * Error displayed as icon (with QuickTip on hover) to right of the bodyEl + */ + side: applyIf({ + prepare: function(owner) { + var errorEl = owner.errorEl; + errorEl.addCls(Ext.baseCSSPrefix + 'form-invalid-icon'); + Ext.layout.component.field.Field.initTip(); + errorEl.dom.setAttribute('data-errorqtip', owner.getActiveError() || ''); + setDisplayed(errorEl, owner.hasActiveError()); + }, + adjustHorizInsets: function(owner, info) { + if (owner.autoFitErrors && owner.hasActiveError()) { + info.insets.right += owner.errorEl.getWidth(); + } + }, + layoutHoriz: function(owner, info) { + if (owner.hasActiveError()) { + setStyle(owner.errorEl, 'left', info.width - info.insets.right + 'px'); + } + }, + layoutVert: function(owner, info) { + if (owner.hasActiveError()) { + setStyle(owner.errorEl, 'top', info.insets.top + 'px'); + } + } + }, base), + + /** + * Error message displayed underneath the bodyEl + */ + under: applyIf({ + prepare: function(owner) { + var errorEl = owner.errorEl, + cls = Ext.baseCSSPrefix + 'form-invalid-under'; + if (!errorEl.hasCls(cls)) { + errorEl.addCls(cls); + } + setDisplayed(errorEl, owner.hasActiveError()); + }, + adjustVertInsets: function(owner, info) { + if (owner.autoFitErrors) { + info.insets.bottom += owner.errorEl.getHeight(); + } + }, + layoutHoriz: function(owner, info) { + var errorEl = owner.errorEl, + insets = info.insets; + + setStyle(errorEl, 'width', info.width - insets.right - insets.left + 'px'); + setStyle(errorEl, 'marginLeft', insets.left + 'px'); + } + }, base), + + /** + * Error displayed as QuickTip on hover of the field container + */ + qtip: applyIf({ + prepare: function(owner) { + setDisplayed(owner.errorEl, false); + Ext.layout.component.field.Field.initTip(); + owner.getActionEl().dom.setAttribute('data-errorqtip', owner.getActiveError() || ''); + } + }, base), + + /** + * Error displayed as title tip on hover of the field container + */ + title: applyIf({ + prepare: function(owner) { + setDisplayed(owner.errorEl, false); + owner.el.dom.title = owner.getActiveError() || ''; + } + }, base), + + /** + * Error message displayed as content of an element with a given id elsewhere in the app + */ + elementId: applyIf({ + prepare: function(owner) { + setDisplayed(owner.errorEl, false); + var targetEl = Ext.fly(owner.msgTarget); + if (targetEl) { + targetEl.dom.innerHTML = owner.getActiveError() || ''; + targetEl.setDisplayed(owner.hasActiveError()); + } + } + }, base) + }; + })(), + + statics: { + /** + * Use a custom QuickTip instance separate from the main QuickTips singleton, so that we + * can give it a custom frame style. Responds to errorqtip rather than the qtip property. + */ + initTip: function() { + var tip = this.tip; + if (!tip) { + tip = this.tip = Ext.create('Ext.tip.QuickTip', { + baseCls: Ext.baseCSSPrefix + 'form-invalid-tip', + renderTo: Ext.getBody() + }); + tip.tagConfig = Ext.apply({}, {attribute: 'errorqtip'}, tip.tagConfig); + } + }, + + /** + * Destroy the error tip instance. + */ + destroyTip: function() { + var tip = this.tip; + if (tip) { + tip.destroy(); + delete this.tip; + } + } + } + +}); + +/** + * @class Ext.form.field.VTypes + *

This is a singleton object which contains a set of commonly used field validation functions. + * The validations provided are basic and intended to be easily customizable and extended.

+ *

To add custom VTypes specify the {@link Ext.form.field.Text#vtype vtype} validation + * test function, and optionally specify any corresponding error text to display and any keystroke + * filtering mask to apply. For example:

+ *

+// custom Vtype for vtype:'time'
+var timeTest = /^([1-9]|1[0-9]):([0-5][0-9])(\s[a|p]m)$/i;
+Ext.apply(Ext.form.field.VTypes, {
+    //  vtype validation function
+    time: function(val, field) {
+        return timeTest.test(val);
+    },
+    // vtype Text property: The error text to display when the validation function returns false
+    timeText: 'Not a valid time.  Must be in the format "12:34 PM".',
+    // vtype Mask property: The keystroke filter mask
+    timeMask: /[\d\s:amp]/i
+});
+ * 
+ * Another example: + *

+// custom Vtype for vtype:'IPAddress'
+Ext.apply(Ext.form.field.VTypes, {
+    IPAddress:  function(v) {
+        return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(v);
+    },
+    IPAddressText: 'Must be a numeric IP address',
+    IPAddressMask: /[\d\.]/i
+});
+ * 
+ * @singleton + */ +Ext.define('Ext.form.field.VTypes', (function(){ + // closure these in so they are only created once. + var alpha = /^[a-zA-Z_]+$/, + alphanum = /^[a-zA-Z0-9_]+$/, + email = /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/, + url = /(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i; + + // All these messages and functions are configurable + return { + singleton: true, + alternateClassName: 'Ext.form.VTypes', + + /** + * The function used to validate email addresses. Note that this is a very basic validation -- complete + * validation per the email RFC specifications is very complex and beyond the scope of this class, although + * this function can be overridden if a more comprehensive validation scheme is desired. See the validation + * section of the Wikipedia article on email addresses + * for additional information. This implementation is intended to validate the following emails: + * 'barney@example.de', 'barney.rubble@example.com', 'barney-rubble@example.coop', 'barney+rubble@example.com' + * . + * @param {String} value The email address + * @return {Boolean} true if the RegExp test passed, and false if not. + */ + 'email' : function(v){ + return email.test(v); + }, + /** + * The error text to display when the email validation function returns false. Defaults to: + * 'This field should be an e-mail address in the format "user@example.com"' + * @type String + */ + 'emailText' : 'This field should be an e-mail address in the format "user@example.com"', + /** + * The keystroke filter mask to be applied on email input. See the {@link #email} method for + * information about more complex email validation. Defaults to: + * /[a-z0-9_\.\-@]/i + * @type RegExp + */ + 'emailMask' : /[a-z0-9_\.\-@\+]/i, + + /** + * The function used to validate URLs + * @param {String} value The URL + * @return {Boolean} true if the RegExp test passed, and false if not. + */ + 'url' : function(v){ + return url.test(v); + }, + /** + * The error text to display when the url validation function returns false. Defaults to: + * 'This field should be a URL in the format "http:/'+'/www.example.com"' + * @type String + */ + 'urlText' : 'This field should be a URL in the format "http:/'+'/www.example.com"', + + /** + * The function used to validate alpha values + * @param {String} value The value + * @return {Boolean} true if the RegExp test passed, and false if not. + */ + 'alpha' : function(v){ + return alpha.test(v); + }, + /** + * The error text to display when the alpha validation function returns false. Defaults to: + * 'This field should only contain letters and _' + * @type String + */ + 'alphaText' : 'This field should only contain letters and _', + /** + * The keystroke filter mask to be applied on alpha input. Defaults to: + * /[a-z_]/i + * @type RegExp + */ + 'alphaMask' : /[a-z_]/i, + + /** + * The function used to validate alphanumeric values + * @param {String} value The value + * @return {Boolean} true if the RegExp test passed, and false if not. + */ + 'alphanum' : function(v){ + return alphanum.test(v); + }, + /** + * The error text to display when the alphanumeric validation function returns false. Defaults to: + * 'This field should only contain letters, numbers and _' + * @type String + */ + 'alphanumText' : 'This field should only contain letters, numbers and _', + /** + * The keystroke filter mask to be applied on alphanumeric input. Defaults to: + * /[a-z0-9_]/i + * @type RegExp + */ + 'alphanumMask' : /[a-z0-9_]/i + }; +})()); + +/** + * @private + * @class Ext.layout.component.field.Text + * @extends Ext.layout.component.field.Field + * Layout class for {@link Ext.form.field.Text} fields. Handles sizing the input field. + */ +Ext.define('Ext.layout.component.field.Text', { + extend: 'Ext.layout.component.field.Field', + alias: 'layout.textfield', + requires: ['Ext.util.TextMetrics'], + + type: 'textfield', + + + /** + * Allow layout to proceed if the {@link Ext.form.field.Text#grow} config is enabled and the value has + * changed since the last layout. + */ + beforeLayout: function(width, height) { + var me = this, + owner = me.owner, + lastValue = this.lastValue, + value = owner.getRawValue(); + this.lastValue = value; + return me.callParent(arguments) || (owner.grow && value !== lastValue); + }, + + + /** + * Size the field body contents given the total dimensions of the bodyEl, taking into account the optional + * {@link Ext.form.field.Text#grow} configurations. + * @param {Number} width The bodyEl width + * @param {Number} height The bodyEl height + */ + sizeBodyContents: function(width, height) { + var size = this.adjustForGrow(width, height); + this.setElementSize(this.owner.inputEl, size[0], size[1]); + }, + + + /** + * Given the target bodyEl dimensions, adjust them if necessary to return the correct final + * size based on the text field's {@link Ext.form.field.Text#grow grow config}. + * @param {Number} width The bodyEl width + * @param {Number} height The bodyEl height + * @return {Array} [inputElWidth, inputElHeight] + */ + adjustForGrow: function(width, height) { + var me = this, + owner = me.owner, + inputEl, value, calcWidth, + result = [width, height]; + + if (owner.grow) { + inputEl = owner.inputEl; + + // Find the width that contains the whole text value + value = (inputEl.dom.value || (owner.hasFocus ? '' : owner.emptyText) || '') + owner.growAppend; + calcWidth = inputEl.getTextWidth(value) + inputEl.getBorderWidth("lr") + inputEl.getPadding("lr"); + + // Constrain + result[0] = Ext.Number.constrain(calcWidth, owner.growMin, + Math.max(owner.growMin, Math.min(owner.growMax, Ext.isNumber(width) ? width : Infinity))); + } + + return result; + } + +}); + +/** + * @private + * @class Ext.layout.component.field.TextArea + * @extends Ext.layout.component.field.Field + * Layout class for {@link Ext.form.field.TextArea} fields. Handles sizing the textarea field. + */ +Ext.define('Ext.layout.component.field.TextArea', { + extend: 'Ext.layout.component.field.Text', + alias: 'layout.textareafield', + + type: 'textareafield', + + + /** + * Given the target bodyEl dimensions, adjust them if necessary to return the correct final + * size based on the text field's {@link Ext.form.field.Text#grow grow config}. Overrides the + * textfield layout's implementation to handle height rather than width. + * @param {Number} width The bodyEl width + * @param {Number} height The bodyEl height + * @return {Array} [inputElWidth, inputElHeight] + */ + adjustForGrow: function(width, height) { + var me = this, + owner = me.owner, + inputEl, value, max, + curWidth, curHeight, calcHeight, + result = [width, height]; + + if (owner.grow) { + inputEl = owner.inputEl; + curWidth = inputEl.getWidth(true); //subtract border/padding to get the available width for the text + curHeight = inputEl.getHeight(); + + // Get and normalize the field value for measurement + value = inputEl.dom.value || ' '; + value += owner.growAppend; + + // Translate newlines to
tags + value = value.replace(/\n/g, '
'); + + // Find the height that contains the whole text value + calcHeight = Ext.util.TextMetrics.measure(inputEl, value, curWidth).height + + inputEl.getBorderWidth("tb") + inputEl.getPadding("tb"); + + // Constrain + max = owner.growMax; + if (Ext.isNumber(height)) { + max = Math.min(max, height); + } + result[1] = Ext.Number.constrain(calcHeight, owner.growMin, max); + } + + return result; + } + +}); +/** + * @class Ext.layout.container.Anchor + * @extends Ext.layout.container.Container + *

This is a layout that enables anchoring of contained elements relative to the container's dimensions. + * If the container is resized, all anchored items are automatically rerendered according to their + * {@link #anchor} rules.

+ *

This class is intended to be extended or created via the layout: 'anchor' {@link Ext.layout.container.AbstractContainer#layout} + * config, and should generally not need to be created directly via the new keyword.

+ *

AnchorLayout does not have any direct config options (other than inherited ones). By default, + * AnchorLayout will calculate anchor measurements based on the size of the container itself. However, the + * container using the AnchorLayout can supply an anchoring-specific config property of anchorSize. + * If anchorSize is specifed, the layout will use it as a virtual container for the purposes of calculating + * anchor measurements based on it instead, allowing the container to be sized independently of the anchoring + * logic if necessary. + * {@img Ext.layout.container.Anchor/Ext.layout.container.Anchor.png Ext.layout.container.Anchor container layout} + * For example: + Ext.create('Ext.Panel', { + width: 500, + height: 400, + title: "AnchorLayout Panel", + layout: 'anchor', + renderTo: Ext.getBody(), + items: [{ + xtype: 'panel', + title: '75% Width and 20% Height', + anchor: '75% 20%' + },{ + xtype: 'panel', + title: 'Offset -300 Width & -200 Height', + anchor: '-300 -200' + },{ + xtype: 'panel', + title: 'Mixed Offset and Percent', + anchor: '-250 20%' + }] + }); + */ + +Ext.define('Ext.layout.container.Anchor', { + + /* Begin Definitions */ + + alias: 'layout.anchor', + extend: 'Ext.layout.container.Container', + alternateClassName: 'Ext.layout.AnchorLayout', + + /* End Definitions */ + + /** + * @cfg {String} anchor + *

This configuation option is to be applied to child items of a container managed by + * this layout (ie. configured with layout:'anchor').


+ * + *

This value is what tells the layout how an item should be anchored to the container. items + * added to an AnchorLayout accept an anchoring-specific config property of anchor which is a string + * containing two values: the horizontal anchor value and the vertical anchor value (for example, '100% 50%'). + * The following types of anchor values are supported:

    + * + *
  • Percentage : Any value between 1 and 100, expressed as a percentage.
    + * The first anchor is the percentage width that the item should take up within the container, and the + * second is the percentage height. For example:
    
    +// two values specified
    +anchor: '100% 50%' // render item complete width of the container and
    +                   // 1/2 height of the container
    +// one value specified
    +anchor: '100%'     // the width value; the height will default to auto
    +     * 
  • + * + *
  • Offsets : Any positive or negative integer value.
    + * This is a raw adjustment where the first anchor is the offset from the right edge of the container, + * and the second is the offset from the bottom edge. For example:
    
    +// two values specified
    +anchor: '-50 -100' // render item the complete width of the container
    +                   // minus 50 pixels and
    +                   // the complete height minus 100 pixels.
    +// one value specified
    +anchor: '-50'      // anchor value is assumed to be the right offset value
    +                   // bottom offset will default to 0
    +     * 
  • + * + *
  • Sides : Valid values are 'right' (or 'r') and 'bottom' + * (or 'b').
    + * Either the container must have a fixed size or an anchorSize config value defined at render time in + * order for these to have any effect.
  • + * + *
  • Mixed :
    + * Anchor values can also be mixed as needed. For example, to render the width offset from the container + * right edge by 50 pixels and 75% of the container's height use: + *
    
    +anchor: '-50 75%'
    +     * 
  • + * + * + *
+ */ + + type: 'anchor', + + /** + * @cfg {String} defaultAnchor + * + * default anchor for all child container items applied if no anchor or specific width is set on the child item. Defaults to '100%'. + * + */ + defaultAnchor: '100%', + + parseAnchorRE: /^(r|right|b|bottom)$/i, + + // private + onLayout: function() { + this.callParent(arguments); + + var me = this, + size = me.getLayoutTargetSize(), + owner = me.owner, + target = me.getTarget(), + ownerWidth = size.width, + ownerHeight = size.height, + overflow = target.getStyle('overflow'), + components = me.getVisibleItems(owner), + len = components.length, + boxes = [], + box, newTargetSize, anchorWidth, anchorHeight, component, anchorSpec, calcWidth, calcHeight, + anchorsArray, anchor, i, el; + + if (ownerWidth < 20 && ownerHeight < 20) { + return; + } + + // Anchor layout uses natural HTML flow to arrange the child items. + // To ensure that all browsers (I'm looking at you IE!) add the bottom margin of the last child to the + // containing element height, we create a zero-sized element with style clear:both to force a "new line" + if (!me.clearEl) { + me.clearEl = target.createChild({ + cls: Ext.baseCSSPrefix + 'clear', + role: 'presentation' + }); + } + + // find the container anchoring size + if (owner.anchorSize) { + if (typeof owner.anchorSize == 'number') { + anchorWidth = owner.anchorSize; + } + else { + anchorWidth = owner.anchorSize.width; + anchorHeight = owner.anchorSize.height; + } + } + else { + anchorWidth = owner.initialConfig.width; + anchorHeight = owner.initialConfig.height; + } + + // Work around WebKit RightMargin bug. We're going to inline-block all the children only ONCE and remove it when we're done + if (!Ext.supports.RightMargin) { + target.addCls(Ext.baseCSSPrefix + 'inline-children'); + } + + for (i = 0; i < len; i++) { + component = components[i]; + el = component.el; + anchor = component.anchor; + + if (!component.anchor && component.items && !Ext.isNumber(component.width) && !(Ext.isIE6 && Ext.isStrict)) { + component.anchor = anchor = me.defaultAnchor; + } + + if (anchor) { + anchorSpec = component.anchorSpec; + // cache all anchor values + if (!anchorSpec) { + anchorsArray = anchor.split(' '); + component.anchorSpec = anchorSpec = { + right: me.parseAnchor(anchorsArray[0], component.initialConfig.width, anchorWidth), + bottom: me.parseAnchor(anchorsArray[1], component.initialConfig.height, anchorHeight) + }; + } + calcWidth = anchorSpec.right ? me.adjustWidthAnchor(anchorSpec.right(ownerWidth) - el.getMargin('lr'), component) : undefined; + calcHeight = anchorSpec.bottom ? me.adjustHeightAnchor(anchorSpec.bottom(ownerHeight) - el.getMargin('tb'), component) : undefined; + + boxes.push({ + component: component, + anchor: true, + width: calcWidth || undefined, + height: calcHeight || undefined + }); + } else { + boxes.push({ + component: component, + anchor: false + }); + } + } + + // Work around WebKit RightMargin bug. We're going to inline-block all the children only ONCE and remove it when we're done + if (!Ext.supports.RightMargin) { + target.removeCls(Ext.baseCSSPrefix + 'inline-children'); + } + + for (i = 0; i < len; i++) { + box = boxes[i]; + me.setItemSize(box.component, box.width, box.height); + } + + if (overflow && overflow != 'hidden' && !me.adjustmentPass) { + newTargetSize = me.getLayoutTargetSize(); + if (newTargetSize.width != size.width || newTargetSize.height != size.height) { + me.adjustmentPass = true; + me.onLayout(); + } + } + + delete me.adjustmentPass; + }, + + // private + parseAnchor: function(a, start, cstart) { + if (a && a != 'none') { + var ratio; + // standard anchor + if (this.parseAnchorRE.test(a)) { + var diff = cstart - start; + return function(v) { + return v - diff; + }; + } + // percentage + else if (a.indexOf('%') != -1) { + ratio = parseFloat(a.replace('%', '')) * 0.01; + return function(v) { + return Math.floor(v * ratio); + }; + } + // simple offset adjustment + else { + a = parseInt(a, 10); + if (!isNaN(a)) { + return function(v) { + return v + a; + }; + } + } + } + return null; + }, + + // private + adjustWidthAnchor: function(value, comp) { + return value; + }, + + // private + adjustHeightAnchor: function(value, comp) { + return value; + } + +}); +/** + * @class Ext.form.action.Load + * @extends Ext.form.action.Action + *

A class which handles loading of data from a server into the Fields of an {@link Ext.form.Basic}.

+ *

Instances of this class are only created by a {@link Ext.form.Basic Form} when + * {@link Ext.form.Basic#load load}ing.

+ *

Response Packet Criteria

+ *

A response packet must contain: + *

    + *
  • success property : Boolean
  • + *
  • data property : Object
  • + *
    The data property contains the values of Fields to load. + * The individual value object for each Field is passed to the Field's + * {@link Ext.form.field.Field#setValue setValue} method.
    + *
+ *

JSON Packets

+ *

By default, response packets are assumed to be JSON, so for the following form load call:


+var myFormPanel = new Ext.form.Panel({
+    title: 'Client and routing info',
+    items: [{
+        fieldLabel: 'Client',
+        name: 'clientName'
+    }, {
+        fieldLabel: 'Port of loading',
+        name: 'portOfLoading'
+    }, {
+        fieldLabel: 'Port of discharge',
+        name: 'portOfDischarge'
+    }]
+});
+myFormPanel.{@link Ext.form.Panel#getForm getForm}().{@link Ext.form.Basic#load load}({
+    url: '/getRoutingInfo.php',
+    params: {
+        consignmentRef: myConsignmentRef
+    },
+    failure: function(form, action) {
+        Ext.Msg.alert("Load failed", action.result.errorMessage);
+    }
+});
+
+ * a success response packet may look like this:


+{
+    success: true,
+    data: {
+        clientName: "Fred. Olsen Lines",
+        portOfLoading: "FXT",
+        portOfDischarge: "OSL"
+    }
+}
+ * while a failure response packet may look like this:


+{
+    success: false,
+    errorMessage: "Consignment reference not found"
+}
+ *

Other data may be placed into the response for processing the {@link Ext.form.Basic Form}'s + * callback or event handler methods. The object decoded from this JSON is available in the + * {@link Ext.form.action.Action#result result} property.

+ */ +Ext.define('Ext.form.action.Load', { + extend:'Ext.form.action.Action', + requires: ['Ext.data.Connection'], + alternateClassName: 'Ext.form.Action.Load', + alias: 'formaction.load', + + type: 'load', + + /** + * @private + */ + run: function() { + Ext.Ajax.request(Ext.apply( + this.createCallback(), + { + method: this.getMethod(), + url: this.getUrl(), + headers: this.headers, + params: this.getParams() + } + )); + }, + + /** + * @private + */ + onSuccess: function(response){ + var result = this.processResponse(response), + form = this.form; + if (result === true || !result.success || !result.data) { + this.failureType = Ext.form.action.Action.LOAD_FAILURE; + form.afterAction(this, false); + return; + } + form.clearInvalid(); + form.setValues(result.data); + form.afterAction(this, true); + }, + + /** + * @private + */ + handleResponse: function(response) { + var reader = this.form.reader, + rs, data; + if (reader) { + rs = reader.read(response); + data = rs.records && rs.records[0] ? rs.records[0].data : null; + return { + success : rs.success, + data : data + }; + } + return Ext.decode(response.responseText); + } +}); + + +/** + * @class Ext.window.Window + * @extends Ext.panel.Panel + *

A specialized panel intended for use as an application window. Windows are floated, {@link #resizable}, and + * {@link #draggable} by default. Windows can be {@link #maximizable maximized} to fill the viewport, + * restored to their prior size, and can be {@link #minimize}d.

+ *

Windows can also be linked to a {@link Ext.ZIndexManager} or managed by the {@link Ext.WindowManager} to provide + * grouping, activation, to front, to back and other application-specific behavior.

+ *

By default, Windows will be rendered to document.body. To {@link #constrain} a Window to another element + * specify {@link Ext.Component#renderTo renderTo}.

+ *

As with all {@link Ext.container.Container Container}s, it is important to consider how you want the Window + * to size and arrange any child Components. Choose an appropriate {@link #layout} configuration which lays out + * child Components in the required manner.

+ * {@img Ext.window.Window/Ext.window.Window.png Window component} + * Example:
+Ext.create('Ext.window.Window', {
+    title: 'Hello',
+    height: 200,
+    width: 400,
+    layout: 'fit',
+    items: {  // Let's put an empty grid in just to illustrate fit layout
+        xtype: 'grid',
+        border: false,
+        columns: [{header: 'World'}],                 // One header just for show. There's no data,
+        store: Ext.create('Ext.data.ArrayStore', {}) // A dummy empty data store
+    }
+}).show();
+
+ * @constructor + * @param {Object} config The config object + * @xtype window + */ +Ext.define('Ext.window.Window', { + extend: 'Ext.panel.Panel', + + alternateClassName: 'Ext.Window', + + requires: ['Ext.util.ComponentDragger', 'Ext.util.Region', 'Ext.EventManager'], + + alias: 'widget.window', + + /** + * @cfg {Number} x + * The X position of the left edge of the window on initial showing. Defaults to centering the Window within + * the width of the Window's container {@link Ext.core.Element Element) (The Element that the Window is rendered to). + */ + /** + * @cfg {Number} y + * The Y position of the top edge of the window on initial showing. Defaults to centering the Window within + * the height of the Window's container {@link Ext.core.Element Element) (The Element that the Window is rendered to). + */ + /** + * @cfg {Boolean} modal + * True to make the window modal and mask everything behind it when displayed, false to display it without + * restricting access to other UI elements (defaults to false). + */ + /** + * @cfg {String/Element} animateTarget + * Id or element from which the window should animate while opening (defaults to null with no animation). + */ + /** + * @cfg {String/Number/Component} defaultFocus + *

Specifies a Component to receive focus when this Window is focused.

+ *

This may be one of:

    + *
  • The index of a footer Button.
  • + *
  • The id or {@link Ext.AbstractComponent#itemId} of a descendant Component.
  • + *
  • A Component.
  • + *
+ */ + /** + * @cfg {Function} onEsc + * Allows override of the built-in processing for the escape key. Default action + * is to close the Window (performing whatever action is specified in {@link #closeAction}. + * To prevent the Window closing when the escape key is pressed, specify this as + * Ext.emptyFn (See {@link Ext#emptyFn Ext.emptyFn}). + */ + /** + * @cfg {Boolean} collapsed + * True to render the window collapsed, false to render it expanded (defaults to false). Note that if + * {@link #expandOnShow} is true (the default) it will override the collapsed config and the window + * will always be expanded when shown. + */ + /** + * @cfg {Boolean} maximized + * True to initially display the window in a maximized state. (Defaults to false). + */ + + /** + * @cfg {String} baseCls + * The base CSS class to apply to this panel's element (defaults to 'x-window'). + */ + baseCls: Ext.baseCSSPrefix + 'window', + + /** + * @cfg {Mixed} resizable + *

Specify as true to allow user resizing at each edge and corner of the window, false to disable + * resizing (defaults to true).

+ *

This may also be specified as a config object to

+ */ + resizable: true, + + /** + * @cfg {Boolean} draggable + *

True to allow the window to be dragged by the header bar, false to disable dragging (defaults to true). Note + * that by default the window will be centered in the viewport, so if dragging is disabled the window may need + * to be positioned programmatically after render (e.g., myWindow.setPosition(100, 100);).

+ */ + draggable: true, + + /** + * @cfg {Boolean} constrain + * True to constrain the window within its containing element, false to allow it to fall outside of its + * containing element. By default the window will be rendered to document.body. To render and constrain the + * window within another element specify {@link #renderTo}. + * (defaults to false). Optionally the header only can be constrained using {@link #constrainHeader}. + */ + constrain: false, + + /** + * @cfg {Boolean} constrainHeader + * True to constrain the window header within its containing element (allowing the window body to fall outside + * of its containing element) or false to allow the header to fall outside its containing element (defaults to + * false). Optionally the entire window can be constrained using {@link #constrain}. + */ + constrainHeader: false, + + /** + * @cfg {Boolean} plain + * True to render the window body with a transparent background so that it will blend into the framing + * elements, false to add a lighter background color to visually highlight the body element and separate it + * more distinctly from the surrounding frame (defaults to false). + */ + plain: false, + + /** + * @cfg {Boolean} minimizable + * True to display the 'minimize' tool button and allow the user to minimize the window, false to hide the button + * and disallow minimizing the window (defaults to false). Note that this button provides no implementation -- + * the behavior of minimizing a window is implementation-specific, so the minimize event must be handled and a + * custom minimize behavior implemented for this option to be useful. + */ + minimizable: false, + + /** + * @cfg {Boolean} maximizable + * True to display the 'maximize' tool button and allow the user to maximize the window, false to hide the button + * and disallow maximizing the window (defaults to false). Note that when a window is maximized, the tool button + * will automatically change to a 'restore' button with the appropriate behavior already built-in that will + * restore the window to its previous size. + */ + maximizable: false, + + // inherit docs + minHeight: 100, + + // inherit docs + minWidth: 200, + + /** + * @cfg {Boolean} expandOnShow + * True to always expand the window when it is displayed, false to keep it in its current state (which may be + * {@link #collapsed}) when displayed (defaults to true). + */ + expandOnShow: true, + + // inherited docs, same default + collapsible: false, + + /** + * @cfg {Boolean} closable + *

True to display the 'close' tool button and allow the user to close the window, false to + * hide the button and disallow closing the window (defaults to true).

+ *

By default, when close is requested by either clicking the close button in the header + * or pressing ESC when the Window has focus, the {@link #close} method will be called. This + * will {@link Ext.Component#destroy destroy} the Window and its content meaning that + * it may not be reused.

+ *

To make closing a Window hide the Window so that it may be reused, set + * {@link #closeAction} to 'hide'.

+ */ + closable: true, + + /** + * @cfg {Boolean} hidden + * Render this Window hidden (default is true). If true, the + * {@link #hide} method will be called internally. + */ + hidden: true, + + // Inherit docs from Component. Windows render to the body on first show. + autoRender: true, + + // Inherit docs from Component. Windows hide using visibility. + hideMode: 'visibility', + + /** @cfg {Boolean} floating @hide Windows are always floating*/ + floating: true, + + ariaRole: 'alertdialog', + + itemCls: 'x-window-item', + + overlapHeader: true, + + ignoreHeaderBorderManagement: true, + + // private + initComponent: function() { + var me = this; + me.callParent(); + me.addEvents( + /** + * @event activate + * Fires after the window has been visually activated via {@link #setActive}. + * @param {Ext.window.Window} this + */ + /** + * @event deactivate + * Fires after the window has been visually deactivated via {@link #setActive}. + * @param {Ext.window.Window} this + */ + /** + * @event resize + * Fires after the window has been resized. + * @param {Ext.window.Window} this + * @param {Number} width The window's new width + * @param {Number} height The window's new height + */ + 'resize', + /** + * @event maximize + * Fires after the window has been maximized. + * @param {Ext.window.Window} this + */ + 'maximize', + /** + * @event minimize + * Fires after the window has been minimized. + * @param {Ext.window.Window} this + */ + 'minimize', + /** + * @event restore + * Fires after the window has been restored to its original size after being maximized. + * @param {Ext.window.Window} this + */ + 'restore' + ); + + if (me.plain) { + me.addClsWithUI('plain'); + } + + if (me.modal) { + me.ariaRole = 'dialog'; + } + }, + + // State Management + // private + + initStateEvents: function(){ + var events = this.stateEvents; + // push on stateEvents if they don't exist + Ext.each(['maximize', 'restore', 'resize', 'dragend'], function(event){ + if (Ext.Array.indexOf(events, event)) { + events.push(event); + } + }); + this.callParent(); + }, + + getState: function() { + var me = this, + state = me.callParent() || {}, + maximized = !!me.maximized; + + state.maximized = maximized; + Ext.apply(state, { + size: maximized ? me.restoreSize : me.getSize(), + pos: maximized ? me.restorePos : me.getPosition() + }); + return state; + }, + + applyState: function(state){ + var me = this; + + if (state) { + me.maximized = state.maximized; + if (me.maximized) { + me.hasSavedRestore = true; + me.restoreSize = state.size; + me.restorePos = state.pos; + } else { + Ext.apply(me, { + width: state.size.width, + height: state.size.height, + x: state.pos[0], + y: state.pos[1] + }); + } + } + }, + + // private + onMouseDown: function () { + if (this.floating) { + this.toFront(); + } + }, + + // private + onRender: function(ct, position) { + var me = this; + me.callParent(arguments); + me.focusEl = me.el; + + // Double clicking a header will toggleMaximize + if (me.maximizable) { + me.header.on({ + dblclick: { + fn: me.toggleMaximize, + element: 'el', + scope: me + } + }); + } + }, + + // private + afterRender: function() { + var me = this, + hidden = me.hidden, + keyMap; + + me.hidden = false; + // Component's afterRender sizes and positions the Component + me.callParent(); + me.hidden = hidden; + + // Create the proxy after the size has been applied in Component.afterRender + me.proxy = me.getProxy(); + + // clickToRaise + me.mon(me.el, 'mousedown', me.onMouseDown, me); + + // Initialize + if (me.maximized) { + me.maximized = false; + me.maximize(); + } + + if (me.closable) { + keyMap = me.getKeyMap(); + keyMap.on(27, me.onEsc, me); + keyMap.disable(); + } + }, + + /** + * @private + * @override + * Override Component.initDraggable. + * Window uses the header element as the delegate. + */ + initDraggable: function() { + var me = this, + ddConfig; + + if (!me.header) { + me.updateHeader(true); + } + + ddConfig = Ext.applyIf({ + el: me.el, + delegate: '#' + me.header.id + }, me.draggable); + + // Add extra configs if Window is specified to be constrained + if (me.constrain || me.constrainHeader) { + ddConfig.constrain = me.constrain; + ddConfig.constrainDelegate = me.constrainHeader; + ddConfig.constrainTo = me.constrainTo || me.container; + } + + /** + *

If this Window is configured {@link #draggable}, this property will contain + * an instance of {@link Ext.util.ComponentDragger} (A subclass of {@link Ext.dd.DragTracker DragTracker}) + * which handles dragging the Window's DOM Element, and constraining according to the {@link #constrain} + * and {@link #constrainHeader} .

+ *

This has implementations of onBeforeStart, onDrag and onEnd + * which perform the dragging action. If extra logic is needed at these points, use + * {@link Ext.Function#createInterceptor createInterceptor} or {@link Ext.Function#createSequence createSequence} to + * augment the existing implementations.

+ * @type Ext.util.ComponentDragger + * @property dd + */ + me.dd = Ext.create('Ext.util.ComponentDragger', this, ddConfig); + me.relayEvents(me.dd, ['dragstart', 'drag', 'dragend']); + }, + + // private + onEsc: function(k, e) { + e.stopEvent(); + this[this.closeAction](); + }, + + // private + beforeDestroy: function() { + var me = this; + if (me.rendered) { + delete this.animateTarget; + me.hide(); + Ext.destroy( + me.keyMap + ); + } + me.callParent(); + }, + + /** + * @private + * @override + * Contribute class-specific tools to the header. + * Called by Panel's initTools. + */ + addTools: function() { + var me = this; + + // Call Panel's initTools + me.callParent(); + + if (me.minimizable) { + me.addTool({ + type: 'minimize', + handler: Ext.Function.bind(me.minimize, me, []) + }); + } + if (me.maximizable) { + me.addTool({ + type: 'maximize', + handler: Ext.Function.bind(me.maximize, me, []) + }); + me.addTool({ + type: 'restore', + handler: Ext.Function.bind(me.restore, me, []), + hidden: true + }); + } + }, + + /** + * Gets the configured default focus item. If a {@link #defaultFocus} is set, it will receive focus, otherwise the + * Container itself will receive focus. + */ + getFocusEl: function() { + var me = this, + f = me.focusEl, + defaultComp = me.defaultButton || me.defaultFocus, + t = typeof db, + el, + ct; + + if (Ext.isDefined(defaultComp)) { + if (Ext.isNumber(defaultComp)) { + f = me.query('button')[defaultComp]; + } else if (Ext.isString(defaultComp)) { + f = me.down('#' + defaultComp); + } else { + f = defaultComp; + } + } + return f || me.focusEl; + }, + + // private + beforeShow: function() { + this.callParent(); + + if (this.expandOnShow) { + this.expand(false); + } + }, + + // private + afterShow: function(animateTarget) { + var me = this, + size; + + // Perform superclass's afterShow tasks + // Which might include animating a proxy from an animTarget + me.callParent(arguments); + + if (me.maximized) { + me.fitContainer(); + } + + if (me.monitorResize || me.constrain || me.constrainHeader) { + Ext.EventManager.onWindowResize(me.onWindowResize, me); + } + me.doConstrain(); + if (me.keyMap) { + me.keyMap.enable(); + } + }, + + // private + doClose: function() { + var me = this; + + // immediate close + if (me.hidden) { + me.fireEvent('close', me); + me[me.closeAction](); + } else { + // close after hiding + me.hide(me.animTarget, me.doClose, me); + } + }, + + // private + afterHide: function() { + var me = this; + + // No longer subscribe to resizing now that we're hidden + if (me.monitorResize || me.constrain || me.constrainHeader) { + Ext.EventManager.removeResizeListener(me.onWindowResize, me); + } + + // Turn off keyboard handling once window is hidden + if (me.keyMap) { + me.keyMap.disable(); + } + + // Perform superclass's afterHide tasks. + me.callParent(arguments); + }, + + // private + onWindowResize: function() { + if (this.maximized) { + this.fitContainer(); + } + this.doConstrain(); + }, + + /** + * Placeholder method for minimizing the window. By default, this method simply fires the {@link #minimize} event + * since the behavior of minimizing a window is application-specific. To implement custom minimize behavior, + * either the minimize event can be handled or this method can be overridden. + * @return {Ext.window.Window} this + */ + minimize: function() { + this.fireEvent('minimize', this); + return this; + }, + + afterCollapse: function() { + var me = this; + + if (me.maximizable) { + me.tools.maximize.hide(); + me.tools.restore.hide(); + } + if (me.resizer) { + me.resizer.disable(); + } + me.callParent(arguments); + }, + + afterExpand: function() { + var me = this; + + if (me.maximized) { + me.tools.restore.show(); + } else if (me.maximizable) { + me.tools.maximize.show(); + } + if (me.resizer) { + me.resizer.enable(); + } + me.callParent(arguments); + }, + + /** + * Fits the window within its current container and automatically replaces + * the {@link #maximizable 'maximize' tool button} with the 'restore' tool button. + * Also see {@link #toggleMaximize}. + * @return {Ext.window.Window} this + */ + maximize: function() { + var me = this; + + if (!me.maximized) { + me.expand(false); + if (!me.hasSavedRestore) { + me.restoreSize = me.getSize(); + me.restorePos = me.getPosition(true); + } + if (me.maximizable) { + me.tools.maximize.hide(); + me.tools.restore.show(); + } + me.maximized = true; + me.el.disableShadow(); + + if (me.dd) { + me.dd.disable(); + } + if (me.collapseTool) { + me.collapseTool.hide(); + } + me.el.addCls(Ext.baseCSSPrefix + 'window-maximized'); + me.container.addCls(Ext.baseCSSPrefix + 'window-maximized-ct'); + + me.setPosition(0, 0); + me.fitContainer(); + me.fireEvent('maximize', me); + } + return me; + }, + + /** + * Restores a {@link #maximizable maximized} window back to its original + * size and position prior to being maximized and also replaces + * the 'restore' tool button with the 'maximize' tool button. + * Also see {@link #toggleMaximize}. + * @return {Ext.window.Window} this + */ + restore: function() { + var me = this, + tools = me.tools; + + if (me.maximized) { + delete me.hasSavedRestore; + me.removeCls(Ext.baseCSSPrefix + 'window-maximized'); + + // Toggle tool visibility + if (tools.restore) { + tools.restore.hide(); + } + if (tools.maximize) { + tools.maximize.show(); + } + if (me.collapseTool) { + me.collapseTool.show(); + } + + // Restore the position/sizing + me.setPosition(me.restorePos); + me.setSize(me.restoreSize); + + // Unset old position/sizing + delete me.restorePos; + delete me.restoreSize; + + me.maximized = false; + + me.el.enableShadow(true); + + // Allow users to drag and drop again + if (me.dd) { + me.dd.enable(); + } + + me.container.removeCls(Ext.baseCSSPrefix + 'window-maximized-ct'); + + me.doConstrain(); + me.fireEvent('restore', me); + } + return me; + }, + + /** + * A shortcut method for toggling between {@link #maximize} and {@link #restore} based on the current maximized + * state of the window. + * @return {Ext.window.Window} this + */ + toggleMaximize: function() { + return this[this.maximized ? 'restore': 'maximize'](); + } + + /** + * @cfg {Boolean} autoWidth @hide + * Absolute positioned element and therefore cannot support autoWidth. + * A width is a required configuration. + **/ +}); +/** + * @class Ext.form.field.Base + * @extends Ext.Component + +Base class for form fields that provides default event handling, rendering, and other common functionality +needed by all form field types. Utilizes the {@link Ext.form.field.Field} mixin for value handling and validation, +and the {@link Ext.form.Labelable} mixin to provide label and error message display. + +In most cases you will want to use a subclass, such as {@link Ext.form.field.Text} or {@link Ext.form.field.Checkbox}, +rather than creating instances of this class directly. However if you are implementing a custom form field, +using this as the parent class is recommended. + +__Values and Conversions__ + +Because BaseField implements the Field mixin, it has a main value that can be initialized with the +{@link #value} config and manipulated via the {@link #getValue} and {@link #setValue} methods. This main +value can be one of many data types appropriate to the current field, for instance a {@link Ext.form.field.Date Date} +field would use a JavaScript Date object as its value type. However, because the field is rendered as a HTML +input, this value data type can not always be directly used in the rendered field. + +Therefore BaseField introduces the concept of a "raw value". This is the value of the rendered HTML input field, +and is normally a String. The {@link #getRawValue} and {@link #setRawValue} methods can be used to directly +work with the raw value, though it is recommended to use getValue and setValue in most cases. + +Conversion back and forth between the main value and the raw value is handled by the {@link #valueToRaw} and +{@link #rawToValue} methods. If you are implementing a subclass that uses a non-String value data type, you +should override these methods to handle the conversion. + +__Rendering__ + +The content of the field body is defined by the {@link #fieldSubTpl} XTemplate, with its argument data +created by the {@link #getSubTplData} method. Override this template and/or method to create custom +field renderings. +{@img Ext.form.BaseField/Ext.form.BaseField.png Ext.form.BaseField BaseField component} +__Example usage:__ + + // A simple subclass of BaseField that creates a HTML5 search field. Redirects to the + // searchUrl when the Enter key is pressed. + Ext.define('Ext.form.SearchField', { + extend: 'Ext.form.field.Base', + alias: 'widget.searchfield', + + inputType: 'search', + + // Config defining the search URL + searchUrl: 'http://www.google.com/search?q={0}', + + // Add specialkey listener + initComponent: function() { + this.callParent(); + this.on('specialkey', this.checkEnterKey, this); + }, + + // Handle enter key presses, execute the search if the field has a value + checkEnterKey: function(field, e) { + var value = this.getValue(); + if (e.getKey() === e.ENTER && !Ext.isEmpty(value)) { + location.href = Ext.String.format(this.searchUrl, value); + } + } + }); + + Ext.create('Ext.form.Panel', { + title: 'BaseField Example', + bodyPadding: 5, + width: 250, + + // Fields will be arranged vertically, stretched to full width + layout: 'anchor', + defaults: { + anchor: '100%' + }, + items: [{ + xtype: 'searchfield', + fieldLabel: 'Search', + name: 'query' + }] + renderTo: Ext.getBody() + }); + + * @constructor + * Creates a new Field + * @param {Object} config Configuration options + * + * @xtype field + * @markdown + * @docauthor Jason Johnston + */ +Ext.define('Ext.form.field.Base', { + extend: 'Ext.Component', + mixins: { + labelable: 'Ext.form.Labelable', + field: 'Ext.form.field.Field' + }, + alias: 'widget.field', + alternateClassName: ['Ext.form.Field', 'Ext.form.BaseField'], + requires: ['Ext.util.DelayedTask', 'Ext.XTemplate', 'Ext.layout.component.field.Field'], + + fieldSubTpl: [ + 'name="{name}" ', + 'size="{size}" ', + 'tabIndex="{tabIdx}" ', + 'class="{fieldCls} {typeCls}" autocomplete="off" />', + { + compiled: true, + disableFormats: true + } + ], + + /** + * @cfg {String} name The name of the field (defaults to undefined). This is used as the parameter + * name when including the field value in a {@link Ext.form.Basic#submit form submit()}. If no name is + * configured, it falls back to the {@link #inputId}. To prevent the field from being included in the + * form submit, set {@link #submitValue} to false. + */ + + /** + * @cfg {String} inputType + *

The type attribute for input fields -- e.g. radio, text, password, file (defaults to 'text'). + * The extended types supported by HTML5 inputs (url, email, etc.) may also be used, though using them + * will cause older browsers to fall back to 'text'.

+ *

The type 'password' must be used to render that field type currently -- there is no separate Ext + * component for that. You can use {@link Ext.form.field.File} which creates a custom-rendered file upload + * field, but if you want a plain unstyled file input you can use a BaseField with inputType:'file'.

+ */ + inputType: 'text', + + /** + * @cfg {Number} tabIndex The tabIndex for this field. Note this only applies to fields that are rendered, + * not those which are built via applyTo (defaults to undefined). + */ + + /** + * @cfg {String} invalidText The error text to use when marking a field invalid and no message is provided + * (defaults to 'The value in this field is invalid') + */ + invalidText : 'The value in this field is invalid', + + /** + * @cfg {String} fieldCls The default CSS class for the field input (defaults to 'x-form-field') + */ + fieldCls : Ext.baseCSSPrefix + 'form-field', + + /** + * @cfg {String} fieldStyle Optional CSS style(s) to be applied to the {@link #inputEl field input element}. + * Should be a valid argument to {@link Ext.core.Element#applyStyles}. Defaults to undefined. See also the + * {@link #setFieldStyle} method for changing the style after initialization. + */ + + /** + * @cfg {String} focusCls The CSS class to use when the field receives focus (defaults to 'x-form-focus') + */ + focusCls : Ext.baseCSSPrefix + 'form-focus', + + /** + * @cfg {String} dirtyCls The CSS class to use when the field value {@link #isDirty is dirty}. + */ + dirtyCls : Ext.baseCSSPrefix + 'form-dirty', + + /** + * @cfg {Array} checkChangeEvents + *

A list of event names that will be listened for on the field's {@link #inputEl input element}, which + * will cause the field's value to be checked for changes. If a change is detected, the + * {@link #change change event} will be fired, followed by validation if the {@link #validateOnChange} + * option is enabled.

+ *

Defaults to ['change', 'propertychange'] in Internet Explorer, and ['change', 'input', + * 'textInput', 'keyup', 'dragdrop'] in other browsers. This catches all the ways that field values + * can be changed in most supported browsers; the only known exceptions at the time of writing are:

+ *
    + *
  • Safari 3.2 and older: cut/paste in textareas via the context menu, and dragging text into textareas
  • + *
  • Opera 10 and 11: dragging text into text fields and textareas, and cut via the context menu in text + * fields and textareas
  • + *
  • Opera 9: Same as Opera 10 and 11, plus paste from context menu in text fields and textareas
  • + *
+ *

If you need to guarantee on-the-fly change notifications including these edge cases, you can call the + * {@link #checkChange} method on a repeating interval, e.g. using {@link Ext.TaskManager}, or if the field is + * within a {@link Ext.form.Panel}, you can use the FormPanel's {@link Ext.form.Panel#pollForChanges} + * configuration to set up such a task automatically.

+ */ + checkChangeEvents: Ext.isIE && (!document.documentMode || document.documentMode < 9) ? + ['change', 'propertychange'] : + ['change', 'input', 'textInput', 'keyup', 'dragdrop'], + + /** + * @cfg {Number} checkChangeBuffer + * Defines a timeout in milliseconds for buffering {@link #checkChangeEvents} that fire in rapid succession. + * Defaults to 50 milliseconds. + */ + checkChangeBuffer: 50, + + componentLayout: 'field', + + /** + * @cfg {Boolean} readOnly true to mark the field as readOnly in HTML + * (defaults to false). + *

Note: this only sets the element's readOnly DOM attribute. + * Setting readOnly=true, for example, will not disable triggering a + * ComboBox or Date; it gives you the option of forcing the user to choose + * via the trigger without typing in the text box. To hide the trigger use + * {@link Ext.form.field.Trigger#hideTrigger hideTrigger}.

+ */ + readOnly : false, + + /** + * @cfg {String} readOnlyCls The CSS class applied to the component's main element when it is {@link #readOnly}. + */ + readOnlyCls: Ext.baseCSSPrefix + 'form-readonly', + + /** + * @cfg {String} inputId + * The id that will be given to the generated input DOM element. Defaults to an automatically generated id. + * If you configure this manually, you must make sure it is unique in the document. + */ + + /** + * @cfg {Boolean} validateOnBlur + * Whether the field should validate when it loses focus (defaults to true). This will cause fields + * to be validated as the user steps through the fields in the form regardless of whether they are making + * changes to those fields along the way. See also {@link #validateOnChange}. + */ + validateOnBlur: true, + + // private + hasFocus : false, + + baseCls: Ext.baseCSSPrefix + 'field', + + maskOnDisable: false, + + // private + initComponent : function() { + var me = this; + + me.callParent(); + + me.subTplData = me.subTplData || {}; + + me.addEvents( + /** + * @event focus + * Fires when this field receives input focus. + * @param {Ext.form.field.Base} this + */ + 'focus', + /** + * @event blur + * Fires when this field loses input focus. + * @param {Ext.form.field.Base} this + */ + 'blur', + /** + * @event specialkey + * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed. + * To handle other keys see {@link Ext.panel.Panel#keys} or {@link Ext.util.KeyMap}. + * You can check {@link Ext.EventObject#getKey} to determine which key was pressed. + * For example:

+var form = new Ext.form.Panel({
+    ...
+    items: [{
+            fieldLabel: 'Field 1',
+            name: 'field1',
+            allowBlank: false
+        },{
+            fieldLabel: 'Field 2',
+            name: 'field2',
+            listeners: {
+                specialkey: function(field, e){
+                    // e.HOME, e.END, e.PAGE_UP, e.PAGE_DOWN,
+                    // e.TAB, e.ESC, arrow keys: e.LEFT, e.RIGHT, e.UP, e.DOWN
+                    if (e.{@link Ext.EventObject#getKey getKey()} == e.ENTER) {
+                        var form = field.up('form').getForm();
+                        form.submit();
+                    }
+                }
+            }
+        }
+    ],
+    ...
+});
+             * 
+ * @param {Ext.form.field.Base} this + * @param {Ext.EventObject} e The event object + */ + 'specialkey' + ); + + // Init mixins + me.initLabelable(); + me.initField(); + + // Default name to inputId + if (!me.name) { + me.name = me.getInputId(); + } + }, + + /** + * Returns the input id for this field. If none was specified via the {@link #inputId} config, + * then an id will be automatically generated. + */ + getInputId: function() { + return this.inputId || (this.inputId = Ext.id()); + }, + + /** + * @protected Creates and returns the data object to be used when rendering the {@link #fieldSubTpl}. + * @return {Object} The template data + */ + getSubTplData: function() { + var me = this, + type = me.inputType, + inputId = me.getInputId(); + + return Ext.applyIf(me.subTplData, { + id: inputId, + name: me.name || inputId, + type: type, + size: me.size || 20, + cls: me.cls, + fieldCls: me.fieldCls, + tabIdx: me.tabIndex, + typeCls: Ext.baseCSSPrefix + 'form-' + (type === 'password' ? 'text' : type) + }); + }, + + /** + * @protected + * Gets the markup to be inserted into the outer template's bodyEl. For fields this is the + * actual input element. + */ + getSubTplMarkup: function() { + return this.getTpl('fieldSubTpl').apply(this.getSubTplData()); + }, + + initRenderTpl: function() { + var me = this; + if (!me.hasOwnProperty('renderTpl')) { + me.renderTpl = me.getTpl('labelableRenderTpl'); + } + return me.callParent(); + }, + + initRenderData: function() { + return Ext.applyIf(this.callParent(), this.getLabelableRenderData()); + }, + + /** + * Set the {@link #fieldStyle CSS style} of the {@link #inputEl field input element}. + * @param {String/Object/Function} style The style(s) to apply. Should be a valid argument to + * {@link Ext.core.Element#applyStyles}. + */ + setFieldStyle: function(style) { + var me = this, + inputEl = me.inputEl; + if (inputEl) { + inputEl.applyStyles(style); + } + me.fieldStyle = style; + }, + + // private + onRender : function() { + var me = this, + fieldStyle = me.fieldStyle, + renderSelectors = me.renderSelectors; + + Ext.applyIf(renderSelectors, me.getLabelableSelectors()); + + Ext.applyIf(renderSelectors, { + /** + * @property inputEl + * @type Ext.core.Element + * The input Element for this Field. Only available after the field has been rendered. + */ + inputEl: '.' + me.fieldCls + }); + + me.callParent(arguments); + + // Make the stored rawValue get set as the input element's value + me.setRawValue(me.rawValue); + + if (me.readOnly) { + me.setReadOnly(true); + } + if (me.disabled) { + me.disable(); + } + if (fieldStyle) { + me.setFieldStyle(fieldStyle); + } + + me.renderActiveError(); + }, + + initAria: function() { + var me = this; + me.callParent(); + + // Associate the field to the error message element + me.getActionEl().dom.setAttribute('aria-describedby', Ext.id(me.errorEl)); + }, + + getFocusEl: function() { + return this.inputEl; + }, + + isFileUpload: function() { + return this.inputType === 'file'; + }, + + extractFileInput: function() { + var me = this, + fileInput = me.isFileUpload() ? me.inputEl.dom : null, + clone; + if (fileInput) { + clone = fileInput.cloneNode(true); + fileInput.parentNode.replaceChild(clone, fileInput); + me.inputEl = Ext.get(clone); + } + return fileInput; + }, + + // private override to use getSubmitValue() as a convenience + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue && !me.isFileUpload()) { + val = me.getSubmitValue(); + if (val !== null) { + data = {}; + data[me.getName()] = val; + } + } + return data; + }, + + /** + *

Returns the value that would be included in a standard form submit for this field. This will be combined + * with the field's name to form a name=value pair in the {@link #getSubmitData submitted parameters}. + * If an empty string is returned then just the name= will be submitted; if null is returned + * then nothing will be submitted.

+ *

Note that the value returned will have been {@link #processRawValue processed} but may or may not have + * been successfully {@link #validate validated}.

+ * @return {String} The value to be submitted, or null. + */ + getSubmitValue: function() { + return this.processRawValue(this.getRawValue()); + }, + + /** + * Returns the raw value of the field, without performing any normalization, conversion, or validation. + * To get a normalized and converted value see {@link #getValue}. + * @return {String} value The raw String value of the field + */ + getRawValue: function() { + var me = this, + v = (me.inputEl ? me.inputEl.getValue() : Ext.value(me.rawValue, '')); + me.rawValue = v; + return v; + }, + + /** + * Sets the field's raw value directly, bypassing {@link #valueToRaw value conversion}, change detection, and + * validation. To set the value with these additional inspections see {@link #setValue}. + * @param {Mixed} value The value to set + * @return {Mixed} value The field value that is set + */ + setRawValue: function(value) { + var me = this; + value = Ext.value(value, ''); + me.rawValue = value; + + // Some Field subclasses may not render an inputEl + if (me.inputEl) { + me.inputEl.dom.value = value; + } + return value; + }, + + /** + *

Converts a mixed-type value to a raw representation suitable for displaying in the field. This allows + * controlling how value objects passed to {@link #setValue} are shown to the user, including localization. + * For instance, for a {@link Ext.form.field.Date}, this would control how a Date object passed to {@link #setValue} + * would be converted to a String for display in the field.

+ *

See {@link #rawToValue} for the opposite conversion.

+ *

The base implementation simply does a standard toString conversion, and converts + * {@link Ext#isEmpty empty values} to an empty string.

+ * @param {Mixed} value The mixed-type value to convert to the raw representation. + * @return {Mixed} The converted raw value. + */ + valueToRaw: function(value) { + return '' + Ext.value(value, ''); + }, + + /** + *

Converts a raw input field value into a mixed-type value that is suitable for this particular field type. + * This allows controlling the normalization and conversion of user-entered values into field-type-appropriate + * values, e.g. a Date object for {@link Ext.form.field.Date}, and is invoked by {@link #getValue}.

+ *

It is up to individual implementations to decide how to handle raw values that cannot be successfully + * converted to the desired object type.

+ *

See {@link #valueToRaw} for the opposite conversion.

+ *

The base implementation does no conversion, returning the raw value untouched.

+ * @param {Mixed} rawValue + * @return {Mixed} The converted value. + */ + rawToValue: function(rawValue) { + return rawValue; + }, + + /** + * Performs any necessary manipulation of a raw field value to prepare it for {@link #rawToValue conversion} + * and/or {@link #validate validation}, for instance stripping out ignored characters. In the base implementation + * it does nothing; individual subclasses may override this as needed. + * @param {Mixed} value The unprocessed string value + * @return {Mixed} The processed string value + */ + processRawValue: function(value) { + return value; + }, + + /** + * Returns the current data value of the field. The type of value returned is particular to the type of the + * particular field (e.g. a Date object for {@link Ext.form.field.Date}), as the result of calling {@link #rawToValue} on + * the field's {@link #processRawValue processed} String value. To return the raw String value, see {@link #getRawValue}. + * @return {Mixed} value The field value + */ + getValue: function() { + var me = this, + val = me.rawToValue(me.processRawValue(me.getRawValue())); + me.value = val; + return val; + }, + + /** + * Sets a data value into the field and runs the change detection and validation. To set the value directly + * without these inspections see {@link #setRawValue}. + * @param {Mixed} value The value to set + * @return {Ext.form.field.Field} this + */ + setValue: function(value) { + var me = this; + me.setRawValue(me.valueToRaw(value)); + return me.mixins.field.setValue.call(me, value); + }, + + + //private + onDisable: function() { + var me = this, + inputEl = me.inputEl; + me.callParent(); + if (inputEl) { + inputEl.dom.disabled = true; + } + }, + + //private + onEnable: function() { + var me = this, + inputEl = me.inputEl; + me.callParent(); + if (inputEl) { + inputEl.dom.disabled = false; + } + }, + + /** + * Sets the read only state of this field. + * @param {Boolean} readOnly Whether the field should be read only. + */ + setReadOnly: function(readOnly) { + var me = this, + inputEl = me.inputEl; + if (inputEl) { + inputEl.dom.readOnly = readOnly; + inputEl.dom.setAttribute('aria-readonly', readOnly); + } + me[readOnly ? 'addCls' : 'removeCls'](me.readOnlyCls); + me.readOnly = readOnly; + }, + + // private + fireKey: function(e){ + if(e.isSpecialKey()){ + this.fireEvent('specialkey', this, Ext.create('Ext.EventObjectImpl', e)); + } + }, + + // private + initEvents : function(){ + var me = this, + inputEl = me.inputEl, + onChangeTask, + onChangeEvent; + if (inputEl) { + me.mon(inputEl, Ext.EventManager.getKeyEvent(), me.fireKey, me); + me.mon(inputEl, 'focus', me.onFocus, me); + + // standardise buffer across all browsers + OS-es for consistent event order. + // (the 10ms buffer for Editors fixes a weird FF/Win editor issue when changing OS window focus) + me.mon(inputEl, 'blur', me.onBlur, me, me.inEditor ? {buffer:10} : null); + + // listen for immediate value changes + onChangeTask = Ext.create('Ext.util.DelayedTask', me.checkChange, me); + me.onChangeEvent = onChangeEvent = function() { + onChangeTask.delay(me.checkChangeBuffer); + }; + Ext.each(me.checkChangeEvents, function(eventName) { + if (eventName === 'propertychange') { + me.usesPropertychange = true; + } + me.mon(inputEl, eventName, onChangeEvent); + }, me); + } + me.callParent(); + }, + + doComponentLayout: function() { + var me = this, + inputEl = me.inputEl, + usesPropertychange = me.usesPropertychange, + ename = 'propertychange', + onChangeEvent = me.onChangeEvent; + + // In IE if propertychange is one of the checkChangeEvents, we need to remove + // the listener prior to layout and re-add it after, to prevent it from firing + // needlessly for attribute and style changes applied to the inputEl. + if (usesPropertychange) { + me.mun(inputEl, ename, onChangeEvent); + } + me.callParent(arguments); + if (usesPropertychange) { + me.mon(inputEl, ename, onChangeEvent); + } + }, + + // private + preFocus: Ext.emptyFn, + + // private + onFocus: function() { + var me = this, + focusCls = me.focusCls, + inputEl = me.inputEl; + me.preFocus(); + if (focusCls && inputEl) { + inputEl.addCls(focusCls); + } + if (!me.hasFocus) { + me.hasFocus = true; + me.fireEvent('focus', me); + } + }, + + // private + beforeBlur : Ext.emptyFn, + + // private + onBlur : function(){ + var me = this, + focusCls = me.focusCls, + inputEl = me.inputEl; + me.beforeBlur(); + if (focusCls && inputEl) { + inputEl.removeCls(focusCls); + } + if (me.validateOnBlur) { + me.validate(); + } + me.hasFocus = false; + me.fireEvent('blur', me); + me.postBlur(); + }, + + // private + postBlur : Ext.emptyFn, + + + /** + * @private Called when the field's dirty state changes. Adds/removes the {@link #dirtyCls} on the main element. + * @param {Boolean} isDirty + */ + onDirtyChange: function(isDirty) { + this[isDirty ? 'addCls' : 'removeCls'](this.dirtyCls); + }, + + + /** + * Returns whether or not the field value is currently valid by + * {@link #getErrors validating} the {@link #processRawValue processed raw value} + * of the field. Note: {@link #disabled} fields are always treated as valid. + * @return {Boolean} True if the value is valid, else false + */ + isValid : function() { + var me = this; + return me.disabled || me.validateValue(me.processRawValue(me.getRawValue())); + }, + + + /** + *

Uses {@link #getErrors} to build an array of validation errors. If any errors are found, they are passed + * to {@link #markInvalid} and false is returned, otherwise true is returned.

+ *

Previously, subclasses were invited to provide an implementation of this to process validations - from 3.2 + * onwards {@link #getErrors} should be overridden instead.

+ * @param {Mixed} value The value to validate + * @return {Boolean} True if all validations passed, false if one or more failed + */ + validateValue: function(value) { + var me = this, + errors = me.getErrors(value), + isValid = Ext.isEmpty(errors); + if (!me.preventMark) { + if (isValid) { + me.clearInvalid(); + } else { + me.markInvalid(errors); + } + } + + return isValid; + }, + + /** + *

Display one or more error messages associated with this field, using {@link #msgTarget} to determine how to + * display the messages and applying {@link #invalidCls} to the field's UI element.

+ *

Note: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to + * return false if the value does pass validation. So simply marking a Field as invalid + * will not prevent submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} + * option set.

+ * @param {String/Array} errors The validation message(s) to display. + */ + markInvalid : function(errors) { + // Save the message and fire the 'invalid' event + var me = this, + oldMsg = me.getActiveError(); + me.setActiveErrors(Ext.Array.from(errors)); + if (oldMsg !== me.getActiveError()) { + me.doComponentLayout(); + } + }, + + /** + *

Clear any invalid styles/messages for this field.

+ *

Note: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to + * return true if the value does not pass validation. So simply clearing a field's errors + * will not necessarily allow submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} + * option set.

+ */ + clearInvalid : function() { + // Clear the message and fire the 'valid' event + var me = this, + hadError = me.hasActiveError(); + me.unsetActiveError(); + if (hadError) { + me.doComponentLayout(); + } + }, + + /** + * @private Overrides the method from the Ext.form.Labelable mixin to also add the invalidCls to the inputEl, + * as that is required for proper styling in IE with nested fields (due to lack of child selector) + */ + renderActiveError: function() { + var me = this, + hasError = me.hasActiveError(); + if (me.inputEl) { + // Add/remove invalid class + me.inputEl[hasError ? 'addCls' : 'removeCls'](me.invalidCls + '-field'); + } + me.mixins.labelable.renderActiveError.call(me); + }, + + + getActionEl: function() { + return this.inputEl || this.el; + } + +}); + +/** + * @class Ext.form.field.Text + * @extends Ext.form.field.Base + +A basic text field. Can be used as a direct replacement for traditional text inputs, +or as the base class for more sophisticated input controls (like {@link Ext.form.field.TextArea} +and {@link Ext.form.field.ComboBox}). Has support for empty-field placeholder values (see {@link #emptyText}). + +#Validation# + +The Text field has a useful set of validations built in: + +- {@link #allowBlank} for making the field required +- {@link #minLength} for requiring a minimum value length +- {@link #maxLength} for setting a maximum value length (with {@link #enforceMaxLength} to add it + as the `maxlength` attribute on the input element) +- {@link regex} to specify a custom regular expression for validation + +In addition, custom validations may be added: + +- {@link #vtype} specifies a virtual type implementation from {@link Ext.form.field.VTypes} which can contain + custom validation logic +- {@link #validator} allows a custom arbitrary function to be called during validation + +The details around how and when each of these validation options get used are described in the +documentation for {@link #getErrors}. + +By default, the field value is checked for validity immediately while the user is typing in the +field. This can be controlled with the {@link #validateOnChange}, {@link #checkChangeEvents}, and +{@link #checkChangeBugger} configurations. Also see the details on Form Validation in the +{@link Ext.form.Panel} class documentation. + +#Masking and Character Stripping# + +Text fields can be configured with custom regular expressions to be applied to entered values before +validation: see {@link #maskRe} and {@link #stripCharsRe} for details. +{@img Ext.form.Text/Ext.form.Text.png Ext.form.Text component} +#Example usage:# + + Ext.create('Ext.form.Panel', { + title: 'Contact Info', + width: 300, + bodyPadding: 10, + renderTo: Ext.getBody(), + items: [{ + xtype: 'textfield', + name: 'name', + fieldLabel: 'Name', + allowBlank: false // requires a non-empty value + }, { + xtype: 'textfield', + name: 'email', + fieldLabel: 'Email Address', + vtype: 'email' // requires value to be a valid email address format + }] + }); + + * @constructor Creates a new TextField + * @param {Object} config Configuration options + * + * @xtype textfield + * @markdown + * @docauthor Jason Johnston + */ +Ext.define('Ext.form.field.Text', { + extend:'Ext.form.field.Base', + alias: 'widget.textfield', + requires: ['Ext.form.field.VTypes', 'Ext.layout.component.field.Text'], + alternateClassName: ['Ext.form.TextField', 'Ext.form.Text'], + + /** + * @cfg {String} vtypeText A custom error message to display in place of the default message provided + * for the {@link #vtype} currently set for this field (defaults to undefined). + * Note: only applies if {@link #vtype} is set, else ignored. + */ + + /** + * @cfg {RegExp} stripCharsRe A JavaScript RegExp object used to strip unwanted content from the value + * before validation (defaults to undefined). + */ + + /** + * @cfg {Number} size An initial value for the 'size' attribute on the text input element. This is only + * used if the field has no configured {@link #width} and is not given a width by its container's layout. + * Defaults to 20. + */ + size: 20, + + /** + * @cfg {Boolean} grow true if this field should automatically grow and shrink to its content + * (defaults to false) + */ + + /** + * @cfg {Number} growMin The minimum width to allow when {@link #grow} = true (defaults + * to 30) + */ + growMin : 30, + + /** + * @cfg {Number} growMax The maximum width to allow when {@link #grow} = true (defaults + * to 800) + */ + growMax : 800, + + /** + * @cfg {String} growAppend + * A string that will be appended to the field's current value for the purposes of calculating the target + * field size. Only used when the {@link #grow} config is true. Defaults to a single capital "W" + * (the widest character in common fonts) to leave enough space for the next typed character and avoid the + * field value shifting before the width is adjusted. + */ + growAppend: 'W', + + /** + * @cfg {String} vtype A validation type name as defined in {@link Ext.form.field.VTypes} (defaults to undefined) + */ + + /** + * @cfg {RegExp} maskRe An input mask regular expression that will be used to filter keystrokes that do + * not match (defaults to undefined) + */ + + /** + * @cfg {Boolean} disableKeyFilter Specify true to disable input keystroke filtering (defaults + * to false) + */ + + /** + * @cfg {Boolean} allowBlank Specify false to validate that the value's length is > 0 (defaults to + * true) + */ + allowBlank : true, + + /** + * @cfg {Number} minLength Minimum input field length required (defaults to 0) + */ + minLength : 0, + + /** + * @cfg {Number} maxLength Maximum input field length allowed by validation (defaults to Number.MAX_VALUE). + * This behavior is intended to provide instant feedback to the user by improving usability to allow pasting + * and editing or overtyping and back tracking. To restrict the maximum number of characters that can be + * entered into the field use the {@link Ext.form.field.Text#enforceMaxLength enforceMaxLength} option. + */ + maxLength : Number.MAX_VALUE, + + /** + * @cfg {Boolean} enforceMaxLength True to set the maxLength property on the underlying input field. Defaults to false + */ + + /** + * @cfg {String} minLengthText Error text to display if the {@link #minLength minimum length} + * validation fails (defaults to 'The minimum length for this field is {minLength}') + */ + minLengthText : 'The minimum length for this field is {0}', + + /** + * @cfg {String} maxLengthText Error text to display if the {@link #maxLength maximum length} + * validation fails (defaults to 'The maximum length for this field is {maxLength}') + */ + maxLengthText : 'The maximum length for this field is {0}', + + /** + * @cfg {Boolean} selectOnFocus true to automatically select any existing field text when the field + * receives input focus (defaults to false) + */ + + /** + * @cfg {String} blankText The error text to display if the {@link #allowBlank} validation + * fails (defaults to 'This field is required') + */ + blankText : 'This field is required', + + /** + * @cfg {Function} validator + *

A custom validation function to be called during field validation ({@link #getErrors}) + * (defaults to undefined). If specified, this function will be called first, allowing the + * developer to override the default validation process.

+ *

This function will be passed the following Parameters:

+ *
    + *
  • value: Mixed + *
    The current field value
  • + *
+ *

This function is to Return:

+ *
    + *
  • true: Boolean + *
    true if the value is valid
  • + *
  • msg: String + *
    An error message if the value is invalid
  • + *
+ */ + + /** + * @cfg {RegExp} regex A JavaScript RegExp object to be tested against the field value during validation + * (defaults to undefined). If the test fails, the field will be marked invalid using + * {@link #regexText}. + */ + + /** + * @cfg {String} regexText The error text to display if {@link #regex} is used and the + * test fails during validation (defaults to '') + */ + regexText : '', + + /** + * @cfg {String} emptyText + *

The default text to place into an empty field (defaults to undefined).

+ *

Note that normally this value will be submitted to the server if this field is enabled; to prevent this + * you can set the {@link Ext.form.action.Action#submitEmptyText submitEmptyText} option of + * {@link Ext.form.Basic#submit} to false.

+ *

Also note that if you use {@link #inputType inputType}:'file', {@link #emptyText} is not + * supported and should be avoided.

+ */ + + /** + * @cfg {String} emptyCls The CSS class to apply to an empty field to style the {@link #emptyText} + * (defaults to 'x-form-empty-field'). This class is automatically added and removed as needed + * depending on the current field value. + */ + emptyCls : Ext.baseCSSPrefix + 'form-empty-field', + + ariaRole: 'textbox', + + /** + * @cfg {Boolean} enableKeyEvents true to enable the proxying of key events for the HTML input field (defaults to false) + */ + + componentLayout: 'textfield', + + initComponent : function(){ + this.callParent(); + this.addEvents( + /** + * @event autosize + * Fires when the {@link #autoSize} function is triggered and the field is + * resized according to the {@link #grow}/{@link #growMin}/{@link #growMax} configs as a result. + * This event provides a hook for the developer to apply additional logic at runtime to resize the + * field if needed. + * @param {Ext.form.field.Text} this This text field + * @param {Number} width The new field width + */ + 'autosize', + + /** + * @event keydown + * Keydown input field event. This event only fires if {@link #enableKeyEvents} + * is set to true. + * @param {Ext.form.field.Text} this This text field + * @param {Ext.EventObject} e + */ + 'keydown', + /** + * @event keyup + * Keyup input field event. This event only fires if {@link #enableKeyEvents} + * is set to true. + * @param {Ext.form.field.Text} this This text field + * @param {Ext.EventObject} e + */ + 'keyup', + /** + * @event keypress + * Keypress input field event. This event only fires if {@link #enableKeyEvents} + * is set to true. + * @param {Ext.form.field.Text} this This text field + * @param {Ext.EventObject} e + */ + 'keypress' + ); + }, + + // private + initEvents : function(){ + var me = this, + el = me.inputEl; + + me.callParent(); + if(me.selectOnFocus || me.emptyText){ + me.mon(el, 'mousedown', me.onMouseDown, me); + } + if(me.maskRe || (me.vtype && me.disableKeyFilter !== true && (me.maskRe = Ext.form.field.VTypes[me.vtype+'Mask']))){ + me.mon(el, 'keypress', me.filterKeys, me); + } + + if (me.enableKeyEvents) { + me.mon(el, { + scope: me, + keyup: me.onKeyUp, + keydown: me.onKeyDown, + keypress: me.onKeyPress + }); + } + }, + + /** + * @private override - treat undefined and null values as equal to an empty string value + */ + isEqual: function(value1, value2) { + return String(Ext.value(value1, '')) === String(Ext.value(value2, '')); + }, + + /** + * @private + * If grow=true, invoke the autoSize method when the field's value is changed. + */ + onChange: function() { + this.callParent(); + this.autoSize(); + }, + + afterRender: function(){ + var me = this; + if (me.enforceMaxLength) { + me.inputEl.dom.maxLength = me.maxLength; + } + me.applyEmptyText(); + me.autoSize(); + me.callParent(); + }, + + onMouseDown: function(e){ + var me = this; + if(!me.hasFocus){ + me.mon(me.inputEl, 'mouseup', Ext.emptyFn, me, { single: true, preventDefault: true }); + } + }, + + /** + * Performs any necessary manipulation of a raw String value to prepare it for {@link #stringToValue conversion} + * and/or {@link #validate validation}. For text fields this applies the configured {@link #stripCharsRe} to the + * raw value. + * @param {String} value The unprocessed string value + * @return {String} The processed string value + */ + processRawValue: function(value) { + var me = this, + stripRe = me.stripCharsRe, + newValue; + + if (stripRe) { + newValue = value.replace(stripRe, ''); + if (newValue !== value) { + me.setRawValue(newValue); + value = newValue; + } + } + return value; + }, + + //private + onDisable: function(){ + this.callParent(); + if (Ext.isIE) { + this.inputEl.dom.unselectable = 'on'; + } + }, + + //private + onEnable: function(){ + this.callParent(); + if (Ext.isIE) { + this.inputEl.dom.unselectable = ''; + } + }, + + onKeyDown: function(e) { + this.fireEvent('keydown', this, e); + }, + + onKeyUp: function(e) { + this.fireEvent('keyup', this, e); + }, + + onKeyPress: function(e) { + this.fireEvent('keypress', this, e); + }, + + /** + * Resets the current field value to the originally-loaded value and clears any validation messages. + * Also adds {@link #emptyText} and {@link #emptyCls} if the + * original value was blank. + */ + reset : function(){ + this.callParent(); + this.applyEmptyText(); + }, + + applyEmptyText : function(){ + var me = this, + emptyText = me.emptyText, + isEmpty; + + if (me.rendered && emptyText) { + isEmpty = me.getRawValue().length < 1 && !me.hasFocus; + + if (Ext.supports.Placeholder) { + me.inputEl.dom.placeholder = emptyText; + } else if (isEmpty) { + me.setRawValue(emptyText); + } + + //all browsers need this because of a styling issue with chrome + placeholders. + //the text isnt vertically aligned when empty (and using the placeholder) + if (isEmpty) { + me.inputEl.addCls(me.emptyCls); + } + + me.autoSize(); + } + }, + + // private + preFocus : function(){ + var me = this, + inputEl = me.inputEl, + emptyText = me.emptyText, + isEmpty; + + if (emptyText && !Ext.supports.Placeholder && inputEl.dom.value === emptyText) { + me.setRawValue(''); + isEmpty = true; + inputEl.removeCls(me.emptyCls); + } else if (Ext.supports.Placeholder) { + me.inputEl.removeCls(me.emptyCls); + } + if (me.selectOnFocus || isEmpty) { + inputEl.dom.select(); + } + }, + + onFocus: function() { + var me = this; + me.callParent(arguments); + if (me.emptyText) { + me.autoSize(); + } + }, + + // private + postBlur : function(){ + this.applyEmptyText(); + }, + + // private + filterKeys : function(e){ + if(e.ctrlKey){ + return; + } + var key = e.getKey(), + charCode = String.fromCharCode(e.getCharCode()); + + if(Ext.isGecko && (e.isNavKeyPress() || key === e.BACKSPACE || (key === e.DELETE && e.button === -1))){ + return; + } + + if(!Ext.isGecko && e.isSpecialKey() && !charCode){ + return; + } + if(!this.maskRe.test(charCode)){ + e.stopEvent(); + } + }, + + /** + * Returns the raw String value of the field, without performing any normalization, conversion, or validation. + * Gets the current value of the input element if the field has been rendered, ignoring the value if it is the + * {@link #emptyText}. To get a normalized and converted value see {@link #getValue}. + * @return {String} value The raw String value of the field + */ + getRawValue: function() { + var me = this, + v = me.callParent(); + if (v === me.emptyText) { + v = ''; + } + return v; + }, + + /** + * Sets a data value into the field and runs the change detection and validation. Also applies any configured + * {@link #emptyText} for text fields. To set the value directly without these inspections see {@link #setRawValue}. + * @param {Mixed} value The value to set + * @return {Ext.form.field.Text} this + */ + setValue: function(value) { + var me = this, + inputEl = me.inputEl; + + if (inputEl && me.emptyText && !Ext.isEmpty(value)) { + inputEl.removeCls(me.emptyCls); + } + + me.callParent(arguments); + + me.applyEmptyText(); + return me; + }, + + /** +Validates a value according to the field's validation rules and returns an array of errors +for any failing validations. Validation rules are processed in the following order: + +1. **Field specific validator** + + A validator offers a way to customize and reuse a validation specification. + If a field is configured with a `{@link #validator}` + function, it will be passed the current field value. The `{@link #validator}` + function is expected to return either: + + - Boolean `true` if the value is valid (validation continues). + - a String to represent the invalid message if invalid (validation halts). + +2. **Basic Validation** + + If the `{@link #validator}` has not halted validation, + basic validation proceeds as follows: + + - `{@link #allowBlank}` : (Invalid message = `{@link #emptyText}`) + + Depending on the configuration of {@link #allowBlank}, a + blank field will cause validation to halt at this step and return + Boolean true or false accordingly. + + - `{@link #minLength}` : (Invalid message = `{@link #minLengthText}`) + + If the passed value does not satisfy the `{@link #minLength}` + specified, validation halts. + + - `{@link #maxLength}` : (Invalid message = `{@link #maxLengthText}`) + + If the passed value does not satisfy the `{@link #maxLength}` + specified, validation halts. + +3. **Preconfigured Validation Types (VTypes)** + + If none of the prior validation steps halts validation, a field + configured with a `{@link #vtype}` will utilize the + corresponding {@link Ext.form.field.VTypes VTypes} validation function. + If invalid, either the field's `{@link #vtypeText}` or + the VTypes vtype Text property will be used for the invalid message. + Keystrokes on the field will be filtered according to the VTypes + vtype Mask property. + +4. **Field specific regex test** + + If none of the prior validation steps halts validation, a field's + configured {@link #regex} test will be processed. + The invalid message for this test is configured with `{@link #regexText}` + + * @param {Mixed} value The value to validate. The processed raw value will be used if nothing is passed + * @return {Array} Array of any validation errors + * @markdown + */ + getErrors: function(value) { + var me = this, + errors = me.callParent(arguments), + validator = me.validator, + emptyText = me.emptyText, + allowBlank = me.allowBlank, + vtype = me.vtype, + vtypes = Ext.form.field.VTypes, + regex = me.regex, + format = Ext.String.format, + msg; + + value = value || me.processRawValue(me.getRawValue()); + + if (Ext.isFunction(validator)) { + msg = validator.call(me, value); + if (msg !== true) { + errors.push(msg); + } + } + + if (value.length < 1 || value === emptyText) { + if (!allowBlank) { + errors.push(me.blankText); + } + //if value is blank, there cannot be any additional errors + return errors; + } + + if (value.length < me.minLength) { + errors.push(format(me.minLengthText, me.minLength)); + } + + if (value.length > me.maxLength) { + errors.push(format(me.maxLengthText, me.maxLength)); + } + + if (vtype) { + if(!vtypes[vtype](value, me)){ + errors.push(me.vtypeText || vtypes[vtype +'Text']); + } + } + + if (regex && !regex.test(value)) { + errors.push(me.regexText || me.invalidText); + } + + return errors; + }, + + /** + * Selects text in this field + * @param {Number} start (optional) The index where the selection should start (defaults to 0) + * @param {Number} end (optional) The index where the selection should end (defaults to the text length) + */ + selectText : function(start, end){ + var me = this, + v = me.getRawValue(), + doFocus = true, + el = me.inputEl.dom, + undef, + range; + + if (v.length > 0) { + start = start === undef ? 0 : start; + end = end === undef ? v.length : end; + if (el.setSelectionRange) { + el.setSelectionRange(start, end); + } + else if(el.createTextRange) { + range = el.createTextRange(); + range.moveStart('character', start); + range.moveEnd('character', end - v.length); + range.select(); + } + doFocus = Ext.isGecko || Ext.isOpera; + } + if (doFocus) { + me.focus(); + } + }, + + /** + * Automatically grows the field to accomodate the width of the text up to the maximum field width allowed. + * This only takes effect if {@link #grow} = true, and fires the {@link #autosize} event if the + * width changes. + */ + autoSize: function() { + var me = this, + width; + if (me.grow && me.rendered) { + me.doComponentLayout(); + width = me.inputEl.getWidth(); + if (width !== me.lastInputWidth) { + me.fireEvent('autosize', width); + me.lastInputWidth = width; + } + } + }, + + initAria: function() { + this.callParent(); + this.getActionEl().dom.setAttribute('aria-required', this.allowBlank === false); + }, + + /** + * @protected override + * To get the natural width of the inputEl, we do a simple calculation based on the + * 'size' config. We use hard-coded numbers to approximate what browsers do natively, + * to avoid having to read any styles which would hurt performance. + */ + getBodyNaturalWidth: function() { + return Math.round(this.size * 6.5) + 20; + } + +}); + +/** + * @class Ext.form.field.TextArea + * @extends Ext.form.field.Text + +This class creates a multiline text field, which can be used as a direct replacement for traditional +textarea fields. In addition, it supports automatically {@link #grow growing} the height of the textarea to +fit its content. + +All of the configuration options from {@link Ext.form.field.Text} can be used on TextArea. +{@img Ext.form.TextArea/Ext.form.TextArea.png Ext.form.TextArea component} +Example usage: + + Ext.create('Ext.form.FormPanel', { + title : 'Sample TextArea', + width : 400, + bodyPadding: 10, + renderTo : Ext.getBody(), + items: [{ + xtype : 'textareafield', + grow : true, + name : 'message', + fieldLabel: 'Message', + anchor : '100%' + }] + }); + +Some other useful configuration options when using {@link #grow} are {@link #growMin} and {@link #growMax}. These +allow you to set the minimum and maximum grow heights for the textarea. + + * @constructor + * Creates a new TextArea + * @param {Object} config Configuration options + * @xtype textareafield + * @docauthor Robert Dougan + */ +Ext.define('Ext.form.field.TextArea', { + extend:'Ext.form.field.Text', + alias: ['widget.textareafield', 'widget.textarea'], + alternateClassName: 'Ext.form.TextArea', + requires: ['Ext.XTemplate', 'Ext.layout.component.field.TextArea'], + + fieldSubTpl: [ + '', + { + compiled: true, + disableFormats: true + } + ], + + /** + * @cfg {Number} growMin The minimum height to allow when {@link Ext.form.field.Text#grow grow}=true + * (defaults to 60) + */ + growMin: 60, + + /** + * @cfg {Number} growMax The maximum height to allow when {@link Ext.form.field.Text#grow grow}=true + * (defaults to 1000) + */ + growMax: 1000, + + /** + * @cfg {String} growAppend + * A string that will be appended to the field's current value for the purposes of calculating the target + * field size. Only used when the {@link #grow} config is true. Defaults to a newline for TextArea + * to ensure there is always a space below the current line. + */ + growAppend: '\n-', + + /** + * @cfg {Number} cols An initial value for the 'cols' attribute on the textarea element. This is only + * used if the component has no configured {@link #width} and is not given a width by its container's + * layout. Defaults to 20. + */ + cols: 20, + + /** + * @cfg {Number} cols An initial value for the 'cols' attribute on the textarea element. This is only + * used if the component has no configured {@link #width} and is not given a width by its container's + * layout. Defaults to 4. + */ + rows: 4, + + /** + * @cfg {Boolean} enterIsSpecial + * True if you want the enter key to be classed as a special key. Special keys are generally navigation + * keys (arrows, space, enter). Setting the config property to true would mean that you could not insert + * returns into the textarea. + * (defaults to false) + */ + enterIsSpecial: false, + + /** + * @cfg {Boolean} preventScrollbars true to prevent scrollbars from appearing regardless of how much text is + * in the field. This option is only relevant when {@link #grow} is true. Equivalent to setting overflow: hidden, defaults to + * false. + */ + preventScrollbars: false, + + // private + componentLayout: 'textareafield', + + // private + onRender: function(ct, position) { + var me = this; + Ext.applyIf(me.subTplData, { + cols: me.cols, + rows: me.rows + }); + + me.callParent(arguments); + }, + + // private + afterRender: function(){ + var me = this; + + me.callParent(arguments); + + if (me.grow) { + if (me.preventScrollbars) { + me.inputEl.setStyle('overflow', 'hidden'); + } + me.inputEl.setHeight(me.growMin); + } + }, + + // private + fireKey: function(e) { + if (e.isSpecialKey() && (this.enterIsSpecial || (e.getKey() !== e.ENTER || e.hasModifier()))) { + this.fireEvent('specialkey', this, e); + } + }, + + /** + * Automatically grows the field to accomodate the height of the text up to the maximum field height allowed. + * This only takes effect if {@link #grow} = true, and fires the {@link #autosize} event if + * the height changes. + */ + autoSize: function() { + var me = this, + height; + + if (me.grow && me.rendered) { + me.doComponentLayout(); + height = me.inputEl.getHeight(); + if (height !== me.lastInputHeight) { + me.fireEvent('autosize', height); + me.lastInputHeight = height; + } + } + }, + + // private + initAria: function() { + this.callParent(arguments); + this.getActionEl().dom.setAttribute('aria-multiline', true); + }, + + /** + * @protected override + * To get the natural width of the textarea element, we do a simple calculation based on the + * 'cols' config. We use hard-coded numbers to approximate what browsers do natively, + * to avoid having to read any styles which would hurt performance. + */ + getBodyNaturalWidth: function() { + return Math.round(this.cols * 6.5) + 20; + } + +}); + + +/** + * @class Ext.window.MessageBox + * @extends Ext.window.Window + +Utility class for generating different styles of message boxes. The singleton instance, `Ext.Msg` can also be used. +Note that a 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 `function` parameter for {@link #show} for more details). + +{@img Ext.window.MessageBox/messagebox1.png alert MessageBox} +{@img Ext.window.MessageBox/messagebox2.png prompt MessageBox} +{@img Ext.window.MessageBox/messagebox3.png show MessageBox} +#Example usage:# + + // Basic alert: + Ext.Msg.alert('Status', 'Changes saved successfully.'); + + // Prompt for user data and process the result using a callback: + Ext.Msg.prompt('Name', 'Please enter your name:', function(btn, text){ + if (btn == 'ok'){ + // process text value and close... + } + }); + + // Show a dialog using config options: + Ext.Msg.show({ + title:'Save Changes?', + msg: 'You are closing a tab that has unsaved changes. Would you like to save your changes?', + buttons: Ext.Msg.YESNOCANCEL, + fn: processResult, + animateTarget: 'elId', + icon: Ext.window.MessageBox.QUESTION + }); + + * @markdown + * @singleton + * @xtype messagebox + */ +Ext.define('Ext.window.MessageBox', { + extend: 'Ext.window.Window', + + requires: [ + 'Ext.toolbar.Toolbar', + 'Ext.form.field.Text', + 'Ext.form.field.TextArea', + 'Ext.button.Button', + 'Ext.layout.container.Anchor', + 'Ext.layout.container.HBox', + 'Ext.ProgressBar' + ], + + alternateClassName: 'Ext.MessageBox', + + alias: 'widget.messagebox', + + /** + * Button config that displays a single OK button + * @type Number + */ + OK : 1, + /** + * Button config that displays a single Yes button + * @type Number + */ + YES : 2, + /** + * Button config that displays a single No button + * @type Number + */ + NO : 4, + /** + * Button config that displays a single Cancel button + * @type Number + */ + CANCEL : 8, + /** + * Button config that displays OK and Cancel buttons + * @type Number + */ + OKCANCEL : 9, + /** + * Button config that displays Yes and No buttons + * @type Number + */ + YESNO : 6, + /** + * Button config that displays Yes, No and Cancel buttons + * @type Number + */ + YESNOCANCEL : 14, + /** + * The CSS class that provides the INFO icon image + * @type String + */ + INFO : 'ext-mb-info', + /** + * The CSS class that provides the WARNING icon image + * @type String + */ + WARNING : 'ext-mb-warning', + /** + * The CSS class that provides the QUESTION icon image + * @type String + */ + QUESTION : 'ext-mb-question', + /** + * The CSS class that provides the ERROR icon image + * @type String + */ + ERROR : 'ext-mb-error', + + // hide it by offsets. Windows are hidden on render by default. + hideMode: 'offsets', + closeAction: 'hide', + resizable: false, + title: ' ', + + width: 600, + height: 500, + minWidth: 250, + maxWidth: 600, + minHeight: 110, + maxHeight: 500, + constrain: true, + + cls: Ext.baseCSSPrefix + 'message-box', + + layout: { + type: 'anchor' + }, + + /** + * The default height in pixels of the message box's multiline textarea if displayed (defaults to 75) + * @type Number + */ + defaultTextHeight : 75, + /** + * The minimum width in pixels of the message box if it is a progress-style dialog. This is useful + * for setting a different minimum width than text-only dialogs may need (defaults to 250). + * @type Number + */ + minProgressWidth : 250, + /** + * The minimum width in pixels of the message box if it is a prompt dialog. This is useful + * for setting a different minimum width than text-only dialogs may need (defaults to 250). + * @type Number + */ + minPromptWidth: 250, + /** + * An object containing the default button text strings that can be overriden for localized language support. + * Supported properties are: ok, cancel, yes and no. Generally you should include a locale-specific + * resource file for handling language support across the framework. + * Customize the default text like so: Ext.window.MessageBox.buttonText.yes = "oui"; //french + * @type Object + */ + buttonText: { + ok: 'OK', + yes: 'Yes', + no: 'No', + cancel: 'Cancel' + }, + + buttonIds: [ + 'ok', 'yes', 'no', 'cancel' + ], + + titleText: { + confirm: 'Confirm', + prompt: 'Prompt', + wait: 'Loading...', + alert: 'Attention' + }, + + iconHeight: 35, + + makeButton: function(btnIdx) { + var btnId = this.buttonIds[btnIdx]; + return Ext.create('Ext.button.Button', { + handler: this.btnCallback, + itemId: btnId, + scope: this, + text: this.buttonText[btnId], + minWidth: 75 + }); + }, + + btnCallback: function(btn) { + var me = this, + value, + field; + + if (me.cfg.prompt || me.cfg.multiline) { + if (me.cfg.multiline) { + field = me.textArea; + } else { + field = me.textField; + } + value = field.getValue(); + field.reset(); + } + + // Important not to have focus remain in the hidden Window; Interferes with DnD. + btn.blur(); + me.hide(); + me.userCallback(btn.itemId, value, me.cfg); + }, + + hide: function() { + var me = this; + me.dd.endDrag(); + me.progressBar.reset(); + me.removeCls(me.cfg.cls); + me.callParent(); + }, + + initComponent: function() { + var me = this, + i, button; + + me.title = ' '; + + me.topContainer = Ext.create('Ext.container.Container', { + anchor: '100%', + style: { + padding: '10px', + overflow: 'hidden' + }, + items: [ + me.iconComponent = Ext.create('Ext.Component', { + cls: 'ext-mb-icon', + width: 50, + height: me.iconHeight, + style: { + 'float': 'left' + } + }), + me.promptContainer = Ext.create('Ext.container.Container', { + layout: { + type: 'anchor' + }, + items: [ + me.msg = Ext.create('Ext.Component', { + autoEl: { tag: 'span' }, + cls: 'ext-mb-text' + }), + me.textField = Ext.create('Ext.form.field.Text', { + anchor: '100%', + enableKeyEvents: true, + listeners: { + keydown: me.onPromptKey, + scope: me + } + }), + me.textArea = Ext.create('Ext.form.field.TextArea', { + anchor: '100%', + height: 75 + }) + ] + }) + ] + }); + me.progressBar = Ext.create('Ext.ProgressBar', { + anchor: '-10', + style: 'margin-left:10px' + }); + + me.items = [me.topContainer, me.progressBar]; + + // Create the buttons based upon passed bitwise config + me.msgButtons = []; + for (i = 0; i < 4; i++) { + button = me.makeButton(i); + me.msgButtons[button.itemId] = button; + me.msgButtons.push(button); + } + me.bottomTb = Ext.create('Ext.toolbar.Toolbar', { + ui: 'footer', + dock: 'bottom', + layout: { + pack: 'center' + }, + items: [ + me.msgButtons[0], + me.msgButtons[1], + me.msgButtons[2], + me.msgButtons[3] + ] + }); + me.dockedItems = [me.bottomTb]; + + me.callParent(); + }, + + onPromptKey: function(textField, e) { + var me = this, + blur; + + if (e.keyCode === Ext.EventObject.RETURN || e.keyCode === 10) { + if (me.msgButtons.ok.isVisible()) { + blur = true; + me.msgButtons.ok.handler.call(me, me.msgButtons.ok); + } else if (me.msgButtons.yes.isVisible()) { + me.msgButtons.yes.handler.call(me, me.msgButtons.yes); + blur = true; + } + + if (blur) { + me.textField.blur(); + } + } + }, + + reconfigure: function(cfg) { + var me = this, + buttons = cfg.buttons || 0, + hideToolbar = true, + initialWidth = me.maxWidth, + i; + + cfg = cfg || {}; + me.cfg = cfg; + if (cfg.width) { + initialWidth = cfg.width; + } + + // Default to allowing the Window to take focus. + delete me.defaultFocus; + + // clear any old animateTarget + me.animateTarget = cfg.animateTarget || undefined; + + // Defaults to modal + me.modal = cfg.modal !== false; + + // Show the title + if (cfg.title) { + me.setTitle(cfg.title||' '); + } + + if (!me.rendered) { + me.width = initialWidth; + me.render(Ext.getBody()); + } else { + me.setSize(initialWidth, me.maxHeight); + } + me.setPosition(-10000, -10000); + + // Hide or show the close tool + me.closable = cfg.closable && !cfg.wait; + if (cfg.closable === false) { + me.tools.close.hide(); + } else { + me.tools.close.show(); + } + + // Hide or show the header + if (!cfg.title && !me.closable) { + me.header.hide(); + } else { + me.header.show(); + } + + // Default to dynamic drag: drag the window, not a ghost + me.liveDrag = !cfg.proxyDrag; + + // wrap the user callback + me.userCallback = Ext.Function.bind(cfg.callback ||cfg.fn || Ext.emptyFn, cfg.scope || Ext.global); + + // Hide or show the icon Component + me.setIcon(cfg.icon); + + // Hide or show the message area + if (cfg.msg) { + me.msg.update(cfg.msg); + me.msg.show(); + } else { + me.msg.hide(); + } + + // Hide or show the input field + if (cfg.prompt || cfg.multiline) { + me.multiline = cfg.multiline; + if (cfg.multiline) { + me.textArea.setValue(cfg.value); + me.textArea.setHeight(cfg.defaultTextHeight || me.defaultTextHeight); + me.textArea.show(); + me.textField.hide(); + me.defaultFocus = me.textArea; + } else { + me.textField.setValue(cfg.value); + me.textArea.hide(); + me.textField.show(); + me.defaultFocus = me.textField; + } + } else { + me.textArea.hide(); + me.textField.hide(); + } + + // Hide or show the progress bar + if (cfg.progress || cfg.wait) { + me.progressBar.show(); + me.updateProgress(0, cfg.progressText); + if(cfg.wait === true){ + me.progressBar.wait(cfg.waitConfig); + } + } else { + me.progressBar.hide(); + } + + // Hide or show buttons depending on flag value sent. + for (i = 0; i < 4; i++) { + if (buttons & Math.pow(2, i)) { + + // Default to focus on the first visible button if focus not already set + if (!me.defaultFocus) { + me.defaultFocus = me.msgButtons[i]; + } + me.msgButtons[i].show(); + hideToolbar = false; + } else { + me.msgButtons[i].hide(); + } + } + + // Hide toolbar if no buttons to show + if (hideToolbar) { + me.bottomTb.hide(); + } else { + me.bottomTb.show(); + } + me.hidden = true; + }, + + /** + * Displays a new message box, or reinitializes an existing message box, based on the config options + * passed in. All display functions (e.g. prompt, alert, etc.) on MessageBox call this function internally, + * although those calls are basic shortcuts and do not support all of the config options allowed here. + * @param {Object} config The following config options are supported:
    + *
  • animateTarget : String/Element
    An id or Element from which the message box should animate as it + * opens and closes (defaults to undefined)
  • + *
  • buttons : Number
    A bitwise button specifier consisting of the sum of any of the following constants:
      + *
    • Ext.window.MessageBox.OK
    • + *
    • Ext.window.MessageBox.YES
    • + *
    • Ext.window.MessageBox.NO
    • + *
    • Ext.window.MessageBox.CANCEL
    • + *
    Or false to not show any buttons (defaults to false)
  • + *
  • closable : Boolean
    False to hide the top-right close button (defaults to true). Note that + * progress and wait dialogs will ignore this property and always hide the close button as they can only + * be closed programmatically.
  • + *
  • cls : String
    A custom CSS class to apply to the message box's container element
  • + *
  • defaultTextHeight : Number
    The default height in pixels of the message box's multiline textarea + * if displayed (defaults to 75)
  • + *
  • fn : Function
    A callback function which is called when the dialog is dismissed either + * by clicking on the configured buttons, or on the dialog close button, or by pressing + * the return button to enter input. + *

    Progress and wait dialogs will ignore this option since they do not respond to user + * actions and can only be closed programmatically, so any required function should be called + * by the same code after it closes the dialog. Parameters passed:

      + *
    • buttonId : String
      The ID of the button pressed, one of:
        + *
      • ok
      • + *
      • yes
      • + *
      • no
      • + *
      • cancel
      • + *
    • + *
    • text : String
      Value of the input field if either prompt + * or multiline is true
    • + *
    • opt : Object
      The config object passed to show.
    • + *

  • + *
  • scope : Object
    The scope (this reference) in which the function will be executed.
  • + *
  • icon : String
    A CSS class that provides a background image to be used as the body icon for the + * dialog (e.g. Ext.window.MessageBox.WARNING or 'custom-class') (defaults to '')
  • + *
  • iconCls : String
    The standard {@link Ext.window.Window#iconCls} to + * add an optional header icon (defaults to '')
  • + *
  • maxWidth : Number
    The maximum width in pixels of the message box (defaults to 600)
  • + *
  • minWidth : Number
    The minimum width in pixels of the message box (defaults to 100)
  • + *
  • modal : Boolean
    False to allow user interaction with the page while the message box is + * displayed (defaults to true)
  • + *
  • msg : String
    A string that will replace the existing message box body text (defaults to the + * XHTML-compliant non-breaking space character '&#160;')
  • + *
  • multiline : Boolean
    + * True to prompt the user to enter multi-line text (defaults to false)
  • + *
  • progress : Boolean
    True to display a progress bar (defaults to false)
  • + *
  • progressText : String
    The text to display inside the progress bar if progress = true (defaults to '')
  • + *
  • prompt : Boolean
    True to prompt the user to enter single-line text (defaults to false)
  • + *
  • proxyDrag : Boolean
    True to display a lightweight proxy while dragging (defaults to false)
  • + *
  • title : String
    The title text
  • + *
  • value : String
    The string value to set into the active textbox element if displayed
  • + *
  • wait : Boolean
    True to display a progress bar (defaults to false)
  • + *
  • waitConfig : Object
    A {@link Ext.ProgressBar#waitConfig} object (applies only if wait = true)
  • + *
  • width : Number
    The width of the dialog in pixels
  • + *
+ * Example usage: + *

+Ext.Msg.show({
+title: 'Address',
+msg: 'Please enter your address:',
+width: 300,
+buttons: Ext.window.MessageBox.OKCANCEL,
+multiline: true,
+fn: saveAddress,
+animateTarget: 'addAddressBtn',
+icon: Ext.window.MessageBox.INFO
+});
+
+ * @return {Ext.window.MessageBox} this + */ + show: function(cfg) { + var me = this; + + me.reconfigure(cfg); + me.addCls(cfg.cls); + if (cfg.animateTarget) { + me.doAutoSize(false); + me.callParent(); + } else { + me.callParent(); + me.doAutoSize(true); + } + return me; + }, + + afterShow: function(){ + if (this.animateTarget) { + this.center(); + } + this.callParent(arguments); + }, + + doAutoSize: function(center) { + var me = this, + icon = me.iconComponent, + iconHeight = me.iconHeight; + + if (!Ext.isDefined(me.frameWidth)) { + me.frameWidth = me.el.getWidth() - me.body.getWidth(); + } + + // reset to the original dimensions + icon.setHeight(iconHeight); + + // Allow per-invocation override of minWidth + me.minWidth = me.cfg.minWidth || Ext.getClass(this).prototype.minWidth; + + // Set best possible size based upon allowing the text to wrap in the maximized Window, and + // then constraining it to within the max with. Then adding up constituent element heights. + me.topContainer.doLayout(); + if (Ext.isIE6 || Ext.isIEQuirks) { + // In IE quirks, the initial full width of the prompt fields will prevent the container element + // from collapsing once sized down, so temporarily force them to a small width. They'll get + // layed out to their final width later when setting the final window size. + me.textField.setCalculatedSize(9); + me.textArea.setCalculatedSize(9); + } + var width = me.cfg.width || me.msg.getWidth() + icon.getWidth() + 25, /* topContainer's layout padding */ + height = (me.header.rendered ? me.header.getHeight() : 0) + + Math.max(me.promptContainer.getHeight(), icon.getHeight()) + + me.progressBar.getHeight() + + (me.bottomTb.rendered ? me.bottomTb.getHeight() : 0) + 20 ;/* topContainer's layout padding */ + + // Update to the size of the content, this way the text won't wrap under the icon. + icon.setHeight(Math.max(iconHeight, me.msg.getHeight())); + me.setSize(width + me.frameWidth, height + me.frameWidth); + if (center) { + me.center(); + } + return me; + }, + + updateText: function(text) { + this.msg.update(text); + return this.doAutoSize(true); + }, + + /** + * Adds the specified icon to the dialog. By default, the class 'ext-mb-icon' is applied for default + * styling, and the class passed in is expected to supply the background image url. Pass in empty string ('') + * to clear any existing icon. This method must be called before the MessageBox is shown. + * The following built-in icon classes are supported, but you can also pass in a custom class name: + *
+Ext.window.MessageBox.INFO
+Ext.window.MessageBox.WARNING
+Ext.window.MessageBox.QUESTION
+Ext.window.MessageBox.ERROR
+     *
+ * @param {String} icon A CSS classname specifying the icon's background image url, or empty string to clear the icon + * @return {Ext.window.MessageBox} this + */ + setIcon : function(icon) { + var me = this; + me.iconComponent.removeCls(me.iconCls); + if (icon) { + me.iconComponent.show(); + me.iconComponent.addCls(Ext.baseCSSPrefix + 'dlg-icon'); + me.iconComponent.addCls(me.iconCls = icon); + } else { + me.iconComponent.removeCls(Ext.baseCSSPrefix + 'dlg-icon'); + me.iconComponent.hide(); + } + return me; + }, + + /** + * Updates a progress-style message box's text and progress bar. Only relevant on message boxes + * initiated via {@link Ext.window.MessageBox#progress} or {@link Ext.window.MessageBox#wait}, + * or by calling {@link Ext.window.MessageBox#show} with progress: true. + * @param {Number} value Any number between 0 and 1 (e.g., .5, defaults to 0) + * @param {String} progressText The progress text to display inside the progress bar (defaults to '') + * @param {String} msg The message box's body text is replaced with the specified string (defaults to undefined + * so that any existing body text will not get overwritten by default unless a new value is passed in) + * @return {Ext.window.MessageBox} this + */ + updateProgress : function(value, progressText, msg){ + this.progressBar.updateProgress(value, progressText); + if (msg){ + this.updateText(msg); + } + return this; + }, + + onEsc: function() { + if (this.closable !== false) { + this.callParent(arguments); + } + }, + + /** + * 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} msg The message box body text + * @param {Function} fn (optional) The callback function invoked after the message box is closed + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. + * @return {Ext.window.MessageBox} this + */ + confirm: function(cfg, msg, fn, scope) { + if (Ext.isString(cfg)) { + cfg = { + title: cfg, + icon: 'ext-mb-question', + msg: msg, + buttons: this.YESNO, + callback: fn, + scope: scope + }; + } + return this.show(cfg); + }, + + /** + * 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. + * @param {String} title The title bar text + * @param {String} msg The message box body text + * @param {Function} fn (optional) The callback function invoked after the message box is closed + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. + * @param {Boolean/Number} multiline (optional) True to create a multiline textbox using the defaultTextHeight + * property, or the height in pixels to create the textbox (defaults to false / single-line) + * @param {String} value (optional) Default value of the text input element (defaults to '') + * @return {Ext.window.MessageBox} this + */ + prompt : function(cfg, msg, fn, scope, multiline, value){ + if (Ext.isString(cfg)) { + cfg = { + prompt: true, + title: cfg, + minWidth: this.minPromptWidth, + msg: msg, + buttons: this.OKCANCEL, + callback: fn, + scope: scope, + multiline: multiline, + value: value + }; + } + return this.show(cfg); + }, + + /** + * Displays a message box with an infinitely auto-updating progress bar. This can be used to block user + * interaction while waiting for a long-running process to complete that does not have defined intervals. + * You are responsible for closing the message box when the process is complete. + * @param {String} msg The message box body text + * @param {String} title (optional) The title bar text + * @param {Object} config (optional) A {@link Ext.ProgressBar#waitConfig} object + * @return {Ext.window.MessageBox} this + */ + wait : function(cfg, title, config){ + if (Ext.isString(cfg)) { + cfg = { + title : title, + msg : cfg, + closable: false, + wait: true, + modal: true, + minWidth: this.minProgressWidth, + waitConfig: config + }; + } + return this.show(cfg); + }, + + /** + * 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 + * 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} msg The message box body text + * @param {Function} fn (optional) The callback function invoked after the message box is closed + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. + * @return {Ext.window.MessageBox} this + */ + alert: function(cfg, msg, fn, scope) { + if (Ext.isString(cfg)) { + cfg = { + title : cfg, + msg : msg, + buttons: this.OK, + fn: fn, + scope : scope, + minWidth: this.minWidth + }; + } + return this.show(cfg); + }, + + /** + * Displays a message box with a progress bar. This message box has no buttons and is not closeable by + * the user. You are responsible for updating the progress bar as needed via {@link Ext.window.MessageBox#updateProgress} + * and closing the message box when the process is complete. + * @param {String} title The title bar text + * @param {String} msg The message box body text + * @param {String} progressText (optional) The text to display inside the progress bar (defaults to '') + * @return {Ext.window.MessageBox} this + */ + progress : function(cfg, msg, progressText){ + if (Ext.isString(cfg)) { + cfg = { + title: cfg, + msg: msg, + progressText: progressText + }; + } + return this.show(cfg); + } +}, function() { + Ext.MessageBox = Ext.Msg = new this(); +}); +/** + * @class Ext.form.Basic + * @extends Ext.util.Observable + +Provides input field management, validation, submission, and form loading services for the collection +of {@link Ext.form.field.Field Field} instances within a {@link Ext.container.Container}. It is recommended +that you use a {@link Ext.form.Panel} as the form container, as that has logic to automatically +hook up an instance of {@link Ext.form.Basic} (plus other conveniences related to field configuration.) + +#Form Actions# + +The Basic class delegates the handling of form loads and submits to instances of {@link Ext.form.action.Action}. +See the various Action implementations for specific details of each one's functionality, as well as the +documentation for {@link #doAction} which details the configuration options that can be specified in +each action call. + +The default submit Action is {@link Ext.form.action.Submit}, which uses an Ajax request to submit the +form's values to a configured URL. To enable normal browser submission of an Ext form, use the +{@link #standardSubmit} config option. + +Note: File uploads are not performed using normal 'Ajax' techniques; see the description for +{@link #hasUpload} for details. + +#Example usage:# + + Ext.create('Ext.form.Panel', { + title: 'Basic Form', + renderTo: Ext.getBody(), + bodyPadding: 5, + width: 350, + + // Any configuration items here will be automatically passed along to + // the Ext.form.Basic instance when it gets created. + + // The form will submit an AJAX request to this URL when submitted + url: 'save-form.php', + + items: [{ + fieldLabel: 'Field', + name: 'theField' + }], + + buttons: [{ + text: 'Submit', + handler: function() { + // The getForm() method returns the Ext.form.Basic instance: + var form = this.up('form').getForm(); + if (form.isValid()) { + // Submit the Ajax request and handle the response + form.submit({ + success: function(form, action) { + Ext.Msg.alert('Success', action.result.msg); + }, + failure: function(form, action) { + Ext.Msg.alert('Failed', action.result.msg); + } + }); + } + } + }] + }); + + * @constructor + * @param {Ext.container.Container} owner The component that is the container for the form, usually a {@link Ext.form.Panel} + * @param {Object} config Configuration options. These are normally specified in the config to the + * {@link Ext.form.Panel} constructor, which passes them along to the BasicForm automatically. + * + * @markdown + * @docauthor Jason Johnston + */ + + + +Ext.define('Ext.form.Basic', { + extend: 'Ext.util.Observable', + alternateClassName: 'Ext.form.BasicForm', + requires: ['Ext.util.MixedCollection', 'Ext.form.action.Load', 'Ext.form.action.Submit', + 'Ext.window.MessageBox', 'Ext.data.Errors'], + + constructor: function(owner, config) { + var me = this, + onItemAddOrRemove = me.onItemAddOrRemove; + + /** + * @property owner + * @type Ext.container.Container + * The container component to which this BasicForm is attached. + */ + me.owner = owner; + + // Listen for addition/removal of fields in the owner container + me.mon(owner, { + add: onItemAddOrRemove, + remove: onItemAddOrRemove, + scope: me + }); + + Ext.apply(me, config); + + // Normalize the paramOrder to an Array + if (Ext.isString(me.paramOrder)) { + me.paramOrder = me.paramOrder.split(/[\s,|]/); + } + + me.addEvents( + /** + * @event beforeaction + * Fires before any action is performed. Return false to cancel the action. + * @param {Ext.form.Basic} this + * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} to be performed + */ + 'beforeaction', + /** + * @event actionfailed + * Fires when an action fails. + * @param {Ext.form.Basic} this + * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that failed + */ + 'actionfailed', + /** + * @event actioncomplete + * Fires when an action is completed. + * @param {Ext.form.Basic} this + * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that completed + */ + 'actioncomplete', + /** + * @event validitychange + * Fires when the validity of the entire form changes. + * @param {Ext.form.Basic} this + * @param {Boolean} valid true if the form is now valid, false if it is now invalid. + */ + 'validitychange', + /** + * @event dirtychange + * Fires when the dirty state of the entire form changes. + * @param {Ext.form.Basic} this + * @param {Boolean} dirty true if the form is now dirty, false if it is no longer dirty. + */ + 'dirtychange' + ); + me.callParent(); + }, + + /** + * Do any post constructor initialization + * @private + */ + initialize: function(){ + this.initialized = true; + this.onValidityChange(!this.hasInvalidField()); + }, + + /** + * @cfg {String} method + * The request method to use (GET or POST) for form actions if one isn't supplied in the action options. + */ + /** + * @cfg {Ext.data.reader.Reader} reader + * An Ext.data.DataReader (e.g. {@link Ext.data.reader.Xml}) to be used to read + * data when executing 'load' actions. This is optional as there is built-in + * support for processing JSON responses. + */ + /** + * @cfg {Ext.data.reader.Reader} errorReader + *

An Ext.data.DataReader (e.g. {@link Ext.data.reader.Xml}) to be used to + * read field error messages returned from 'submit' actions. This is optional + * as there is built-in support for processing JSON responses.

+ *

The Records which provide messages for the invalid Fields must use the + * Field name (or id) as the Record ID, and must contain a field called 'msg' + * which contains the error message.

+ *

The errorReader does not have to be a full-blown implementation of a + * Reader. It simply needs to implement a read(xhr) function + * which returns an Array of Records in an object with the following + * structure:


+{
+    records: recordArray
+}
+
+ */ + + /** + * @cfg {String} url + * The URL to use for form actions if one isn't supplied in the + * {@link #doAction doAction} options. + */ + + /** + * @cfg {Object} baseParams + *

Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.

+ *

Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode Ext.Object.toQueryString}.

+ */ + + /** + * @cfg {Number} timeout Timeout for form actions in seconds (default is 30 seconds). + */ + timeout: 30, + + /** + * @cfg {Object} api (Optional) If specified, load and submit actions will be handled + * with {@link Ext.form.action.DirectLoad} and {@link Ext.form.action.DirectLoad}. + * Methods which have been imported by {@link Ext.direct.Manager} can be specified here to load and submit + * forms. + * Such as the following:

+api: {
+    load: App.ss.MyProfile.load,
+    submit: App.ss.MyProfile.submit
+}
+
+ *

Load actions can use {@link #paramOrder} or {@link #paramsAsHash} + * to customize how the load method is invoked. + * Submit actions will always use a standard form submit. The formHandler configuration must + * be set on the associated server-side method which has been imported by {@link Ext.direct.Manager}.

+ */ + + /** + * @cfg {Array/String} paramOrder

A list of params to be executed server side. + * Defaults to undefined. Only used for the {@link #api} + * load configuration.

+ *

Specify the params in the order in which they must be executed on the + * server-side as either (1) an Array of String values, or (2) a String of params + * delimited by either whitespace, comma, or pipe. For example, + * any of the following would be acceptable:


+paramOrder: ['param1','param2','param3']
+paramOrder: 'param1 param2 param3'
+paramOrder: 'param1,param2,param3'
+paramOrder: 'param1|param2|param'
+     
+ */ + + /** + * @cfg {Boolean} paramsAsHash Only used for the {@link #api} + * load configuration. If true, parameters will be sent as a + * single hash collection of named arguments (defaults to false). Providing a + * {@link #paramOrder} nullifies this configuration. + */ + paramsAsHash: false, + + /** + * @cfg {String} waitTitle + * The default title to show for the waiting message box (defaults to 'Please Wait...') + */ + waitTitle: 'Please Wait...', + + /** + * @cfg {Boolean} trackResetOnLoad If set to true, {@link #reset}() resets to the last loaded + * or {@link #setValues}() data instead of when the form was first created. Defaults to false. + */ + trackResetOnLoad: false, + + /** + * @cfg {Boolean} standardSubmit + *

If set to true, a standard HTML form submit is used instead + * of a XHR (Ajax) style form submission. Defaults to false. All of + * the field values, plus any additional params configured via {@link #baseParams} + * and/or the options to {@link #submit}, will be included in the + * values submitted in the form.

+ */ + + /** + * @cfg {Mixed} waitMsgTarget + * By default wait messages are displayed with Ext.MessageBox.wait. You can target a specific + * element by passing it or its id or mask the form itself by passing in true. Defaults to undefined. + */ + + + // Private + wasDirty: false, + + + /** + * Destroys this object. + */ + destroy: function() { + this.clearListeners(); + }, + + /** + * @private + * Handle addition or removal of descendant items. Invalidates the cached list of fields + * so that {@link #getFields} will do a fresh query next time it is called. Also adds listeners + * for state change events on added fields, and tracks components with formBind=true. + */ + onItemAddOrRemove: function(parent, child) { + var me = this, + isAdding = !!child.ownerCt, + isContainer = child.isContainer; + + function handleField(field) { + // Listen for state change events on fields + me[isAdding ? 'mon' : 'mun'](field, { + validitychange: me.checkValidity, + dirtychange: me.checkDirty, + scope: me, + buffer: 100 //batch up sequential calls to avoid excessive full-form validation + }); + // Flush the cached list of fields + delete me._fields; + } + + if (child.isFormField) { + handleField(child); + } + else if (isContainer) { + // Walk down + Ext.Array.forEach(child.query('[isFormField]'), handleField); + } + + // Flush the cached list of formBind components + delete this._boundItems; + + // Check form bind, but only after initial add + if (me.initialized) { + me.onValidityChange(!me.hasInvalidField()); + } + }, + + /** + * Return all the {@link Ext.form.field.Field} components in the owner container. + * @return {Ext.util.MixedCollection} Collection of the Field objects + */ + getFields: function() { + var fields = this._fields; + if (!fields) { + fields = this._fields = Ext.create('Ext.util.MixedCollection'); + fields.addAll(this.owner.query('[isFormField]')); + } + return fields; + }, + + getBoundItems: function() { + var boundItems = this._boundItems; + if (!boundItems) { + boundItems = this._boundItems = Ext.create('Ext.util.MixedCollection'); + boundItems.addAll(this.owner.query('[formBind]')); + } + return boundItems; + }, + + /** + * Returns true if the form contains any invalid fields. No fields will be marked as invalid + * as a result of calling this; to trigger marking of fields use {@link #isValid} instead. + */ + hasInvalidField: function() { + return !!this.getFields().findBy(function(field) { + var preventMark = field.preventMark, + isValid; + field.preventMark = true; + isValid = field.isValid(); + field.preventMark = preventMark; + return !isValid; + }); + }, + + /** + * Returns true if client-side validation on the form is successful. Any invalid fields will be + * marked as invalid. If you only want to determine overall form validity without marking anything, + * use {@link #hasInvalidField} instead. + * @return Boolean + */ + isValid: function() { + var me = this, + invalid; + me.batchLayouts(function() { + invalid = me.getFields().filterBy(function(field) { + return !field.validate(); + }); + }); + return invalid.length < 1; + }, + + /** + * Check whether the validity of the entire form has changed since it was last checked, and + * if so fire the {@link #validitychange validitychange} event. This is automatically invoked + * when an individual field's validity changes. + */ + checkValidity: function() { + var me = this, + valid = !me.hasInvalidField(); + if (valid !== me.wasValid) { + me.onValidityChange(valid); + me.fireEvent('validitychange', me, valid); + me.wasValid = valid; + } + }, + + /** + * @private + * Handle changes in the form's validity. If there are any sub components with + * formBind=true then they are enabled/disabled based on the new validity. + * @param {Boolean} valid + */ + onValidityChange: function(valid) { + var boundItems = this.getBoundItems(); + if (boundItems) { + boundItems.each(function(cmp) { + if (cmp.disabled === valid) { + cmp.setDisabled(!valid); + } + }); + } + }, + + /** + *

Returns true if any fields in this form have changed from their original values.

+ *

Note that if this BasicForm was configured with {@link #trackResetOnLoad} then the + * Fields' original values are updated when the values are loaded by {@link #setValues} + * or {@link #loadRecord}.

+ * @return Boolean + */ + isDirty: function() { + return !!this.getFields().findBy(function(f) { + return f.isDirty(); + }); + }, + + /** + * Check whether the dirty state of the entire form has changed since it was last checked, and + * if so fire the {@link #dirtychange dirtychange} event. This is automatically invoked + * when an individual field's dirty state changes. + */ + checkDirty: function() { + var dirty = this.isDirty(); + if (dirty !== this.wasDirty) { + this.fireEvent('dirtychange', this, dirty); + this.wasDirty = dirty; + } + }, + + /** + *

Returns true if the form contains a file upload field. This is used to determine the + * method for submitting the form: File uploads are not performed using normal 'Ajax' techniques, + * that is they are not performed using XMLHttpRequests. Instead a hidden <form> + * element containing all the fields is created temporarily and submitted with its + * target set to refer + * to a dynamically generated, hidden <iframe> which is inserted into the document + * but removed after the return data has been gathered.

+ *

The server response is parsed by the browser to create the document for the IFRAME. If the + * server is using JSON to send the return object, then the + * Content-Type header + * must be set to "text/html" in order to tell the browser to insert the text unchanged into the document body.

+ *

Characters which are significant to an HTML parser must be sent as HTML entities, so encode + * "<" as "&lt;", "&" as "&amp;" etc.

+ *

The response text is retrieved from the document, and a fake XMLHttpRequest object + * is created containing a responseText property in order to conform to the + * requirements of event handlers and callbacks.

+ *

Be aware that file upload packets are sent with the content type multipart/form + * and some server technologies (notably JEE) may require some custom processing in order to + * retrieve parameter names and parameter values from the packet content.

+ * @return Boolean + */ + hasUpload: function() { + return !!this.getFields().findBy(function(f) { + return f.isFileUpload(); + }); + }, + + /** + * Performs a predefined action (an implementation of {@link Ext.form.action.Action}) + * to perform application-specific processing. + * @param {String/Ext.form.action.Action} action The name of the predefined action type, + * or instance of {@link Ext.form.action.Action} to perform. + * @param {Object} options (optional) The options to pass to the {@link Ext.form.action.Action} + * that will get created, if the action argument is a String. + *

All of the config options listed below are supported by both the + * {@link Ext.form.action.Submit submit} and {@link Ext.form.action.Load load} + * actions unless otherwise noted (custom actions could also accept + * other config options):

    + * + *
  • url : String
    The url for the action (defaults + * to the form's {@link #url}.)
  • + * + *
  • method : String
    The form method to use (defaults + * to the form's method, or POST if not defined)
  • + * + *
  • params : String/Object

    The params to pass + * (defaults to the form's baseParams, or none if not defined)

    + *

    Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode Ext.Object.toQueryString}.

  • + * + *
  • headers : Object
    Request headers to set for the action.
  • + * + *
  • success : Function
    The callback that will + * be invoked after a successful response (see top of + * {@link Ext.form.action.Submit submit} and {@link Ext.form.action.Load load} + * for a description of what constitutes a successful response). + * The function is passed the following parameters:
      + *
    • form : The {@link Ext.form.Basic} that requested the action.
    • + *
    • action : The {@link Ext.form.action.Action Action} object which performed the operation. + *
      The action object contains these properties of interest:
        + *
      • {@link Ext.form.action.Action#response response}
      • + *
      • {@link Ext.form.action.Action#result result} : interrogate for custom postprocessing
      • + *
      • {@link Ext.form.action.Action#type type}
      • + *
  • + * + *
  • failure : Function
    The callback that will be invoked after a + * failed transaction attempt. The function is passed the following parameters:
      + *
    • form : The {@link Ext.form.Basic} that requested the action.
    • + *
    • action : The {@link Ext.form.action.Action Action} object which performed the operation. + *
      The action object contains these properties of interest:
        + *
      • {@link Ext.form.action.Action#failureType failureType}
      • + *
      • {@link Ext.form.action.Action#response response}
      • + *
      • {@link Ext.form.action.Action#result result} : interrogate for custom postprocessing
      • + *
      • {@link Ext.form.action.Action#type type}
      • + *
  • + * + *
  • scope : Object
    The scope in which to call the + * callback functions (The this reference for the callback functions).
  • + * + *
  • clientValidation : Boolean
    Submit Action only. + * Determines whether a Form's fields are validated in a final call to + * {@link Ext.form.Basic#isValid isValid} prior to submission. Set to false + * to prevent this. If undefined, pre-submission field validation is performed.
+ * + * @return {Ext.form.Basic} this + */ + doAction: function(action, options) { + if (Ext.isString(action)) { + action = Ext.ClassManager.instantiateByAlias('formaction.' + action, Ext.apply({}, options, {form: this})); + } + if (this.fireEvent('beforeaction', this, action) !== false) { + this.beforeAction(action); + Ext.defer(action.run, 100, action); + } + return this; + }, + + /** + * Shortcut to {@link #doAction do} a {@link Ext.form.action.Submit submit action}. This will use the + * {@link Ext.form.action.Submit AJAX submit action} by default. If the {@link #standardsubmit} config is + * enabled it will use a standard form element to submit, or if the {@link #api} config is present it will + * use the {@link Ext.form.action.DirectLoad Ext.direct.Direct submit action}. + * @param {Object} options The options to pass to the action (see {@link #doAction} for details).
+ *

The following code:


+myFormPanel.getForm().submit({
+    clientValidation: true,
+    url: 'updateConsignment.php',
+    params: {
+        newStatus: 'delivered'
+    },
+    success: function(form, action) {
+       Ext.Msg.alert('Success', action.result.msg);
+    },
+    failure: function(form, action) {
+        switch (action.failureType) {
+            case Ext.form.action.Action.CLIENT_INVALID:
+                Ext.Msg.alert('Failure', 'Form fields may not be submitted with invalid values');
+                break;
+            case Ext.form.action.Action.CONNECT_FAILURE:
+                Ext.Msg.alert('Failure', 'Ajax communication failed');
+                break;
+            case Ext.form.action.Action.SERVER_INVALID:
+               Ext.Msg.alert('Failure', action.result.msg);
+       }
+    }
+});
+
+ * would process the following server response for a successful submission:

+{
+    "success":true, // note this is Boolean, not string
+    "msg":"Consignment updated"
+}
+
+ * and the following server response for a failed submission:

+{
+    "success":false, // note this is Boolean, not string
+    "msg":"You do not have permission to perform this operation"
+}
+
+ * @return {Ext.form.Basic} this + */ + submit: function(options) { + return this.doAction(this.standardSubmit ? 'standardsubmit' : this.api ? 'directsubmit' : 'submit', options); + }, + + /** + * Shortcut to {@link #doAction do} a {@link Ext.form.action.Load load action}. + * @param {Object} options The options to pass to the action (see {@link #doAction} for details) + * @return {Ext.form.Basic} this + */ + load: function(options) { + return this.doAction(this.api ? 'directload' : 'load', options); + }, + + /** + * Persists the values in this form into the passed {@link Ext.data.Model} object in a beginEdit/endEdit block. + * @param {Ext.data.Record} record The record to edit + * @return {Ext.form.Basic} this + */ + updateRecord: function(record) { + var fields = record.fields, + values = this.getFieldValues(), + name, + obj = {}; + + fields.each(function(f) { + name = f.name; + if (name in values) { + obj[name] = values[name]; + } + }); + + record.beginEdit(); + record.set(obj); + record.endEdit(); + + return this; + }, + + /** + * Loads an {@link Ext.data.Model} into this form by calling {@link #setValues} with the + * {@link Ext.data.Model#data record data}. + * See also {@link #trackResetOnLoad}. + * @param {Ext.data.Model} record The record to load + * @return {Ext.form.Basic} this + */ + loadRecord: function(record) { + this._record = record; + return this.setValues(record.data); + }, + + /** + * Returns the last Ext.data.Model instance that was loaded via {@link #loadRecord} + * @return {Ext.data.Model} The record + */ + getRecord: function() { + return this._record; + }, + + /** + * @private + * Called before an action is performed via {@link #doAction}. + * @param {Ext.form.action.Action} action The Action instance that was invoked + */ + beforeAction: function(action) { + var waitMsg = action.waitMsg, + maskCls = Ext.baseCSSPrefix + 'mask-loading', + waitMsgTarget; + + // Call HtmlEditor's syncValue before actions + this.getFields().each(function(f) { + if (f.isFormField && f.syncValue) { + f.syncValue(); + } + }); + + if (waitMsg) { + waitMsgTarget = this.waitMsgTarget; + if (waitMsgTarget === true) { + this.owner.el.mask(waitMsg, maskCls); + } else if (waitMsgTarget) { + waitMsgTarget = this.waitMsgTarget = Ext.get(waitMsgTarget); + waitMsgTarget.mask(waitMsg, maskCls); + } else { + Ext.MessageBox.wait(waitMsg, action.waitTitle || this.waitTitle); + } + } + }, + + /** + * @private + * Called after an action is performed via {@link #doAction}. + * @param {Ext.form.action.Action} action The Action instance that was invoked + * @param {Boolean} success True if the action completed successfully, false, otherwise. + */ + afterAction: function(action, success) { + if (action.waitMsg) { + var MessageBox = Ext.MessageBox, + waitMsgTarget = this.waitMsgTarget; + if (waitMsgTarget === true) { + this.owner.el.unmask(); + } else if (waitMsgTarget) { + waitMsgTarget.unmask(); + } else { + MessageBox.updateProgress(1); + MessageBox.hide(); + } + } + if (success) { + if (action.reset) { + this.reset(); + } + Ext.callback(action.success, action.scope || action, [this, action]); + this.fireEvent('actioncomplete', this, action); + } else { + Ext.callback(action.failure, action.scope || action, [this, action]); + this.fireEvent('actionfailed', this, action); + } + }, + + + /** + * Find a specific {@link Ext.form.field.Field} in this form by id or name. + * @param {String} id The value to search for (specify either a {@link Ext.Component#id id} or + * {@link Ext.form.field.Field#getName name or hiddenName}). + * @return Ext.form.field.Field The first matching field, or null if none was found. + */ + findField: function(id) { + return this.getFields().findBy(function(f) { + return f.id === id || f.getName() === id; + }); + }, + + + /** + * Mark fields in this form invalid in bulk. + * @param {Array/Object} errors Either an array in the form [{id:'fieldId', msg:'The message'}, ...], + * an object hash of {id: msg, id2: msg2}, or a {@link Ext.data.Errors} object. + * @return {Ext.form.Basic} this + */ + markInvalid: function(errors) { + var me = this; + + function mark(fieldId, msg) { + var field = me.findField(fieldId); + if (field) { + field.markInvalid(msg); + } + } + + if (Ext.isArray(errors)) { + Ext.each(errors, function(err) { + mark(err.id, err.msg); + }); + } + else if (errors instanceof Ext.data.Errors) { + errors.each(function(err) { + mark(err.field, err.message); + }); + } + else { + Ext.iterate(errors, mark); + } + return this; + }, + + /** + * Set values for fields in this form in bulk. + * @param {Array/Object} values Either an array in the form:

+[{id:'clientName', value:'Fred. Olsen Lines'},
+ {id:'portOfLoading', value:'FXT'},
+ {id:'portOfDischarge', value:'OSL'} ]
+ * or an object hash of the form:

+{
+    clientName: 'Fred. Olsen Lines',
+    portOfLoading: 'FXT',
+    portOfDischarge: 'OSL'
+}
+ * @return {Ext.form.Basic} this + */ + setValues: function(values) { + var me = this; + + function setVal(fieldId, val) { + var field = me.findField(fieldId); + if (field) { + field.setValue(val); + if (me.trackResetOnLoad) { + field.resetOriginalValue(); + } + } + } + + if (Ext.isArray(values)) { + // array of objects + Ext.each(values, function(val) { + setVal(val.id, val.value); + }); + } else { + // object hash + Ext.iterate(values, setVal); + } + return this; + }, + + /** + * Retrieves the fields in the form as a set of key/value pairs, using their + * {@link Ext.form.field.Field#getSubmitData getSubmitData()} method to collect the values. + * If multiple fields return values under the same name those values will be combined into an Array. + * This is similar to {@link #getFieldValues} except that this method collects only String values for + * submission, while getFieldValues collects type-specific data values (e.g. Date objects for date fields.) + * @param {Boolean} asString (optional) If true, will return the key/value collection as a single + * URL-encoded param string. Defaults to false. + * @param {Boolean} dirtyOnly (optional) If true, only fields that are dirty will be included in the result. + * Defaults to false. + * @param {Boolean} includeEmptyText (optional) If true, the configured emptyText of empty fields will be used. + * Defaults to false. + * @return {String/Object} + */ + getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues) { + var values = {}; + + this.getFields().each(function(field) { + if (!dirtyOnly || field.isDirty()) { + var data = field[useDataValues ? 'getModelData' : 'getSubmitData'](includeEmptyText); + if (Ext.isObject(data)) { + Ext.iterate(data, function(name, val) { + if (includeEmptyText && val === '') { + val = field.emptyText || ''; + } + if (name in values) { + var bucket = values[name], + isArray = Ext.isArray; + if (!isArray(bucket)) { + bucket = values[name] = [bucket]; + } + if (isArray(val)) { + values[name] = bucket.concat(val); + } else { + bucket.push(val); + } + } else { + values[name] = val; + } + }); + } + } + }); + + if (asString) { + values = Ext.Object.toQueryString(values); + } + return values; + }, + + /** + * Retrieves the fields in the form as a set of key/value pairs, using their + * {@link Ext.form.field.Field#getModelData getModelData()} method to collect the values. + * If multiple fields return values under the same name those values will be combined into an Array. + * This is similar to {@link #getValues} except that this method collects type-specific data values + * (e.g. Date objects for date fields) while getValues returns only String values for submission. + * @param {Boolean} dirtyOnly (optional) If true, only fields that are dirty will be included in the result. + * Defaults to false. + * @return {Object} + */ + getFieldValues: function(dirtyOnly) { + return this.getValues(false, dirtyOnly, false, true); + }, + + /** + * Clears all invalid field messages in this form. + * @return {Ext.form.Basic} this + */ + clearInvalid: function() { + var me = this; + me.batchLayouts(function() { + me.getFields().each(function(f) { + f.clearInvalid(); + }); + }); + return me; + }, + + /** + * Resets all fields in this form. + * @return {Ext.form.Basic} this + */ + reset: function() { + var me = this; + me.batchLayouts(function() { + me.getFields().each(function(f) { + f.reset(); + }); + }); + return me; + }, + + /** + * Calls {@link Ext#apply Ext.apply} for all fields in this form with the passed object. + * @param {Object} obj The object to be applied + * @return {Ext.form.Basic} this + */ + applyToFields: function(obj) { + this.getFields().each(function(f) { + Ext.apply(f, obj); + }); + return this; + }, + + /** + * Calls {@link Ext#applyIf Ext.applyIf} for all field in this form with the passed object. + * @param {Object} obj The object to be applied + * @return {Ext.form.Basic} this + */ + applyIfToFields: function(obj) { + this.getFields().each(function(f) { + Ext.applyIf(f, obj); + }); + return this; + }, + + /** + * @private + * Utility wrapper that suspends layouts of all field parent containers for the duration of a given + * function. Used during full-form validation and resets to prevent huge numbers of layouts. + * @param {Function} fn + */ + batchLayouts: function(fn) { + var me = this, + suspended = new Ext.util.HashMap(); + + // Temporarily suspend layout on each field's immediate owner so we don't get a huge layout cascade + me.getFields().each(function(field) { + var ownerCt = field.ownerCt; + if (!suspended.contains(ownerCt)) { + suspended.add(ownerCt); + ownerCt.oldSuspendLayout = ownerCt.suspendLayout; + ownerCt.suspendLayout = true; + } + }); + + // Invoke the function + fn(); + + // Un-suspend the container layouts + suspended.each(function(id, ct) { + ct.suspendLayout = ct.oldSuspendLayout; + delete ct.oldSuspendLayout; + }); + + // Trigger a single layout + me.owner.doComponentLayout(); + } +}); + +/** + * @class Ext.form.FieldAncestor + +A mixin for {@link Ext.container.Container} components that are likely to have form fields in their +items subtree. Adds the following capabilities: + +- Methods for handling the addition and removal of {@link Ext.form.Labelable} and {@link Ext.form.field.Field} + instances at any depth within the container. +- Events ({@link #fieldvaliditychange} and {@link #fielderrorchange}) for handling changes to the state + of individual fields at the container level. +- Automatic application of {@link #fieldDefaults} config properties to each field added within the + container, to facilitate uniform configuration of all fields. + +This mixin is primarily for internal use by {@link Ext.form.Panel} and {@link Ext.form.FieldContainer}, +and should not normally need to be used directly. + + * @markdown + * @docauthor Jason Johnston + */ +Ext.define('Ext.form.FieldAncestor', { + + /** + * @cfg {Object} fieldDefaults + *

If specified, the properties in this object are used as default config values for each + * {@link Ext.form.Labelable} instance (e.g. {@link Ext.form.field.Base} or {@link Ext.form.FieldContainer}) + * that is added as a descendant of this container. Corresponding values specified in an individual field's + * own configuration, or from the {@link Ext.container.Container#defaults defaults config} of its parent container, + * will take precedence. See the documentation for {@link Ext.form.Labelable} to see what config + * options may be specified in the fieldDefaults.

+ *

Example:

+ *
new Ext.form.Panel({
+    fieldDefaults: {
+        labelAlign: 'left',
+        labelWidth: 100
+    },
+    items: [{
+        xtype: 'fieldset',
+        defaults: {
+            labelAlign: 'top'
+        },
+        items: [{
+            name: 'field1'
+        }, {
+            name: 'field2'
+        }]
+    }, {
+        xtype: 'fieldset',
+        items: [{
+            name: 'field3',
+            labelWidth: 150
+        }, {
+            name: 'field4'
+        }]
+    }]
+});
+ *

In this example, field1 and field2 will get labelAlign:'top' (from the fieldset's defaults) + * and labelWidth:100 (from fieldDefaults), field3 and field4 will both get labelAlign:'left' (from + * fieldDefaults and field3 will use the labelWidth:150 from its own config.

+ */ + + + /** + * @protected Initializes the FieldAncestor's state; this must be called from the initComponent method + * of any components importing this mixin. + */ + initFieldAncestor: function() { + var me = this, + onSubtreeChange = me.onFieldAncestorSubtreeChange; + + me.addEvents( + /** + * @event fielderrorchange + * Fires when the validity state of any one of the {@link Ext.form.field.Field} instances within this + * container changes. + * @param {Ext.form.FieldAncestor} this + * @param {Ext.form.Labelable} The Field instance whose validity changed + * @param {String} isValid The field's new validity state + */ + 'fieldvaliditychange', + + /** + * @event fielderrorchange + * Fires when the active error message is changed for any one of the {@link Ext.form.Labelable} + * instances within this container. + * @param {Ext.form.FieldAncestor} this + * @param {Ext.form.Labelable} The Labelable instance whose active error was changed + * @param {String} error The active error message + */ + 'fielderrorchange' + ); + + // Catch addition and removal of descendant fields + me.on('add', onSubtreeChange, me); + me.on('remove', onSubtreeChange, me); + + me.initFieldDefaults(); + }, + + /** + * @private Initialize the {@link #fieldDefaults} object + */ + initFieldDefaults: function() { + if (!this.fieldDefaults) { + this.fieldDefaults = {}; + } + }, + + /** + * @private + * Handle the addition and removal of components in the FieldAncestor component's child tree. + */ + onFieldAncestorSubtreeChange: function(parent, child) { + var me = this, + isAdding = !!child.ownerCt; + + function handleCmp(cmp) { + var isLabelable = cmp.isFieldLabelable, + isField = cmp.isFormField; + if (isLabelable || isField) { + if (isLabelable) { + me['onLabelable' + (isAdding ? 'Added' : 'Removed')](cmp); + } + if (isField) { + me['onField' + (isAdding ? 'Added' : 'Removed')](cmp); + } + } + else if (cmp.isContainer) { + Ext.Array.forEach(cmp.getRefItems(), handleCmp); + } + } + handleCmp(child); + }, + + /** + * @protected Called when a {@link Ext.form.Labelable} instance is added to the container's subtree. + * @param {Ext.form.Labelable} labelable The instance that was added + */ + onLabelableAdded: function(labelable) { + var me = this; + + // buffer slightly to avoid excessive firing while sub-fields are changing en masse + me.mon(labelable, 'errorchange', me.handleFieldErrorChange, me, {buffer: 10}); + + labelable.setFieldDefaults(me.fieldDefaults); + }, + + /** + * @protected Called when a {@link Ext.form.field.Field} instance is added to the container's subtree. + * @param {Ext.form.field.Field} field The field which was added + */ + onFieldAdded: function(field) { + var me = this; + me.mon(field, 'validitychange', me.handleFieldValidityChange, me); + }, + + /** + * @protected Called when a {@link Ext.form.Labelable} instance is removed from the container's subtree. + * @param {Ext.form.Labelable} labelable The instance that was removed + */ + onLabelableRemoved: function(labelable) { + var me = this; + me.mun(labelable, 'errorchange', me.handleFieldErrorChange, me); + }, + + /** + * @protected Called when a {@link Ext.form.field.Field} instance is removed from the container's subtree. + * @param {Ext.form.field.Field} field The field which was removed + */ + onFieldRemoved: function(field) { + var me = this; + me.mun(field, 'validitychange', me.handleFieldValidityChange, me); + }, + + /** + * @private Handle validitychange events on sub-fields; invoke the aggregated event and method + */ + handleFieldValidityChange: function(field, isValid) { + var me = this; + me.fireEvent('fieldvaliditychange', me, field, isValid); + me.onFieldValidityChange(); + }, + + /** + * @private Handle errorchange events on sub-fields; invoke the aggregated event and method + */ + handleFieldErrorChange: function(labelable, activeError) { + var me = this; + me.fireEvent('fielderrorchange', me, labelable, activeError); + me.onFieldErrorChange(); + }, + + /** + * @protected Fired when the validity of any field within the container changes. + * @param {Ext.form.field.Field} The sub-field whose validity changed + * @param {String} The new validity state + */ + onFieldValidityChange: Ext.emptyFn, + + /** + * @protected Fired when the error message of any field within the container changes. + * @param {Ext.form.Labelable} The sub-field whose active error changed + * @param {String} The new active error message + */ + onFieldErrorChange: Ext.emptyFn + +}); +/** + * @class Ext.layout.container.CheckboxGroup + * @extends Ext.layout.container.Container + *

This layout implements the column arrangement for {@link Ext.form.CheckboxGroup} and {@link Ext.form.RadioGroup}. + * It groups the component's sub-items into columns based on the component's + * {@link Ext.form.CheckboxGroup#columns columns} and {@link Ext.form.CheckboxGroup#vertical} config properties.

+ * + */ +Ext.define('Ext.layout.container.CheckboxGroup', { + extend: 'Ext.layout.container.Container', + alias: ['layout.checkboxgroup'], + + + onLayout: function() { + var numCols = this.getColCount(), + shadowCt = this.getShadowCt(), + owner = this.owner, + items = owner.items, + shadowItems = shadowCt.items, + numItems = items.length, + colIndex = 0, + i, numRows; + + // Distribute the items into the appropriate column containers. We add directly to the + // containers' items collection rather than calling container.add(), because we need the + // checkboxes to maintain their original ownerCt. The distribution is done on each layout + // in case items have been added, removed, or reordered. + + shadowItems.each(function(col) { + col.items.clear(); + }); + + // If columns="auto", then the number of required columns may change as checkboxes are added/removed + // from the CheckboxGroup; adjust to match. + while (shadowItems.length > numCols) { + shadowCt.remove(shadowItems.last()); + } + while (shadowItems.length < numCols) { + shadowCt.add({ + xtype: 'container', + cls: owner.groupCls, + flex: 1 + }); + } + + if (owner.vertical) { + numRows = Math.ceil(numItems / numCols); + for (i = 0; i < numItems; i++) { + if (i > 0 && i % numRows === 0) { + colIndex++; + } + shadowItems.getAt(colIndex).items.add(items.getAt(i)); + } + } else { + for (i = 0; i < numItems; i++) { + colIndex = i % numCols; + shadowItems.getAt(colIndex).items.add(items.getAt(i)); + } + } + + if (!shadowCt.rendered) { + shadowCt.render(this.getRenderTarget()); + } else { + // Ensure all items are rendered in the correct place in the correct column - this won't + // get done by the column containers themselves if their dimensions are not changing. + shadowItems.each(function(col) { + var layout = col.getLayout(); + layout.renderItems(layout.getLayoutItems(), layout.getRenderTarget()); + }); + } + + shadowCt.doComponentLayout(); + }, + + + // We don't want to render any items to the owner directly, that gets handled by each column's own layout + renderItems: Ext.emptyFn, + + + /** + * @private + * Creates and returns the shadow hbox container that will be used to arrange the owner's items + * into columns. + */ + getShadowCt: function() { + var me = this, + shadowCt = me.shadowCt, + owner, items, item, columns, columnsIsArray, numCols, i; + + if (!shadowCt) { + // Create the column containers based on the owner's 'columns' config + owner = me.owner; + columns = owner.columns; + columnsIsArray = Ext.isArray(columns); + numCols = me.getColCount(); + items = []; + for(i = 0; i < numCols; i++) { + item = { + xtype: 'container', + cls: owner.groupCls + }; + if (columnsIsArray) { + // Array can contain mixture of whole numbers, used as fixed pixel widths, and fractional + // numbers, used as relative flex values. + if (columns[i] < 1) { + item.flex = columns[i]; + } else { + item.width = columns[i]; + } + } + else { + // All columns the same width + item.flex = 1; + } + items.push(item); + } + + // Create the shadow container; delay rendering until after items are added to the columns + shadowCt = me.shadowCt = Ext.createWidget('container', { + layout: 'hbox', + items: items, + ownerCt: owner + }); + } + + return shadowCt; + }, + + + /** + * @private Get the number of columns in the checkbox group + */ + getColCount: function() { + var owner = this.owner, + colsCfg = owner.columns; + return Ext.isArray(colsCfg) ? colsCfg.length : (Ext.isNumber(colsCfg) ? colsCfg : owner.items.length); + } + +}); + +/** + * @class Ext.form.FieldContainer + * @extends Ext.container.Container + +FieldContainer is a derivation of {@link Ext.container.Container Container} that implements the +{@link Ext.form.Labelable Labelable} mixin. This allows it to be configured so that it is rendered with +a {@link #fieldLabel field label} and optional {@link #msgTarget error message} around its sub-items. +This is useful for arranging a group of fields or other components within a single item in a form, so +that it lines up nicely with other fields. A common use is for grouping a set of related fields under +a single label in a form. + +The container's configured {@link #items} will be layed out within the field body area according to the +configured {@link #layout} type. The default layout is `'autocontainer'`. + +Like regular fields, FieldContainer can inherit its decoration configuration from the +{@link Ext.form.Panel#fieldDefaults fieldDefaults} of an enclosing FormPanel. In addition, +FieldContainer itself can pass {@link #fieldDefaults} to any {@link Ext.form.Labelable fields} +it may itself contain. + +If you are grouping a set of {@link Ext.form.field.Checkbox Checkbox} or {@link Ext.form.field.Radio Radio} +fields in a single labeled container, consider using a {@link Ext.form.CheckboxGroup} +or {@link Ext.form.RadioGroup} instead as they are specialized for handling those types. +{@img Ext.form.FieldContainer/Ext.form.FieldContainer1.png Ext.form.FieldContainer component} +__Example usage:__ + + Ext.create('Ext.form.Panel', { + title: 'FieldContainer Example', + width: 550, + bodyPadding: 10, + + items: [{ + xtype: 'fieldcontainer', + fieldLabel: 'Last Three Jobs', + labelWidth: 100, + + // The body area will contain three text fields, arranged + // horizontally, separated by draggable splitters. + layout: 'hbox', + items: [{ + xtype: 'textfield', + flex: 1 + }, { + xtype: 'splitter' + }, { + xtype: 'textfield', + flex: 1 + }, { + xtype: 'splitter' + }, { + xtype: 'textfield', + flex: 1 + }] + }], + renderTo: Ext.getBody() + }); + +__Usage of {@link #fieldDefaults}:__ +{@img Ext.form.FieldContainer/Ext.form.FieldContainer2.png Ext.form.FieldContainer component} + + Ext.create('Ext.form.Panel', { + title: 'FieldContainer Example', + width: 350, + bodyPadding: 10, + + items: [{ + xtype: 'fieldcontainer', + fieldLabel: 'Your Name', + labelWidth: 75, + defaultType: 'textfield', + + // Arrange fields vertically, stretched to full width + layout: 'anchor', + defaults: { + layout: '100%' + }, + + // These config values will be applied to both sub-fields, except + // for Last Name which will use its own msgTarget. + fieldDefaults: { + msgTarget: 'under', + labelAlign: 'top' + }, + + items: [{ + fieldLabel: 'First Name', + name: 'firstName' + }, { + fieldLabel: 'Last Name', + name: 'lastName', + msgTarget: 'under' + }] + }], + renderTo: Ext.getBody() + }); + + + * @constructor + * Creates a new Ext.form.FieldContainer instance. + * @param {Object} config The component configuration. + * + * @xtype fieldcontainer + * @markdown + * @docauthor Jason Johnston + */ +Ext.define('Ext.form.FieldContainer', { + extend: 'Ext.container.Container', + mixins: { + labelable: 'Ext.form.Labelable', + fieldAncestor: 'Ext.form.FieldAncestor' + }, + alias: 'widget.fieldcontainer', + + componentLayout: 'field', + + /** + * @cfg {Boolean} combineLabels + * If set to true, and there is no defined {@link #fieldLabel}, the field container will automatically + * generate its label by combining the labels of all the fields it contains. Defaults to false. + */ + combineLabels: false, + + /** + * @cfg {String} labelConnector + * The string to use when joining the labels of individual sub-fields, when {@link #combineLabels} is + * set to true. Defaults to ', '. + */ + labelConnector: ', ', + + /** + * @cfg {Boolean} combineErrors + * If set to true, the field container will automatically combine and display the validation errors from + * all the fields it contains as a single error on the container, according to the configured + * {@link #msgTarget}. Defaults to false. + */ + combineErrors: false, + + maskOnDisable: false, + + initComponent: function() { + var me = this, + onSubCmpAddOrRemove = me.onSubCmpAddOrRemove; + + // Init mixins + me.initLabelable(); + me.initFieldAncestor(); + + me.callParent(); + }, + + /** + * @protected Called when a {@link Ext.form.Labelable} instance is added to the container's subtree. + * @param {Ext.form.Labelable} labelable The instance that was added + */ + onLabelableAdded: function(labelable) { + var me = this; + me.mixins.fieldAncestor.onLabelableAdded.call(this, labelable); + me.updateLabel(); + }, + + /** + * @protected Called when a {@link Ext.form.Labelable} instance is removed from the container's subtree. + * @param {Ext.form.Labelable} labelable The instance that was removed + */ + onLabelableRemoved: function(labelable) { + var me = this; + me.mixins.fieldAncestor.onLabelableRemoved.call(this, labelable); + me.updateLabel(); + }, + + onRender: function() { + var me = this, + renderSelectors = me.renderSelectors, + applyIf = Ext.applyIf; + + applyIf(renderSelectors, me.getLabelableSelectors()); + + me.callParent(arguments); + }, + + initRenderTpl: function() { + var me = this; + if (!me.hasOwnProperty('renderTpl')) { + me.renderTpl = me.getTpl('labelableRenderTpl'); + } + return me.callParent(); + }, + + initRenderData: function() { + return Ext.applyIf(this.callParent(), this.getLabelableRenderData()); + }, + + /** + * Returns the combined field label if {@link #combineLabels} is set to true and if there is no + * set {@link #fieldLabel}. Otherwise returns the fieldLabel like normal. You can also override + * this method to provide a custom generated label. + */ + getFieldLabel: function() { + var label = this.fieldLabel || ''; + if (!label && this.combineLabels) { + label = Ext.Array.map(this.query('[isFieldLabelable]'), function(field) { + return field.getFieldLabel(); + }).join(this.labelConnector); + } + return label; + }, + + /** + * @private Updates the content of the labelEl if it is rendered + */ + updateLabel: function() { + var me = this, + label = me.labelEl; + if (label) { + label.update(me.getFieldLabel()); + } + }, + + + /** + * @private Fired when the error message of any field within the container changes, and updates the + * combined error message to match. + */ + onFieldErrorChange: function(field, activeError) { + if (this.combineErrors) { + var me = this, + oldError = me.getActiveError(), + invalidFields = Ext.Array.filter(me.query('[isFormField]'), function(field) { + return field.hasActiveError(); + }), + newErrors = me.getCombinedErrors(invalidFields); + + if (newErrors) { + me.setActiveErrors(newErrors); + } else { + me.unsetActiveError(); + } + + if (oldError !== me.getActiveError()) { + me.doComponentLayout(); + } + } + }, + + /** + * Takes an Array of invalid {@link Ext.form.field.Field} objects and builds a combined list of error + * messages from them. Defaults to prepending each message by the field name and a colon. This + * can be overridden to provide custom combined error message handling, for instance changing + * the format of each message or sorting the array (it is sorted in order of appearance by default). + * @param {Array} invalidFields An Array of the sub-fields which are currently invalid. + * @return {Array} The combined list of error messages + */ + getCombinedErrors: function(invalidFields) { + var forEach = Ext.Array.forEach, + errors = []; + forEach(invalidFields, function(field) { + forEach(field.getActiveErrors(), function(error) { + var label = field.getFieldLabel(); + errors.push((label ? label + ': ' : '') + error); + }); + }); + return errors; + }, + + getTargetEl: function() { + return this.bodyEl || this.callParent(); + } +}); + +/** + * @class Ext.form.CheckboxGroup + * @extends Ext.form.FieldContainer + *

A {@link Ext.form.FieldContainer field container} which has a specialized layout for arranging + * {@link Ext.form.field.Checkbox} controls into columns, and provides convenience {@link Ext.form.field.Field} methods + * for {@link #getValue getting}, {@link #setValue setting}, and {@link #validate validating} the group + * of checkboxes as a whole.

+ *

Validation: Individual checkbox fields themselves have no default validation behavior, but + * sometimes you want to require a user to select at least one of a group of checkboxes. CheckboxGroup + * allows this by setting the config {@link #allowBlank}:false; when the user does not check at + * least one of the checkboxes, the entire group will be highlighted as invalid and the + * {@link #blankText error message} will be displayed according to the {@link #msgTarget} config.

+ *

Layout: The default layout for CheckboxGroup makes it easy to arrange the checkboxes into + * columns; see the {@link #columns} and {@link #vertical} config documentation for details. You may also + * use a completely different layout by setting the {@link #layout} to one of the other supported layout + * types; for instance you may wish to use a custom arrangement of hbox and vbox containers. In that case + * the checkbox components at any depth will still be managed by the CheckboxGroup's validation.

+ * {@img Ext.form.RadioGroup/Ext.form.RadioGroup.png Ext.form.RadioGroup component} + *

Example usage:

+ *

+    Ext.create('Ext.form.Panel', {
+        title: 'RadioGroup Example',
+        width: 300,
+        height: 125,
+        bodyPadding: 10,
+        renderTo: Ext.getBody(),        
+        items:[{            
+            xtype: 'radiogroup',
+            fieldLabel: 'Two Columns',
+            // Arrange radio buttons into two columns, distributed vertically
+            columns: 2,
+            vertical: true,
+            items: [
+                {boxLabel: 'Item 1', name: 'rb', inputValue: '1'},
+                {boxLabel: 'Item 2', name: 'rb', inputValue: '2', checked: true},
+                {boxLabel: 'Item 3', name: 'rb', inputValue: '3'},
+                {boxLabel: 'Item 4', name: 'rb', inputValue: '4'},
+                {boxLabel: 'Item 5', name: 'rb', inputValue: '5'},
+                {boxLabel: 'Item 6', name: 'rb', inputValue: '6'}
+            ]
+        }]
+    });
+ * 
+ * @constructor + * Creates a new CheckboxGroup + * @param {Object} config Configuration options + * @xtype checkboxgroup + */ +Ext.define('Ext.form.CheckboxGroup', { + extend:'Ext.form.FieldContainer', + mixins: { + field: 'Ext.form.field.Field' + }, + alias: 'widget.checkboxgroup', + requires: ['Ext.layout.container.CheckboxGroup', 'Ext.form.field.Base'], + + /** + * @cfg {String} name + * @hide + */ + + /** + * @cfg {Array} items An Array of {@link Ext.form.field.Checkbox Checkbox}es or Checkbox config objects + * to arrange in the group. + */ + + /** + * @cfg {String/Number/Array} columns Specifies the number of columns to use when displaying grouped + * checkbox/radio controls using automatic layout. This config can take several types of values: + *
  • 'auto' :

    The controls will be rendered one per column on one row and the width + * of each column will be evenly distributed based on the width of the overall field container. This is the default.

  • + *
  • Number :

    If you specific a number (e.g., 3) that number of columns will be + * created and the contained controls will be automatically distributed based on the value of {@link #vertical}.

  • + *
  • Array :

    You can also specify an array of column widths, mixing integer + * (fixed width) and float (percentage width) values as needed (e.g., [100, .25, .75]). Any integer values will + * be rendered first, then any float values will be calculated as a percentage of the remaining space. Float + * values do not have to add up to 1 (100%) although if you want the controls to take up the entire field + * container you should do so.

+ */ + columns : 'auto', + + /** + * @cfg {Boolean} vertical True to distribute contained controls across columns, completely filling each column + * top to bottom before starting on the next column. The number of controls in each column will be automatically + * calculated to keep columns as even as possible. The default value is false, so that controls will be added + * to columns one at a time, completely filling each row left to right before starting on the next row. + */ + vertical : false, + + /** + * @cfg {Boolean} allowBlank False to validate that at least one item in the group is checked (defaults to true). + * If no items are selected at validation time, {@link #blankText} will be used as the error text. + */ + allowBlank : true, + + /** + * @cfg {String} blankText Error text to display if the {@link #allowBlank} validation fails (defaults to "You must + * select at least one item in this group") + */ + blankText : "You must select at least one item in this group", + + // private + defaultType : 'checkboxfield', + + // private + groupCls : Ext.baseCSSPrefix + 'form-check-group', + + /** + * @cfg {String} fieldBodyCls + * An extra CSS class to be applied to the body content element in addition to {@link #baseBodyCls}. + * Defaults to 'x-form-checkboxgroup-body'. + */ + fieldBodyCls: Ext.baseCSSPrefix + 'form-checkboxgroup-body', + + // private + layout: 'checkboxgroup', + + initComponent: function() { + var me = this; + me.callParent(); + me.initField(); + }, + + /** + * @protected + * Initializes the field's value based on the initial config. If the {@link #value} config is specified + * then we use that to set the value; otherwise we initialize the originalValue by querying the values of + * all sub-checkboxes after they have been initialized. + */ + initValue: function() { + var me = this, + valueCfg = me.value; + me.originalValue = me.lastValue = valueCfg || me.getValue(); + if (valueCfg) { + me.setValue(valueCfg); + } + }, + + /** + * @protected + * When a checkbox is added to the group, monitor it for changes + */ + onFieldAdded: function(field) { + var me = this; + if (field.isCheckbox) { + me.mon(field, 'change', me.checkChange, me); + } + me.callParent(arguments); + }, + + onFieldRemoved: function(field) { + var me = this; + if (field.isCheckbox) { + me.mun(field, 'change', me.checkChange, me); + } + me.callParent(arguments); + }, + + // private override - the group value is a complex object, compare using object serialization + isEqual: function(value1, value2) { + var toQueryString = Ext.Object.toQueryString; + return toQueryString(value1) === toQueryString(value2); + }, + + /** + * Runs CheckboxGroup's validations and returns an array of any errors. The only error by default + * is if allowBlank is set to true and no items are checked. + * @return {Array} Array of all validation errors + */ + getErrors: function() { + var errors = []; + if (!this.allowBlank && Ext.isEmpty(this.getChecked())) { + errors.push(this.blankText); + } + return errors; + }, + + /** + * @private Returns all checkbox components within the container + */ + getBoxes: function() { + return this.query('[isCheckbox]'); + }, + + /** + * @private Convenience function which calls the given function for every checkbox in the group + * @param {Function} fn The function to call + * @param {Object} scope Optional scope object + */ + eachBox: function(fn, scope) { + Ext.Array.forEach(this.getBoxes(), fn, scope || this); + }, + + /** + * Returns an Array of all checkboxes in the container which are currently checked + * @return {Array} Array of Ext.form.field.Checkbox components + */ + getChecked: function() { + return Ext.Array.filter(this.getBoxes(), function(cb) { + return cb.getValue(); + }); + }, + + // private override + isDirty: function(){ + return Ext.Array.some(this.getBoxes(), function(cb) { + return cb.isDirty(); + }); + }, + + // private override + setReadOnly: function(readOnly) { + this.eachBox(function(cb) { + cb.setReadOnly(readOnly); + }); + this.readOnly = readOnly; + }, + + /** + * Resets the checked state of all {@link Ext.form.field.Checkbox checkboxes} in the group to their + * originally loaded values and clears any validation messages. + * See {@link Ext.form.Basic}.{@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} + */ + reset: function() { + var me = this, + hadError = me.hasActiveError(), + preventMark = me.preventMark; + me.preventMark = true; + me.batchChanges(function() { + me.eachBox(function(cb) { + cb.reset(); + }); + }); + me.preventMark = preventMark; + me.unsetActiveError(); + if (hadError) { + me.doComponentLayout(); + } + }, + + // private override + resetOriginalValue: function() { + // Defer resetting of originalValue until after all sub-checkboxes have been reset so we get + // the correct data from getValue() + Ext.defer(function() { + this.callParent(); + }, 1, this); + }, + + + /** + *

Sets the value(s) of all checkboxes in the group. The expected format is an Object of + * name-value pairs corresponding to the names of the checkboxes in the group. Each pair can + * have either a single or multiple values:

+ *
    + *
  • A single Boolean or String value will be passed to the setValue method of the + * checkbox with that name. See the rules in {@link Ext.form.field.Checkbox#setValue} for accepted values.
  • + *
  • An Array of String values will be matched against the {@link Ext.form.field.Checkbox#inputValue inputValue} + * of checkboxes in the group with that name; those checkboxes whose inputValue exists in the array will be + * checked and others will be unchecked.
  • + *
+ *

If a checkbox's name is not in the mapping at all, it will be unchecked.

+ *

An example:

+ *
var myCheckboxGroup = new Ext.form.CheckboxGroup({
+    columns: 3,
+    items: [{
+        name: 'cb1',
+        boxLabel: 'Single 1'
+    }, {
+        name: 'cb2',
+        boxLabel: 'Single 2'
+    }, {
+        name: 'cb3',
+        boxLabel: 'Single 3'
+    }, {
+        name: 'cbGroup',
+        boxLabel: 'Grouped 1'
+        inputValue: 'value1'
+    }, {
+        name: 'cbGroup',
+        boxLabel: 'Grouped 2'
+        inputValue: 'value2'
+    }, {
+        name: 'cbGroup',
+        boxLabel: 'Grouped 3'
+        inputValue: 'value3'
+    }]
+});
+
+myCheckboxGroup.setValue({
+    cb1: true,
+    cb3: false,
+    cbGroup: ['value1', 'value3']
+});
+ *

The above code will cause the checkbox named 'cb1' to be checked, as well as the first and third + * checkboxes named 'cbGroup'. The other three checkboxes will be unchecked.

+ * @param {Object} value The mapping of checkbox names to values. + * @return {Ext.form.CheckboxGroup} this + */ + setValue: function(value) { + var me = this; + me.batchChanges(function() { + me.eachBox(function(cb) { + var name = cb.getName(), + cbValue = false; + if (value && name in value) { + if (Ext.isArray(value[name])) { + cbValue = Ext.Array.contains(value[name], cb.inputValue); + } else { + // single value, let the checkbox's own setValue handle conversion + cbValue = value[name]; + } + } + cb.setValue(cbValue); + }); + }); + return me; + }, + + + /** + *

Returns an object containing the values of all checked checkboxes within the group. Each key-value pair + * in the object corresponds to a checkbox {@link Ext.form.field.Checkbox#name name}. If there is only one checked + * checkbox with a particular name, the value of that pair will be the String + * {@link Ext.form.field.Checkbox#inputValue inputValue} of that checkbox. If there are multiple checked checkboxes + * with that name, the value of that pair will be an Array of the selected inputValues.

+ *

The object format returned from this method can also be passed directly to the {@link #setValue} method.

+ *

NOTE: In Ext 3, this method returned an array of Checkbox components; this was changed to make it more + * consistent with other field components and with the {@link #setValue} argument signature. If you need the old + * behavior in Ext 4+, use the {@link #getChecked} method instead.

+ */ + getValue: function() { + var values = {}; + this.eachBox(function(cb) { + var name = cb.getName(), + inputValue = cb.inputValue, + bucket; + if (cb.getValue()) { + if (name in values) { + bucket = values[name]; + if (!Ext.isArray(bucket)) { + bucket = values[name] = [bucket]; + } + bucket.push(inputValue); + } else { + values[name] = inputValue; + } + } + }); + return values; + }, + + /* + * Don't return any data for submit; the form will get the info from the individual checkboxes themselves. + */ + getSubmitData: function() { + return null; + }, + + /* + * Don't return any data for the model; the form will get the info from the individual checkboxes themselves. + */ + getModelData: function() { + return null; + }, + + validate: function() { + var me = this, + errors = me.getErrors(), + isValid = Ext.isEmpty(errors), + wasValid = !me.hasActiveError(); + + if (isValid) { + me.unsetActiveError(); + } else { + me.setActiveError(errors); + } + if (isValid !== wasValid) { + me.fireEvent('validitychange', me, isValid); + me.doComponentLayout(); + } + + return isValid; + } + +}, function() { + + this.borrow(Ext.form.field.Base, ['markInvalid', 'clearInvalid']); + +}); + + +/** + * @private + * Private utility class for managing all {@link Ext.form.field.Checkbox} fields grouped by name. + */ +Ext.define('Ext.form.CheckboxManager', { + extend: 'Ext.util.MixedCollection', + singleton: true, + + getByName: function(name) { + return this.filterBy(function(item) { + return item.name == name; + }); + }, + + getWithValue: function(name, value) { + return this.filterBy(function(item) { + return item.name == name && item.inputValue == value; + }); + }, + + getChecked: function(name) { + return this.filterBy(function(item) { + return item.name == name && item.checked; + }); + } +}); + +/** + * @class Ext.form.FieldSet + * @extends Ext.container.Container + * + * A container for grouping sets of fields, rendered as a HTML `fieldset` element. The {@link #title} + * config will be rendered as the fieldset's `legend`. + * + * While FieldSets commonly contain simple groups of fields, they are general {@link Ext.container.Container Containers} + * and may therefore contain any type of components in their {@link #items}, including other nested containers. + * The default {@link #layout} for the FieldSet's items is `'anchor'`, but it can be configured to use any other + * layout type. + * + * FieldSets may also be collapsed if configured to do so; this can be done in two ways: + * + * 1. Set the {@link #collapsible} config to true; this will result in a collapse button being rendered next to + * the {@link #title legend title}, or: + * 2. Set the {@link #checkboxToggle} config to true; this is similar to using {@link #collapsible} but renders + * a {@link Ext.form.field.Checkbox checkbox} in place of the toggle button. The fieldset will be expanded when the + * checkbox is checked and collapsed when it is unchecked. The checkbox will also be included in the + * {@link Ext.form.Basic#submit form submit parameters} using the {@link #checkboxName} as its parameter name. + * + * {@img Ext.form.FieldSet/Ext.form.FieldSet.png Ext.form.FieldSet component} + * + * ## Example usage + * + * Ext.create('Ext.form.Panel', { + * title: 'Simple Form with FieldSets', + * labelWidth: 75, // label settings here cascade unless overridden + * url: 'save-form.php', + * frame: true, + * bodyStyle: 'padding:5px 5px 0', + * width: 550, + * renderTo: Ext.getBody(), + * layout: 'column', // arrange fieldsets side by side + * defaults: { + * bodyPadding: 4 + * }, + * items: [{ + * // Fieldset in Column 1 - collapsible via toggle button + * xtype:'fieldset', + * columnWidth: 0.5, + * title: 'Fieldset 1', + * collapsible: true, + * defaultType: 'textfield', + * defaults: {anchor: '100%'}, + * layout: 'anchor', + * items :[{ + * fieldLabel: 'Field 1', + * name: 'field1' + * }, { + * fieldLabel: 'Field 2', + * name: 'field2' + * }] + * }, { + * // Fieldset in Column 2 - collapsible via checkbox, collapsed by default, contains a panel + * xtype:'fieldset', + * title: 'Show Panel', // title or checkboxToggle creates fieldset header + * columnWidth: 0.5, + * checkboxToggle: true, + * collapsed: true, // fieldset initially collapsed + * layout:'anchor', + * items :[{ + * xtype: 'panel', + * anchor: '100%', + * title: 'Panel inside a fieldset', + * frame: true, + * height: 52 + * }] + * }] + * }); + * + * @constructor + * Create a new FieldSet + * @param {Object} config Configuration options + * @xtype fieldset + * @docauthor Jason Johnston + */ +Ext.define('Ext.form.FieldSet', { + extend: 'Ext.container.Container', + alias: 'widget.fieldset', + uses: ['Ext.form.field.Checkbox', 'Ext.panel.Tool', 'Ext.layout.container.Anchor', 'Ext.layout.component.FieldSet'], + + /** + * @cfg {String} title + * A title to be displayed in the fieldset's legend. May contain HTML markup. + */ + + /** + * @cfg {Boolean} checkboxToggle + * Set to true to render a checkbox into the fieldset frame just + * in front of the legend to expand/collapse the fieldset when the checkbox is toggled. (defaults + * to false). This checkbox will be included in form submits using the {@link #checkboxName}. + */ + + /** + * @cfg {String} checkboxName + * The name to assign to the fieldset's checkbox if {@link #checkboxToggle} = true + * (defaults to '[fieldset id]-checkbox'). + */ + + /** + * @cfg {Boolean} collapsible + * Set to true to make the fieldset collapsible and have the expand/collapse toggle button automatically + * rendered into the legend element, false to keep the fieldset statically sized with no collapse + * button (defaults to false). Another option is to configure {@link #checkboxToggle}. + * Use the {@link #collapsed} config to collapse the fieldset by default. + */ + + /** + * @cfg {Boolean} collapsed + * Set to true to render the fieldset as collapsed by default. If {@link #checkboxToggle} is specified, + * the checkbox will also be unchecked by default. + */ + collapsed: false, + + /** + * @property legend + * @type Ext.Component + * The component for the fieldset's legend. Will only be defined if the configuration requires a legend + * to be created, by setting the {@link #title} or {@link #checkboxToggle} options. + */ + + /** + * @cfg {String} baseCls The base CSS class applied to the fieldset (defaults to 'x-fieldset'). + */ + baseCls: Ext.baseCSSPrefix + 'fieldset', + + /** + * @cfg {String} layout The {@link Ext.container.Container#layout} for the fieldset's immediate child items. + * Defaults to 'anchor'. + */ + layout: 'anchor', + + componentLayout: 'fieldset', + + // No aria role necessary as fieldset has its own recognized semantics + ariaRole: '', + + renderTpl: ['
'], + + maskOnDisable: false, + + getElConfig: function(){ + return {tag: 'fieldset', id: this.id}; + }, + + initComponent: function() { + var me = this, + baseCls = me.baseCls; + + me.callParent(); + + // Create the Legend component if needed + me.initLegend(); + + // Add body el selector + Ext.applyIf(me.renderSelectors, { + body: '.' + baseCls + '-body' + }); + + if (me.collapsed) { + me.addCls(baseCls + '-collapsed'); + me.collapse(); + } + }, + + // private + onRender: function(container, position) { + this.callParent(arguments); + // Make sure the legend is created and rendered + this.initLegend(); + }, + + /** + * @private + * Initialize and render the legend component if necessary + */ + initLegend: function() { + var me = this, + legendItems, + legend = me.legend; + + // Create the legend component if needed and it hasn't been already + if (!legend && (me.title || me.checkboxToggle || me.collapsible)) { + legendItems = []; + + // Checkbox + if (me.checkboxToggle) { + legendItems.push(me.createCheckboxCmp()); + } + // Toggle button + else if (me.collapsible) { + legendItems.push(me.createToggleCmp()); + } + + // Title + legendItems.push(me.createTitleCmp()); + + legend = me.legend = Ext.create('Ext.container.Container', { + baseCls: me.baseCls + '-header', + ariaRole: '', + getElConfig: function(){ + return {tag: 'legend', cls: this.baseCls}; + }, + items: legendItems + }); + } + + // Make sure legend is rendered if the fieldset is rendered + if (legend && !legend.rendered && me.rendered) { + me.legend.render(me.el, me.body); //insert before body element + } + }, + + /** + * @protected + * Creates the legend title component. This is only called internally, but could be overridden in subclasses + * to customize the title component. + * @return Ext.Component + */ + createTitleCmp: function() { + var me = this; + me.titleCmp = Ext.create('Ext.Component', { + html: me.title, + cls: me.baseCls + '-header-text' + }); + return me.titleCmp; + + }, + + /** + * @property checkboxCmp + * @type Ext.form.field.Checkbox + * Refers to the {@link Ext.form.field.Checkbox} component that is added next to the title in the legend. Only + * populated if the fieldset is configured with {@link #checkboxToggle}:true. + */ + + /** + * @protected + * Creates the checkbox component. This is only called internally, but could be overridden in subclasses + * to customize the checkbox's configuration or even return an entirely different component type. + * @return Ext.Component + */ + createCheckboxCmp: function() { + var me = this, + suffix = '-checkbox'; + + me.checkboxCmp = Ext.create('Ext.form.field.Checkbox', { + name: me.checkboxName || me.id + suffix, + cls: me.baseCls + '-header' + suffix, + checked: !me.collapsed, + listeners: { + change: me.onCheckChange, + scope: me + } + }); + return me.checkboxCmp; + }, + + /** + * @property toggleCmp + * @type Ext.panel.Tool + * Refers to the {@link Ext.panel.Tool} component that is added as the collapse/expand button next + * to the title in the legend. Only populated if the fieldset is configured with {@link #collapsible}:true. + */ + + /** + * @protected + * Creates the toggle button component. This is only called internally, but could be overridden in + * subclasses to customize the toggle component. + * @return Ext.Component + */ + createToggleCmp: function() { + var me = this; + me.toggleCmp = Ext.create('Ext.panel.Tool', { + type: 'toggle', + handler: me.toggle, + scope: me + }); + return me.toggleCmp; + }, + + /** + * Sets the title of this fieldset + * @param {String} title The new title + * @return {Ext.form.FieldSet} this + */ + setTitle: function(title) { + var me = this; + me.title = title; + me.initLegend(); + me.titleCmp.update(title); + return me; + }, + + getTargetEl : function() { + return this.body || this.frameBody || this.el; + }, + + getContentTarget: function() { + return this.body; + }, + + /** + * @private + * Include the legend component in the items for ComponentQuery + */ + getRefItems: function(deep) { + var refItems = this.callParent(arguments), + legend = this.legend; + + // Prepend legend items to ensure correct order + if (legend) { + refItems.unshift(legend); + if (deep) { + refItems.unshift.apply(refItems, legend.getRefItems(true)); + } + } + return refItems; + }, + + /** + * Expands the fieldset. + * @return {Ext.form.FieldSet} this + */ + expand : function(){ + return this.setExpanded(true); + }, + + /** + * Collapses the fieldset. + * @return {Ext.form.FieldSet} this + */ + collapse : function() { + return this.setExpanded(false); + }, + + /** + * @private Collapse or expand the fieldset + */ + setExpanded: function(expanded) { + var me = this, + checkboxCmp = me.checkboxCmp, + toggleCmp = me.toggleCmp; + + expanded = !!expanded; + + if (checkboxCmp) { + checkboxCmp.setValue(expanded); + } + + if (expanded) { + me.removeCls(me.baseCls + '-collapsed'); + } else { + me.addCls(me.baseCls + '-collapsed'); + } + me.collapsed = !expanded; + me.doComponentLayout(); + return me; + }, + + /** + * Toggle the fieldset's collapsed state to the opposite of what it is currently + */ + toggle: function() { + this.setExpanded(!!this.collapsed); + }, + + /** + * @private Handle changes in the checkbox checked state + */ + onCheckChange: function(cmp, checked) { + this.setExpanded(checked); + }, + + beforeDestroy : function() { + var legend = this.legend; + if (legend) { + legend.destroy(); + } + this.callParent(); + } +}); + +/** + * @class Ext.form.Label + * @extends Ext.Component + +Produces a standalone `