- * b
: ButtonThis Button.
- * e
: EventObjectThe click event.
+ * @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
+ *
+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
+ *
+layout: 'vbox'
+
+ *
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 {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/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 {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 {Object/Object[]} 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
- /**
- * @cfg {Boolean} hidden True to start hidden (defaults to false)
+// 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 {Boolean} disabled True to start disabled (defaults to false)
+ * @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.
+ *
+ * Defaults are applied to both config objects and instantiated components conditionally so as not to override
+ * existing properties in the item (see {@link Ext#applyIf}).
+ *
+ * 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: [
+ * // default will not be applied here, panel1 will be autoScroll: false
+ * {
+ * xtype: 'panel',
+ * id: 'panel1',
+ * autoScroll: false
+ * },
+ * // this component will have autoScroll: true
+ * new Ext.panel.Panel({
+ * id: 'panel2'
+ * })
+ * ]
*/
- /**
- * @cfg {Boolean} pressed True to start pressed (only if enableToggle = 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 {String} toggleGroup The group this toggle button is a member of (only 1 per group can be pressed)
+ /** @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 {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 {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',
- /**
- * @cfg {Number} tabIndex Set a DOM tabIndex for this button (defaults to undefined)
- */
+ isContainer : true,
/**
- * @cfg {Boolean} allowDepress
- * False to not allow a pressed Button to be depressed (defaults to undefined). Only valid when {@link #enableToggle} is true.
+ * The number of container layout calls made on this object.
+ * @property layoutCounter
+ * @type {Number}
+ * @private
*/
+ layoutCounter : 0,
+
+ baseCls: Ext.baseCSSPrefix + 'container',
/**
- * @cfg {Boolean} enableToggle
- * True to enable pressed/not pressed toggling (defaults to false)
+ * @cfg {String[]} 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']
.
*/
- enableToggle: false,
+ bubbleEvents: ['add', 'remove'],
- /**
- * @cfg {Function} toggleHandler
- * Function called when a Button with {@link #enableToggle} set to true is clicked. Two arguments are passed:
- */
-
- /**
- * @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',
-
- /**
- * @cfg {Object} baseParams
- * An object literal of parameters to pass to the url when the {@link #href} property is specified.
- */
-
- /**
- * @cfg {Object} params
- * An object literal of parameters to pass to the url when the {@link #href} property is specified.
- * Any params override {@link #baseParams}. New params can be set using the {@link #setParams} method.
- */
-
- ariaRole: 'button',
-
- // inherited
- renderTpl:
- '
' +
- '' +
- ' tabIndex="{tabIndex}" role="link">' +
- '{text} ' +
- '' +
- '' +
- '' +
- ' tabIndex="{tabIndex}" role="button" autocomplete="off">' +
- '{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() {
+ // @private
+ 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
+ * @event afterlayout
+ * Fires when the components in this container are arranged by the associated layout manager.
+ * @param {Ext.container.Container} this
+ * @param {Ext.layout.container.Container} layout The ContainerLayout implementation for this container
*/
- 'mouseout',
-
+ 'afterlayout',
/**
- * @event menushow
- * If this button has a menu, this event fires when it is shown
- * @param {Button} this
- * @param {Menu} menu
+ * @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
*/
- 'menushow',
-
+ 'beforeadd',
/**
- * @event menuhide
- * If this button has a menu, this event fires when it is hidden
- * @param {Button} this
- * @param {Menu} menu
+ * @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
*/
- 'menuhide',
-
+ 'beforeremove',
/**
- * @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
+ * @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
*/
- 'menutriggerover',
-
+ 'add',
/**
- * @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
+ * @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
*/
- 'menutriggerout'
+ 'remove'
);
- if (me.menu) {
- // Flag that we'll have a splitCls
- me.split = true;
+ // layoutOnShow stack
+ me.layoutOnShow = Ext.create('Ext.util.MixedCollection');
+ me.callParent();
+ me.initItems();
+ },
- // retrieve menu by id or instantiate instance if needed
- me.menu = Ext.menu.Manager.get(me.menu);
- me.menu.ownerCt = me;
- }
+ // @private
+ initItems : function() {
+ var me = this,
+ items = me.items;
- // Accept url as a synonym for href
- if (me.url) {
- me.href = me.url;
- }
+ /**
+ * 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);
- // preventDefault defaults to false for links
- if (me.href && !me.hasOwnProperty('preventDefault')) {
- me.preventDefault = false;
- }
+ if (items) {
+ if (!Ext.isArray(items)) {
+ items = [items];
+ }
- if (Ext.isString(me.toggleGroup)) {
- me.enableToggle = true;
+ me.add(items);
}
-
},
- // private
- initAria: function() {
+ // @private
+ afterRender : function() {
+ this.getLayout();
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() {
+ renderChildren: function () {
var me = this,
- el = me.el,
- cls = [];
+ layout = me.getLayout();
- 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);
+ me.callParent();
+ // this component's elements exist, so now create the child components' elements
+
+ if (layout) {
+ me.suspendLayout = true;
+ layout.renderChildren();
+ delete me.suspendLayout;
}
},
-
- // 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;
+ // @private
+ setLayout : function(layout) {
+ var currentLayout = this.layout;
+
+ if (currentLayout && currentLayout.isLayout && currentLayout != layout) {
+ currentLayout.setOwner(null);
}
- // Render internal structure
- me.callParent(arguments);
+ this.layout = layout;
+ layout.setOwner(this);
+ },
- // If it is a split button + has a toolip for the arrow
- if (me.split && me.arrowTooltip) {
- me.arrowEl.dom[me.tooltipType] = me.arrowTooltip;
+ /**
+ * 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'));
}
- // Add listeners to the focus and blur events on the element
- me.mon(me.btnEl, {
- scope: me,
- focus: me.onFocus,
- blur : me.onBlur
- });
+ return me.layout;
+ },
- // Set btn as a local variable for easy access
- btn = me.el;
+ /**
+ * Manually force this container's layout to be recalculated. The framework uses this internally to refresh layouts
+ * form most cases.
+ * @return {Ext.container.Container} this
+ */
+ doLayout : function() {
+ var me = this,
+ layout = me.getLayout();
- if (me.icon) {
- me.setIcon(me.icon);
+ if (me.rendered && layout && !me.suspendLayout) {
+ // If either dimension is being auto-set, then it requires a ComponentLayout to be run.
+ if (!me.isFixedWidth() || !me.isFixedHeight()) {
+ // 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 set, either by configuration, or by an owning layout, run a ContainerLayout
+ else {
+ // Only run the ContainerLayout if it is not already in progress
+ if (layout.layoutBusy !== true) {
+ layout.layout();
+ }
+ }
}
- if (me.iconCls) {
- me.setIconCls(me.iconCls);
- }
+ return me;
+ },
- if (me.tooltip) {
- me.setTooltip(me.tooltip, true);
+ // @private
+ afterLayout : function(layout) {
+ ++this.layoutCounter;
+ this.fireEvent('afterlayout', this, layout);
+ },
+
+ // @private
+ prepareItems : function(items, applyDefaults) {
+ if (!Ext.isArray(items)) {
+ items = [items];
}
- // Add the mouse events to the button
- if (me.handleMouseEvents) {
- me.mon(btn, {
- scope: me,
- mouseover: me.onMouseOver,
- mouseout: me.onMouseOut,
- mousedown: me.onMouseDown
- });
+ // Make sure defaults are applied and item is initialized
+ var i = 0,
+ len = items.length,
+ item;
- if (me.split) {
- me.mon(btn, {
- mousemove: me.onMouseMove,
- scope: me
- });
+ for (; i < len; i++) {
+ item = items[i];
+ if (applyDefaults) {
+ item = this.applyDefaults(item);
}
+ items[i] = this.lookupComponent(item);
}
+ return items;
+ },
- // Check if the button has a menu
- if (me.menu) {
- me.mon(me.menu, {
- scope: me,
- show: me.onMenuShow,
- hide: me.onMenuHide
- });
+ // @private
+ applyDefaults : function(config) {
+ var defaults = this.defaults;
- me.keyMap = Ext.create('Ext.util.KeyMap', me.el, {
- key: Ext.EventObject.DOWN,
- handler: me.onDownKey,
- scope: me
- });
- }
+ if (defaults) {
+ if (Ext.isFunction(defaults)) {
+ defaults = defaults.call(this, config);
+ }
- // 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);
+ if (Ext.isString(config)) {
+ config = Ext.ComponentManager.get(config);
+ }
+ Ext.applyIf(config, defaults);
}
- // Register the button in the toggle manager
- Ext.ButtonToggleManager.register(me);
+ return config;
},
- /**
- *
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 = '';
+ // @private
+ lookupComponent : function(comp) {
+ return Ext.isString(comp) ? Ext.ComponentManager.get(comp) : this.createComponent(comp);
+ },
- // 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(' ');
- }
+ // @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 {
- href : me.getHref(),
- target : me.target || '_blank',
- type : me.type,
- splitCls : me.getSplitCls(),
- cls : me.cls,
- text : me.text || ' ',
- tabIndex : me.tabIndex,
- innerSpanStyle: innerSpanStyle
- };
+ return Ext.ComponentManager.create(config, defaultType || this.defaultType);
},
- /**
- * @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,
- params = Ext.apply({}, me.baseParams);
-
- // write baseParams first, then write any params
- params = Ext.apply(params, me.params);
- return me.href ? Ext.urlAppend(me.href, Ext.Object.toQueryString(params)) : false;
+ // @private - used as the key lookup function for the items collection
+ getComponentId : function(comp) {
+ return comp.getItemId();
},
/**
- *
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} params Parameters to use in the href URL.
- */
- setParams: function(params) {
- this.params = params;
- this.btnEl.dom.href = this.getHref();
- },
- getSplitCls: function() {
- var me = this;
- return me.split ? (me.baseCls + '-' + me.arrowCls) + ' ' + (me.baseCls + '-' + me.arrowCls + '-' + me.arrowAlign) : '';
- },
+Adds {@link Ext.Component Component}(s) to this Container.
- // private
- afterRender: function() {
- var me = this;
- me.useSetClass = true;
- me.setButtonCls();
- me.doc = Ext.getDoc();
- this.callParent(arguments);
- },
+##Description:##
- /**
- * 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
+- 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 {Ext.Component[]/Ext.Component...} component
+ * Either one or more Components to add or an Array of Components to add.
+ * See `{@link #items}` for additional information.
+ *
+ * @return {Ext.Component[]/Ext.Component} The Components that were added.
+ * @markdown
*/
- setIconCls: function(cls) {
+ add : function() {
var me = this,
- btnInnerEl = me.btnInnerEl;
- if (btnInnerEl) {
- // Remove the previous iconCls from the button
- btnInnerEl.removeCls(me.iconCls);
- btnInnerEl.addCls(cls || '');
- me.setButtonCls();
+ args = Array.prototype.slice.call(arguments),
+ hasMultipleArgs,
+ items,
+ results = [],
+ i,
+ ln,
+ item,
+ index = -1,
+ cmp;
+
+ if (typeof args[0] == 'number') {
+ index = args.shift();
}
- 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;
+ hasMultipleArgs = args.length > 1;
+ if (hasMultipleArgs || Ext.isArray(args[0])) {
- 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;
- },
+ 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];
- // private
- getRefItems: function(deep){
- var menu = this.menu,
- items;
+ //
+ if (!item) {
+ Ext.Error.raise("Trying to add a null item as a child of Container with itemId/id: " + me.getItemId());
+ }
+ //
- if (menu) {
- items = menu.getRefItems(deep);
- items.unshift(menu);
+ 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;
}
- return items || [];
- },
- // private
- clearTip: function() {
- if (Ext.isObject(this.tooltip)) {
- Ext.tip.QuickTipManager.unregister(this.btnEl);
- }
- },
+ cmp = me.prepareItems(args[0], true)[0];
- // 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);
+ // 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();
}
- Ext.destroy(me.repeater);
+ return cmp;
},
- // 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();
- },
+ onAdd : Ext.emptyFn,
+ onRemove : Ext.emptyFn,
/**
- * 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
+ * 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.
*/
- setHandler: function(handler, scope) {
- this.handler = handler;
- this.scope = scope;
- return this;
+ insert : function(index, comp) {
+ return this.add(index, comp);
},
/**
- * Sets this Button's text
- * @param {String} text The button text
- * @return {Ext.button.Button} this
+ * 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.
*/
- setText: function(text) {
- var me = this;
- me.text = text;
- if (me.el) {
- me.btnInnerEl.update(text || ' ');
- me.setButtonCls();
+ move : function(fromIdx, toIdx) {
+ var items = this.items,
+ item;
+ item = items.removeAt(fromIdx);
+ if (item === false) {
+ return false;
}
- me.doComponentLayout();
- return me;
+ items.insert(toIdx, item);
+ this.doLayout();
+ return item;
},
- /**
- * 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();
+ // @private
+ onBeforeAdd : function(item) {
+ var me = this;
+
+ if (item.ownerCt) {
+ item.ownerCt.remove(item, false);
}
- return me;
- },
- /**
- * Gets the text for this Button
- * @return {String} The button text
- */
- getText: function() {
- return this.text;
+ if (me.border === false || me.border === 0) {
+ item.border = (item.border === true);
+ }
},
/**
- * 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
+ * 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 {Ext.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.
*/
- 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]);
+ 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 me;
+
+ return c;
},
- /**
- * 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();
- }
+ // @private
+ doRemove : function(component, autoDestroy) {
+ var me = this,
+ layout = me.layout,
+ hasLayout = layout && me.rendered;
- me.menu.showBy(me.el, me.menuAlign);
+ me.items.remove(component);
+ component.onRemoved();
+
+ if (hasLayout) {
+ layout.onRemove(component);
}
- return me;
- },
- /**
- * Hide this button's menu (if it has one)
- */
- hideMenu: function() {
- if (this.hasVisibleMenu()) {
- this.menu.hide();
+ me.onRemove(component, autoDestroy);
+
+ if (autoDestroy === true || (autoDestroy !== false && me.autoDestroy)) {
+ component.destroy();
+ }
+
+ if (hasLayout && !autoDestroy) {
+ layout.afterRemove(component);
+ }
+
+ if (!me.destroying) {
+ me.doLayout();
}
- return this;
},
/**
- * Returns true if the button has a menu and it is visible
- * @return {Boolean}
+ * 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 {Ext.Component[]} Array of the destroyed components
*/
- hasVisibleMenu: function() {
- var menu = this.menu;
- return menu && menu.rendered && menu.isVisible();
- },
+ removeAll : function(autoDestroy) {
+ var me = this,
+ removeItems = me.items.items.slice(),
+ items = [],
+ i = 0,
+ len = removeItems.length,
+ item;
- // private
- onRepeatClick: function(repeat, e) {
- this.onClick(e);
- },
+ // Suspend Layouts while we remove multiple items from the container
+ me.suspendLayout = true;
+ for (; i < len; i++) {
+ item = removeItems[i];
+ me.remove(item, autoDestroy);
- // 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);
+ if (item.ownerCt !== me) {
+ items.push(item);
}
- 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);
+ // Resume Layouts now that all items have been removed and do a single layout (if we removed anything!)
+ me.suspendLayout = false;
+ if (len) {
+ me.doLayout();
}
+ return items;
},
- /**
- * @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);
+ // 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));
}
- me.onMouseLeave(e);
}
+
+ // 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;
},
/**
- * @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.
+ * 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] The scope of the function (defaults to current component)
+ * @param {Array} [args] The args to call the function with. The current component always passed as the last argument.
+ * @return {Ext.Container} this
*/
- onMouseMove: function(e) {
+ cascade : function(fn, scope, origArgs){
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();
- }
+ cs = me.items ? me.items.items : [],
+ len = cs.length,
+ i = 0,
+ c,
+ args = origArgs ? origArgs.concat(me) : [me],
+ componentIndex = args.length - 1;
- if (overlap > (btnSize - me.getTriggerSize())) {
- if (!over) {
- me.onMenuTriggerOver(e);
- }
- } else {
- if (over) {
- me.onMenuTriggerOut(e);
+ 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;
},
/**
- * @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.
+ * 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).
*/
- 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);
+ getComponent : function(comp) {
+ if (Ext.isObject(comp)) {
+ comp = comp.getItemId();
}
- return size;
+
+ return this.items.get(comp);
},
/**
- * @private virtual mouseenter handler called when it is detected that the mouseout event
- * signified the mouse entering the encapsulating element.
- * @param e
+ * Retrieves all descendant components which match the passed selector.
+ * Executes an Ext.ComponentQuery.query using this container as its root.
+ * @param {String} selector (optional) Selector complying to an Ext.ComponentQuery selector.
+ * If no selector is specified all items will be returned.
+ * @return {Ext.Component[]} Components which matched the selector
*/
- onMouseEnter: function(e) {
- var me = this;
- me.addClsWithUI(me.overCls);
- me.fireEvent('mouseover', me, e);
+ query : function(selector) {
+ selector = selector || '*';
+ return Ext.ComponentQuery.query(selector, this);
},
/**
- * @private virtual mouseleave handler called when it is detected that the mouseover event
- * signified the mouse entering the encapsulating element.
- * @param e
+ * 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 (optional) An Ext.ComponentQuery selector. If no selector is
+ * specified, the first child will be returned.
+ * @return Ext.Component
*/
- onMouseLeave: function(e) {
- var me = this;
- me.removeClsWithUI(me.overCls);
- me.fireEvent('mouseout', me, e);
+ child : function(selector) {
+ selector = selector || '';
+ return this.query('> ' + selector)[0] || null;
},
/**
- * @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
+ * 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 (optional) An Ext.ComponentQuery selector. If no selector is
+ * specified, the first child will be returned.
+ * @return Ext.Component
*/
- onMenuTriggerOver: function(e) {
- var me = this;
- me.overMenuTrigger = true;
- me.fireEvent('menutriggerover', me, me.menu, e);
+ down : function(selector) {
+ return this.query(selector)[0] || null;
},
- /**
- * @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;
+ show : function() {
+ this.callParent(arguments);
+ this.performDeferredLayouts();
+ return this;
+ },
- me.callParent(arguments);
-
- me.removeClsWithUI('disabled');
+ // 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;
- return me;
+ 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();
},
- // inherit docs
- disable : function(silent) {
- var me = this;
-
- me.callParent(arguments);
-
- me.addClsWithUI('disabled');
+ //@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();
+ },
- return me;
+ // @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();
},
-
+
/**
- * Method to change the scale of the button. See {@link #scale} for allowed configurations.
- * @param {String} scale The scale to change to.
+ * Occurs before componentLayout is run. Returning false from this method will prevent the containerLayout
+ * from being executed.
*/
- setScale: function(scale) {
+ beforeLayout: function() {
+ return true;
+ },
+
+ // @private
+ beforeDestroy : function() {
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(', ') + ')');
+ items = me.items,
+ c;
+
+ if (items) {
+ while ((c = items.first())) {
+ me.doRemove(c, true);
+ }
}
-
- 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;
+
+ Ext.destroy(
+ me.layout
+ );
+ me.callParent();
+ }
+});
+
+/**
+ * Base class for any 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 Ext.panel.Panel, Ext.window.Window and
+ * 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.
+ *
+ * The code below illustrates how to explicitly create a Container:
+ *
+ * @example
+ * // 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 {@link 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
+ * Ext.layout.container.Card, Ext.layout.container.Anchor, Ext.layout.container.VBox,
+ * Ext.layout.container.HBox, and 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.
+ */
+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 {Ext.Element/HTMLElement/String} el The element to test (or ID of element).
+ * @return {Ext.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;
+ }
+});
+
+/**
+ * A non-rendering placeholder item which instructs the Toolbar's Layout to begin using
+ * the right-justified button container.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Toolbar Fill Example',
+ * width: 300,
+ * height: 200,
+ * tbar : [
+ * 'Item 1',
+ * { xtype: 'tbfill' },
+ * 'Item 2'
+ * ],
+ * renderTo: Ext.getBody()
+ * });
+ */
+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.
+ */
+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').
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Toolbar Seperator Example',
+ * width: 300,
+ * height: 200,
+ * tbar : [
+ * 'Item 1',
+ * { xtype: 'tbseparator' },
+ * 'Item 2'
+ * ],
+ * renderTo: Ext.getBody()
+ * });
+ */
+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.callParent([ui]);
-
- // Set all the state classNames, as they need to include the UI
- // me.disabledCls += ' ' + me.baseCls + '-' + me.ui + '-disabled';
+ me.active = Ext.create('Ext.util.MixedCollection');
+ Ext.getDoc().addKeyListener(27, function() {
+ if (me.active.length > 0) {
+ me.hideAll();
+ }
+ }, me);
},
-
- // private
- onFocus: function(e) {
- var me = this;
- if (!me.disabled) {
- me.addClsWithUI(me.focusCls);
+
+ /**
+ * 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;
},
- // private
- onBlur: function(e) {
- var me = this;
- me.removeClsWithUI(me.focusCls);
+ 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;
+ }
},
- // private
- onMouseDown: function(e) {
- var me = this;
- if (!me.disabled && e.button === 0) {
- me.addClsWithUI(me.pressedCls);
- me.doc.on('mouseup', me.onMouseUp, me);
+ 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();
},
- // 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);
+
+ onBeforeHide: function(m) {
+ if (m.activeChild) {
+ m.activeChild.hide();
+ }
+ if (m.autoHideTimer) {
+ clearTimeout(m.autoHideTimer);
+ delete m.autoHideTimer;
}
- },
- // 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);
+ 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
- restoreClick: function() {
- this.ignoreNextClick = 0;
+ onMouseDown: function(e) {
+ var me = this,
+ active = me.active,
+ lastShow = me.lastShow,
+ target = e.target;
+
+ if (Ext.Date.getElapsed(lastShow) > 50 && active.length > 0 && !e.getTarget('.' + Ext.baseCSSPrefix + 'menu')) {
+ me.hideAll();
+ // in IE, if we mousedown on a focusable element, the focus gets cancelled and the focus event is never
+ // fired on the element, so we'll focus it here
+ if (Ext.isIE && Ext.fly(target).focusable()) {
+ target.focus();
+ }
+ }
},
// private
- onDownKey: function() {
+ register: function(menu) {
var me = this;
- if (!me.disabled) {
- if (me.menu) {
- me.showMenu();
- }
+ 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
+ });
}
},
/**
- * @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]
+ * 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
*/
- getPersistentBtnPadding: function() {
- var cls = Ext.button.Button,
- padding = cls.persistentPadding,
- btn, leftTop, btnEl, btnInnerEl;
+ 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');
+ }
+ },
- if (!padding) {
- padding = cls.persistentPadding = [0, 0, 0, 0]; //set early to prevent recursion
+ // private
+ unregister: function(menu) {
+ var me = this,
+ menus = me.menus,
+ active = me.active;
- 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
+ delete menus[menu.id];
+ active.remove(menu);
+ menu.un({
+ beforehide: me.onBeforeHide,
+ hide: me.onHide,
+ beforeshow: me.onBeforeShow,
+ show: me.onShow,
+ scope: me
+ });
+ },
- 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];
+ // private
+ registerCheckable: function(menuItem) {
+ var groups = this.groups,
+ groupId = menuItem.group;
- btn.destroy();
+ if (groupId) {
+ if (!groups[groupId]) {
+ groups[groupId] = [];
}
+
+ groups[groupId].push(menuItem);
}
+ },
- return padding;
- }
+ // private
+ unregisterCheckable: function(menuItem) {
+ var groups = this.groups,
+ groupId = menuItem.group;
-}, function() {
- var groups = {},
- g, i, l;
+ if (groupId) {
+ Ext.Array.remove(groups[groupId], menuItem);
+ }
+ },
- 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);
+ 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);
}
}
}
}
- // 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);
- },
+});
+/**
+ * Component layout for buttons
+ * @class Ext.layout.component.Button
+ * @extends Ext.layout.component.Component
+ * @private
+ */
+Ext.define('Ext.layout.component.Button', {
- 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;
- }
- };
-});
+ /* Begin Definitions */
-/**
- * @class Ext.layout.container.boxOverflow.Menu
- * @extends Ext.layout.container.boxOverflow.None
- * @private
- */
-Ext.define('Ext.layout.container.boxOverflow.Menu', {
+ alias: ['layout.button'],
- /* Begin Definitions */
+ extend: 'Ext.layout.component.Component',
- 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
- */
+ type: 'button',
+
+ cellClsRE: /-btn-(tl|br)\b/,
+ htmlRE: /<.*>/,
+
+ beforeLayout: function() {
+ return this.callParent(arguments) || this.lastText !== this.owner.text;
+ },
/**
- * @property noItemsMenuText
- * @type String
- * HTML fragment to render into the toolbar overflow menu if there are no items to display
+ * Set the dimensions of the inner <button> element to match the
+ * component dimensions.
*/
- noItemsMenuText : '(None)
',
-
- constructor: function(layout) {
- var me = this;
+ onLayout: function(width, height) {
+ var me = this,
+ isNum = Ext.isNumber,
+ owner = me.owner,
+ ownerEl = owner.el,
+ btnEl = owner.btnEl,
+ btnInnerEl = owner.btnInnerEl,
+ btnIconEl = owner.btnIconEl,
+ sizeIconEl = (owner.icon || owner.iconCls) && (owner.iconAlign == "top" || owner.iconAlign == "bottom"),
+ minWidth = owner.minWidth,
+ maxWidth = owner.maxWidth,
+ ownerWidth, btnFrameWidth, metrics;
+ me.getTargetInfo();
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);
+ btnInnerEl.unclip();
+ me.setTargetSize(width, height);
- 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 = [];
- },
-
- onRemove: function(comp){
- Ext.Array.remove(this.menuItems, comp);
+ 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.isIE6 || 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);
+
+ if (sizeIconEl) {
+ btnIconEl.setWidth(metrics.width + btnFrameWidth);
+ }
+ } else {
+ // Remove any previous fixed widths
+ ownerEl.setWidth(null);
+ btnEl.setWidth(null);
+ btnInnerEl.setWidth(null);
+ btnIconEl.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;
},
- handleOverflow: function(calculations, targetSize) {
+ setTargetSize: function(width, height) {
var me = this,
- layout = me.layout,
- methodName = 'get' + layout.parallelPrefixCap,
- newSize = {},
- posArgs = [null, null];
+ 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);
- 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);
+ me.setElementSize(owner.btnEl, btnWidth, btnHeight);
+ me.setElementSize(btnInnerEl, btnWidth, btnHeight);
+ if (btnHeight >= 0) {
+ btnInnerEl.setStyle('line-height', btnHeight - btnFrameHeight + 'px');
+ }
- return { targetSize: newSize };
+ // 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);
+ }
},
- /**
- * @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) {
+ getTargetInfo: function() {
var me = this,
- newWidth = targetSize ? targetSize.width + (me.afterCt ? me.afterCt.getWidth() : 0) : 0,
- items = me.menuItems,
- i = 0,
- length = items.length,
- item;
+ owner = me.owner,
+ ownerEl = owner.el,
+ frameSize = me.frameSize,
+ frameBody = owner.frameBody,
+ btnWrap = owner.btnWrap,
+ innerEl = owner.btnInnerEl;
- me.hideTrigger();
- for (; i < length; i++) {
- items[i].show();
+ 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')
+ });
}
- items.length = 0;
- return targetSize ? {
- targetSize: {
- height: targetSize.height,
- width : newWidth
- }
- } : null;
- },
+ return me.callParent();
+ }
+});
+/**
+ * @docauthor Robert Dougan
+ *
+ * Create simple buttons with this component. Customisations include {@link #iconAlign aligned}
+ * {@link #iconCls icons}, {@link #menu dropdown menus}, {@link #tooltip tooltips}
+ * and {@link #scale sizing options}. Specify a {@link #handler handler} to run code when
+ * a user clicks the button, or use {@link #listeners listeners} for other events such as
+ * {@link #mouseover mouseover}. Example usage:
+ *
+ * @example
+ * 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:
+ *
+ * @example
+ * Ext.create('Ext.Button', {
+ * text : 'Dynamic 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:
+ *
+ * @example
+ * 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'`
+ *
+ * Example usage:
+ *
+ * @example
+ * 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`.
+ * Example usage:
+ *
+ * @example
+ * 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. Example usage:
+ *
+ * @example
+ * 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:
+ *
+ * @example
+ * 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.');
+ * }
+ * }
+ * }
+ * });
+ */
+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',
/**
- * @private
+ * @property {Boolean} hidden
+ * True if this button is hidden. Read-only.
*/
- showTrigger: function() {
- this.menuTrigger.show();
- },
+ hidden: false,
/**
- * @private
+ * @property {Boolean} disabled
+ * True if this button is disabled. Read-only.
*/
- hideTrigger: function() {
- if (this.menuTrigger !== undefined) {
- this.menuTrigger.hide();
- }
- },
+ disabled: false,
/**
- * @private
- * Called before the overflow menu is shown. This constructs the menu's items, caching them for as long as it can.
+ * @property {Boolean} pressed
+ * True if this button is pressed (only if enableToggle = true). Read-only.
*/
- 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);
- };
+ pressed: false,
- me.clearMenu();
- menu.removeAll();
+ /**
+ * @cfg {String} text
+ * The button text to be used as innerHTML (html tags are accepted).
+ */
- for (; i < len; i++) {
- item = items[i];
+ /**
+ * @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')
+ */
- // 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('-');
- }
+ /**
+ * @cfg {Function} handler
+ * A function called when the button is clicked (can be used instead of click event).
+ * @cfg {Ext.button.Button} handler.button This button.
+ * @cfg {Ext.EventObject} handler.e The click event.
+ */
- me.addComponentToMenu(menu, item);
- prev = item;
- }
+ /**
+ * @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}.
+ */
- // 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
+ * @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.
*/
- createMenuConfig : function(component, hideOnClick) {
- var config = Ext.apply({}, component.initialConfig),
- group = component.toggleGroup;
- Ext.copyTo(config, component, [
- 'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu'
- ]);
+ /**
+ * @cfg {Boolean} [hidden=false]
+ * True to start hidden.
+ */
- Ext.apply(config, {
- text : component.overflowText || component.text,
- hideOnClick: hideOnClick,
- destroyMenu: false
- });
+ /**
+ * @cfg {Boolean} [disabled=true]
+ * True to start disabled.
+ */
- if (group || component.enableToggle) {
- Ext.apply(config, {
- group : group,
- checked: component.pressed,
- listeners: {
- checkchange: function(item, checked){
- component.toggle(checked);
- }
- }
- });
- }
+ /**
+ * @cfg {Boolean} [pressed=false]
+ * True to start pressed (only if enableToggle = true)
+ */
- delete config.ownerCt;
- delete config.xtype;
- delete config.id;
- return config;
- },
+ /**
+ * @cfg {String} toggleGroup
+ * The group this toggle button is a member of (only 1 per group can be pressed)
+ */
/**
- * @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
+ * @cfg {Boolean/Object} [repeat=false]
+ * True to repeat fire the click event while the mouse is down. This can also be a
+ * {@link Ext.util.ClickRepeater ClickRepeater} config object.
*/
- 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));
+ /**
+ * @cfg {Number} tabIndex
+ * Set a DOM tabIndex for this button.
+ */
- } 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)));
- }
- }
- },
+ /**
+ * @cfg {Boolean} [allowDepress=true]
+ * False to not allow a pressed Button to be depressed. Only valid when {@link #enableToggle} is true.
+ */
/**
- * @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
+ * @cfg {Boolean} [enableToggle=false]
+ * True to enable pressed/not pressed toggling.
*/
- clearMenu : function() {
- var menu = this.moreMenu;
- if (menu && menu.items) {
- menu.items.each(function(item) {
- if (item.menu) {
- delete item.menu;
- }
- });
- }
- },
+ enableToggle: false,
/**
- * @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
+ * @cfg {Function} toggleHandler
+ * Function called when a Button with {@link #enableToggle} set to true is clicked.
+ * @cfg {Ext.button.Button} toggleHandler.button This button.
+ * @cfg {Boolean} toggleHandler.state The next state of the Button, true means pressed.
*/
- 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();
+ /**
+ * @cfg {Ext.menu.Menu/String/Object} menu
+ * Standard menu attribute consisting of a reference to a menu object, a menu id or a menu config blob.
+ */
- /**
- * @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
- }
- });
+ /**
+ * @cfg {String} menuAlign
+ * The position to align the menu to (see {@link Ext.Element#alignTo} for more details).
+ */
+ menuAlign: 'tl-bl?',
- /**
- * @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();
+ /**
+ * @cfg {String} textAlign
+ * The text alignment for this button (center, left, right).
+ */
+ textAlign: 'center',
- // 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();
- }
- }
- },
+ /**
+ * @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}`.
+ */
/**
- * @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
+ * @cfg {String} iconCls
+ * A css class which sets a background image to be used as the icon for this button.
*/
- 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');
- }
- },
+ /**
+ * @cfg {String} type
+ * The type of ` ` to create: submit, reset or button.
+ */
+ type: 'button',
/**
- * @private
+ * @cfg {String} clickEvent
+ * The DOM event that will fire the handler of the button. This can be any valid event name (dblclick, contextmenu).
*/
- 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.
- */
+ clickEvent: 'click',
-Ext.define('Ext.util.Region', {
+ /**
+ * @cfg {Boolean} preventDefault
+ * True to prevent the default action when the {@link #clickEvent} is processed.
+ */
+ preventDefault: true,
- /* Begin Definitions */
+ /**
+ * @cfg {Boolean} handleMouseEvents
+ * False to disable visual cues on mouseover, mouseout and mousedown.
+ */
+ handleMouseEvents: true,
- 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);
- },
+ /**
+ * @cfg {String} tooltipType
+ * The type of tooltip to use. Either 'qtip' for QuickTips or 'title' for title attribute.
+ */
+ tooltipType: 'qtip',
- /**
- * @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);
- }
- },
+ /**
+ * @cfg {String} [baseCls='x-btn']
+ * The base CSS class to add to all buttons.
+ */
+ baseCls: Ext.baseCSSPrefix + 'btn',
- /* End Definitions */
+ /**
+ * @cfg {String} pressedCls
+ * The CSS class to add to a button when it is in the pressed state.
+ */
+ pressedCls: 'pressed',
/**
- * @constructor
- * @param {Number} top Top
- * @param {Number} right Right
- * @param {Number} bottom Bottom
- * @param {Number} left Left
+ * @cfg {String} overCls
+ * The CSS class to add to a button when it is in the over (hovered) state.
*/
- 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;
- },
+ overCls: 'over',
/**
- * Checks if this region completely contains the region that is passed in.
- * @param {Ext.util.Region} region
+ * @cfg {String} focusCls
+ * The CSS class to add to a button when it is in the focussed state.
*/
- contains : function(region) {
- var me = this;
- return (region.x >= me.x &&
- region.right <= me.right &&
- region.y >= me.y &&
- region.bottom <= me.bottom);
+ focusCls: 'focus',
- },
+ /**
+ * @cfg {String} menuActiveCls
+ * The CSS class to add to a button when it's menu is active.
+ */
+ menuActiveCls: 'menu-active',
/**
- * 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.
+ * @cfg {String} href
+ * The URL to visit when the button is clicked. Specifying this config is equivalent to specifying:
+ *
+ * handler: function() { window.location = "http://www.sencha.com" }
*/
- 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;
- }
- },
+ /**
+ * @cfg {Object} baseParams
+ * An object literal of parameters to pass to the url when the {@link #href} property is specified.
+ */
/**
- * Returns the smallest region that contains the current AND targetRegion.
- * @param {Ext.util.Region} region
+ * @cfg {Object} params
+ * An object literal of parameters to pass to the url when the {@link #href} property is specified. Any params
+ * override {@link #baseParams}. New params can be set using the {@link #setParams} method.
*/
- 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);
- },
+ ariaRole: 'button',
+
+ // inherited
+ renderTpl:
+ '' +
+ '' +
+ ' tabIndex="{tabIndex}" role="link">' +
+ '' +
+ '{text}' +
+ ' ' +
+ ' ' +
+ '' +
+ '' +
+ '' +
+ ' tabIndex="{tabIndex}" role="button" autocomplete="off">' +
+ '' +
+ '{text}' +
+ ' ' +
+ ' ' +
+ '' +
+ '' +
+ ' ' ,
/**
- * Modifies the current region to be constrained to the targetRegion.
- * @param {Ext.util.Region} targetRegion
+ * @cfg {String} scale
+ * 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.
*/
- 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;
- },
+ scale: 'small',
/**
- * 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
+ * @private
+ * An array of allowed scales.
*/
- 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;
- },
+ allowedScales: ['small', 'medium', 'large'],
/**
- * 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}
+ * @cfg {Object} scope
+ * The scope (**this** reference) in which the `{@link #handler}` and `{@link #toggleHandler}` is executed.
+ * Defaults to this Button.
*/
- 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}
+ * @cfg {String} iconAlign
+ * The side of the Button box to render the icon. Four values are allowed:
+ *
+ * - 'top'
+ * - 'right'
+ * - 'bottom'
+ * - 'left'
*/
- getOutOfBoundOffsetX: function(p) {
- if (p <= this.x) {
- return this.x - p;
- } else if (p >= this.right) {
- return this.right - p;
- }
-
- return 0;
- },
+ iconAlign: 'left',
/**
- * Get the offset amount on the y-axis
- * @param {Number} p the offset
- * @return {Number}
+ * @cfg {String} arrowAlign
+ * The side of the Button box to render the arrow if the button has an associated {@link #menu}. Two
+ * values are allowed:
+ *
+ * - 'right'
+ * - 'bottom'
*/
- getOutOfBoundOffsetY: function(p) {
- if (p <= this.y) {
- return this.y - p;
- } else if (p >= this.bottom) {
- return this.bottom - p;
- }
+ arrowAlign: 'right',
- return 0;
- },
+ /**
+ * @cfg {String} arrowCls
+ * The className used for the inner arrow element if the button has a menu.
+ */
+ arrowCls: 'arrow',
/**
- * Check whether the point / offset is out of bound
- * @param {String} axis optional
- * @param {Ext.util.Point/Number} p the point / offset
- * @return {Boolean}
+ * @property {Ext.Template} template
+ * 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}.
*/
- 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}
+ * @cfg {String} cls
+ * A CSS class string to apply to the button's main element.
*/
- 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}
+ * @property {Ext.menu.Menu} menu
+ * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the {@link #menu} config
+ * option.
*/
- 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}
+ /**
+ * @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.
*/
- restrict: function(axis, p, factor) {
- if (Ext.isObject(axis)) {
- var newP;
- factor = p;
- p = axis;
+ maskOnDisable: false,
- if (p.copy) {
- newP = p.copy();
- }
- else {
- newP = {
- x: p.x,
- y: p.y
- };
- }
+ // inherit docs
+ initComponent: function() {
+ var me = this;
+ me.callParent(arguments);
- 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);
- }
- }
- },
+ me.addEvents(
+ /**
+ * @event click
+ * Fires when this button is clicked
+ * @param {Ext.button.Button} this
+ * @param {Event} e The click event
+ */
+ 'click',
- /*
- * 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;
- }
+ /**
+ * @event toggle
+ * Fires when the 'pressed' state of this button changes (only if enableToggle = true)
+ * @param {Ext.button.Button} this
+ * @param {Boolean} pressed
+ */
+ 'toggle',
- if (p <= this.x) {
- p -= (p - this.x) * factor;
+ /**
+ * @event mouseover
+ * Fires when the mouse hovers over the button
+ * @param {Ext.button.Button} this
+ * @param {Event} e The event object
+ */
+ 'mouseover',
+
+ /**
+ * @event mouseout
+ * Fires when the mouse exits the button
+ * @param {Ext.button.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 {Ext.button.Button} this
+ * @param {Ext.menu.Menu} menu
+ */
+ 'menushow',
+
+ /**
+ * @event menuhide
+ * If this button has a menu, this event fires when it is hidden
+ * @param {Ext.button.Button} this
+ * @param {Ext.menu.Menu} menu
+ */
+ 'menuhide',
+
+ /**
+ * @event menutriggerover
+ * If this button has a menu, this event fires when the mouse enters the menu triggering element
+ * @param {Ext.button.Button} this
+ * @param {Ext.menu.Menu} menu
+ * @param {Event} e
+ */
+ 'menutriggerover',
+
+ /**
+ * @event menutriggerout
+ * If this button has a menu, this event fires when the mouse leaves the menu triggering element
+ * @param {Ext.button.Button} this
+ * @param {Ext.menu.Menu} menu
+ * @param {Event} 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;
}
- else if (p >= this.right) {
- p -= (p - this.right) * factor;
+
+ // Accept url as a synonym for href
+ if (me.url) {
+ me.href = me.url;
}
- 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;
+ // preventDefault defaults to false for links
+ if (me.href && !me.hasOwnProperty('preventDefault')) {
+ me.preventDefault = false;
}
- if (p <= this.y) {
- p -= (p - this.y) * factor;
+ if (Ext.isString(me.toggleGroup)) {
+ me.enableToggle = true;
}
- else if (p >= this.bottom) {
- p -= (p - this.bottom) * factor;
+
+ },
+
+ // private
+ initAria: function() {
+ this.callParent();
+ var actionEl = this.getActionEl();
+ if (this.menu) {
+ actionEl.dom.setAttribute('aria-haspopup', true);
}
- 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
- };
+ // inherit docs
+ getActionEl: function() {
+ return this.btnEl;
},
- /**
- * Copy a new instance
- * @return {Ext.util.Region}
- */
- copy: function() {
- return new this.self(this.y, this.right, this.bottom, this.x);
+ // inherit docs
+ getFocusEl: function() {
+ return this.btnEl;
},
- /**
- * 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;
+ // private
+ setButtonCls: function() {
+ var me = this,
+ cls = [],
+ btnIconEl = me.btnIconEl,
+ hide = 'x-hide-display';
- return this;
- },
+ if (me.useSetClass) {
+ if (!Ext.isEmpty(me.oldCls)) {
+ me.removeClsWithUI(me.oldCls);
+ me.removeClsWithUI(me.pressedCls);
+ }
- /**
- * Dump this to an eye-friendly string, great for debugging
- * @return {String}
- */
- toString: function() {
- return "Region[" + this.top + "," + this.right + "," + this.bottom + "," + this.left + "]";
+ // 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');
+ }
+ if (btnIconEl) {
+ btnIconEl.removeCls(hide);
+ }
+ } else {
+ if (me.text) {
+ cls.push('noicon');
+ }
+ if (btnIconEl) {
+ btnIconEl.addCls(hide);
+ }
+ }
+
+ 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());
- /**
- * 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;
+ me.addChildEls('btnEl', 'btnWrap', 'btnInnerEl', 'btnIconEl');
+
+ if (me.scale) {
+ me.ui = me.ui + '-' + me.scale;
}
- var me = this;
- me.top = me.y += y;
- me.right += x;
- me.bottom += y;
- me.left = me.x += x;
- return me;
- },
+ // Render internal structure
+ me.callParent(arguments);
- /**
- * 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);
+ // If it is a split button + has a toolip for the arrow
+ if (me.split && me.arrowTooltip) {
+ me.arrowEl.dom.setAttribute(me.getTipAttr(), me.arrowTooltip);
+ }
- return me;
- },
+ // Add listeners to the focus and blur events on the element
+ me.mon(me.btnEl, {
+ scope: me,
+ focus: me.onFocus,
+ blur : me.onBlur
+ });
- /**
- * 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);
- }
-});
+ // Set btn as a local variable for easy access
+ btn = me.el;
-/*
- * 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
- */
+ if (me.icon) {
+ me.setIcon(me.icon);
+ }
+ if (me.iconCls) {
+ me.setIconCls(me.iconCls);
+ }
-/**
- * @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,
+ if (me.tooltip) {
+ me.setTooltip(me.tooltip, true);
+ }
- requires: ['Ext.util.Region'],
+ if (me.textAlign) {
+ me.setTextAlign(me.textAlign);
+ }
- uses: ['Ext.tip.QuickTipManager'],
+ // Add the mouse events to the button
+ if (me.handleMouseEvents) {
+ me.mon(btn, {
+ scope: me,
+ mouseover: me.onMouseOver,
+ mouseout: me.onMouseOut,
+ mousedown: me.onMouseDown
+ });
- // 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: {},
+ if (me.split) {
+ me.mon(btn, {
+ mousemove: me.onMouseMove,
+ scope: me
+ });
+ }
+ }
- /**
- * 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: {},
+ // Check if the button has a menu
+ if (me.menu) {
+ me.mon(me.menu, {
+ scope: me,
+ show: me.onMenuShow,
+ hide: me.onMenuHide
+ });
- /**
- * the DragDrop object that is currently being dragged
- * @property dragCurrent
- * @type DragDrop
- * @private
- * @static
- **/
- dragCurrent: null,
+ me.keyMap = Ext.create('Ext.util.KeyMap', me.el, {
+ key: Ext.EventObject.DOWN,
+ handler: me.onDownKey,
+ scope: me
+ });
+ }
- /**
- * the DragDrop object(s) that are being hovered over
- * @property dragOvers
- * @type Array
- * @private
- * @static
- */
- dragOvers: {},
+ // 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);
+ }
- /**
- * the X distance between the cursor and the object being dragged
- * @property deltaX
- * @type int
- * @private
- * @static
- */
- deltaX: 0,
+ // Register the button in the toggle manager
+ Ext.ButtonToggleManager.register(me);
+ },
/**
- * the Y distance between the cursor and the object being dragged
- * @property deltaY
- * @type int
- * @private
- * @static
+ * 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.
+ *
+ * @return {Object} Substitution data for a Template. The default implementation which provides data for the default
+ * {@link #template} returns an Object containing the following properties:
+ * @return {String} return.type The ``'s {@link #type}
+ * @return {String} return.splitCls A CSS class to determine the presence and position of an arrow icon.
+ * (`'x-btn-arrow'` or `'x-btn-arrow-bottom'` or `''`)
+ * @return {String} return.cls A CSS class name applied to the Button's main `` element which determines the
+ * button's scale and icon alignment.
+ * @return {String} return.text The {@link #text} to display ion the Button.
+ * @return {Number} return.tabIndex The tab index within the input flow.
*/
- deltaY: 0,
+ getTemplateArgs: function() {
+ var me = this,
+ persistentPadding = me.getPersistentBtnPadding(),
+ innerSpanStyle = '';
- /**
- * 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,
+ // 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(' ');
+ }
- /**
- * 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,
+ return {
+ href : me.getHref(),
+ target : me.target || '_blank',
+ type : me.type,
+ splitCls : me.getSplitCls(),
+ cls : me.cls,
+ iconCls : me.iconCls || '',
+ text : me.text || ' ',
+ tabIndex : me.tabIndex,
+ innerSpanStyle: innerSpanStyle
+ };
+ },
/**
- * Internal flag that is set to true when drag and drop has been
- * intialized
- * @property initialized
* @private
- * @static
+ * If there is a configured href for this Button, returns the href with parameters appended.
+ * @returns The href string with parameters appended.
*/
- initialized: false,
+ getHref: function() {
+ var me = this,
+ params = Ext.apply({}, me.baseParams);
- /**
- * All drag and drop can be disabled.
- * @property locked
- * @private
- * @static
- */
- locked: false,
+ // write baseParams first, then write any params
+ params = Ext.apply(params, me.params);
+ return me.href ? Ext.urlAppend(me.href, Ext.Object.toQueryString(params)) : false;
+ },
/**
- * Called the first time an element is registered.
- * @method init
- * @private
- * @static
+ * Sets the href of the link dynamically according to the params passed, and any {@link #baseParams} configured.
+ *
+ * **Only valid if the Button was originally configured with a {@link #href}**
+ *
+ * @param {Object} params Parameters to use in the href URL.
*/
- init: function() {
- this.initialized = true;
+ setParams: function(params) {
+ this.params = params;
+ this.btnEl.dom.href = this.getHref();
},
- /**
- * 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,
+ getSplitCls: function() {
+ var me = this;
+ return me.split ? (me.baseCls + '-' + me.arrowCls) + ' ' + (me.baseCls + '-' + me.arrowCls + '-' + me.arrowAlign) : '';
+ },
- /**
- * 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,
+ // private
+ afterRender: function() {
+ var me = this;
+ me.useSetClass = true;
+ me.setButtonCls();
+ me.doc = Ext.getDoc();
+ this.callParent(arguments);
+ },
/**
- * The current drag and drop mode. Default: POINT
- * @property mode
- * @type int
- * @static
+ * 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
*/
- mode: 0,
+ setIconCls: function(cls) {
+ var me = this,
+ btnIconEl = me.btnIconEl,
+ oldCls = me.iconCls;
+
+ me.iconCls = cls;
+ if (btnIconEl) {
+ // Remove the previous iconCls from the button
+ btnIconEl.removeCls(oldCls);
+ btnIconEl.addCls(cls || '');
+ me.setButtonCls();
+ }
+ return me;
+ },
/**
- * Runs method on all drag and drop objects
- * @method _execOnAll
- * @private
- * @static
+ * 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
*/
- _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);
+ 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(me.getTipAttr(), tooltip);
}
+ } else {
+ me.tooltip = tooltip;
}
+ return me;
},
/**
- * Drag and drop initialization. Sets up the global event handlers
- * @method _onLoad
- * @private
- * @static
+ * Sets the text alignment for this button.
+ * @param {String} align The new alignment of the button text. See {@link #textAlign}.
*/
- _onLoad: function() {
+ setTextAlign: function(align) {
+ var me = this,
+ btnEl = me.btnEl;
- this.init();
+ if (btnEl) {
+ btnEl.removeCls(me.baseCls + '-' + me.textAlign);
+ btnEl.addCls(me.baseCls + '-' + align);
+ }
+ me.textAlign = align;
+ return me;
+ },
- 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);
+ getTipAttr: function(){
+ return this.tooltipType == 'qtip' ? 'data-qtip' : 'title';
+ },
+ // private
+ getRefItems: function(deep){
+ var menu = this.menu,
+ items;
+
+ if (menu) {
+ items = menu.getRefItems(deep);
+ items.unshift(menu);
+ }
+ return items || [];
},
- /**
- * Reset constraints on all drag and drop objs
- * @method _onResize
- * @private
- * @static
- */
- _onResize: function(e) {
- this._execOnAll("resetConstraints", []);
+ // private
+ clearTip: function() {
+ if (Ext.isObject(this.tooltip)) {
+ Ext.tip.QuickTipManager.unregister(this.btnEl);
+ }
},
- /**
- * Lock all drag and drop functionality
- * @method lock
- * @static
- */
- lock: function() { this.locked = true; },
+ // private
+ beforeDestroy: function() {
+ var me = this;
+ if (me.rendered) {
+ me.clearTip();
+ }
+ if (me.menu && me.destroyMenu !== false) {
+ Ext.destroy(me.menu);
+ }
+ Ext.destroy(me.btnInnerEl, me.repeater);
+ me.callParent();
+ },
- /**
- * Unlock all drag and drop functionality
- * @method unlock
- * @static
- */
- unlock: function() { this.locked = false; },
+ // 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;
+ Ext.ButtonToggleManager.unregister(me);
- /**
- * Is drag and drop locked?
- * @method isLocked
- * @return {boolean} True if drag and drop is locked, false otherwise.
- * @static
- */
- isLocked: function() { return this.locked; },
+ Ext.destroy(me.keyMap);
+ delete me.keyMap;
+ }
+ me.callParent();
+ },
/**
- * 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
+ * Assigns this Button's click handler
+ * @param {Function} handler The function to call when the button is clicked
+ * @param {Object} [scope] The scope (`this` reference) in which the handler function is executed.
+ * Defaults to this Button.
+ * @return {Ext.button.Button} this
*/
- locationCache: {},
+ setHandler: function(handler, scope) {
+ this.handler = handler;
+ this.scope = scope;
+ return this;
+ },
/**
- * 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
+ * Sets this Button's text
+ * @param {String} text The button text
+ * @return {Ext.button.Button} this
*/
- useCache: true,
+ setText: function(text) {
+ var me = this;
+ me.text = text;
+ if (me.el) {
+ me.btnInnerEl.update(text || ' ');
+ me.setButtonCls();
+ }
+ me.doComponentLayout();
+ return me;
+ },
/**
- * 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
+ * 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
*/
- clickPixelThresh: 3,
+ setIcon: function(icon) {
+ var me = this,
+ iconEl = me.btnIconEl;
+
+ me.icon = icon;
+ if (iconEl) {
+ iconEl.setStyle('background-image', icon ? 'url(' + icon + ')': '');
+ me.setButtonCls();
+ }
+ return me;
+ },
/**
- * 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
+ * Gets the text for this Button
+ * @return {String} The button text
*/
- clickTimeThresh: 350,
+ getText: function() {
+ return this.text;
+ },
/**
- * Flag that indicates that either the drag pixel threshold or the
- * mousdown time threshold has been met
- * @property dragThreshMet
- * @type boolean
- * @private
- * @static
+ * If a state it passed, it becomes the pressed state otherwise the current state is toggled.
+ * @param {Boolean} [state] Force a particular state
+ * @param {Boolean} [suppressEvent=false] True to stop events being fired when calling this method.
+ * @return {Ext.button.Button} this
*/
- dragThreshMet: false,
+ 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;
+ },
+
+ maybeShowMenu: function(){
+ var me = this;
+ if (me.menu && !me.hasVisibleMenu() && !me.ignoreNextClick) {
+ me.showMenu();
+ }
+ },
/**
- * Timeout used for the click time threshold
- * @property clickTimeout
- * @type Object
- * @private
- * @static
+ * Shows this button's menu (if it has one)
*/
- clickTimeout: null,
+ showMenu: function() {
+ var me = this;
+ if (me.rendered && me.menu) {
+ if (me.tooltip && me.getTipAttr() != 'title') {
+ Ext.tip.QuickTipManager.getQuickTip().cancelShow(me.btnEl);
+ }
+ if (me.menu.isVisible()) {
+ me.menu.hide();
+ }
- /**
- * 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,
+ me.menu.showBy(me.el, me.menuAlign);
+ }
+ return me;
+ },
/**
- * The Y position of the mousedown event stored for later use when a
- * drag threshold is met.
- * @property startY
- * @type int
- * @private
- * @static
+ * Hides this button's menu (if it has one)
*/
- startY: 0,
+ hideMenu: function() {
+ if (this.hasVisibleMenu()) {
+ this.menu.hide();
+ }
+ return this;
+ },
/**
- * 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
+ * Returns true if the button has a menu and it is visible
+ * @return {Boolean}
*/
- regDragDrop: function(oDD, sGroup) {
- if (!this.initialized) { this.init(); }
+ hasVisibleMenu: function() {
+ var menu = this.menu;
+ return menu && menu.rendered && menu.isVisible();
+ },
- if (!this.ids[sGroup]) {
- this.ids[sGroup] = {};
+ // 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) {
+ me.doToggle();
+ me.maybeShowMenu();
+ me.fireHandler(e);
+ }
+ },
+
+ fireHandler: function(e){
+ var me = this,
+ handler = me.handler;
+
+ me.fireEvent('click', me, e);
+ if (handler) {
+ handler.call(me.scope || me, me, e);
+ }
+ me.onBlur();
+ },
+
+ doToggle: function(){
+ var me = this;
+ if (me.enableToggle && (me.allowDepress !== false || !me.pressed)) {
+ me.toggle();
}
- 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
+ * @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
*/
- 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];
+ onMouseOver: function(e) {
+ var me = this;
+ if (!me.disabled && !e.within(me.el, true, true)) {
+ me.onMouseEnter(e);
}
},
/**
- * Unregisters a drag and drop item. This is executed in
- * DragDrop.unreg, use that method instead of calling this directly.
- * @method _remove
* @private
- * @static
+ * 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
*/
- _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];
+ onMouseOut: function(e) {
+ var me = this;
+ if (!e.within(me.el, true, true)) {
+ if (me.overMenuTrigger) {
+ me.onMenuTriggerOut(e);
}
+ me.onMouseLeave(e);
}
- 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
+ * @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.
*/
- regHandle: function(sDDId, sHandleId) {
- if (!this.handleIds[sDDId]) {
- this.handleIds[sDDId] = {};
- }
- this.handleIds[sDDId][sHandleId] = sHandleId;
- },
+ onMouseMove: function(e) {
+ var me = this,
+ el = me.el,
+ over = me.overMenuTrigger,
+ overlap, btnSize;
- /**
- * 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;
- },
+ if (me.split) {
+ if (me.arrowAlign === 'right') {
+ overlap = e.getX() - el.getX();
+ btnSize = el.getWidth();
+ } else {
+ overlap = e.getY() - el.getY();
+ btnSize = el.getHeight();
+ }
- /**
- * 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 (overlap > (btnSize - me.getTriggerSize())) {
+ if (!over) {
+ me.onMenuTriggerOver(e);
}
- if (!bTargetsOnly || dd.isTarget) {
- oDDs[oDDs.length] = dd;
+ } else {
+ if (over) {
+ me.onMenuTriggerOut(e);
}
}
}
-
- 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
+ * @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.
*/
- isLegalTarget: function (oDD, oTargetDD) {
- var targets = this.getRelated(oDD, true);
- for (var i=0, len=targets.length;i.
+ * @param e
*/
- getDDById: function(id) {
- for (var i in this.ids) {
- if (this.ids[i][id]) {
- return this.ids[i][id];
- }
- }
- return null;
+ onMenuTriggerOver: function(e) {
+ var me = this;
+ me.overMenuTrigger = true;
+ me.fireEvent('menutriggerover', me, me.menu, e);
},
/**
- * Fired after a registered DragDrop object gets the mousedown event.
- * Sets up the events required to track the object being dragged
- * @method handleMouseDown
- * @param {Event} e the event
- * @param oDD the DragDrop object being dragged
* @private
- * @static
+ * 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
*/
- handleMouseDown: function(e, oDD) {
- if(Ext.tip.QuickTipManager){
- Ext.tip.QuickTipManager.ddDisable();
- }
- if(this.dragCurrent){
- // the original browser mouseup wasn't handled (e.g. outside FF browser window)
- // so clean up first to avoid breaking the next drag
- this.handleMouseUp(e);
- }
-
- this.currentTarget = e.getTarget();
- this.dragCurrent = oDD;
-
- var el = oDD.getEl();
+ onMenuTriggerOut: function(e) {
+ var me = this;
+ delete me.overMenuTrigger;
+ me.fireEvent('menutriggerout', me, me.menu, e);
+ },
- // track start position
- this.startX = e.getPageX();
- this.startY = e.getPageY();
+ // inherit docs
+ enable : function(silent) {
+ var me = this;
- this.deltaX = this.startX - el.offsetLeft;
- this.deltaY = this.startY - el.offsetTop;
+ me.callParent(arguments);
- this.dragThreshMet = false;
+ me.removeClsWithUI('disabled');
- this.clickTimeout = setTimeout(
- function() {
- var DDM = Ext.dd.DragDropManager;
- DDM.startDrag(DDM.startX, DDM.startY);
- },
- this.clickTimeThresh );
+ return me;
},
- /**
- * Fired when either the drag pixel threshol or the mousedown hold
- * time threshold has been met.
- * @method startDrag
- * @param x {int} the X position of the original mousedown
- * @param y {int} the Y position of the original mousedown
- * @static
- */
- startDrag: function(x, y) {
- clearTimeout(this.clickTimeout);
- if (this.dragCurrent) {
- this.dragCurrent.b4StartDrag(x, y);
- this.dragCurrent.startDrag(x, y);
- }
- this.dragThreshMet = true;
+ // inherit docs
+ disable : function(silent) {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.addClsWithUI('disabled');
+ me.removeClsWithUI(me.overCls);
+
+ return me;
},
/**
- * Internal function to handle the mouseup event. Will be invoked
- * from the context of the document.
- * @method handleMouseUp
- * @param {Event} e the event
- * @private
- * @static
+ * Method to change the scale of the button. See {@link #scale} for allowed configurations.
+ * @param {String} scale The scale to change to.
*/
- handleMouseUp: function(e) {
+ setScale: function(scale) {
+ var me = this,
+ ui = me.ui.replace('-' + me.scale, '');
- if(Ext.tip.QuickTipManager){
- Ext.tip.QuickTipManager.ddEnable();
- }
- if (! this.dragCurrent) {
- return;
+ //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(', ') + ')');
}
- clearTimeout(this.clickTimeout);
+ me.scale = scale;
+ me.setUI(ui);
+ },
- if (this.dragThreshMet) {
- this.fireEvents(e, true);
- } else {
+ // 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;
}
- this.stopDrag(e);
+ me.callParent([ui]);
- this.stopEvent(e);
+ // Set all the state classNames, as they need to include the UI
+ // me.disabledCls += ' ' + me.baseCls + '-' + me.ui + '-disabled';
},
- /**
- * Utility to stop event propagation and event default, if these
- * features are turned on.
- * @method stopEvent
- * @param {Event} e the event as returned by this.getEvent()
- * @static
- */
- stopEvent: function(e){
- if(this.stopPropagation) {
- e.stopPropagation();
+ // private
+ onFocus: function(e) {
+ var me = this;
+ if (!me.disabled) {
+ me.addClsWithUI(me.focusCls);
}
+ },
- if (this.preventDefault) {
- e.preventDefault();
- }
+ // private
+ onBlur: function(e) {
+ var me = this;
+ me.removeClsWithUI(me.focusCls);
},
- /**
- * Internal function to clean up event handlers after the drag
- * operation is complete
- * @method stopDrag
- * @param {Event} e the event
- * @private
- * @static
- */
- stopDrag: function(e) {
- // Fire the drag end event for the item that was dragged
- if (this.dragCurrent) {
- if (this.dragThreshMet) {
- this.dragCurrent.b4EndDrag(e);
- this.dragCurrent.endDrag(e);
+ // 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);
}
-
- this.dragCurrent.onMouseUp(e);
+ me.doc.un('mouseup', me.onMouseUp, me);
}
-
- this.dragCurrent = null;
- this.dragOvers = {};
+ },
+ // private
+ onMenuShow: function(e) {
+ var me = this;
+ me.ignoreNextClick = 0;
+ me.addClsWithUI(me.menuActiveCls);
+ me.fireEvent('menushow', me, me.menu);
},
- /**
- * Internal function to handle the mousemove event. Will be invoked
- * from the context of the html element.
- *
- * @TODO figure out what we can do about mouse events lost when the
- * user drags objects beyond the window boundary. Currently we can
- * detect this in internet explorer by verifying that the mouse is
- * down during the mousemove event. Firefox doesn't give us the
- * button state on the mousemove event.
- * @method handleMouseMove
- * @param {Event} e the event
- * @private
- * @static
- */
- handleMouseMove: function(e) {
- if (! this.dragCurrent) {
- return true;
- }
- // var button = e.which || e.button;
+ // 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);
+ },
- // check for IE mouseup outside of page boundary
- if (Ext.isIE && (e.button !== 0 && e.button !== 1 && e.button !== 2)) {
- this.stopEvent(e);
- return this.handleMouseUp(e);
- }
+ // private
+ restoreClick: function() {
+ this.ignoreNextClick = 0;
+ },
- if (!this.dragThreshMet) {
- var diffX = Math.abs(this.startX - e.getPageX());
- var diffY = Math.abs(this.startY - e.getPageY());
- if (diffX > this.clickPixelThresh ||
- diffY > this.clickPixelThresh) {
- this.startDrag(this.startX, this.startY);
- }
- }
+ // private
+ onDownKey: function() {
+ var me = this;
- if (this.dragThreshMet) {
- this.dragCurrent.b4Drag(e);
- this.dragCurrent.onDrag(e);
- if(!this.dragCurrent.moveOnly){
- this.fireEvents(e, false);
+ if (!me.disabled) {
+ if (me.menu) {
+ me.showMenu();
}
}
-
- 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
+ * 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 {Number[]} [top, right, bottom, left]
*/
- 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 = [];
+ getPersistentBtnPadding: function() {
+ var cls = Ext.button.Button,
+ padding = cls.persistentPadding,
+ btn, leftTop, btnEl, btnInnerEl;
- // 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) {
+ if (!padding) {
+ padding = cls.persistentPadding = [0, 0, 0, 0]; //set early to prevent recursion
- var ddo = this.dragOvers[i];
+ 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
- if (! this.isTypeOfDD(ddo)) {
- continue;
- }
+ 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];
- if (! this.isOverTarget(pt, ddo, this.mode)) {
- outEvts.push( ddo );
+ btn.destroy();
}
-
- 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 {
+ return padding;
+ }
- // initial drag over: dragEnter fires
- if (!oldOvers[oDD.id]) {
- enterEvts.push( oDD );
- // subsequent drag overs: dragOver fires
- } else {
- overEvts.push( oDD );
- }
+}, function() {
+ var groups = {};
- this.dragOvers[oDD.id] = oDD;
- }
- }
+ function toggleGroup(btn, state) {
+ var g, i, l;
+ if (state) {
+ g = groups[btn.toggleGroup];
+ for (i = 0, l = g.length; i < l; i++) {
+ if (g[i] !== btn) {
+ g[i].toggle(false);
}
}
}
+ }
- if (this.mode) {
- if (outEvts.length) {
- dc.b4DragOut(e, outEvts);
- dc.onDragOut(e, outEvts);
- }
-
- if (enterEvts.length) {
- dc.onDragEnter(e, enterEvts);
+ /**
+ * Private utility class used by Button
+ * @hide
+ */
+ Ext.ButtonToggleManager = {
+ register: function(btn) {
+ if (!btn.toggleGroup) {
+ return;
}
-
- if (overEvts.length) {
- dc.b4DragOver(e, overEvts);
- dc.onDragOver(e, overEvts);
+ var group = groups[btn.toggleGroup];
+ if (!group) {
+ group = groups[btn.toggleGroup] = [];
}
+ group.push(btn);
+ btn.on('toggle', toggleGroup);
+ },
- if (dropEvts.length) {
- dc.b4DragDrop(e, dropEvts);
- dc.onDragDrop(e, dropEvts);
+ unregister: function(btn) {
+ if (!btn.toggleGroup) {
+ return;
}
-
- } else {
- // fire dragout events
- var len = 0;
- for (i=0, len=outEvts.length; i(None)',
- var len = dds.length;
+ constructor: function(layout) {
+ var me = this;
- if (len == 1) {
- winner = dds[0];
- } else {
- // Loop through the targeted items
- for (var i=0; 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
+ * @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.
*/
- 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];
+ 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;
- 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();
- }
- }
- }
+ me.hideTrigger();
+ for (; i < length; i++) {
+ items[i].show();
}
+ items.length = 0;
+
+ return targetSize ? {
+ targetSize: {
+ height: targetSize.height,
+ width : newWidth
+ }
+ } : null;
},
/**
- * 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
+ * @private
*/
- 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;
+ showTrigger: function() {
+ this.menuTrigger.show();
},
/**
- * 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
+ * @private
*/
- getLocation: function(oDD) {
- if (! this.isTypeOfDD(oDD)) {
- return null;
+ hideTrigger: function() {
+ if (this.menuTrigger !== undefined) {
+ this.menuTrigger.hide();
}
+ },
- //delegate getLocation method to the
- //drag and drop target.
- if (oDD.getRegion) {
- return oDD.getRegion();
- }
+ /**
+ * @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 el = oDD.getEl(), pos, x1, x2, y1, y2, t, r, b, l;
+ var needsSep = function(group, prev){
+ return group.isXType('buttongroup') && !(prev instanceof Ext.toolbar.Separator);
+ };
- try {
- pos= Ext.core.Element.getXY(el);
- } catch (e) { }
+ me.clearMenu();
+ menu.removeAll();
- if (!pos) {
- return null;
- }
+ for (; i < len; i++) {
+ item = items[i];
- x1 = pos[0];
- x2 = x1 + el.offsetWidth;
- y1 = pos[1];
- y2 = y1 + el.offsetHeight;
+ // 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('-');
+ }
- t = y1 - oDD.padding[0];
- r = x2 + oDD.padding[1];
- b = y2 + oDD.padding[2];
- l = x1 - oDD.padding[3];
+ me.addComponentToMenu(menu, item);
+ prev = item;
+ }
- return Ext.create('Ext.util.Region', t, r, b, l);
+ // put something so the menu isn't empty if no compatible items found
+ if (menu.items.length < 1) {
+ menu.add(me.noItemsMenuText);
+ }
},
-
+
/**
- * 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
+ * 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
*/
- 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;
-
- }
+ createMenuConfig : function(component, hideOnClick) {
+ var config = Ext.apply({}, component.initialConfig),
+ group = component.toggleGroup;
- if (!loc) {
- return false;
- }
+ Ext.copyTo(config, component, [
+ 'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu'
+ ]);
- oTarget.cursorIsOver = loc.contains( pt );
+ Ext.apply(config, {
+ text : component.overflowText || component.text,
+ hideOnClick: hideOnClick,
+ destroyMenu: false
+ });
- // 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;
+ if (group || component.enableToggle) {
+ Ext.apply(config, {
+ group : group,
+ checked: component.pressed,
+ listeners: {
+ checkchange: function(item, checked){
+ component.toggle(checked);
+ }
+ }
+ });
}
- 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);
+ delete config.ownerCt;
+ delete config.xtype;
+ delete config.id;
+ return config;
+ },
- var el = dc.getDragEl();
- var curRegion = Ext.create('Ext.util.Region', pos.y,
- pos.x + el.offsetWidth,
- pos.y + el.offsetHeight,
- pos.x );
+ /**
+ * @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));
- var overlap = curRegion.intersect(loc);
+ } else if (component.isXType('button')) {
+ menu.add(me.createMenuConfig(component, !component.menu));
- if (overlap) {
- oTarget.overlap = overlap;
- return (intersect) ? true : oTarget.cursorIsOver;
- } else {
- return false;
+ } 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)));
+ }
}
},
/**
- * unload event handler
- * @method _onUnload
* @private
- * @static
+ * 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
*/
- _onUnload: function(e, me) {
- Ext.dd.DragDropManager.unregAll();
+ clearMenu : function() {
+ var menu = this.moreMenu;
+ if (menu && menu.items) {
+ menu.items.each(function(item) {
+ if (item.menu) {
+ delete item.menu;
+ }
+ });
+ }
},
/**
- * Cleans up the drag and drop events and objects.
- * @method unregAll
* @private
- * @static
+ * 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
*/
- unregAll: function() {
+ 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 (this.dragCurrent) {
- this.stopDrag();
- this.dragCurrent = null;
- }
+ if (!me.menuTrigger) {
+ me.createInnerElements();
- this._execOnAll("unreg", []);
+ /**
+ * @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', {
+ listeners: {
+ scope: me,
+ beforeshow: me.beforeMenuShow
+ }
+ });
- for (var i in this.elementCache) {
- delete this.elementCache[i];
+ /**
+ * @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 : me.layout.owner.menuTriggerCls,
+ 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();
- this.elementCache = {};
- this.ids = {};
+ // 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();
+ }
+ }
},
/**
- * A cache of DOM elements
- * @property elementCache
* @private
- * @static
+ * 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.Element} target The target Element
*/
- elementCache: {},
+ createInnerElements: function() {
+ var me = this,
+ target = me.layout.getRenderTarget();
- /**
- * 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));
+ 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');
}
- 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
+ * @private
*/
- getElement: function(id) {
- return Ext.getDom(id);
- },
+ destroy: function() {
+ Ext.destroy(this.menu, this.menuTrigger);
+ }
+});
+/**
+ * This class represents a rectangular region in X,Y space, and performs geometric
+ * transformations or tests upon the region.
+ *
+ * This class may be used to compare the document regions occupied by elements.
+ */
+Ext.define('Ext.util.Region', {
- /**
- * 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;
- },
+ /* Begin Definitions */
- /**
- * 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;
+ requires: ['Ext.util.Offset'],
+
+ statics: {
+ /**
+ * @static
+ * Retrieves an Ext.util.Region for a particular element.
+ * @param {String/HTMLElement/Ext.Element} el An element ID, htmlElement or Ext.Element representing an element in the document.
+ * @returns {Ext.util.Region} region
+ */
+ getRegion: function(el) {
+ return Ext.fly(el).getPageBox(true);
},
- /**
- * 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);
+ /**
+ * @static
+ * Creates a Region from a "box" Object which contains four numeric properties `top`, `right`, `bottom` and `left`.
+ * @param {Object} o An object with `top`, `right`, `bottom` and `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 */
+
/**
- * 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
+ * Creates a region from the bounding sides.
+ * @param {Number} top Top The topmost pixel of the Region.
+ * @param {Number} right Right The rightmost pixel of the Region.
+ * @param {Number} bottom Bottom The bottom pixel of the Region.
+ * @param {Number} left Left The leftmost pixel of the Region.
*/
- getPosY: function(el) {
- return Ext.core.Element.getY(el);
+ 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;
},
/**
- * 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
+ * Checks if this region completely contains the region that is passed in.
+ * @param {Ext.util.Region} region
+ * @return {Boolean}
*/
- swapNode: function(n1, n2) {
- if (n1.swapNode) {
- n1.swapNode(n2);
- } else {
- var p = n2.parentNode;
- var s = n2.nextSibling;
+ contains : function(region) {
+ var me = this;
+ return (region.x >= me.x &&
+ region.right <= me.right &&
+ region.y >= me.y &&
+ region.bottom <= me.bottom);
- 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
+ * 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.
*/
- 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;
- }
+ 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;
}
- 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
+ * Returns the smallest region that contains the current AND targetRegion.
+ * @param {Ext.util.Region} region
+ * @return {Ext.util.Region} a new region
*/
- getStyle: function(el, styleProp) {
- return Ext.fly(el).getStyle(styleProp);
- },
+ 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);
- /**
- * Gets the scrollTop
- * @method getScrollTop
- * @return {int} the document's scrollTop
- * @static
- */
- getScrollTop: function () {
- return this.getScroll().top;
+ return new this.self(t, r, b, l);
},
/**
- * Gets the scrollLeft
- * @method getScrollLeft
- * @return {int} the document's scrollTop
- * @static
+ * Modifies the current region to be constrained to the targetRegion.
+ * @param {Ext.util.Region} targetRegion
+ * @return {Ext.util.Region} this
*/
- getScrollLeft: function () {
- return this.getScroll().left;
+ 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;
},
/**
- * 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
+ * Modifies the current region to be adjusted by offsets.
+ * @param {Number} top top offset
+ * @param {Number} right right offset
+ * @param {Number} bottom bottom offset
+ * @param {Number} left left offset
+ * @return {Ext.util.Region} this
*/
- moveToEl: function (moveEl, targetEl) {
- var aCoord = Ext.core.Element.getXY(targetEl);
- Ext.core.Element.setXY(moveEl, aCoord);
+ 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;
},
/**
- * Numeric array sort function
- * @method numericSort
- * @static
+ * Get the offset amount of a point outside the region
+ * @param {String} [axis]
+ * @param {Ext.util.Point} [p] the point
+ * @return {Ext.util.Offset}
*/
- numericSort: function(a, b) {
- return (a - b);
+ 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;
+ }
+
},
/**
- * Internal counter
- * @property _timeoutCount
- * @private
- * @static
+ * Get the offset amount on the x-axis
+ * @param {Number} p the offset
+ * @return {Number}
*/
- _timeoutCount: 0,
+ getOutOfBoundOffsetX: function(p) {
+ if (p <= this.x) {
+ return this.x - p;
+ } else if (p >= this.right) {
+ return this.right - p;
+ }
+
+ return 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
+ * Get the offset amount on the y-axis
+ * @param {Number} p the offset
+ * @return {Number}
*/
- _addListeners: function() {
- if ( document ) {
- this._onLoad();
- } else {
- if (this._timeoutCount > 2000) {
- } else {
- setTimeout(this._addListeners, 10);
- if (document && document.body) {
- this._timeoutCount += 1;
- }
- }
+ getOutOfBoundOffsetY: function(p) {
+ if (p <= this.y) {
+ return this.y - p;
+ } else if (p >= this.bottom) {
+ return this.bottom - p;
}
+
+ return 0;
},
/**
- * 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
+ * Check whether the point / offset is out of bound
+ * @param {String} [axis]
+ * @param {Ext.util.Point/Number} [p] the point / offset
+ * @return {Boolean}
*/
- 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;
- }
+ 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));
}
-
- 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.
+ * 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);
+ },
/**
- * @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}
- *
+ * Check whether the offset is out of bound in the y-axis
+ * @param {Number} p the offset
+ * @return {Boolean}
*/
- defaultMargins: {
- top: 0,
- right: 0,
- bottom: 0,
- left: 0
+ isOutOfBoundY: function(p) {
+ return (p < this.y || p > this.bottom);
},
/**
- * @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"
+ * Restrict a point within the region by a certain factor.
+ * @param {String} [axis]
+ * @param {Ext.util.Point/Ext.util.Offset/Object} [p]
+ * @param {Number} [factor]
+ * @return {Ext.util.Point/Ext.util.Offset/Object/Number}
+ * @private
*/
- padding: '0',
- // documented in subclasses
- pack: 'start',
+ restrict: function(axis, p, factor) {
+ if (Ext.isObject(axis)) {
+ var newP;
- /**
- * @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).
- */
+ factor = p;
+ p = axis;
- type: 'box',
- scrollOffset: 0,
- itemCls: Ext.baseCSSPrefix + 'box-item',
- targetCls: Ext.baseCSSPrefix + 'box-layout-ct',
- innerCls: Ext.baseCSSPrefix + 'box-inner',
+ if (p.copy) {
+ newP = p.copy();
+ }
+ else {
+ newP = {
+ x: p.x,
+ y: p.y
+ };
+ }
- bindToOwnerCtContainer: true,
+ 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);
+ }
+ }
+ },
- 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
+ * Restrict an offset within the region by a certain factor, on the x-axis
+ * @param {Number} p
+ * @param {Number} [factor=1] The factor.
+ * @return {Number}
+ * @private
*/
- 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;
+ restrictX : function(p, factor) {
+ if (!factor) {
+ factor = 1;
}
- return a - b;
- },
- // Sort into *descending* order.
- minSizeSortFn: function(a, b) {
- return b.available - a.available;
+ if (p <= this.x) {
+ p -= (p - this.x) * factor;
+ }
+ else if (p >= this.right) {
+ p -= (p - this.right) * factor;
+ }
+ return p;
},
- 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);
+ /**
+ * Restrict an offset within the region by a certain factor, on the y-axis
+ * @param {Number} p
+ * @param {Number} [factor] The factor, defaults to 1
+ * @return {Number}
+ * @private
+ */
+ restrictY : function(p, factor) {
+ if (!factor) {
+ factor = 1;
+ }
- me.initOverflowHandler();
+ 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
* @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;
+ getSize: function() {
return {
- left: child.getLeft(true),
- top: child.getTop(true),
- width: child.getWidth(),
- height: child.getHeight()
+ width: this.right - this.x,
+ height: this.bottom - this.y
};
},
/**
- * @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.
+ * Create a copy of this Region.
+ * @return {Ext.util.Region}
*/
- 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];
- }
- }
+ copy: function() {
+ return new this.self(this.y, this.right, this.bottom, this.x);
},
/**
- * @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
+ * Copy the values of another Region to this Region
+ * @param {Ext.util.Region} p The region to copy from.
+ * @return {Ext.util.Region} This Region
*/
- 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,
+ 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;
- 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),
+ return this;
+ },
- isStart = me.pack == 'start',
- isCenter = me.pack == 'center',
- isEnd = me.pack == 'end',
+ /*
+ * Dump this to an eye-friendly string, great for debugging
+ * @return {String}
+ */
+ toString: function() {
+ return "Region[" + this.top + "," + this.right + "," + this.bottom + "," + this.left + "]";
+ },
- constrain = Ext.Number.constrain,
- visibleCount = visibleItems.length,
- nonFlexSize = 0,
- totalFlex = 0,
- desiredSize = 0,
- minimumSize = 0,
- maxSize = 0,
- boxes = [],
- minSizes = [],
- calculatedWidth,
+ /**
+ * Translate this region by the given offset amount
+ * @param {Ext.util.Offset/Object} x Object containing the `x` and `y` properties.
+ * Or the x value is using the two argument form.
+ * @param {Number} y 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;
- 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;
+ return me;
+ },
- //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];
+ /**
+ * 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);
- // Create the box description object for this child item.
- tmpObj = {
- component: child,
- margins: childMargins
- };
+ return me;
+ },
- // 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];
- }
+ /**
+ * 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);
+ }
+});
- nonFlexSize += parallelMargins + (childParallel || 0);
- desiredSize += parallelMargins + (child.flex ? child[parallelMinString] || 0 : childParallel);
- minimumSize += parallelMargins + (child[parallelMinString] || childParallel || 0);
+/*
+ * 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
+ */
- // 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]);
+/**
+ * @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,
- tmpObj[parallelPrefix] = childParallel || undefinedValue;
- tmpObj[perpendicularPrefix] = childPerpendicular || undefinedValue;
- boxes.push(tmpObj);
- }
- shortfall = desiredSize - parallelSize;
- tooNarrow = minimumSize > parallelSize;
+ requires: ['Ext.util.Region'],
- //the space available to the flexed items
- availableSpace = mmax(0, parallelSize - nonFlexSize - paddingParallel - (me.reserveOffset ? me.availableSpaceOffset : 0));
+ uses: ['Ext.tip.QuickTipManager'],
- 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;
+ // shorter ClassName, to save bytes and use internally
+ alternateClassName: ['Ext.dd.DragDropMgr', 'Ext.dd.DDM'],
- //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
- });
- }
- }
+ /**
+ * 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
+ */
+ ids: {},
- //sort by descending amount of width remaining before minWidth is reached
- Ext.Array.sort(minSizes, me.minSizeSortFn);
+ /**
+ * 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
+ */
+ handleIds: {},
- /*
- * 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;
+ /**
+ * the DragDrop object that is currently being dragged
+ * @property {Ext.dd.DragDrop} dragCurrent
+ * @private
+ **/
+ dragCurrent: null,
- if (itemIndex == undefinedValue) {
- continue;
- }
- item = visibleItems[itemIndex];
- minSize = minSizes[i].minSize;
+ /**
+ * the DragDrop object(s) that are being hovered over
+ * @property {Ext.dd.DragDrop[]} dragOvers
+ * @private
+ */
+ dragOvers: {},
- box = boxes[itemIndex];
- oldSize = box[parallelPrefix];
- newSize = mmax(minSize, oldSize - math.ceil(shortfall / (length - i)));
- reduction = oldSize - newSize;
+ /**
+ * the X distance between the cursor and the object being dragged
+ * @property deltaX
+ * @type Number
+ * @private
+ */
+ deltaX: 0,
- box.dirtySize = box.dirtySize || box[parallelPrefix] != newSize;
- box[parallelPrefix] = newSize;
- shortfall -= reduction;
- }
- }
- else {
- remainingSpace = availableSpace;
- remainingFlex = totalFlex;
- flexedBoxes = [];
+ /**
+ * the Y distance between the cursor and the object being dragged
+ * @property deltaY
+ * @type Number
+ * @private
+ */
+ deltaY: 0,
- // 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);
+ /**
+ * 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
+ */
+ preventDefault: true,
- // 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;
+ /**
+ * 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
+ */
+ stopPropagation: true,
- flexedSize = math.ceil((child.flex / remainingFlex) * remainingSpace);
+ /**
+ * Internal flag that is set to true when drag and drop has been
+ * intialized
+ * @property initialized
+ * @private
+ */
+ initialized: false,
- // Implement maxSize and minSize check
- flexedSize = Math.max(child['min' + parallelPrefixCap] || 0, math.min(child['max' + parallelPrefixCap] || infiniteValue, flexedSize));
+ /**
+ * All drag and drop can be disabled.
+ * @property locked
+ * @private
+ */
+ locked: false,
- // Remaining space has already had all parallel margins subtracted from it, so just subtract consumed size
- remainingSpace -= flexedSize;
- remainingFlex -= child.flex;
+ /**
+ * Called the first time an element is registered.
+ * @method init
+ * @private
+ */
+ init: function() {
+ this.initialized = true;
+ },
- calcs.dirtySize = calcs.dirtySize || calcs[parallelPrefix] != flexedSize;
- calcs[parallelPrefix] = flexedSize;
- }
- }
- }
+ /**
+ * In point mode, drag and drop interaction is defined by the
+ * location of the cursor during the drag/drop
+ * @property POINT
+ * @type Number
+ */
+ POINT: 0,
- if (isCenter) {
- parallelOffset += availableSpace / 2;
- }
- else if (isEnd) {
- parallelOffset += availableSpace;
- }
+ /**
+ * In intersect mode, drag and drop interaction is defined by the
+ * overlap of two or more drag and drop objects.
+ * @property INTERSECT
+ * @type Number
+ */
+ INTERSECT: 1,
- // 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') {
+ /**
+ * The current drag and drop mode. Default: POINT
+ * @property mode
+ * @type Number
+ */
+ mode: 0,
- 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;
+ /**
+ * Runs method on all drag and drop objects
+ * @method _execOnAll
+ * @private
+ */
+ _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);
}
- // 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];
+ /**
+ * Drag and drop initialization. Sets up the global event handlers
+ * @method _onLoad
+ * @private
+ */
+ _onLoad: function() {
- childMargins = calcs.margins;
+ this.init();
- perpendicularMargins = childMargins[me.perpendicularLeftTop] + childMargins[me.perpendicularRightBottom];
+ 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);
- // Advance past the "before" margin
- parallelOffset += childMargins[me.parallelBefore];
+ },
- calcs[me.parallelBefore] = parallelOffset;
- calcs[me.perpendicularLeftTop] = perpendicularOffset + childMargins[me.perpendicularLeftTop];
+ /**
+ * Reset constraints on all drag and drop objs
+ * @method _onResize
+ * @private
+ */
+ _onResize: function(e) {
+ this._execOnAll("resetConstraints", []);
+ },
- 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);
- }
- }
+ /**
+ * Lock all drag and drop functionality
+ * @method lock
+ */
+ lock: function() { this.locked = true; },
- // Advance past the box size and the "after" margin
- parallelOffset += (calcs[parallelPrefix] || 0) + childMargins[me.parallelAfter];
- }
+ /**
+ * Unlock all drag and drop functionality
+ * @method unlock
+ */
+ unlock: function() { this.locked = false; },
- return {
- boxes: boxes,
- meta : {
- calculatedWidth: calculatedWidth,
- maxSize: maxSize,
- nonFlexSize: nonFlexSize,
- desiredSize: desiredSize,
- minimumSize: minimumSize,
- shortfall: shortfall,
- tooNarrow: tooNarrow
- }
- };
- },
-
- onRemove: function(comp){
- this.callParent(arguments);
- if (this.overflowHandler) {
- this.overflowHandler.onRemove(comp);
- }
- },
+ /**
+ * Is drag and drop locked?
+ * @method isLocked
+ * @return {Boolean} True if drag and drop is locked, false otherwise.
+ */
+ 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
*/
- initOverflowHandler: function() {
- var handler = this.overflowHandler;
+ locationCache: {},
- if (typeof handler == 'string') {
- handler = {
- type: handler
- };
- }
+ /**
+ * 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
+ */
+ useCache: true,
- var handlerType = 'None';
- if (handler && handler.type !== undefined) {
- handlerType = handler.type;
- }
+ /**
+ * The number of pixels that the mouse needs to move after the
+ * mousedown before the drag is initiated. Default=3;
+ * @property clickPixelThresh
+ * @type Number
+ */
+ clickPixelThresh: 3,
- var constructor = Ext.layout.container.boxOverflow[handlerType];
- if (constructor[this.type]) {
- constructor = constructor[this.type];
- }
+ /**
+ * 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 Number
+ */
+ clickTimeThresh: 350,
- this.overflowHandler = Ext.create('Ext.layout.container.boxOverflow.' + handlerType, this, handler);
- },
+ /**
+ * Flag that indicates that either the drag pixel threshold or the
+ * mousdown time threshold has been met
+ * @property dragThreshMet
+ * @type Boolean
+ * @private
+ */
+ dragThreshMet: false,
/**
+ * Timeout used for the click time threshold
+ * @property clickTimeout
+ * @type Object
* @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);
- }
+ clickTimeout: 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';
+ /**
+ * The X position of the mousedown event stored for later use when a
+ * drag threshold is met.
+ * @property startX
+ * @type Number
+ * @private
+ */
+ startX: 0,
- results = handler[method](calcs, targetSize);
+ /**
+ * The Y position of the mousedown event stored for later use when a
+ * drag threshold is met.
+ * @property startY
+ * @type Number
+ * @private
+ */
+ startY: 0,
- if (results) {
- if (results.targetSize) {
- targetSize = results.targetSize;
- }
+ /**
+ * Each DragDrop instance must be registered with the DragDropManager.
+ * This is executed in DragDrop.init()
+ * @method regDragDrop
+ * @param {Ext.dd.DragDrop} oDD the DragDrop object to register
+ * @param {String} sGroup the name of the group this element belongs to
+ */
+ regDragDrop: function(oDD, sGroup) {
+ if (!this.initialized) { this.init(); }
- if (results.recalculate) {
- items = me.getVisibleItems(owner);
- calcs = me.calculateChildBoxes(items, targetSize);
- boxes = calcs.boxes;
- }
- }
- } else {
- me.overflowHandler.clearOverflow();
+ if (!this.ids[sGroup]) {
+ this.ids[sGroup] = {};
}
-
- /**
- * @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);
+ this.ids[sGroup][oDD.id] = oDD;
},
/**
- * Resizes and repositions each child component
- * @param {Array} boxes The box measurements
+ * Removes the supplied dd instance from the supplied group. Executed
+ * by DragDrop.removeFromGroup, so don't call this function directly.
+ * @method removeDDFromGroup
+ * @private
*/
- 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;
+ removeDDFromGroup: function(oDD, sGroup) {
+ if (!this.ids[sGroup]) {
+ this.ids[sGroup] = {};
+ }
- for (; i < length; i++) {
- newBox = boxes[i];
- comp = newBox.component;
+ var obj = this.ids[sGroup];
+ if (obj && obj[oDD.id]) {
+ delete obj[oDD.id];
+ }
+ },
- // 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;
+ /**
+ * Unregisters a drag and drop item. This is executed in
+ * DragDrop.unreg, use that method instead of calling this directly.
+ * @method _remove
+ * @private
+ */
+ _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];
+ },
- changed = false;
+ /**
+ * 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
+ */
+ regHandle: function(sDDId, sHandleId) {
+ if (!this.handleIds[sDDId]) {
+ this.handleIds[sDDId] = {};
+ }
+ this.handleIds[sDDId][sHandleId] = sHandleId;
+ },
- oldBox = me.getChildBox(comp);
+ /**
+ * 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
+ */
+ isDragDrop: function(id) {
+ return ( this.getDDById(id) ) ? true : false;
+ },
- // 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)) {
+ /**
+ * Returns the drag and drop instances that are in all groups the
+ * passed in instance belongs to.
+ * @method getRelated
+ * @param {Ext.dd.DragDrop} p_oDD the obj to get related data for
+ * @param {Boolean} bTargetsOnly if true, only return targetable objs
+ * @return {Ext.dd.DragDrop[]} the related instances
+ */
+ 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;
}
- comp.setPosition(newBox.left, newBox.top);
+ if (!bTargetsOnly || dd.isTarget) {
+ oDDs[oDDs.length] = dd;
+ }
}
}
- // Kick off any queued animations
- length = animQueue.length;
- if (length) {
+ return oDDs;
+ },
- // 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();
- }
- }
- };
+ /**
+ * Returns true if the specified dd target is a legal target for
+ * the specifice drag obj
+ * @method isLegalTarget
+ * @param {Ext.dd.DragDrop} oDD the drag obj
+ * @param {Ext.dd.DragDrop} oTargetDD the target
+ * @return {Boolean} true if the target is a legal target for the
+ * dd obj
+ */
+ isLegalTarget: function (oDD, oTargetDD) {
+ var targets = this.getRelated(oDD, true);
+ for (var i=0, len=targets.length;i meta.calculatedWidth) {
- me.owner.el.setWidth(meta.calculatedWidth);
- }
+ this.deltaX = this.startX - el.offsetLeft;
+ this.deltaY = this.startY - el.offsetTop;
- if (me.innerCt.dom.scrollTop) {
- me.innerCt.dom.scrollTop = 0;
- }
+ this.dragThreshMet = false;
+
+ this.clickTimeout = setTimeout(
+ function() {
+ var DDM = Ext.dd.DragDropManager;
+ DDM.startDrag(DDM.startX, DDM.startY);
+ },
+ this.clickTimeThresh );
},
/**
- * @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.
+ * Fired when either the drag pixel threshol or the mousedown hold
+ * time threshold has been met.
+ * @method startDrag
+ * @param {Number} x the X position of the original mousedown
+ * @param {Number} y the Y position of the original mousedown
*/
- 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;
- }
+ startDrag: function(x, y) {
+ clearTimeout(this.clickTimeout);
+ if (this.dragCurrent) {
+ this.dragCurrent.b4StartDrag(x, y);
+ this.dragCurrent.startDrag(x, y);
}
-
- delete this.adjustmentPass;
+ this.dragThreshMet = true;
},
- // 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;
- },
+ /**
+ * Internal function to handle the mouseup event. Will be invoked
+ * from the context of the document.
+ * @method handleMouseUp
+ * @param {Event} e the event
+ * @private
+ */
+ handleMouseUp: function(e) {
- // 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);
+ if(Ext.tip && Ext.tip.QuickTipManager){
+ Ext.tip.QuickTipManager.ddEnable();
+ }
+ if (! this.dragCurrent) {
+ return;
}
- 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;
+ clearTimeout(this.clickTimeout);
- // 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});
- }
+ if (this.dragThreshMet) {
+ this.fireEvents(e, true);
} 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';
+ this.stopDrag(e);
- // Item must reference calculated margins.
- item.margins = margins;
+ this.stopEvent(e);
},
/**
- * @private
+ * Utility to stop event propagation and event default, if these
+ * features are turned on.
+ * @method stopEvent
+ * @param {Event} e the event as returned by this.getEvent()
*/
- 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 */
+ stopEvent: function(e){
+ if(this.stopPropagation) {
+ e.stopPropagation();
+ }
- alias: ['layout.hbox'],
- extend: 'Ext.layout.container.Box',
- alternateClassName: 'Ext.layout.HBoxLayout',
-
- /* End Definitions */
+ if (this.preventDefault) {
+ e.preventDefault();
+ }
+ },
/**
- * @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.
- *
+ * Internal function to clean up event handlers after the drag
+ * operation is complete
+ * @method stopDrag
+ * @param {Event} e the event
+ * @private
*/
- align: 'top', // top, middle, stretch, strechmax
-
- //@private
- alignCenteringString: 'middle',
+ stopDrag: function(e) {
+ // Fire the drag end event for the item that was dragged
+ if (this.dragCurrent) {
+ if (this.dragThreshMet) {
+ this.dragCurrent.b4EndDrag(e);
+ this.dragCurrent.endDrag(e);
+ }
- type : 'hbox',
+ this.dragCurrent.onMouseUp(e);
+ }
- 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 */
+ this.dragCurrent = null;
+ this.dragOvers = {};
+ },
/**
- * @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.
- *
+ * Internal function to handle the mousemove event. Will be invoked
+ * from the context of the html element.
+ *
+ * @TODO figure out what we can do about mouse events lost when the
+ * user drags objects beyond the window boundary. Currently we can
+ * detect this in internet explorer by verifying that the mouse is
+ * down during the mousemove event. Firefox doesn't give us the
+ * button state on the mousemove event.
+ * @method handleMouseMove
+ * @param {Event} e the event
+ * @private
*/
- 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.
+ handleMouseMove: function(e) {
+ if (! this.dragCurrent) {
+ return true;
+ }
+ // var button = e.which || e.button;
-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.
+ // check for IE mouseup outside of page boundary
+ if (Ext.isIE && (e.button !== 0 && e.button !== 1 && e.button !== 2)) {
+ this.stopEvent(e);
+ return this.handleMouseUp(e);
+ }
-To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}.
+ if (!this.dragThreshMet) {
+ var diffX = Math.abs(this.startX - e.getPageX());
+ var diffY = Math.abs(this.startY - e.getPageY());
+ if (diffX > this.clickPixelThresh ||
+ diffY > this.clickPixelThresh) {
+ this.startDrag(this.startX, this.startY);
+ }
+ }
-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.
+ if (this.dragThreshMet) {
+ this.dragCurrent.b4Drag(e);
+ this.dragCurrent.onDrag(e);
+ if(!this.dragCurrent.moveOnly){
+ this.fireEvents(e, false);
+ }
+ }
- * @singleton
- * @markdown
- * @author Jarred Nicholls
- * @docauthor Jarred Nicholls
- */
-Ext.define('Ext.FocusManager', {
- singleton: true,
- alternateClassName: 'Ext.FocusMgr',
+ this.stopEvent(e);
- mixins: {
- observable: 'Ext.util.Observable'
+ return true;
},
- 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
+ * 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
*/
+ fireEvents: function(e, isDrop) {
+ var dc = this.dragCurrent;
- focusElementCls: Ext.baseCSSPrefix + 'focus-element',
-
- focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
+ // 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;
+ }
- /**
- * @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'
- ],
+ var pt = e.getPoint();
- tabIndexWhitelist: [
- 'a',
- 'button',
- 'embed',
- 'frame',
- 'iframe',
- 'img',
- 'input',
- 'object',
- 'select',
- 'textarea'
- ],
+ // cache the previous dragOver array
+ var oldOvers = [];
- constructor: function() {
- var me = this,
- CQ = Ext.ComponentQuery;
+ var outEvts = [];
+ var overEvts = [];
+ var dropEvts = [];
+ var enterEvts = [];
- 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',
+ // 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) {
- /**
- * @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',
+ var ddo = this.dragOvers[i];
- /**
- * @event disable
- * Fires when the FocusManager is disabled
- * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
- */
- 'disable',
+ if (! this.isTypeOfDD(ddo)) {
+ continue;
+ }
- /**
- * @event enable
- * Fires when the FocusManager is enabled
- * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
- */
- 'enable'
- );
+ if (! this.isOverTarget(pt, ddo, this.mode)) {
+ outEvts.push( ddo );
+ }
- // 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,
+ oldOvers[i] = true;
+ delete this.dragOvers[i];
+ }
- backspace: me.focusLast,
- enter: me.navigateIn,
- esc: me.navigateOut,
- tab: me.navigateSiblings
+ for (var sGroup in dc.groups) {
- //space: me.navigateIn,
- //del: me.focusLast,
- //left: me.navigateSiblings,
- //right: me.navigateSiblings,
- //down: me.navigateSiblings,
- //up: me.navigateSiblings
- });
+ if ("string" != typeof sGroup) {
+ continue;
+ }
- me.focusData = {};
- me.subscribers = Ext.create('Ext.util.HashMap');
- me.focusChain = {};
+ for (i in this.ids[sGroup]) {
+ var oDD = this.ids[sGroup][i];
+ if (! this.isTypeOfDD(oDD)) {
+ continue;
+ }
- // Setup some ComponentQuery pseudos
- Ext.apply(CQ.pseudos, {
- focusable: function(cmps) {
- var len = cmps.length,
- results = [],
- i = 0,
- c,
+ 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 {
- 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()}');
- };
+ // initial drag over: dragEnter fires
+ if (!oldOvers[oDD.id]) {
+ enterEvts.push( oDD );
+ // subsequent drag overs: dragOver fires
+ } else {
+ overEvts.push( oDD );
+ }
- for (; i < len; i++) {
- c = cmps[i];
- if (isFocusable(c)) {
- results.push(c);
+ this.dragOvers[oDD.id] = oDD;
+ }
}
}
+ }
+ }
- 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;
- }
+ if (this.mode) {
+ if (outEvts.length) {
+ dc.b4DragOut(e, outEvts);
+ dc.onDragOut(e, outEvts);
+ }
- c = cmps[i];
- if (CQ.is(c, ':focusable')) {
- return [c];
- } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) {
- return [c.placeholder];
- }
- }
+ if (enterEvts.length) {
+ dc.onDragEnter(e, enterEvts);
+ }
- return [];
- },
+ if (overEvts.length) {
+ dc.b4DragOver(e, overEvts);
+ dc.onDragOver(e, overEvts);
+ }
- prevFocus: function(cmps, idx) {
- return this.nextFocus(cmps, idx, -1);
- },
+ if (dropEvts.length) {
+ dc.b4DragDrop(e, dropEvts);
+ dc.onDragDrop(e, dropEvts);
+ }
- root: function(cmps) {
- var len = cmps.length,
- results = [],
- i = 0,
- c;
+ } 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
*/
- enable: function(options) {
- var me = this;
+ 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 (options === true) {
- options = { focusFrame: true };
+ 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();
+ }
+ }
+ }
}
- me.options = options = options || {};
+ },
- if (me.enabled) {
- return;
+ /**
+ * 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
+ */
+ verifyEl: function(el) {
+ if (el) {
+ var parent;
+ if(Ext.isIE){
+ try{
+ parent = el.offsetParent;
+ }catch(e){}
+ }else{
+ parent = el.offsetParent;
+ }
+ if (parent) {
+ return true;
+ }
}
- // 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);
+ return false;
},
- focusLast: function(e) {
- var me = this;
-
- if (me.isWhitelisted(me.focusedCmp)) {
- return true;
+ /**
+ * Returns a Region object containing the drag and drop element's position
+ * and size, including the padding configured for it
+ * @method getLocation
+ * @param {Ext.dd.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.
+ */
+ getLocation: function(oDD) {
+ if (! this.isTypeOfDD(oDD)) {
+ return null;
}
- // Go back to last focused item
- if (me.previousFocusedCmp) {
- me.previousFocusedCmp.focus();
+ //delegate getLocation method to the
+ //drag and drop target.
+ if (oDD.getRegion) {
+ return oDD.getRegion();
}
- },
-
- 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);
- },
+ var el = oDD.getEl(), pos, x1, x2, y1, y2, t, r, b, l;
- initDOM: function(options) {
- var me = this,
- sp = ' ',
- cls = me.focusFrameCls;
+ try {
+ pos= Ext.Element.getXY(el);
+ } catch (e) { }
- if (!Ext.isReady) {
- Ext.onReady(me.initDOM, me);
- return;
+ if (!pos) {
+ return null;
}
- // Create global focus element
- if (!me.focusEl) {
- me.focusEl = Ext.getBody().createChild({
- tabIndex: '-1',
- cls: me.focusElementCls,
- html: sp
- });
- }
+ x1 = pos[0];
+ x2 = x1 + el.offsetWidth;
+ y1 = pos[1];
+ y2 = y1 + el.offsetHeight;
- // 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);
- }
- },
+ t = y1 - oDD.padding[0];
+ r = x2 + oDD.padding[1];
+ b = y2 + oDD.padding[2];
+ l = x1 - oDD.padding[3];
- isWhitelisted: function(cmp) {
- return cmp && Ext.Array.some(this.whitelist, function(x) {
- return cmp.isXType(x);
- });
+ return Ext.create('Ext.util.Region', t, r, b, l);
},
- navigateIn: function(e) {
- var me = this,
- focusedCmp = me.focusedCmp,
- rootCmps,
- firstChild;
+ /**
+ * Checks the cursor location to see if it over the target
+ * @method isOverTarget
+ * @param {Ext.util.Point} pt The point to evaluate
+ * @param {Ext.dd.DragDrop} oTarget the DragDrop object we are inspecting
+ * @return {Boolean} true if the mouse is over the target
+ * @private
+ */
+ 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 (!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;
+ if (!loc) {
+ return false;
}
- 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;
+ oTarget.cursorIsOver = loc.contains( pt );
- focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
- if (!focusedCmp && !parent) {
- return;
+ // 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;
}
- if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
- return true;
- }
+ oTarget.overlap = null;
- 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;
- }
- }
- },
+ // 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);
- onComponentBlur: function(cmp, e) {
- var me = this;
+ var el = dc.getDragEl();
+ var curRegion = Ext.create('Ext.util.Region', pos.y,
+ pos.x + el.offsetWidth,
+ pos.y + el.offsetHeight,
+ pos.x );
- if (me.focusedCmp === cmp) {
- me.previousFocusedCmp = cmp;
- delete me.focusedCmp;
- }
+ var overlap = curRegion.intersect(loc);
- if (me.focusFrame) {
- me.focusFrame.hide();
+ if (overlap) {
+ oTarget.overlap = overlap;
+ return (intersect) ? true : oTarget.cursorIsOver;
+ } else {
+ return false;
}
},
- onComponentCreated: function(hash, id, cmp) {
- this.setFocus(cmp, true, this.options);
- },
-
- onComponentDestroy: function(cmp) {
- this.setFocus(cmp, false);
+ /**
+ * unload event handler
+ * @method _onUnload
+ * @private
+ */
+ _onUnload: function(e, me) {
+ Ext.dd.DragDropManager.unregAll();
},
- onComponentFocus: function(cmp, e) {
- var me = this,
- chain = me.focusChain;
-
- if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
- me.clearComponent(cmp);
+ /**
+ * Cleans up the drag and drop events and objects.
+ * @method unregAll
+ * @private
+ */
+ unregAll: function() {
- // 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;
- }
+ if (this.dragCurrent) {
+ this.stopDrag();
+ this.dragCurrent = null;
+ }
- // 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();
- }
+ this._execOnAll("unreg", []);
- return;
+ for (var i in this.elementCache) {
+ delete this.elementCache[i];
}
- // Clear our focus chain when we have a focusable component
- me.focusChain = {};
+ this.elementCache = {};
+ this.ids = {};
+ },
- // 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;
- }
+ /**
+ * A cache of DOM elements
+ * @property elementCache
+ * @private
+ */
+ elementCache: {},
- if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
- me.clearComponent(cmp);
- return;
+ /**
+ * Get the wrapper for the DOM element specified
+ * @method getElWrapper
+ * @param {String} id the id of the element to get
+ * @return {Ext.dd.DragDropManager.ElementWrapper} the wrapped element
+ * @private
+ * @deprecated This wrapper isn't that useful
+ */
+ getElWrapper: function(id) {
+ var oWrapper = this.elementCache[id];
+ if (!oWrapper || !oWrapper.el) {
+ oWrapper = this.elementCache[id] =
+ new this.ElementWrapper(Ext.getDom(id));
}
+ return oWrapper;
+ },
- 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(),
+ /**
+ * 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
+ */
+ getElement: function(id) {
+ return Ext.getDom(id);
+ },
- // 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');
+ /**
+ * 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
+ */
+ getCss: function(id) {
+ var el = Ext.getDom(id);
+ return (el) ? el.style : null;
+ },
- 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);
+ /**
+ * @class Ext.dd.DragDropManager.ElementWrapper
+ * Inner class for cached elements
+ * @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;
+ },
- ff.show();
- }
+ // The DragDropManager class continues
+ /** @class Ext.dd.DragDropManager */
- me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
+ /**
+ * Returns the X position of an html element
+ * @param {HTMLElement} el the element for which to get the position
+ * @return {Number} the X coordinate
+ */
+ getPosX: function(el) {
+ return Ext.Element.getX(el);
},
- onComponentHide: function(cmp) {
- var me = this,
- CQ = Ext.ComponentQuery,
- cmpHadFocus = false,
- focusedCmp,
- parent;
+ /**
+ * Returns the Y position of an html element
+ * @param {HTMLElement} el the element for which to get the position
+ * @return {Number} the Y coordinate
+ */
+ getPosY: function(el) {
+ return Ext.Element.getY(el);
+ },
- if (me.focusedCmp) {
- focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
- cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
+ /**
+ * Swap two nodes. In IE, we use the native method, for others we
+ * emulate the IE behavior
+ * @param {HTMLElement} n1 the first node to swap
+ * @param {HTMLElement} n2 the other node to swap
+ */
+ swapNode: function(n1, n2) {
+ if (n1.swapNode) {
+ n1.swapNode(n2);
+ } else {
+ var p = n2.parentNode;
+ var s = n2.nextSibling;
- if (focusedCmp) {
- me.clearComponent(focusedCmp);
+ 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);
}
}
+ },
- me.clearComponent(cmp);
+ /**
+ * Returns the current scroll position
+ * @private
+ */
+ getScroll: function () {
+ var doc = window.document,
+ docEl = doc.documentElement,
+ body = doc.body,
+ top = 0,
+ left = 0;
- if (cmpHadFocus) {
- parent = CQ.query('^:focusable', cmp)[0];
- if (parent) {
- parent.focus();
+ 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
+ };
},
- 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;
+ /**
+ * Returns the specified element style property
+ * @param {HTMLElement} el the element
+ * @param {String} styleProp the style property
+ * @return {String} The value of the style property
+ */
+ getStyle: function(el, styleProp) {
+ return Ext.fly(el).getStyle(styleProp);
},
/**
- * Removes the specified xtype from the {@link #whitelist}.
- * @param {String/Array} xtype Removes the xtype(s) from the {@link #whitelist}.
+ * Gets the scrollTop
+ * @return {Number} the document's scrollTop
*/
- removeXTypeFromWhitelist: function(xtype) {
- var me = this;
-
- if (Ext.isArray(xtype)) {
- Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
- return;
- }
+ getScrollTop: function () {
+ return this.getScroll().top;
+ },
- Ext.Array.remove(me.whitelist, xtype);
+ /**
+ * Gets the scrollLeft
+ * @return {Number} the document's scrollTop
+ */
+ getScrollLeft: function () {
+ return this.getScroll().left;
},
- setFocus: function(cmp, focusable, options) {
- var me = this,
- el, dom, data,
+ /**
+ * Sets the x/y position of an element to the location of the
+ * target element.
+ * @param {HTMLElement} moveEl The element to move
+ * @param {HTMLElement} targetEl The position reference element
+ */
+ moveToEl: function (moveEl, targetEl) {
+ var aCoord = Ext.Element.getXY(targetEl);
+ Ext.Element.setXY(moveEl, aCoord);
+ },
- needsTabIndex = function(n) {
- return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
- && n.tabIndex <= 0;
- };
+ /**
+ * Numeric array sort function
+ * @param {Number} a
+ * @param {Number} b
+ * @returns {Number} positive, negative or 0
+ */
+ numericSort: function(a, b) {
+ return (a - b);
+ },
- options = options || {};
+ /**
+ * Internal counter
+ * @property {Number} _timeoutCount
+ * @private
+ */
+ _timeoutCount: 0,
- // 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;
+ /**
+ * Trying to make the load order less important. Without this we get
+ * an error if this file is loaded before the Event Utility.
+ * @private
+ */
+ _addListeners: function() {
+ if ( document ) {
+ this._onLoad();
+ } else {
+ if (this._timeoutCount > 2000) {
} else {
- data = me.focusData[cmp.id];
- if ('tabIndex' in data) {
- dom.tabIndex = data.tabIndex;
+ setTimeout(this._addListeners, 10);
+ if (document && document.body) {
+ this._timeoutCount += 1;
}
- 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
- },
+ /**
+ * Recursively searches the immediate parent and all child nodes for
+ * the handle element in order to determine wheter or not it was
+ * clicked.
+ * @param {HTMLElement} node the html element to inspect
+ */
+ 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;
- 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);
+ while (p) {
+ if (this.isHandle(id, p.id)) {
+ return true;
} else {
- return me.navigateSiblings(e);
+ p = p.parentNode;
}
- };
+ }
+ }
- Ext.iterate(keys, function(key, cb) {
- handlers[key] = function(e) {
- var ret = navSiblings(e);
+ return false;
+ }
+}, function() {
+ this._addListeners();
+});
- if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
- return true;
- }
+/**
+ * @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.
+ */
- return ret;
- };
- }, me);
+Ext.define('Ext.layout.container.Box', {
- return Ext.create('Ext.util.KeyNav', el, handlers);
- },
+ /* Begin Definitions */
- shouldShowFocusFrame: function(cmp) {
- var me = this,
- opts = me.options || {};
+ alias: ['layout.box'],
+ extend: 'Ext.layout.container.Container',
+ alternateClassName: 'Ext.layout.BoxLayout',
- if (!me.focusFrame || !cmp) {
- return false;
- }
+ requires: [
+ 'Ext.layout.container.boxOverflow.None',
+ 'Ext.layout.container.boxOverflow.Menu',
+ 'Ext.layout.container.boxOverflow.Scroller',
+ 'Ext.util.Format',
+ 'Ext.dd.DragDropManager'
+ ],
- // Global trumps
- if (opts.focusFrame) {
- return true;
- }
+ /* End Definitions */
- if (me.focusData[cmp.id].focusFrame) {
- return true;
- }
+ /**
+ * @cfg {Boolean/Number/Object} 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.
+ */
- return false;
+ /**
+ * @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.
+ *
+ */
+ defaultMargins: {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
},
/**
- * 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:
+ * @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.
+ *
+ */
+ 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).
+ */
- - 'left'
- - 'right'
- - 'up'
- - 'down'
+ type: 'box',
+ scrollOffset: 0,
+ itemCls: Ext.baseCSSPrefix + 'box-item',
+ targetCls: Ext.baseCSSPrefix + 'box-layout-ct',
+ innerCls: Ext.baseCSSPrefix + 'box-inner',
- 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.:
+ bindToOwnerCtContainer: true,
- {
- left: this.onLeftKey,
- right: this.onRightKey,
- scope: this
- }
+ // availableSpaceOffset is used to adjust the availableWidth, typically used
+ // to reserve space for a scrollbar
+ availableSpaceOffset: 0,
- - focusFrame : Boolean (optional)
- `true` to show the focus frame around a component when it is focused. Defaults to `false`.
- * @markdown
+ // whether or not to reserve the availableSpaceOffset in layout calculations
+ reserveOffset: true,
+
+ /**
+ * @cfg {Boolean} shrinkToFit
+ * True (the default) to allow fixed size components to shrink (limited to their
+ * minimum size) to avoid overflow. False to preserve fixed sizes even if they cause
+ * overflow.
*/
- subscribe: function(container, options) {
- var me = this,
- EA = Ext.Array,
- data = {},
- subs = me.subscribers,
+ shrinkToFit: true,
- // 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);
- }
- };
+ /**
+ * @cfg {Boolean} clearInnerCtOnLayout
+ */
+ clearInnerCtOnLayout: false,
- // We only accept containers
- if (!container || !container.isContainer) {
- return;
+ 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;
+ },
- if (!container.rendered) {
- container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
- return;
- }
+ // Sort into *descending* order.
+ minSizeSortFn: function(a, b) {
+ return b.available - a.available;
+ },
- // Init the DOM, incase this is the first time it will be used
- me.initDOM(options);
+ constructor: function(config) {
+ var me = this;
- // Create key navigation for subscriber based on keys option
- data.keyNav = me.setupSubscriberKeys(container, options.keys);
+ me.callParent(arguments);
- // 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);
+ // 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 {Ext.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;
+ var size = child.getBox(false, true);
+ return {
+ left: size.left,
+ top: size.top,
+ width: size.width,
+ height: size.height
};
- container.on('beforedestroy', me.unsubscribe, me);
+ },
- // Now we setup focusing abilities for the container and all its components
- safeSetFocus(container);
+ /**
+ * @private
+ * Calculates the size and positioning of the passed child item.
+ * @param {Ext.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;
- // Add to our subscribers list
- subs.add(container.id, data);
+ child = me.owner.getComponent(child);
+ for (; i < ln; i++) {
+ if (boxes[i].component === child) {
+ return boxes[i];
+ }
+ }
},
/**
- * 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
+ * @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
*/
- unsubscribe: function(container) {
+ calculateChildBoxes: function(visibleItems, targetSize) {
var me = this,
- EA = Ext.Array,
- subs = me.subscribers,
- data,
+ math = Math,
+ mmax = math.max,
+ infiniteValue = Infinity,
+ undefinedValue,
- // 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);
- }
- };
+ parallelPrefix = me.parallelPrefix,
+ parallelPrefixCap = me.parallelPrefixCap,
+ perpendicularPrefix = me.perpendicularPrefix,
+ perpendicularPrefixCap = me.perpendicularPrefixCap,
+ parallelMinString = 'min' + parallelPrefixCap,
+ perpendicularMinString = 'min' + perpendicularPrefixCap,
+ perpendicularMaxString = 'max' + perpendicularPrefixCap,
- if (!container || !subs.containsKey(container.id)) {
- return;
- }
+ 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),
- 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
+ innerCtBorderWidth = me.innerCt.getBorderWidth(me.perpendicularLT + me.perpendicularRB),
-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.
+ isStart = me.pack == 'start',
+ isCenter = me.pack == 'center',
+ isEnd = me.pack == 'end',
-__Some items have shortcut strings for creation:__
+ constrain = Ext.Number.constrain,
+ visibleCount = visibleItems.length,
+ nonFlexSize = 0,
+ totalFlex = 0,
+ desiredSize = 0,
+ minimumSize = 0,
+ maxSize = 0,
+ boxes = [],
+ minSizes = [],
+ calculatedWidth,
-| 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 |
+ 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;
-{@img Ext.toolbar.Toolbar/Ext.toolbar.Toolbar1.png Toolbar component}
-Example usage:
+ //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];
+ if (!child.flex || !(me.align == 'stretch' || me.align == 'stretchmax')) {
+ if (child.componentLayout.initialized !== true) {
+ me.layoutItem(child);
+ }
+ }
- 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'
- ]
- });
+ childMargins = child.margins;
+ parallelMargins = childMargins[me.parallelBefore] + childMargins[me.parallelAfter];
-Toolbars have {@link #enable} and {@link #disable} methods which when called, will enable/disable all items within your toolbar.
+ // Create the box description object for this child item.
+ tmpObj = {
+ component: child,
+ margins: childMargins
+ };
-{@img Ext.toolbar.Toolbar/Ext.toolbar.Toolbar2.png Toolbar component}
-Example usage:
+ // 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];
+ }
- 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'
+ 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]();
}
- ]
- });
-{@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();
+ // Track the maximum perpendicular size for use by the stretch and stretchmax align config values.
+ // Ensure that the tracked maximum perpendicular size takes into account child min[Width|Height] settings!
+ maxSize = mmax(maxSize, mmax(childPerpendicular, child[perpendicularMinString]||0) + childMargins[me.perpendicularLeftTop] + childMargins[me.perpendicularRightBottom]);
+
+ tmpObj[parallelPrefix] = childParallel || undefinedValue;
+ tmpObj.dirtySize = child.componentLayout.lastComponentSize ? (tmpObj[parallelPrefix] !== child.componentLayout.lastComponentSize[parallelPrefix]) : false;
+ tmpObj[perpendicularPrefix] = childPerpendicular || undefinedValue;
+ boxes.push(tmpObj);
}
- });
-
- 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.
+ // Only calculate parallel overflow indicators if we are not auto sizing
+ if (!me.autoSize) {
+ shortfall = desiredSize - parallelSize;
+ tooNarrow = minimumSize > parallelSize;
+ }
-{@img Ext.toolbar.Toolbar/Ext.toolbar.Toolbar4.png Toolbar component}
-Example usage:
+ //the space available to the flexed items
+ availableSpace = mmax(0, parallelSize - nonFlexSize - paddingParallel - (me.reserveOffset ? me.availableSpaceOffset : 0));
- 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();
- }
+ 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;
- * @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,
+ //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 if (me.shrinkToFit) {
+ minSizes.push({
+ minSize: minSize,
+ available: boxes[i][parallelPrefix] - minSize,
+ index: i
+ });
+ }
+ }
- /**
- * @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.
- */
+ //sort by descending amount of width remaining before minWidth is reached
+ Ext.Array.sort(minSizes, me.minSizeSortFn);
- /**
- * @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;
+ /*
+ * 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;
- // 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;
+ 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;
+ }
+ tooNarrow = (shortfall > 0);
+ }
+ 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;
+ }
+ }
}
- 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');
+ if (isCenter) {
+ parallelOffset += availableSpace / 2;
}
-
- // @TODO: remove this hack and implement a more general solution
- if (me.ui === 'footer') {
- me.ignoreBorderManagement = true;
+ else if (isEnd) {
+ parallelOffset += availableSpace;
}
-
- 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
- */
+ // 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') {
- // 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
- };
+ 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;
}
- this.applyDefaults(c);
+ // 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);
}
- 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);
+ //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) - innerCtBorderWidth - 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 c;
- },
- // private
- trackMenu: function(item, remove) {
- if (this.trackMenus && item.menu) {
- var method = remove ? 'mun' : 'mon',
- me = this;
+ return {
+ boxes: boxes,
+ meta : {
+ calculatedWidth: calculatedWidth,
+ maxSize: maxSize,
+ nonFlexSize: nonFlexSize,
+ desiredSize: desiredSize,
+ minimumSize: minimumSize,
+ shortfall: shortfall,
+ tooNarrow: tooNarrow
+ }
+ };
+ },
- me[method](item, 'menutriggerover', me.onButtonTriggerOver, me);
- me[method](item, 'menushow', me.onButtonMenuShow, me);
- me[method](item, 'menuhide', me.onButtonMenuHide, me);
+ onRemove: function(comp){
+ this.callParent(arguments);
+ if (this.overflowHandler) {
+ this.overflowHandler.onRemove(comp);
}
},
- // private
- constructButton: function(item) {
- return item.events ? item : this.createComponent(item, item.split ? 'splitbutton' : this.defaultType);
- },
+ /**
+ * @private
+ */
+ initOverflowHandler: function() {
+ var handler = this.overflowHandler;
- // 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');
+ if (typeof handler == 'string') {
+ handler = {
+ type: handler
+ };
}
-
- this.callParent(arguments);
- },
- // private
- onAdd: function(component) {
- this.callParent(arguments);
+ var handlerType = 'None';
+ if (handler && handler.type !== undefined) {
+ handlerType = handler.type;
+ }
- this.trackMenu(component);
- if (this.disabled) {
- component.disable();
+ var constructor = Ext.layout.container.boxOverflow[handlerType];
+ if (constructor[this.type]) {
+ constructor = constructor[this.type];
}
- },
- // private
- onRemove: function(c) {
- this.callParent(arguments);
- this.trackMenu(c, true);
+ this.overflowHandler = Ext.create('Ext.layout.container.boxOverflow.' + handlerType, this, handler);
},
- // private
- onButtonTriggerOver: function(btn){
- if (this.activeMenuBtn && this.activeMenuBtn != btn) {
- this.activeMenuBtn.hideMenu();
- btn.showMenu();
- this.activeMenuBtn = btn;
+ /**
+ * @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);
}
- },
- // private
- onButtonMenuShow: function(btn) {
- this.activeMenuBtn = btn;
- },
+ var me = this,
+ targetSize = me.getLayoutTargetSize(),
+ items = me.getVisibleItems(),
+ calcs = me.calculateChildBoxes(items, targetSize),
+ boxes = calcs.boxes,
+ meta = calcs.meta,
+ handler, method, results;
- // 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', {
+ if (me.autoSize && calcs.meta.desiredSize) {
+ targetSize[me.parallelPrefix] = calcs.meta.desiredSize;
+ }
- /* Begin Definitions */
+ //invoke the overflow handler, if one is configured
+ if (meta.shortfall > 0) {
+ handler = me.overflowHandler;
+ method = meta.tooNarrow ? 'handleOverflow': 'clearOverflow';
- extend: 'Ext.container.Container',
+ results = handler[method](calcs, targetSize);
- requires: ['Ext.util.MixedCollection', 'Ext.core.Element', 'Ext.toolbar.Toolbar'],
+ if (results) {
+ if (results.targetSize) {
+ targetSize = results.targetSize;
+ }
- /* End Definitions */
+ if (results.recalculate) {
+ items = me.getVisibleItems();
+ calcs = me.calculateChildBoxes(items, targetSize);
+ boxes = calcs.boxes;
+ }
+ }
+ } else {
+ me.overflowHandler.clearOverflow();
+ }
- /**
- * @cfg {String} baseCls
- * The base CSS class to apply to this panel's element (defaults to 'x-panel'
).
- */
- baseCls : Ext.baseCSSPrefix + 'panel',
+ /**
+ * @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;
- /**
- * @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
.
- */
+ /**
+ * @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;
- /**
- * @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
.
- */
+ me.updateInnerCtSize(targetSize, calcs);
+ me.updateChildBoxes(boxes);
+ me.handleTargetOverflow(targetSize);
+ },
- /**
- * @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;'
+ animCallback: Ext.emptyFn,
-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']
- *
+ * Resizes and repositions each child component
+ * @param {Object[]} 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;
- isPanel: true,
-
- componentLayout: 'dock',
-
- renderTpl: [' {bodyCls} {baseCls}-body-{ui} {parent.baseCls}-body-{parent.ui}-{.} " style="{bodyStyle}" >
'],
+ for (; i < length; i++) {
+ newBox = boxes[i];
+ comp = newBox.component;
- // 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,
+ // 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;
+ }
- 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'
- );
+ changed = false;
- 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();
- },
+ oldBox = me.getChildBox(comp);
- // @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);
+ // 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);
+ }
}
- },
- /**
- * 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);
- },
+ // Kick off any queued animations
+ length = animQueue.length;
+ if (length) {
- /**
- * 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);
+ // 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.animCallback(anim);
+ 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);
+ }
}
- 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
+ * 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 {Object} calculations The new box calculations of all items to be laid out
*/
- initBodyStyles: function() {
+ updateInnerCtSize: function(tSize, calcs) {
var me = this,
- bodyStyle = me.bodyStyle,
- styles = [],
- Element = Ext.core.Element,
- prop;
+ mmax = Math.max,
+ align = me.align,
+ padding = me.padding,
+ width = tSize.width,
+ height = tSize.height,
+ meta = calcs.meta,
+ innerCtWidth,
+ innerCtHeight;
- if (Ext.isFunction(bodyStyle)) {
- bodyStyle = bodyStyle();
- }
- if (Ext.isString(bodyStyle)) {
- styles = bodyStyle.split(';');
+ 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 {
- for (prop in bodyStyle) {
- if (bodyStyle.hasOwnProperty(prop)) {
- styles.push(prop + ':' + bodyStyle[prop]);
- }
+ 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 (me.bodyPadding !== undefined) {
- styles.push('padding: ' + Element.unitizeBox((me.bodyPadding === true) ? 5 : me.bodyPadding));
+ // 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.frame && me.bodyBorder) {
- if (!Ext.isNumber(me.bodyBorder)) {
- me.bodyBorder = 1;
- }
- styles.push('border-width: ' + Element.unitizeBox(me.bodyBorder));
+
+ if (me.innerCt.dom.scrollTop) {
+ me.innerCt.dom.scrollTop = 0;
}
- 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
+ * 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.Element} target The target element
+ * @return True if the layout overflowed, and was reflowed in a secondary onLayout call.
*/
- initBodyCls: function() {
- var me = this,
- cls = '',
- bodyCls = me.bodyCls;
-
- if (bodyCls) {
- Ext.each(bodyCls, function(v) {
- cls += " " + v;
+ 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'
});
- delete me.bodyCls;
+ this.padding = Ext.util.Format.parseBox(this.padding);
}
- return cls.length > 0 ? cls : undefined;
+ 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');
+ margins.height = margins.top + margins.bottom;
+ margins.width = margins.left + margins.right;
+ style.marginTop = style.marginRight = style.marginBottom = style.marginLeft = '0';
+
+ // Item must reference calculated margins.
+ item.margins = margins;
+ },
+
/**
- * 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()
- });
- },
+ destroy: function() {
+ Ext.destroy(this.innerCt, this.overflowHandler);
+ this.callParent(arguments);
+ }
+});
+/**
+ * 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.
+ *
+ * @example
+ * 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 */
/**
- * 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
+ * @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.
*/
- addDocked : function(items, pos) {
- var me = this,
- i = 0,
- item, length;
+ align: 'top', // top, middle, stretch, strechmax
- items = me.prepareItems(items);
- length = items.length;
+ //@private
+ alignCenteringString: 'middle',
- for (; i < length; i++) {
- item = items[i];
- item.dock = item.dock || 'top';
+ type : 'hbox',
- // 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;
- }
+ direction: 'horizontal',
- if (pos !== undefined) {
- me.dockedItems.insert(pos + i, item);
- }
- else {
- me.dockedItems.add(item);
- }
- item.onAdded(me, i);
- me.onDockedAdd(item);
+ // 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',
+ configureItem: function(item) {
+ if (item.flex) {
+ item.layoutManagedWidth = 1;
+ } else {
+ item.layoutManagedWidth = 2;
}
- if (me.rendered && !me.suspendLayout) {
- me.doComponentLayout();
+
+ if (this.align === 'stretch' || this.align === 'stretchmax') {
+ item.layoutManagedHeight = 1;
+ } else {
+ item.layoutManagedHeight = 2;
}
- return items;
- },
+ this.callParent(arguments);
+ }
+});
+/**
+ * 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.
+ *
+ * @example
+ * 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', {
- // Placeholder empty functions
- onDockedAdd : Ext.emptyFn,
- onDockedRemove : Ext.emptyFn,
+ /* Begin Definitions */
- /**
- * 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);
- },
+ alias: ['layout.vbox'],
+ extend: 'Ext.layout.container.Box',
+ alternateClassName: 'Ext.layout.VBoxLayout',
+
+ /* End Definitions */
/**
- * Removes the docked item from the panel.
- * @param {Ext.Component} item. The Component to remove.
- * @param {Boolean} autoDestroy (optional) Destroy the component after removal.
+ * @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.
*/
- removeDocked : function(item, autoDestroy) {
- var me = this,
- layout,
- hasLayout;
-
- if (!me.dockedItems.contains(item)) {
- return item;
- }
+ align : 'left', // left, center, stretch, strechmax
- layout = me.componentLayout;
- hasLayout = layout && me.rendered;
+ //@private
+ alignCenteringString: 'center',
- if (hasLayout) {
- layout.onRemove(item);
- }
+ type: 'vbox',
- me.dockedItems.remove(item);
- item.onRemoved();
- me.onDockedRemove(item);
+ direction: 'vertical',
- if (autoDestroy === true || (autoDestroy !== false && me.autoDestroy)) {
- item.destroy();
- }
+ // When creating an argument list to setSize, use this order
+ parallelSizeIndex: 1,
+ perpendicularSizeIndex: 0,
- if (hasLayout && !autoDestroy) {
- layout.afterRemove(item);
+ 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',
+ configureItem: function(item) {
+ if (item.flex) {
+ item.layoutManagedHeight = 1;
+ } else {
+ item.layoutManagedHeight = 2;
}
-
- if (!this.destroying) {
- me.doComponentLayout();
+
+ if (this.align === 'stretch' || this.align === 'stretchmax') {
+ item.layoutManagedWidth = 1;
+ } else {
+ item.layoutManagedWidth = 2;
}
+ this.callParent(arguments);
+ }
+});
+/**
+ * @class Ext.FocusManager
- return item;
- },
+The FocusManager is responsible for globally:
- /**
- * 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;
+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.
- 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();
- }
+To activate the FocusManager, simply call `Ext.FocusManager.enable();`. In turn, you may
+deactivate the FocusManager by subsequently calling `Ext.FocusManager.disable();. The
+FocusManager is disabled by default.
- 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);
- }
- },
+To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}.
- // @private
- getTargetEl : function() {
- return this.body;
- },
+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.
- 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);
+ * @singleton
+ * @author Jarred Nicholls
+ * @docauthor Jarred Nicholls
+ */
+Ext.define('Ext.FocusManager', {
+ singleton: true,
+ alternateClassName: 'Ext.FocusMgr',
+
+ mixins: {
+ observable: 'Ext.util.Observable'
},
- beforeDestroy: function(){
- var docked = this.dockedItems,
- c;
+ requires: [
+ 'Ext.ComponentManager',
+ 'Ext.ComponentQuery',
+ 'Ext.util.HashMap',
+ 'Ext.util.KeyNav'
+ ],
- 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',
+ /**
+ * @property {Boolean} enabled
+ * Whether or not the FocusManager is currently enabled
+ */
+ enabled: false,
- isHeader : true,
- defaultType : 'tool',
- indicateDrag : false,
- weight : -1,
+ /**
+ * @property {Ext.Component} focusedCmp
+ * The currently focused component. Defaults to `undefined`.
+ */
+
+ focusElementCls: Ext.baseCSSPrefix + 'focus-element',
- renderTpl: [' {bodyCls} {parent.baseCls}-body-{parent.ui}-{.} " style="{bodyStyle}" >
'],
+ focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
- initComponent: function() {
+ /**
+ * @property {String[]} 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.
+ */
+ whitelist: [
+ 'textfield'
+ ],
+
+ tabIndexWhitelist: [
+ 'a',
+ 'button',
+ 'embed',
+ 'frame',
+ 'iframe',
+ 'img',
+ 'input',
+ 'object',
+ 'select',
+ 'textarea'
+ ],
+
+ constructor: function() {
var me = this,
- rule,
- style,
- titleTextEl,
- ui;
+ CQ = Ext.ComponentQuery;
- 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';
+ 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.
+ */
+ 'beforecomponentfocus',
- //add the dock as a ui
- //this is so we support top/right/left/bottom headers
- me.addClsWithUI(me.orientation);
- me.addClsWithUI(me.dock);
+ /**
+ * @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.
+ */
+ 'componentfocus',
- Ext.applyIf(me.renderSelectors, {
- body: '.' + me.baseCls + '-body'
+ /**
+ * @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
});
- // Add Icon
- if (!Ext.isEmpty(me.iconCls)) {
- me.initIconCmp();
- me.items.push(me.iconCmp);
- }
+ me.focusData = {};
+ me.subscribers = Ext.create('Ext.util.HashMap');
+ me.focusChain = {};
- // 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;
- }
+ // Setup some ComponentQuery pseudos
+ Ext.apply(CQ.pseudos, {
+ focusable: function(cmps) {
+ var len = cmps.length,
+ results = [],
+ i = 0,
+ c,
- 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'
+ 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);
+ }
}
- });
- }
- me.items.push(me.titleCmp);
- // Spacer ->
- me.items.push({
- xtype: 'component',
- html : ' ',
- flex : 1,
- focusable: false
- });
+ return results;
+ },
- // Add Tools
- me.items = me.items.concat(me.tools);
- this.callParent();
- },
+ nextFocus: function(cmps, idx, step) {
+ step = step || 1;
+ idx = parseInt(idx, 10);
- 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
+ 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 [];
},
- renderSelectors: {
- iconEl: '.' + this.baseCls + '-icon'
+
+ prevFocus: function(cmps, idx) {
+ return this.nextFocus(cmps, idx, -1);
},
- iconCls: this.iconCls
+
+ 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;
+ }
});
},
- afterRender: function() {
+ /**
+ * Adds the specified xtype to the {@link #whitelist}.
+ * @param {String/String[]} xtype Adds the xtype(s) to the {@link #whitelist}.
+ */
+ addXTypeToWhitelist: function(xtype) {
var me = this;
- me.el.unselectable();
- if (me.indicateDrag) {
- me.el.addCls(me.indicateDragCls);
+ if (Ext.isArray(xtype)) {
+ Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
+ return;
}
- me.mon(me.el, {
- click: me.onClick,
- scope: me
- });
- me.callParent();
- },
- afterLayout: function() {
- var me = this;
- me.callParent(arguments);
+ if (!Ext.Array.contains(me.whitelist, xtype)) {
+ me.whitelist.push(xtype);
+ }
+ },
- // IE7 needs a forced repaint to make the top framing div expand to full width
- if (Ext.isIE7) {
- me.el.repaint();
+ clearComponent: function(cmp) {
+ clearTimeout(this.cmpFocusDelay);
+ if (!cmp.isDestroyed) {
+ cmp.blur();
}
},
- // inherit docs
- addUIClsToElement: function(cls, force) {
+ /**
+ * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
+ */
+ disable: function() {
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);
+ if (!me.enabled) {
+ return;
}
- },
- // inherit docs
- removeUIClsFromElement: function(cls, force) {
- var me = this;
+ delete me.options;
+ me.enabled = false;
- me.callParent(arguments);
+ Ext.ComponentManager.all.un('add', me.onComponentCreated, me);
- if (!force && me.rendered) {
- me.body.removeCls(me.baseCls + '-body-' + cls);
- me.body.removeCls(me.baseCls + '-body-' + me.ui + '-' + cls);
- }
+ me.removeDOM();
+
+ // Stop handling key navigation
+ me.keyNav.disable();
+
+ // disable focus for all components
+ me.setFocusAll(false);
+
+ me.fireEvent('disable', me);
},
- // inherit docs
- addUIToElement: function(force) {
+ /**
+ * 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;
- me.callParent(arguments);
-
- if (!force && me.rendered) {
- me.body.addCls(me.baseCls + '-body-' + me.ui);
+ if (options === true) {
+ options = { focusFrame: true };
}
+ me.options = options = options || {};
- if (!force && me.titleCmp && me.titleCmp.rendered && me.titleCmp.textEl) {
- me.titleCmp.textEl.addCls(me.baseCls + '-text-' + me.ui);
+ 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);
},
- // inherit docs
- removeUIFromElement: function() {
+ focusLast: function(e) {
var me = this;
- me.callParent(arguments);
-
- if (me.rendered) {
- me.body.removeCls(me.baseCls + '-body-' + me.ui);
+ if (me.isWhitelisted(me.focusedCmp)) {
+ return true;
}
- if (me.titleCmp && me.titleCmp.rendered && me.titleCmp.textEl) {
- me.titleCmp.textEl.removeCls(me.baseCls + '-text-' + me.ui);
+ // Go back to last focused item
+ if (me.previousFocusedCmp) {
+ me.previousFocusedCmp.focus();
}
},
- onClick: function(e) {
- if (!e.getTarget(Ext.baseCSSPrefix + 'tool')) {
- this.fireEvent('click', e);
- }
+ 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);
},
- getTargetEl: function() {
- return this.body || this.frameBody || this.el;
- },
+ initDOM: function(options) {
+ var me = this,
+ sp = ' ',
+ cls = me.focusFrameCls;
- /**
- * 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;
+ if (!Ext.isReady) {
+ Ext.onReady(me.initDOM, me);
+ return;
+ }
- 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
+ // Create global focus element
+ if (!me.focusEl) {
+ me.focusEl = Ext.getBody().createChild({
+ tabIndex: '-1',
+ cls: me.focusElementCls,
+ html: sp
});
}
- },
- /**
- * 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);
+ // 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.Element.DISPLAY);
+ me.focusFrameWidth = 2;
+ me.focusFrame.hide().setLeftTop(0, 0);
}
- 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;
+ 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();
+ }
}
}
},
- /**
- * Add a tool to the header
- * @param {Object} tool
- */
- addTool: function(tool) {
- this.tools.push(this.add(tool));
- },
+ navigateOut: function(e) {
+ var me = this,
+ parent;
- /**
- * @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;
+ if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
+ me.focusEl.focus();
+ } else {
+ parent.focus();
}
- }
-});
-/**
- * @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', {
+ // In some browsers (Chrome) FocusManager can handle this before other
+ // handlers. Ext Windows have their own Esc key handling, so we need to
+ // return true here to allow the event to bubble.
+ return true;
+ },
- /* Begin Definitions */
-
- extend: 'Ext.fx.target.Target',
-
- /* End Definitions */
+ 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;
- type: 'element',
+ focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
+ if (!focusedCmp && !parent) {
+ return;
+ }
- 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);
+ 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;
}
}
- return val;
},
- getAttr: function(attr, val) {
- var el = this.target;
- return [[ el, this.getElVal(el, attr, val)]];
- },
+ onComponentBlur: function(cmp, e) {
+ var me = this;
- 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);
- }
- }
- }
- }
+ if (me.focusedCmp === cmp) {
+ me.previousFocusedCmp = cmp;
+ delete me.focusedCmp;
}
- }
-});
-/**
- * @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', {
+ if (me.focusFrame) {
+ me.focusFrame.hide();
+ }
+ },
- /* Begin Definitions */
+ onComponentCreated: function(hash, id, cmp) {
+ this.setFocus(cmp, true, this.options);
+ },
- extend: 'Ext.fx.target.Element',
+ onComponentDestroy: function(cmp) {
+ this.setFocus(cmp, false);
+ },
- /* End Definitions */
+ onComponentFocus: function(cmp, e) {
+ var me = this,
+ chain = me.focusChain;
- isComposite: true,
-
- constructor: function(target) {
- target.id = target.id || Ext.id(null, 'ext-composite-');
- this.callParent([target]);
- },
+ if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
+ me.clearComponent(cmp);
- getAttr: function(attr, val) {
- var out = [],
- target = this.target;
- target.each(function(el) {
- out.push([el, this.getElVal(el, attr, val)]);
- }, this);
- return out;
- }
-});
+ // 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;
+ }
-/**
- * @class Ext.fx.Manager
- * Animation Manager which keeps track of all current animations and manages them on a frame by frame basis.
- * @private
- * @singleton
- */
+ // 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();
+ }
-Ext.define('Ext.fx.Manager', {
+ return;
+ }
- /* Begin Definitions */
+ // Clear our focus chain when we have a focusable component
+ me.focusChain = {};
- singleton: true,
+ // 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;
+ }
- requires: ['Ext.util.MixedCollection',
- 'Ext.fx.target.Element',
- 'Ext.fx.target.CompositeElement',
- 'Ext.fx.target.Sprite',
- 'Ext.fx.target.CompositeSprite',
- 'Ext.fx.target.Component'],
+ if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
+ me.clearComponent(cmp);
+ return;
+ }
- mixins: {
- queue: 'Ext.fx.Queue'
- },
+ me.focusedCmp = cmp;
- /* End Definitions */
+ // 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(),
- constructor: function() {
- this.items = Ext.create('Ext.util.MixedCollection');
- this.mixins.queue.constructor.call(this);
+ // 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');
- // 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);
- // };
- // }
- // })();
- },
+ ft.setWidth(bw).setLeftTop(bl, bt);
+ fb.setWidth(bw).setLeftTop(bl, bt + bh - fw);
+ fl.setHeight(bh - fw - fw).setLeftTop(bl, bt + fw);
+ fr.setHeight(bh - fw - fw).setLeftTop(bl + bw - fw, bt + fw);
- /**
- * @cfg {Number} interval Default interval in miliseconds to calculate each frame. Defaults to 16ms (~60fps)
- */
- interval: 16,
+ ff.show();
+ }
- /**
- * @cfg {Boolean} forceJS Turn off to not use CSS3 transitions when they are available
- */
- forceJS: true,
+ me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
+ },
- // @private Target factory
- createTarget: function(target) {
+ onComponentHide: function(cmp) {
var me = this,
- useCSS3 = !me.forceJS && Ext.supports.Transitions,
- targetObj;
+ CQ = Ext.ComponentQuery,
+ cmpHadFocus = false,
+ focusedCmp,
+ parent;
- me.useCSS3 = useCSS3;
+ if (me.focusedCmp) {
+ focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
+ cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
- // 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;
+ if (focusedCmp) {
+ me.clearComponent(focusedCmp);
}
- else {
- return null;
+ }
+
+ me.clearComponent(cmp);
+
+ if (cmpHadFocus) {
+ parent = CQ.query('^:focusable', cmp)[0];
+ if (parent) {
+ parent.focus();
}
- me.targets.add(targetObj);
- return targetObj;
}
- else {
- return null;
+ },
+
+ 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;
},
/**
- * Add an Anim to the manager. This is done automatically when an Anim instance is created.
- * @param {Ext.fx.Anim} anim
+ * Removes the specified xtype from the {@link #whitelist}.
+ * @param {String/String[]} xtype Removes the xtype(s) from the {@link #whitelist}.
*/
- 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);
+ removeXTypeFromWhitelist: function(xtype) {
+ var me = this;
- // 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);
+ if (Ext.isArray(xtype)) {
+ Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
+ return;
}
- // //Start the timer if not already running
- // if (!me.task && items.length) {
- // me.requestAnimFrame(cb);
- // }
+ Ext.Array.remove(me.whitelist, xtype);
},
- /**
- * 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;
- }
- },
+ setFocus: function(cmp, focusable, options) {
+ var me = this,
+ el, dom, data,
- /**
- * @private
- * Filter function to determine which animations need to be started
- */
- startingFilter: function(o) {
- return o.paused === false && o.running === false && o.iterations > 0;
- },
+ needsTabIndex = function(n) {
+ return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
+ && n.tabIndex <= 0;
+ };
- /**
- * @private
- * Filter function to determine which animations are still running
- */
- runningFilter: function(o) {
- return o.paused === false && o.running === true && o.isAnimator !== true;
- },
+ options = options || {};
- /**
- * @private
- * Runner function being called each frame
- */
- runner: function() {
- var me = this,
- items = me.items;
+ // 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;
+ }
- me.targetData = {};
- me.targetArr = {};
+ el = cmp.getFocusEl();
+ dom = el.dom;
- // Single timestamp for all animations this interval
- me.timestamp = new Date();
+ // 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
+ };
- // Start any items not current running
- items.filterBy(me.startingFilter).each(me.startAnim, me);
+ // 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;
+ }
- // Build the new attributes to be applied for all targets in this frame
- items.filterBy(me.runningFilter).each(me.runAnim, me);
+ 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
+ });
- // Apply all the pending changes to their targets
- me.applyPendingAttrs();
- },
+ 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);
- /**
- * @private
- * Start the individual animation (initialization)
- */
- startAnim: function(anim) {
- anim.start(this.timestamp);
+ delete me.focusData[cmp.id];
+ }
+ }
},
- /**
- * @private
- * Run the individual animation for this frame
- */
- runAnim: function(anim) {
- if (!anim) {
- return;
+ 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,
- targetId = anim.target.getId(),
- useCSS3 = me.useCSS3 && anim.target.type == 'element',
- elapsedTime = me.timestamp - anim.startTime,
- target, o;
+ el = container.getFocusEl(),
+ scope = keys.scope,
+ handlers = {
+ backspace: me.focusLast,
+ enter: me.navigateIn,
+ esc: me.navigateOut,
+ scope: me
+ },
- this.collectTargetData(anim, elapsedTime, useCSS3);
+ 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);
+ }
+ };
- // 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);
+ Ext.iterate(keys, function(key, cb) {
+ handlers[key] = function(e) {
+ var ret = navSiblings(e);
- // Add the end frame data
- me.targetData[targetId] = [];
- me.collectTargetData(anim, anim.duration, useCSS3);
+ if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
+ return true;
+ }
- // Pause the animation so runAnim doesn't keep getting called
- anim.paused = true;
+ return ret;
+ };
+ }, me);
- 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();
- }
+ return Ext.create('Ext.util.KeyNav', el, handlers);
+ },
- // Listen for the transitionend event
- o = {};
- o[Ext.supports.CSS3TransitionEnd] = anim.lastFrame;
- o.scope = anim;
- o.single = true;
- target.on(o);
+ shouldShowFocusFrame: function(cmp) {
+ var me = this,
+ opts = me.options || {};
+
+ if (!me.focusFrame || !cmp) {
+ return false;
}
- // 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();
+
+ // Global trumps
+ if (opts.focusFrame) {
+ return true;
}
- },
- /**
- * 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;
+ if (me.focusData[cmp.id].focusFrame) {
+ return true;
}
- 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);
+ return false;
},
/**
- * @private
- * Apply all pending attribute changes to their targets
+ * 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
+ * @param {Array/Object} options.keys
+ * 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
+ * }
+ *
+ * @param {Boolean} options.focusFrame
+ * `true` to show the focus frame around a component when it is focused. Defaults to `false`.
*/
- 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.
+ subscribe: function(container, options) {
+ var me = this,
+ EA = Ext.Array,
+ data = {},
+ subs = me.subscribers,
-__Example Usage__
-In the example below, we modify the values of the element at each fifth throughout the animation.
+ // 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);
+ }
+ };
- 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'
- }
+ // We only accept containers
+ if (!container || !container.isContainer) {
+ return;
}
- });
-
- * @markdown
- */
-Ext.define('Ext.fx.Animator', {
- /* Begin Definitions */
+ if (!container.rendered) {
+ container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
+ return;
+ }
- mixins: {
- observable: 'Ext.util.Observable'
- },
+ // Init the DOM, incase this is the first time it will be used
+ me.initDOM(options);
- requires: ['Ext.fx.Manager'],
+ // Create key navigation for subscriber based on keys option
+ data.keyNav = me.setupSubscriberKeys(container, options.keys);
- /* End Definitions */
+ // 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);
- isAnimator: true,
+ // Now we setup focusing abilities for the container and all its components
+ safeSetFocus(container);
- /**
- * @cfg {Number} duration
- * Time in milliseconds for the animation to last. Defaults to 250.
- */
- duration: 250,
+ // Add to our subscribers list
+ subs.add(container.id, data);
+ },
/**
- * @cfg {Number} delay
- * Time to delay before starting the animation. Defaults to 0.
+ * 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.
*/
- delay: 0,
+ unsubscribe: function(container) {
+ var me = this,
+ EA = Ext.Array,
+ subs = me.subscribers,
+ data,
- /* private used to track a delayed starting time */
- delayStart: 0,
+ // 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);
+ }
+ };
- /**
- * @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,
+ if (!container || !subs.containsKey(container.id)) {
+ return;
+ }
- /**
- * @cfg {String} easing
+ data = subs.get(container.id);
+ data.keyNav.destroy();
+ container.un('beforedestroy', me.unsubscribe, me);
+ subs.removeAtKey(container.id);
+ safeSetFocus(container);
+ me.removeDOM();
+ }
+});
+/**
+ * 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
+ * |:---------|:--------------|:------------------------------|:---------------------------------------------------
+ * | `->` | `tbfill` | {@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
+ *
+ * @example
+ * 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' }
+ * {
+ * 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.
+ *
+ * @example
+ * 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'
+ * }
+ * ]
+ * });
+ *
+ * Example
+ *
+ * @example
+ * 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.
+ *
+ * @example
+ * 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/Object[]} config A config object or an array of buttons to {@link #add}
+ * @docauthor Robert Dougan
+ */
+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',
-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.
+ isToolbar: true,
+ baseCls : Ext.baseCSSPrefix + 'toolbar',
+ ariaRole : 'toolbar',
- * @markdown
- */
- easing: 'ease',
+ defaultType: 'button',
/**
- * Flag to determine if the animation has started
- * @property running
- * @type boolean
+ * @cfg {Boolean} vertical
+ * Set to `true` to make the toolbar vertical. The layout will become a `vbox`.
*/
- running: false,
+ vertical: 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
+ * @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.
*/
- paused: false,
/**
- * @private
+ * @cfg {Boolean} enableOverflow
+ * Configure true to make the toolbar provide a button which activates a dropdown Menu to show
+ * items which overflow the Toolbar's width.
*/
- damper: 1,
+ enableOverflow: false,
/**
- * @cfg {Number} iterations
- * Number of times to execute the animation. Defaults to 1.
+ * @cfg {String} menuTriggerCls
+ * Configure the icon class of the overflow button.
*/
- iterations: 1,
+ menuTriggerCls: Ext.baseCSSPrefix + 'toolbar-more-icon',
+
+ // private
+ trackMenus: true,
- /**
- * Current iteration the animation is running.
- * @property currentIteration
- * @type int
- */
- currentIteration: 0,
+ itemCls: Ext.baseCSSPrefix + 'toolbar-item',
- /**
- * Current keyframe step of the animation.
- * @property keyframeStep
- * @type Number
- */
- keyframeStep: 0,
+ initComponent: function() {
+ var me = this,
+ keys;
- /**
- * @private
- */
- animKeyFramesRE: /^(from|to|\d+%?)$/,
+ // check for simplified (old-style) overflow config:
+ if (!me.layout && me.enableOverflow) {
+ me.layout = { overflowHandler: 'Menu' };
+ }
- /**
- * @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.
- */
+ if (me.dock === 'right' || me.dock === 'left') {
+ me.vertical = true;
+ }
- /**
- * @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);
+ me.layout = Ext.applyIf(Ext.isString(me.layout) ? {
+ type: me.layout
+ } : me.layout || {}, {
+ type: me.vertical ? 'vbox' : 'hbox',
+ align: me.vertical ? 'stretchmax' : 'middle',
+ clearInnerCtOnLayout: true
+ });
+
+ if (me.vertical) {
+ me.addClsWithUI('vertical');
}
- },
- /**
- * @private
- */
- sorter: function (a, b) {
- return a.pct - b.pct;
+ // @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
+ });
},
- /**
- * @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) {
+ getRefItems: function(deep) {
var me = this,
- attrs = [],
- to = me.to || {},
- duration = me.duration,
- prevMs, ms, i, ln, pct, anim, nextAnim, attr;
+ items = me.callParent(arguments),
+ layout = me.layout,
+ handler;
- 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);
+ if (deep && me.enableOverflow) {
+ handler = layout.overflowHandler;
+ if (handler && handler.menu) {
+ items = items.concat(handler.menu.getRefItems(deep));
}
}
- // 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
- });
- }
+ return items;
},
/**
- * Applies animation to the Ext.fx.target
- * @private
- * @param target
- * @type string/object
+ * 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 {Object...} args The following types of arguments are all valid:
+ * - `{@link Ext.button.Button config}`: A valid button config object
+ * - `HtmlElement`: Any standard HTML element
+ * - `Field`: Any form field
+ * - `Item`: Any subclass of {@link Ext.toolbar.Item}
+ * - `String`: Any generic string (gets wrapped in a {@link Ext.toolbar.TextItem}).
+ * Note that there are a few special strings that are treated differently as explained next.
+ * - `'-'`: Creates a separator element
+ * - `' '`: Creates a spacer element
+ * - `'->'`: Creates a fill element
+ *
+ * @method add
*/
- 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);
+ // 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
+ };
}
- anims[ln - 1].on('afteranimate', function() {
- this.lastFrame();
- }, me);
+ this.applyDefaults(c);
}
+ return this.callParent(arguments);
},
- /*
- * @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);
- }
+ // 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);
}
}
- if (me.fireEvent('beforeanimate', me) !== false) {
- me.startTime = startTime;
- me.running = true;
- me.animations[me.keyframeStep].paused = false;
+ return c;
+ },
+
+ // private
+ trackMenu: function(item, remove) {
+ if (this.trackMenus && item.menu) {
+ var method = remove ? 'mun' : 'mon',
+ me = this;
+
+ me[method](item, 'mouseover', me.onButtonOver, me);
+ me[method](item, 'menushow', me.onButtonMenuShow, me);
+ me[method](item, 'menuhide', me.onButtonMenuHide, me);
}
},
- /*
- * @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;
+ // private
+ constructButton: function(item) {
+ return item.events ? item : this.createComponent(item, item.split ? 'splitbutton' : this.defaultType);
+ },
- iterCount++;
- if (iterCount < iter) {
- me.startTime = new Date();
- me.currentIteration = iterCount;
- me.keyframeStep = 0;
- me.applyAnimator(me.target);
- me.animations[me.keyframeStep].paused = false;
+ // private
+ onBeforeAdd: function(component) {
+ if (component.is('field') || (component.is('button') && this.ui != 'footer')) {
+ component.ui = component.ui + '-toolbar';
}
- else {
- me.currentIteration = 0;
- me.end();
+
+ // 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);
},
- /*
- * 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');
+ // private
+ onAdd: function(component) {
+ this.callParent(arguments);
-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)
- };
+ this.trackMenu(component);
+ if (this.disabled) {
+ component.disable();
+ }
+ },
- 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;
+ // private
+ onRemove: function(c) {
+ this.callParent(arguments);
+ this.trackMenu(c, true);
+ },
+
+ // private
+ onButtonOver: function(btn){
+ if (this.activeMenuBtn && this.activeMenuBtn != btn) {
+ this.activeMenuBtn.hideMenu();
+ btn.showMenu();
+ this.activeMenuBtn = btn;
}
- });
- 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
- });
+ },
+
+ // private
+ onButtonMenuShow: function(btn) {
+ this.activeMenuBtn = btn;
+ },
+
+ // private
+ onButtonMenuHide: function(btn) {
+ delete this.activeMenuBtn;
+ }
+}, function() {
+ this.shortcuts = {
+ '-' : 'tbseparator',
+ ' ' : 'tbspacer',
+ '->': 'tbfill'
+ };
});
-/*
- * @class Ext.draw.Draw
- * Base Drawing class. Provides base drawing functions.
+/**
+ * @class Ext.panel.AbstractPanel
+ * @extends Ext.container.Container
+ * A base class which provides methods common to Panel classes across the Sencha product range.
+ * @private
*/
+Ext.define('Ext.panel.AbstractPanel', {
-Ext.define('Ext.draw.Draw', {
/* Begin Definitions */
- singleton: true,
+ extend: 'Ext.container.Container',
- requires: ['Ext.draw.Color'],
+ requires: ['Ext.util.MixedCollection', 'Ext.Element', 'Ext.toolbar.Toolbar'],
/* 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,
+ /**
+ * @cfg {String} [baseCls='x-panel']
+ * The base CSS class to apply to this panel's element.
+ */
+ baseCls : Ext.baseCSSPrefix + 'panel',
- 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
- },
+ /**
+ * @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.
+ */
- 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;
- },
+ /**
+ * @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`.
+ */
- 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);
- },
+ /**
+ * @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;'
- 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);
- }
- },
+bodyStyle: {
+ background: '#ffc',
+ padding: '10px'
+}
+ *
+ */
- // To be deprecated, converts itself (an arrayPath) to a proper SVG path string
- path2string: function () {
- return this.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1");
- },
+ /**
+ * @cfg {String/String[]} 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']
+ *
+ */
- // Convert the passed arrayPath to a proper SVG path string (d attribute)
- pathToString: function(arrayPath) {
- return arrayPath.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1");
+ isPanel: true,
+
+ componentLayout: 'dock',
+
+ /**
+ * @cfg {Object} defaultDockWeights
+ * This object holds the default weights applied to dockedItems that have no weight. These start with a
+ * weight of 1, to allow negative weights to insert before top items and are odd numbers
+ * so that even weights can be used to get between different dock orders.
+ *
+ * To make default docking order match border layout, do this:
+ *
+Ext.panel.AbstractPanel.prototype.defaultDockWeights = { top: 1, bottom: 3, left: 5, right: 7 };
+ * Changing these defaults as above or individually on this object will effect all Panels.
+ * To change the defaults on a single panel, you should replace the entire object:
+ *
+initComponent: function () {
+ // NOTE: Don't change members of defaultDockWeights since the object is shared.
+ this.defaultDockWeights = { top: 1, bottom: 3, left: 5, right: 7 };
+
+ this.callParent();
+}
+ *
+ * To change only one of the default values, you do this:
+ *
+initComponent: function () {
+ // NOTE: Don't change members of defaultDockWeights since the object is shared.
+ this.defaultDockWeights = Ext.applyIf({ top: 10 }, this.defaultDockWeights);
+
+ this.callParent();
+}
+ */
+ defaultDockWeights: { top: 1, left: 3, right: 5, bottom: 7 },
+
+ 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/Object[]} 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'
+ );
+
+ me.addChildEls('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();
},
- parsePathString: function (pathString) {
- if (!pathString) {
- return null;
+ // @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);
}
- 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);
+ },
+
+ /**
+ * 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();
}
- 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;
- }
- }
- });
+ 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 {@link #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);
}
- data.toString = me.path2string;
- return data;
+ return component;
},
- mapPath: function (path, matrix) {
- if (!matrix) {
- return path;
+ /**
+ * 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.Element,
+ prop;
+
+ if (Ext.isFunction(bodyStyle)) {
+ bodyStyle = bodyStyle();
}
- 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;
+ if (Ext.isString(bodyStyle)) {
+ styles = bodyStyle.split(';');
+ } else {
+ for (prop in bodyStyle) {
+ if (bodyStyle.hasOwnProperty(prop)) {
+ styles.push(prop + ':' + bodyStyle[prop]);
+ }
}
}
- 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);
+ if (me.bodyPadding !== undefined) {
+ styles.push('padding: ' + Element.unitizeBox((me.bodyPadding === true) ? 5 : me.bodyPadding));
}
- 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];
+ if (me.frame && me.bodyBorder) {
+ if (!Ext.isNumber(me.bodyBorder)) {
+ me.bodyBorder = 1;
}
+ styles.push('border-width: ' + Element.unitizeBox(me.bodyBorder));
}
- res.toString = this.path2string;
- return res;
+ delete me.bodyStyle;
+ return styles.length ? styles.join(';') : undefined;
},
- pathToAbsolute: function (pathArray) {
- if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { // rough assumption
- pathArray = this.parsePathString(pathArray);
+ /**
+ * 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;
}
- var res = [],
- x = 0,
- y = 0,
- mx = 0,
- my = 0,
+ 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/Object[]} 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,
- ln = pathArray.length,
- r, pathSegment, j, ln2;
- // MoveTo initial x/y position
- if (ln && pathArray[0][0] == "M") {
- x = +pathArray[0][1];
- y = +pathArray[0][2];
- mx = x;
- my = y;
- i++;
- res[0] = ["M", x, y];
- }
- for (; i < ln; i++) {
- r = res[i] = [];
- pathSegment = pathArray[i];
- if (pathSegment[0] != pathSegment[0].toUpperCase()) {
- r[0] = pathSegment[0].toUpperCase();
- switch (r[0]) {
- // Elliptical Arc
- case "A":
- r[1] = pathSegment[1];
- r[2] = pathSegment[2];
- r[3] = pathSegment[3];
- r[4] = pathSegment[4];
- r[5] = pathSegment[5];
- r[6] = +(pathSegment[6] + x);
- r[7] = +(pathSegment[7] + y);
- break;
- // Vertical LineTo
- case "V":
- r[1] = +pathSegment[1] + y;
- break;
- // Horizontal LineTo
- case "H":
- r[1] = +pathSegment[1] + x;
- break;
- case "M":
- // MoveTo
- mx = +pathSegment[1] + x;
- my = +pathSegment[2] + y;
- default:
- j = 1;
- ln2 = pathSegment.length;
- for (; j < ln2; j++) {
- r[j] = +pathSegment[j] + ((j % 2) ? x : y);
- }
- }
+ 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;
}
- else {
- j = 0;
- ln2 = pathSegment.length;
- for (; j < ln2; j++) {
- res[i][j] = pathSegment[j];
- }
+
+ if (pos !== undefined) {
+ me.dockedItems.insert(pos + i, item);
}
- switch (r[0]) {
- // ClosePath
- case "Z":
- x = mx;
- y = my;
- break;
- // Horizontal LineTo
- case "H":
- x = r[1];
- break;
- // Vertical LineTo
- case "V":
- y = r[1];
- break;
- // MoveTo
- case "M":
- pathSegment = res[i];
- ln2 = pathSegment.length;
- mx = pathSegment[ln2 - 2];
- my = pathSegment[ln2 - 1];
- default:
- pathSegment = res[i];
- ln2 = pathSegment.length;
- x = pathSegment[ln2 - 2];
- y = pathSegment[ln2 - 1];
+ else {
+ me.dockedItems.add(item);
}
+ item.onAdded(me, i);
+ me.onDockedAdd(item);
}
- res.toString = this.path2string;
- return res;
+
+ // Set flag which means that beforeLayout will not veto the layout due to the size not changing
+ me.componentLayout.childrenChanged = true;
+ if (me.rendered && !me.suspendLayout) {
+ me.doComponentLayout();
+ }
+ return items;
},
- // TO BE DEPRECATED
- pathToRelative: function (pathArray) {
- if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) {
- pathArray = this.parsePathString(pathArray);
+ // 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/Object[]} 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;
}
- 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]);
+
+ layout = me.componentLayout;
+ hasLayout = layout && me.rendered;
+
+ if (hasLayout) {
+ layout.onRemove(item);
}
- 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);
- }
- }
+
+ me.dockedItems.remove(item);
+ item.onRemoved();
+ me.onDockedRemove(item);
+
+ if (autoDestroy === true || (autoDestroy !== false && me.autoDestroy)) {
+ item.destroy();
+ } else if (hasLayout) {
+ // not destroying, make any layout related removals
+ layout.afterRemove(item);
+ }
+
+
+ // Set flag which means that beforeLayout will not veto the layout due to the size not changing
+ me.componentLayout.childrenChanged = true;
+ if (!me.destroying && !me.suspendLayout) {
+ 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 {Ext.Component[]} An array of components.
+ */
+ getDockedItems : function(cqSelector) {
+ var me = this,
+ defaultWeight = me.defaultDockWeights,
+ 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 {
- 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];
+ 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)
+ 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;
}
- res.toString = this.path2string;
- return res;
+ return [];
},
- // Returns a path converted to a set of curveto commands
- path2curve: function (path) {
+ // inherit docs
+ addUIClsToElement: function(cls, force) {
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)));
+ result = me.callParent(arguments),
+ classes = [Ext.baseCSSPrefix + cls, me.baseCls + '-body-' + cls, me.baseCls + '-body-' + me.ui + '-' + cls],
+ array, i;
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.addCls(me.bodyCls);
+ } else {
+ me.body.addCls(classes);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ for (i = 0; i < classes.length; i++) {
+ if (!Ext.Array.contains(array, classes[i])) {
+ array.push(classes[i]);
}
- 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;
+
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = classes.join(' ');
+ }
}
- return points;
+
+ return result;
},
-
- interpolatePaths: function (path, path2) {
+
+ // inherit docs
+ removeUIClsFromElement: function(cls, force) {
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);
+ result = me.callParent(arguments),
+ classes = [Ext.baseCSSPrefix + cls, me.baseCls + '-body-' + cls, me.baseCls + '-body-' + me.ui + '-' + cls],
+ array, i;
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.removeCls(me.bodyCls);
+ } else {
+ me.body.removeCls(classes);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ for (i = 0; i < classes.length; i++) {
+ Ext.Array.remove(array, classes[i]);
}
- };
- 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;
+
+ me.bodyCls = array.join(' ');
+ }
}
- 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};
+ return result;
},
- 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
+ // inherit docs
+ addUIToElement: function(force) {
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));
+ cls = me.baseCls + '-body-' + me.ui,
+ array;
- 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;
+ me.callParent(arguments);
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.addCls(me.bodyCls);
+ } else {
+ me.body.addCls(cls);
}
- }
- 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;
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ if (!Ext.Array.contains(array, cls)) {
+ array.push(cls);
+ }
+
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = cls;
}
- return newres;
}
},
- // TO BE DEPRECATED
- 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;
+ // inherit docs
+ removeUIFromElement: function() {
+ var me = this,
+ cls = me.baseCls + '-body-' + me.ui,
+ array;
+
+ me.callParent(arguments);
+
+ if (me.rendered) {
+ if (me.bodyCls) {
+ me.body.removeCls(me.bodyCls);
} 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;
- }
+ me.body.removeCls(cls);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+ Ext.Array.remove(array, cls);
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = cls;
}
}
- return res;
},
- // TO BE DEPRECATED
- 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
- };
+ // @private
+ getTargetEl : function() {
+ return this.body;
},
- 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 = [],
+ 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,
- ln = path.length,
- p, xmin, ymin, dim;
+ item;
+
+ // Find the index where we go from top/left docked items to right/bottom docked items
for (; i < ln; 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];
+ item = dockedItems[i];
+ if (item.dock === 'right' || item.dock === 'bottom') {
+ break;
}
}
- 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
- };
- },
- intersectInside: function(path, cp1, cp2) {
- return (cp2[0] - cp1[0]) * (path[1] - cp1[1]) > (cp2[1] - cp1[1]) * (path[0] - cp1[0]);
+ // 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 Ext.Array.splice(dockedItems, 0, i).concat(items).concat(dockedItems);
},
- intersectIntersection: function(s, e, cp1, cp2) {
- var p = [],
- 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;
- },
+ beforeDestroy: function(){
+ var docked = this.dockedItems,
+ c;
- intersect: function(subjectPolygon, clipPolygon) {
- var me = this,
- i = 0,
- ln = clipPolygon.length,
- cp1 = clipPolygon[ln - 1],
- outputList = subjectPolygon,
- cp2, s, e, point, ln2, inputList, j;
- for (; i < ln; ++i) {
- cp2 = clipPolygon[i];
- inputList = outputList;
- outputList = [];
- s = inputList[inputList.length - 1];
- j = 0;
- ln2 = inputList.length;
- for (; j < ln2; j++) {
- e = inputList[j];
- if (me.intersectInside(e, cp1, cp2)) {
- if (!me.intersectInside(s, cp1, cp2)) {
- outputList.push(me.intersectIntersection(s, e, cp1, cp2));
- }
- outputList.push(e);
- }
- else if (me.intersectInside(s, cp1, cp2)) {
- outputList.push(me.intersectIntersection(s, e, cp1, cp2));
- }
- s = e;
+ if (docked) {
+ while ((c = docked.first())) {
+ this.removeDocked(c, true);
}
- cp1 = cp2;
}
- return outputList;
+ this.callParent();
},
- 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);
+ setBorder: function(border) {
+ var me = this;
+ me.border = (border !== undefined) ? border : true;
+ if (me.rendered) {
+ me.doComponentLayout();
}
- 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)}
- };
- },
+ }
+});
+/**
+ * @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}
+ */
+Ext.define('Ext.panel.Header', {
+ extend: 'Ext.container.Container',
+ uses: ['Ext.panel.Tool', 'Ext.draw.Component', 'Ext.util.CSS'],
+ alias: 'widget.header',
- 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;
- },
+ isHeader : true,
+ defaultType : 'tool',
+ indicateDrag : false,
+ weight : -1,
- /* 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.
+ renderTpl: [
+ ' {bodyCls}',
+ '',
+ ' {parent.baseCls}-body-{parent.ui}-{.} ',
+ ' "',
+ ' style="{bodyStyle}" >
'],
+
+ /**
+ * @cfg {String} title
+ * The title text to display
*/
- 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;
+
+ /**
+ * @cfg {String} iconCls
+ * CSS class for icon in header. Used for displaying an icon to the left of a title.
+ */
+
+ initComponent: function() {
+ var me = this,
+ ruleStyle,
+ 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);
+
+ me.addChildEls('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;
}
- 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);
+
+ 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];
}
- newp.push(["C", x, y, points.x1, points.y1, pathi[pathil - 2], pathi[pathil - 1]]);
- x = points.x2;
- y = points.y2;
+ ruleStyle = '.' + me.baseCls + '-text-' + ui;
+ if (Ext.scopeResetCSS) {
+ ruleStyle = '.' + Ext.baseCSSPrefix + 'reset ' + ruleStyle;
+ }
+ rule = Ext.util.CSS.getRule(ruleStyle);
+ 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,
+ flex : 1,
+ autoSize: true,
+ margins: '5 0 0 0',
+ items: [ me.textConfig ],
+ // this is a bit of a cheat: we are not selecting an element of titleCmp
+ // but rather of titleCmp.items[0] (so we cannot use childEls)
+ 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,
+ flex : 1,
+ cls: me.baseCls + '-text-container',
+ renderTpl : [
+ '{title} '
+ ],
+ renderData: {
+ title: me.title,
+ cls : me.baseCls,
+ ui : me.ui
+ },
+ childEls: ['textEl']
+ });
}
- return newp;
+ me.items.push(me.titleCmp);
+
+ // Add Tools
+ me.items = me.items.concat(me.tools);
+ this.callParent();
},
- 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
- };
+ 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
+ },
+ childEls: ['iconEl'],
+ iconCls: this.iconCls
+ });
},
- 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++;
+ afterRender: function() {
+ var me = this;
+
+ me.el.unselectable();
+ if (me.indicateDrag) {
+ me.el.addCls(me.indicateDragCls);
}
- to = +cur.toFixed(10);
- return {
- from: from,
- to: to,
- power: level,
- step: step,
- steps: stepCount
- };
+ me.mon(me.el, {
+ click: me.onClick,
+ scope: me
+ });
+ me.callParent();
},
- sorter: function (a, b) {
- return a.offset - b.offset;
+ 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();
+ }
},
- rad: function(degrees) {
- return degrees % 360 * Math.PI / 180;
+ // inherit docs
+ addUIClsToElement: function(cls, force) {
+ var me = this,
+ result = me.callParent(arguments),
+ classes = [me.baseCls + '-body-' + cls, me.baseCls + '-body-' + me.ui + '-' + cls],
+ array, i;
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.addCls(me.bodyCls);
+ } else {
+ me.body.addCls(classes);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ for (i = 0; i < classes.length; i++) {
+ if (!Ext.Array.contains(array, classes[i])) {
+ array.push(classes[i]);
+ }
+ }
+
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = classes.join(' ');
+ }
+ }
+
+ return result;
},
- degrees: function(radian) {
- return radian * 180 / Math.PI % 360;
+ // inherit docs
+ removeUIClsFromElement: function(cls, force) {
+ var me = this,
+ result = me.callParent(arguments),
+ classes = [me.baseCls + '-body-' + cls, me.baseCls + '-body-' + me.ui + '-' + cls],
+ array, i;
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.removeCls(me.bodyCls);
+ } else {
+ me.body.removeCls(classes);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ for (i = 0; i < classes.length; i++) {
+ Ext.Array.remove(array, classes[i]);
+ }
+
+ me.bodyCls = array.join(' ');
+ }
+ }
+
+ return result;
},
- withinBox: function(x, y, bbox) {
- bbox = bbox || {};
- return (x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height));
+ // inherit docs
+ addUIToElement: function(force) {
+ var me = this,
+ array, cls;
+
+ me.callParent(arguments);
+
+ cls = me.baseCls + '-body-' + me.ui;
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.addCls(me.bodyCls);
+ } else {
+ me.body.addCls(cls);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ if (!Ext.Array.contains(array, cls)) {
+ array.push(cls);
+ }
+
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = cls;
+ }
+ }
+
+ if (!force && me.titleCmp && me.titleCmp.rendered && me.titleCmp.textEl) {
+ me.titleCmp.textEl.addCls(me.baseCls + '-text-' + me.ui);
+ }
},
- parseGradient: function(gradient) {
+ // inherit docs
+ removeUIFromElement: function() {
var me = this,
- type = gradient.type || 'linear',
- angle = gradient.angle || 0,
- radian = me.radian,
- stops = gradient.stops,
- stopsArr = [],
- stop,
- vector,
- max,
- stopObj;
+ array, cls;
- 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;
+ me.callParent(arguments);
+
+ cls = me.baseCls + '-body-' + me.ui;
+ if (me.rendered) {
+ if (me.bodyCls) {
+ me.body.removeCls(me.bodyCls);
+ } else {
+ me.body.removeCls(cls);
}
- if (vector[3] < 0) {
- vector[1] = -vector[3];
- vector[3] = 0;
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+ Ext.Array.remove(array, cls);
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = cls;
}
}
- 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);
+ 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
+ });
}
- // Sort by pct property
- Ext.Array.sort(stopsArr, me.sorter);
- if (type == 'linear') {
- return {
- id: gradient.id,
- type: type,
- vector: vector,
- stops: stopsArr
- };
+ },
+
+ /**
+ * Sets the CSS class that provides the icon image for this header. This method will replace any existing
+ * icon class if one has already been set.
+ * @param {String} cls The new CSS class name
+ */
+ setIconCls: function(cls) {
+ var me = this,
+ isEmpty = !cls || !cls.length,
+ iconCmp = me.iconCmp,
+ el;
+
+ me.iconCls = cls;
+ if (!me.iconCmp && !isEmpty) {
+ me.initIconCmp();
+ me.insert(0, me.iconCmp);
+ } else if (iconCmp) {
+ if (isEmpty) {
+ me.iconCmp.destroy();
+ } else {
+ el = iconCmp.iconEl;
+ el.removeCls(iconCmp.iconCls);
+ el.addCls(cls);
+ iconCmp.iconCls = cls;
+ }
}
- 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
- };
+ },
+
+ /**
+ * 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.PropertyHandler
- * @ignore
+ * @class Ext.fx.target.Element
+ * @extends Ext.fx.target.Target
+ *
+ * This class represents a animation target for an {@link Ext.Element}. In general this class will not be
+ * created directly, the {@link Ext.Element} will be passed to the animation and
+ * and the appropriate target will be created.
*/
-Ext.define('Ext.fx.PropertyHandler', {
+Ext.define('Ext.fx.target.Element', {
/* Begin Definitions */
+
+ extend: 'Ext.fx.target.Target',
+
+ /* End Definitions */
- requires: ['Ext.draw.Draw'],
+ type: 'element',
- statics: {
- defaultHandler: {
- pixelDefaults: ['width', 'height', 'top', 'left'],
- unitRE: /^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/,
+ 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;
+ },
- 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';
+ 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);
+ }
}
}
- 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
- };
- },
+/**
+ * @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.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', {
- 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;
- },
+ /* Begin Definitions */
- 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,
+ extend: 'Ext.fx.target.Element',
- parseColor : function(color, damper) {
- damper = (typeof damper == 'number') ? damper : 1;
- var base,
- out = false,
- match;
+ /* End Definitions */
- 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;
- },
+ isComposite: true,
+
+ constructor: function(target) {
+ target.id = target.id || Ext.id(null, 'ext-composite-');
+ this.callParent([target]);
+ },
- 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)
- }
- };
- },
+ getAttr: function(attr, val) {
+ var out = [],
+ target = this.target;
+ target.each(function(el) {
+ out.push([el, this.getElVal(el, attr, val)]);
+ }, this);
+ return out;
+ }
+});
- 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;
- },
+/**
+ * @class Ext.fx.Manager
+ * Animation Manager which keeps track of all current animations and manages them on a frame by frame basis.
+ * @private
+ * @singleton
+ */
- 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;
- },
+Ext.define('Ext.fx.Manager', {
- computeDelta: function(from, end, damper, initial) {
- from = this.interpolate(from);
- end = this.interpolate(end, damper);
- var start = initial ? initial : from,
- delta = {},
- p;
+ /* Begin Definitions */
- for(p in end) {
- delta[p] = end[p] - start[p];
- }
- return {
- from: from,
- delta: delta
- };
- },
+ singleton: true,
- 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;
- },
+ requires: ['Ext.util.MixedCollection',
+ 'Ext.fx.target.Element',
+ 'Ext.fx.target.CompositeElement',
+ 'Ext.fx.target.Sprite',
+ 'Ext.fx.target.CompositeSprite',
+ 'Ext.fx.target.Component'],
- 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;
- }
- },
+ mixins: {
+ queue: 'Ext.fx.Queue'
+ },
- 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
- };
- },
+ /* End Definitions */
- forcePath: function(path) {
- if (!Ext.isArray(path) && !Ext.isArray(path[0])) {
- path = Ext.draw.Draw.parsePathString(path);
- }
- return path;
- },
+ constructor: function() {
+ this.items = Ext.create('Ext.util.MixedCollection');
+ this.mixins.queue.constructor.call(this);
- 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]);
+ // 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);
+ // };
+ // }
+ // })();
+ },
- deltaPath = Ext.draw.Draw.interpolatePaths(startPath, endPath);
- startPath = deltaPath[0];
- endPath = deltaPath[1];
+ /**
+ * @cfg {Number} interval Default interval in miliseconds to calculate each frame. Defaults to 16ms (~60fps)
+ */
+ interval: 16,
- 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;
- },
+ /**
+ * @cfg {Boolean} forceJS Turn off to not use CSS3 transitions when they are available
+ */
+ forceJS: true,
- 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;
+ // @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;
}
- /* 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', {
+ else {
+ return null;
+ }
+ },
- /* Begin Definitions */
+ /**
+ * 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;
+ // }
+ // };
- mixins: {
- observable: 'Ext.util.Observable'
- },
+ items.add(anim);
- requires: ['Ext.fx.Manager', 'Ext.fx.Animator', 'Ext.fx.Easing', 'Ext.fx.CubicBezier', 'Ext.fx.PropertyHandler'],
+ // 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);
+ }
- /* End Definitions */
+ // //Start the timer if not already running
+ // if (!me.task && items.length) {
+ // me.requestAnimFrame(cb);
+ // }
+ },
- 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.
+ * Remove an Anim from the manager. This is done automatically when an Anim ends.
+ * @param {Ext.fx.Anim} anim
*/
- duration: 250,
+ 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;
+ }
+ },
/**
- * @cfg {Number} delay
- * Time to delay before starting the animation. Defaults to 0.
+ * @private
+ * Filter function to determine which animations need to be started
*/
- delay: 0,
-
- /* private used to track a delayed starting time */
- delayStart: 0,
+ startingFilter: function(o) {
+ return o.paused === false && o.running === false && o.iterations > 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.
+ * @private
+ * Filter function to determine which animations are still running
*/
- dynamic: false,
+ runningFilter: function(o) {
+ return o.paused === false && o.running === true && o.isAnimator !== true;
+ },
/**
- * @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.
+ * @private
+ * Runner function being called each frame
+ */
+ runner: function() {
+ var me = this,
+ items = me.items;
- -backIn
- -backOut
- -bounceIn
- -bounceOut
- -ease
- -easeIn
- -easeOut
- -easeInOut
- -elasticIn
- -elasticOut
- -cubic-bezier(x1, y1, x2, y2)
+ me.targetData = {};
+ me.targetArr = {};
-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',
+ // Single timestamp for all animations this interval
+ me.timestamp = new Date();
- /**
- * @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
+ // 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();
},
- '60%': {
- left: 75
+
+ /**
+ * @private
+ * Start the individual animation (initialization)
+ */
+ startAnim: function(anim) {
+ anim.start(this.timestamp);
},
- '100%': {
- left: 100
- }
-}
-
- */
/**
* @private
+ * Run the individual animation for this frame
*/
- damper: 1,
+ 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
*/
- bezierRE: /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/,
+ 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
+ *
+ * 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
+ *
+ * In the example below, we modify the values of the element at each fifth throughout the animation.
+ *
+ * @example
+ * 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'
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.fx.Animator', {
+
+ /* Begin Definitions */
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: ['Ext.fx.Manager'],
+
+ /* End Definitions */
+
+ isAnimator: true,
/**
- * Run the animation from the end to the beginning
- * Defaults to false.
- * @cfg {Boolean} reverse
+ * @cfg {Number} duration
+ * Time in milliseconds for the animation to last. Defaults to 250.
*/
- reverse: false,
+ 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][0]
+ * specification. 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.
+ *
+ * [0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
+ */
+ easing: 'ease',
/**
* Flag to determine if the animation has started
* @property running
- * @type boolean
+ * @type Boolean
*/
running: false,
@@ -31018,177 +30945,209 @@ keyframes : {
* 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
+ * @type Boolean
*/
paused: false,
/**
- * Number of times to execute the animation. Defaults to 1.
- * @cfg {int} iterations
+ * @private
*/
- iterations: 1,
+ damper: 1,
/**
- * Used in conjunction with iterations to reverse the animation each time an iteration completes.
- * @cfg {Boolean} alternate
- * Defaults to false.
+ * @cfg {Number} iterations
+ * Number of times to execute the animation. Defaults to 1.
*/
- alternate: false,
+ iterations: 1,
/**
* Current iteration the animation is running.
* @property currentIteration
- * @type int
+ * @type Number
*/
currentIteration: 0,
/**
- * Starting time of the animation.
- * @property startTime
- * @type Date
+ * Current keyframe step of the animation.
+ * @property keyframeStep
+ * @type Number
*/
- startTime: 0,
+ keyframeStep: 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.
*/
+ animKeyFramesRE: /^(from|to|\d+%?)$/,
/**
- * @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 {Ext.fx.target.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} to
- * An object containing property/value pairs for the end of the animation. For example:
+ /**
+ * @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:
- to : {
- opacity: 1, // Opaque
- color: '#00ff00', // Green
- left: 500
- }
+keyframes : {
+ '0%': {
+ left: 100
+ },
+ '40%': {
+ left: 150
+ },
+ '60%': {
+ left: 75
+ },
+ '100%': {
+ left: 100
+ }
+}
- */
-
- // @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 = {};
+ config = Ext.apply(me, config || {});
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.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.Anim} this
+ * @param {Ext.fx.Animator} 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
+ /**
+ * @event keyframe
+ * Fires at each keyframe.
+ * @param {Ext.fx.Animator} this
+ * @param {Number} keyframe step number
*/
- 'lastframe'
+ '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);
- if (config.callback) {
- me.on('afteranimate', config.callback, config.scope);
+ me.timeline = [];
+ me.createTimeline(me.keyframes);
+ if (me.target) {
+ me.applyAnimator(me.target);
+ Ext.fx.Manager.addAnim(me);
}
- return me;
},
/**
* @private
- * Helper to the target
*/
- setAttr: function(attr, value) {
- return Ext.fx.Manager.items.get(this.id).setAttr(this.target, attr, value);
+ sorter: function (a, b) {
+ return a.pct - b.pct;
},
- /*
+ /**
* @private
- * Set up the initial currentAttrs hash.
+ * 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.
*/
- initAttrs: function() {
+ createTimeline: function(keyframes) {
var me = this,
- from = me.from,
- to = me.to,
- initialFrom = me.initialFrom || {},
- out = {},
- start, end, propHandler, attr;
+ attrs = [],
+ to = me.to || {},
+ duration = me.duration,
+ prevMs, ms, i, ln, pct, anim, nextAnim, 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;
- }
+ 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;
}
- // Use custom handler
- else {
- propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler[attr];
+ else if (pct == "to") {
+ pct = 100;
}
- out[attr] = propHandler.get(start, end, me.damper, initialFrom[attr], attr);
+ // convert % values into integers
+ attr.pct = parseInt(pct, 10);
+ attrs.push(attr);
}
}
- me.currentAttrs = out;
+ // 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.
*/
@@ -31215,46 +31174,12 @@ from : {
}
if (me.fireEvent('beforeanimate', me) !== false) {
me.startTime = startTime;
- if (!me.paused && !me.currentAttrs) {
- me.initAttrs();
- }
me.running = true;
+ me.animations[me.keyframeStep].paused = false;
}
},
- /*
- * @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.
@@ -31266,272 +31191,2362 @@ from : {
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;
+ me.keyframeStep = 0;
+ me.applyAnimator(me.target);
+ me.animations[me.keyframeStep].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);
+ me.fireEvent('afteranimate', me, me.startTime, new Date() - 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.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][0]
+ * specification. 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.
+ *
+ * [0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
+ *
+ * @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.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
+ * @class Ext.draw.Draw
+ * Base Drawing class. Provides base drawing functions.
+ * @private
*/
+Ext.define('Ext.draw.Draw', {
+ /* Begin Definitions */
-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,
+ singleton: true,
- /**
- * 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,
+ requires: ['Ext.draw.Color'],
- /**
- * An Array of CSS class names for elements to be considered in valid as drag handles.
- * @property invalidHandleClasses
- * @type Array
- */
- invalidHandleClasses: null,
+ /* End Definitions */
- /**
- * The linked element's absolute X position at the time the drag was
- * started
- * @property startPageX
- * @type int
- * @private
- */
- startPageX: 0,
+ 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,
- /**
- * The linked element's absolute X position at the time the drag was
- * started
- * @property startPageY
- * @type int
- * @private
- */
- startPageY: 0,
+ 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
+ },
- /**
- * 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,
+ 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;
+ },
- /**
- * Individual drag/drop instances can be locked. This will prevent
- * onmousedown start drag.
- * @property locked
- * @type boolean
- * @private
- */
- locked: false,
+ 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);
+ },
- /**
- * Lock this instance
- * @method lock
- */
- lock: function() {
- this.locked = true;
+ 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);
+ }
},
- /**
- * 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,
+ // To be deprecated, converts itself (an arrayPath) to a proper SVG path string
+ path2string: function () {
+ return this.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1");
+ },
- /**
- * Unlock this instace
- * @method unlock
- */
- unlock: function() {
- this.locked = false;
+ // Convert the passed arrayPath to a proper SVG path string (d attribute)
+ pathToString: function(arrayPath) {
+ return arrayPath.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1");
},
- /**
- * By default, all instances can be a drop target. This can be disabled by
- * setting isTarget to false.
- * @property isTarget
- * @type boolean
- */
- isTarget: true,
+ 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(Ext.Array.splice(params, 0, 2)));
+ name = "l";
+ b = (b == "m") ? "l" : "L";
+ }
+ while (params.length >= paramCounts[name]) {
+ data.push([b].concat(Ext.Array.splice(params, 0, paramCounts[name])));
+ if (!paramCounts[name]) {
+ break;
+ }
+ }
+ });
+ }
+ data.toString = me.path2string;
+ return data;
+ },
- /**
- * The padding configured for this drag and drop object for calculating
- * the drop zone intersection with this object.
- * An array containing the 4 padding values: [top, right, bottom, left]
- * @property {[int]} padding
- */
- padding: null,
+ 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;
+ },
- /**
- * Cached reference to the linked element
- * @property _domRef
- * @private
- */
- _domRef: null,
+ 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;
+ },
- /**
- * Internal typeof flag
- * @property __ygDragDrop
- * @private
- */
- __ygDragDrop: true,
+ 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,
+ i = 0,
+ ln = pathArray.length,
+ r, pathSegment, j, ln2;
+ // MoveTo initial x/y position
+ if (ln && pathArray[0][0] == "M") {
+ x = +pathArray[0][1];
+ y = +pathArray[0][2];
+ mx = x;
+ my = y;
+ i++;
+ res[0] = ["M", x, y];
+ }
+ for (; i < ln; i++) {
+ r = res[i] = [];
+ pathSegment = pathArray[i];
+ if (pathSegment[0] != pathSegment[0].toUpperCase()) {
+ r[0] = pathSegment[0].toUpperCase();
+ switch (r[0]) {
+ // Elliptical Arc
+ case "A":
+ r[1] = pathSegment[1];
+ r[2] = pathSegment[2];
+ r[3] = pathSegment[3];
+ r[4] = pathSegment[4];
+ r[5] = pathSegment[5];
+ r[6] = +(pathSegment[6] + x);
+ r[7] = +(pathSegment[7] + y);
+ break;
+ // Vertical LineTo
+ case "V":
+ r[1] = +pathSegment[1] + y;
+ break;
+ // Horizontal LineTo
+ case "H":
+ r[1] = +pathSegment[1] + x;
+ break;
+ case "M":
+ // MoveTo
+ mx = +pathSegment[1] + x;
+ my = +pathSegment[2] + y;
+ default:
+ j = 1;
+ ln2 = pathSegment.length;
+ for (; j < ln2; j++) {
+ r[j] = +pathSegment[j] + ((j % 2) ? x : y);
+ }
+ }
+ }
+ else {
+ j = 0;
+ ln2 = pathSegment.length;
+ for (; j < ln2; j++) {
+ res[i][j] = pathSegment[j];
+ }
+ }
+ switch (r[0]) {
+ // ClosePath
+ case "Z":
+ x = mx;
+ y = my;
+ break;
+ // Horizontal LineTo
+ case "H":
+ x = r[1];
+ break;
+ // Vertical LineTo
+ case "V":
+ y = r[1];
+ break;
+ // MoveTo
+ case "M":
+ pathSegment = res[i];
+ ln2 = pathSegment.length;
+ mx = pathSegment[ln2 - 2];
+ my = pathSegment[ln2 - 1];
+ default:
+ pathSegment = res[i];
+ ln2 = pathSegment.length;
+ x = pathSegment[ln2 - 2];
+ y = pathSegment[ln2 - 1];
+ }
+ }
+ res.toString = this.path2string;
+ return res;
+ },
- /**
+ // TO BE DEPRECATED
+ 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) {
+ Ext.Array.splice(points, i++, 0, ["C"].concat(Ext.Array.splice(point, 0, 6)));
+ }
+ Ext.Array.erase(points, 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) {
+ Ext.Array.splice(pp, i++, 0, ["C"].concat(Ext.Array.splice(pi, 0, 6)));
+ }
+ Ext.Array.erase(pp, 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") {
+ Ext.Array.splice(path2, 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;
+ }
+ },
+
+ // TO BE DEPRECATED
+ 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;
+ },
+
+ // TO BE DEPRECATED
+ 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
+ };
+ },
+
+ 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 = [],
+ i = 0,
+ ln = path.length,
+ p, xmin, ymin, dim;
+ for (; i < ln; 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
+ };
+ },
+
+ intersectInside: function(path, cp1, cp2) {
+ return (cp2[0] - cp1[0]) * (path[1] - cp1[1]) > (cp2[1] - cp1[1]) * (path[0] - cp1[0]);
+ },
+
+ intersectIntersection: function(s, e, cp1, cp2) {
+ var p = [],
+ 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;
+ },
+
+ intersect: function(subjectPolygon, clipPolygon) {
+ var me = this,
+ i = 0,
+ ln = clipPolygon.length,
+ cp1 = clipPolygon[ln - 1],
+ outputList = subjectPolygon,
+ cp2, s, e, point, ln2, inputList, j;
+ for (; i < ln; ++i) {
+ cp2 = clipPolygon[i];
+ inputList = outputList;
+ outputList = [];
+ s = inputList[inputList.length - 1];
+ j = 0;
+ ln2 = inputList.length;
+ for (; j < ln2; j++) {
+ e = inputList[j];
+ if (me.intersectInside(e, cp1, cp2)) {
+ if (!me.intersectInside(s, cp1, cp2)) {
+ outputList.push(me.intersectIntersection(s, e, cp1, cp2));
+ }
+ outputList.push(e);
+ }
+ else if (me.intersectInside(s, cp1, cp2)) {
+ outputList.push(me.intersectIntersection(s, e, cp1, cp2));
+ }
+ 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)}
+ };
+ },
+
+ /**
+ * @private
+ *
+ * Calculates bezier curve control anchor points for a particular point in a path, with a
+ * smoothing curve applied. The smoothness of the curve is controlled by the 'value' parameter.
+ * Note that this algorithm assumes that the line being smoothed is normalized going from left
+ * to right; it makes special adjustments assuming this orientation.
+ *
+ * @param {Number} prevX X coordinate of the previous point in the path
+ * @param {Number} prevY Y coordinate of the previous point in the path
+ * @param {Number} curX X coordinate of the current point in the path
+ * @param {Number} curY Y coordinate of the current point in the path
+ * @param {Number} nextX X coordinate of the next point in the path
+ * @param {Number} nextY Y coordinate of the next point in the path
+ * @param {Number} value A value to control the smoothness of the curve; this is used to
+ * divide the distance between points, so a value of 2 corresponds to
+ * half the distance between points (a very smooth line) while higher values
+ * result in less smooth curves. Defaults to 4.
+ * @return {Object} Object containing x1, y1, x2, y2 bezier control anchor points; x1 and y1
+ * are the control point for the curve toward the previous path point, and
+ * x2 and y2 are the control point for the curve toward the next path point.
+ */
+ getAnchors: function (prevX, prevY, curX, curY, nextX, nextY, value) {
+ value = value || 4;
+ var M = Math,
+ PI = M.PI,
+ halfPI = PI / 2,
+ abs = M.abs,
+ sin = M.sin,
+ cos = M.cos,
+ atan = M.atan,
+ control1Length, control2Length, control1Angle, control2Angle,
+ control1X, control1Y, control2X, control2Y, alpha;
+
+ // Find the length of each control anchor line, by dividing the horizontal distance
+ // between points by the value parameter.
+ control1Length = (curX - prevX) / value;
+ control2Length = (nextX - curX) / value;
+
+ // Determine the angle of each control anchor line. If the middle point is a vertical
+ // turnaround then we force it to a flat horizontal angle to prevent the curve from
+ // dipping above or below the middle point. Otherwise we use an angle that points
+ // toward the previous/next target point.
+ if ((curY >= prevY && curY >= nextY) || (curY <= prevY && curY <= nextY)) {
+ control1Angle = control2Angle = halfPI;
+ } else {
+ control1Angle = atan((curX - prevX) / abs(curY - prevY));
+ if (prevY < curY) {
+ control1Angle = PI - control1Angle;
+ }
+ control2Angle = atan((nextX - curX) / abs(curY - nextY));
+ if (nextY < curY) {
+ control2Angle = PI - control2Angle;
+ }
+ }
+
+ // Adjust the calculated angles so they point away from each other on the same line
+ alpha = halfPI - ((control1Angle + control2Angle) % (PI * 2)) / 2;
+ if (alpha > halfPI) {
+ alpha -= PI;
+ }
+ control1Angle += alpha;
+ control2Angle += alpha;
+
+ // Find the control anchor points from the angles and length
+ control1X = curX - control1Length * sin(control1Angle);
+ control1Y = curY + control1Length * cos(control1Angle);
+ control2X = curX + control2Length * sin(control2Angle);
+ control2Y = curY + control2Length * cos(control2Angle);
+
+ // One last adjustment, make sure that no control anchor point extends vertically past
+ // its target prev/next point, as that results in curves dipping above or below and
+ // bending back strangely. If we find this happening we keep the control angle but
+ // reduce the length of the control line so it stays within bounds.
+ if ((curY > prevY && control1Y < prevY) || (curY < prevY && control1Y > prevY)) {
+ control1X += abs(prevY - control1Y) * (control1X - curX) / (control1Y - curY);
+ control1Y = prevY;
+ }
+ if ((curY > nextY && control2Y < nextY) || (curY < nextY && control2Y > nextY)) {
+ control2X -= abs(nextY - control2Y) * (control2X - curX) / (control2Y - curY);
+ control2Y = nextY;
+ }
+
+ return {
+ x1: control1X,
+ y1: control1Y,
+ x2: control2X,
+ y2: control2Y
+ };
+ },
+
+ /* 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
+ };
+ },
+
+ /**
+ * A utility method to deduce an appropriate tick configuration for the data set of given
+ * feature.
+ *
+ * @param {Number/Date} from The minimum value in the data
+ * @param {Number/Date} to The maximum value in the data
+ * @param {Number} stepsMax The maximum number of ticks
+ * @return {Object} The calculated step and ends info; When `from` and `to` are Dates, refer to the
+ * return value of {@link #snapEndsByDate}. For numerical `from` and `to` the return value contains:
+ * @return {Number} return.from The result start value, which may be lower than the original start value
+ * @return {Number} return.to The result end value, which may be higher than the original end value
+ * @return {Number} return.power The calculate power.
+ * @return {Number} return.step The value size of each step
+ * @return {Number} return.steps The number of steps.
+ */
+ snapEnds: function (from, to, stepsMax) {
+ if (Ext.isDate(from)) {
+ return this.snapEndsByDate(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
+ };
+ },
+
+ /**
+ * A utility method to deduce an appropriate tick configuration for the data set of given
+ * feature when data is Dates. Refer to {@link #snapEnds} for numeric data.
+ *
+ * @param {Date} from The minimum value in the data
+ * @param {Date} to The maximum value in the data
+ * @param {Number} stepsMax The maximum number of ticks
+ * @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values
+ * and will not be adjusted
+ * @return {Object} The calculated step and ends info; properties are:
+ * @return {Date} return.from The result start value, which may be lower than the original start value
+ * @return {Date} return.to The result end value, which may be higher than the original end value
+ * @return {Number} return.step The value size of each step
+ * @return {Number} return.steps The number of steps.
+ * NOTE: the steps may not divide the from/to range perfectly evenly;
+ * there may be a smaller distance between the last step and the end value than between prior
+ * steps, particularly when the `endsLocked` param is true. Therefore it is best to not use
+ * the `steps` result when finding the axis tick points, instead use the `step`, `to`, and
+ * `from` to find the correct point for each tick.
+ */
+ snapEndsByDate: function (from, to, stepsMax, lockEnds) {
+ var selectedStep = false, scales = [
+ [Ext.Date.MILLI, [1, 2, 3, 5, 10, 20, 30, 50, 100, 200, 300, 500]],
+ [Ext.Date.SECOND, [1, 2, 3, 5, 10, 15, 30]],
+ [Ext.Date.MINUTE, [1, 2, 3, 5, 10, 20, 30]],
+ [Ext.Date.HOUR, [1, 2, 3, 4, 6, 12]],
+ [Ext.Date.DAY, [1, 2, 3, 7, 14]],
+ [Ext.Date.MONTH, [1, 2, 3, 4, 6]]
+ ], j, yearDiff;
+
+ // Find the most desirable scale
+ Ext.each(scales, function(scale, i) {
+ for (j = 0; j < scale[1].length; j++) {
+ if (to < Ext.Date.add(from, scale[0], scale[1][j] * stepsMax)) {
+ selectedStep = [scale[0], scale[1][j]];
+ return false;
+ }
+ }
+ });
+ if (!selectedStep) {
+ yearDiff = this.snapEnds(from.getFullYear(), to.getFullYear() + 1, stepsMax, lockEnds);
+ selectedStep = [Date.YEAR, Math.round(yearDiff.step)];
+ }
+ return this.snapEndsByDateAndStep(from, to, selectedStep, lockEnds);
+ },
+
+
+ /**
+ * A utility method to deduce an appropriate tick configuration for the data set of given
+ * feature and specific step size.
+ * @param {Date} from The minimum value in the data
+ * @param {Date} to The maximum value in the data
+ * @param {Array} step 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.).
+ * @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values
+ * and will not be adjusted
+ * @return {Object} See the return value of {@link #snapEndsByDate}.
+ */
+ snapEndsByDateAndStep: function(from, to, step, lockEnds) {
+ var fromStat = [from.getFullYear(), from.getMonth(), from.getDate(),
+ from.getHours(), from.getMinutes(), from.getSeconds(), from.getMilliseconds()],
+ steps = 0, testFrom, testTo;
+ if (lockEnds) {
+ testFrom = from;
+ } else {
+ switch (step[0]) {
+ case Ext.Date.MILLI:
+ testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+ fromStat[4], fromStat[5], Math.floor(fromStat[6] / step[1]) * step[1]);
+ break;
+ case Ext.Date.SECOND:
+ testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+ fromStat[4], Math.floor(fromStat[5] / step[1]) * step[1], 0);
+ break;
+ case Ext.Date.MINUTE:
+ testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+ Math.floor(fromStat[4] / step[1]) * step[1], 0, 0);
+ break;
+ case Ext.Date.HOUR:
+ testFrom = new Date(fromStat[0], fromStat[1], fromStat[2],
+ Math.floor(fromStat[3] / step[1]) * step[1], 0, 0, 0);
+ break;
+ case Ext.Date.DAY:
+ testFrom = new Date(fromStat[0], fromStat[1],
+ Math.floor(fromStat[2] - 1 / step[1]) * step[1] + 1, 0, 0, 0, 0);
+ break;
+ case Ext.Date.MONTH:
+ testFrom = new Date(fromStat[0], Math.floor(fromStat[1] / step[1]) * step[1], 1, 0, 0, 0, 0);
+ break;
+ default: // Ext.Date.YEAR
+ testFrom = new Date(Math.floor(fromStat[0] / step[1]) * step[1], 0, 1, 0, 0, 0, 0);
+ break;
+ }
+ }
+
+ testTo = testFrom;
+ // TODO(zhangbei) : We can do it better somehow...
+ while (testTo < to) {
+ testTo = Ext.Date.add(testTo, step[0], step[1]);
+ steps++;
+ }
+
+ if (lockEnds) {
+ testTo = to;
+ }
+ return {
+ from : +testFrom,
+ to : +testTo,
+ step : (testTo - testFrom) / steps,
+ steps : steps
+ };
+ },
+
+ 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: {
+ pixelDefaultsRE: /width|height|top$|bottom$|left$|right$/i,
+ unitRE: /^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/,
+ scrollRE: /^scroll/i,
+
+ computeDelta: function(from, end, damper, initial, attr) {
+ damper = (typeof damper == 'number') ? damper : 1;
+ var unitRE = this.unitRE,
+ match = unitRE.exec(from),
+ start, units;
+ if (match) {
+ from = match[1];
+ units = match[2];
+ if (!this.scrollRE.test(attr) && !units && this.pixelDefaultsRE.test(attr)) {
+ units = 'px';
+ }
+ }
+ from = +from || 0;
+
+ match = 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
+ *
+ * @example
+ * var myComponent = Ext.create('Ext.Component', {
+ * renderTo: document.body,
+ * width: 200,
+ * height: 200,
+ * style: 'border: 1px solid red;'
+ * });
+ *
+ * Ext.create('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 {Function} callback
+ * A function to be run after the animation has completed.
+ */
+
+ /**
+ * @cfg {Function} scope
+ * The scope that the {@link #callback} function will be called with
+ */
+
+ /**
+ * @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][0]
+specification. 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.
+
+[0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
+
+ * @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 {Number} 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 Number
+ */
+ 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,
+ curve;
+
+ 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) {
+ curve = me.easingFn;
+ me.easingFn = Ext.fx.CubicBezier.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
+ */
+
+
+/**
+ * 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);
+ * }
+ *
+ */
+Ext.define('Ext.dd.DragDrop', {
+ requires: ['Ext.dd.DragDropManager'],
+
+ /**
+ * Creates new DragDrop.
+ * @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
+ */
+ 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 {String[]} invalidHandleClasses
+ */
+ invalidHandleClasses: null,
+
+ /**
+ * The linked element's absolute X position at the time the drag was
+ * started
+ * @property startPageX
+ * @type Number
+ * @private
+ */
+ startPageX: 0,
+
+ /**
+ * The linked element's absolute X position at the time the drag was
+ * started
+ * @property startPageY
+ * @type Number
+ * @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,
+
+ /**
+ * Locks this instance
+ */
+ 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,
+
+ /**
+ * Unlocks this instace
+ */
+ 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.
+ * An array containing the 4 padding values: [top, right, bottom, left]
+ * @property {Number[]} padding
+ */
+ 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
+ * @type Boolean
* @private
*/
constrainX: false,
@@ -31539,7 +33554,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Set to true when vertical contraints are applied
* @property constrainY
- * @type boolean
+ * @type Boolean
* @private
*/
constrainY: false,
@@ -31547,7 +33562,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* The left constraint
* @property minX
- * @type int
+ * @type Number
* @private
*/
minX: 0,
@@ -31555,7 +33570,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* The right constraint
* @property maxX
- * @type int
+ * @type Number
* @private
*/
maxX: 0,
@@ -31563,7 +33578,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* The up constraint
* @property minY
- * @type int
+ * @type Number
* @private
*/
minY: 0,
@@ -31571,7 +33586,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* The down constraint
* @property maxY
- * @type int
+ * @type Number
* @private
*/
maxY: 0,
@@ -31582,7 +33597,7 @@ Ext.define('Ext.dd.DragDrop', {
* when the page changes
*
* @property maintainOffset
- * @type boolean
+ * @type Boolean
*/
maintainOffset: false,
@@ -31590,7 +33605,7 @@ Ext.define('Ext.dd.DragDrop', {
* 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 {[int]} xTicks
+ * @property {Number[]} xTicks
*/
xTicks: null,
@@ -31598,7 +33613,7 @@ Ext.define('Ext.dd.DragDrop', {
* 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 {[int]} yTicks
+ * @property {Number[]} yTicks
*/
yTicks: null,
@@ -31608,14 +33623,14 @@ Ext.define('Ext.dd.DragDrop', {
* allow drag and drop to start with any mouse click that is propogated
* by the browser
* @property primaryButtonOnly
- * @type boolean
+ * @type Boolean
*/
primaryButtonOnly: true,
/**
* The available property is false until the linked dom element is accessible.
* @property available
- * @type boolean
+ * @type Boolean
*/
available: false,
@@ -31624,17 +33639,15 @@ Ext.define('Ext.dd.DragDrop', {
* 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.
+ * if outer handles are defined. Defaults to false.
*
* @property hasOuterHandles
- * @type boolean
- * @default false
+ * @type Boolean
*/
hasOuterHandles: false,
/**
* Code that executes immediately before the startDrag event
- * @method b4StartDrag
* @private
*/
b4StartDrag: function(x, y) { },
@@ -31642,15 +33655,13 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {Number} X click location
+ * @param {Number} Y click location
*/
startDrag: function(x, y) { /* override this */ },
/**
* Code that executes immediately before the onDrag event
- * @method b4Drag
* @private
*/
b4Drag: function(e) { },
@@ -31658,7 +33669,6 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Abstract method called during the onMouseMove event while dragging an
* object.
- * @method onDrag
* @param {Event} e the mousemove event
*/
onDrag: function(e) { /* override this */ },
@@ -31666,9 +33676,8 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {String/Ext.dd.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.
*/
@@ -31676,7 +33685,6 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Code that executes immediately before the onDragOver event
- * @method b4DragOver
* @private
*/
b4DragOver: function(e) { },
@@ -31684,9 +33692,8 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
* id this is hovering over. In INTERSECT mode, an array of dd items
* being hovered over.
*/
@@ -31694,16 +33701,14 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {String/Ext.dd.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.
*/
@@ -31711,7 +33716,6 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Code that executes immediately before the onDragDrop event
- * @method b4DragDrop
* @private
*/
b4DragDrop: function(e) { },
@@ -31719,9 +33723,8 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
* id this was dropped on. In INTERSECT mode, an array of dd items this
* was dropped on.
*/
@@ -31730,43 +33733,37 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * Called when we are done dragging the object
* @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
+ * Called when a drag/drop obj gets a mousedown
* @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
+ * Called when a drag/drop obj gets a mouseup
* @param {Event} e the mouseup event
*/
onMouseUp: function(e) { /* override this */ },
@@ -31774,14 +33771,13 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @property {Object} defaultPadding
+ * Provides default constraint padding to "constrainTo" elements.
*/
defaultPadding: {
left: 0,
@@ -31792,27 +33788,27 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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.
+ *
+ * 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.Element} object:
+ *
+ * Ext.get("dragDiv1").initDDProxy("proxytest", {dragElId: "existingProxyDiv"}, {
+ * startDrag : function(){
+ * this.constrainTo("parent-id");
+ * }
+ * });
+ *
+ * @param {String/HTMLElement/Ext.Element} constrainTo The element or element ID 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}
+ * 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){
@@ -31823,10 +33819,10 @@ Ext.define('Ext.dd.DragDrop', {
var b = Ext.get(this.getEl()).getBox(),
ce = Ext.get(constrainTo),
s = ce.getScroll(),
- c,
+ 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()};
+ c = { x: s.left, y: s.top, width: Ext.Element.getViewWidth(), height: Ext.Element.getViewHeight()};
}else{
var xy = ce.getXY();
c = {x : xy[0], y: xy[1], width: cd.clientWidth, height: cd.clientHeight};
@@ -31849,7 +33845,6 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Returns a reference to the linked element
- * @method getEl
* @return {HTMLElement} the html element
*/
getEl: function() {
@@ -31864,7 +33859,6 @@ Ext.define('Ext.dd.DragDrop', {
* 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() {
@@ -31874,10 +33868,9 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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} id the id of the linked element
* @param {String} sGroup the group of related items
- * @param {object} config configuration attributes
+ * @param {Object} config configuration attributes
*/
init: function(id, sGroup, config) {
this.initTarget(id, sGroup, config);
@@ -31888,13 +33881,11 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Initializes Targeting functionality only... the object does not
* get a mousedown handler.
- * @method initTarget
- * @param id the id of the linked element
+ * @param {String} id the id of the linked element
* @param {String} sGroup the group of related items
- * @param {object} config configuration attributes
+ * @param {Object} config configuration attributes
*/
initTarget: function(id, sGroup, config) {
-
// configuration attributes
this.config = config || {};
@@ -31938,7 +33929,6 @@ Ext.define('Ext.dd.DragDrop', {
* 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() {
@@ -31953,7 +33943,6 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Executed when the linked element is available
- * @method handleOnAvailable
* @private
*/
handleOnAvailable: function() {
@@ -31962,17 +33951,16 @@ Ext.define('Ext.dd.DragDrop', {
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
+ * @param {Number} iTop Top pad
+ * @param {Number} iRight Right pad
+ * @param {Number} iBot Bot pad
+ * @param {Number} iLeft Left pad
*/
setPadding: function(iTop, iRight, iBot, iLeft) {
// this.padding = [iLeft, iRight, iTop, iBot];
@@ -31987,9 +33975,8 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {Number} diffX the X offset, default 0
+ * @param {Number} diffY the Y offset, default 0
*/
setInitPosition: function(diffX, diffY) {
var el = this.getEl();
@@ -32001,7 +33988,7 @@ Ext.define('Ext.dd.DragDrop', {
var dx = diffX || 0;
var dy = diffY || 0;
- var p = Ext.core.Element.getXY( el );
+ var p = Ext.Element.getXY( el );
this.initPageX = p[0] - dx;
this.initPageY = p[1] - dy;
@@ -32015,12 +34002,11 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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() );
+ var p = pos || Ext.Element.getXY( this.getEl() );
this.deltaSetXY = null;
this.startPageX = p[0];
@@ -32028,11 +34014,10 @@ Ext.define('Ext.dd.DragDrop', {
},
/**
- * Add this instance to a group of related drag/drop objects. All
+ * Adds 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
+ * @param {String} sGroup the name of the group
*/
addToGroup: function(sGroup) {
this.groups[sGroup] = true;
@@ -32040,9 +34025,8 @@ Ext.define('Ext.dd.DragDrop', {
},
/**
- * Remove's this instance from the supplied interaction group
- * @method removeFromGroup
- * @param {string} sGroup The group to drop
+ * Removes this instance from the supplied interaction group
+ * @param {String} sGroup The group to drop
*/
removeFromGroup: function(sGroup) {
if (this.groups[sGroup]) {
@@ -32055,8 +34039,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {String} id the id of the element that will be used to initiate the drag
*/
setDragElId: function(id) {
this.dragElId = id;
@@ -32069,8 +34052,7 @@ Ext.define('Ext.dd.DragDrop', {
* 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
+ * @param {String} id the id of the element that will be used to
* initiate the drag.
*/
setHandleElId: function(id) {
@@ -32084,8 +34066,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {String} id the id of the element that will be used to initiate the drag
*/
setOuterHandleElId: function(id) {
if (typeof id !== "string") {
@@ -32098,8 +34079,7 @@ Ext.define('Ext.dd.DragDrop', {
},
/**
- * Remove all drag and drop hooks for this element
- * @method unreg
+ * Removes all drag and drop hooks for this element
*/
unreg: function() {
Ext.EventManager.un(this.id, "mousedown", this.handleMouseDown, this);
@@ -32114,8 +34094,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @return {Boolean} true if this obj or all drag/drop is locked, else
* false
*/
isLocked: function() {
@@ -32123,8 +34102,7 @@ Ext.define('Ext.dd.DragDrop', {
},
/**
- * Fired when this object is clicked
- * @method handleMouseDown
+ * Called when this object is clicked
* @param {Event} e
* @param {Ext.dd.DragDrop} oDD the clicked dd object (this dd obj)
* @private
@@ -32171,7 +34149,7 @@ Ext.define('Ext.dd.DragDrop', {
* 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
+ * @param {String} tagName the type of element to exclude
*/
addInvalidHandleType: function(tagName) {
var type = tagName.toUpperCase();
@@ -32182,7 +34160,7 @@ Ext.define('Ext.dd.DragDrop', {
* 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
+ * @param {String} id the element id of the element you wish to ignore
*/
addInvalidHandleId: function(id) {
if (typeof id !== "string") {
@@ -32193,8 +34171,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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
+ * @param {String} cssClass the class of the elements you wish to ignore
*/
addInvalidHandleClass: function(cssClass) {
this.invalidHandleClasses.push(cssClass);
@@ -32202,8 +34179,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Unsets an excluded tag name set by addInvalidHandleType
- * @method removeInvalidHandleType
- * @param {string} tagName the type of element to unexclude
+ * @param {String} tagName the type of element to unexclude
*/
removeInvalidHandleType: function(tagName) {
var type = tagName.toUpperCase();
@@ -32213,8 +34189,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Unsets an invalid handle id
- * @method removeInvalidHandleId
- * @param {string} id the id of the element to re-enable
+ * @param {String} id the id of the element to re-enable
*/
removeInvalidHandleId: function(id) {
if (typeof id !== "string") {
@@ -32225,8 +34200,7 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Unsets an invalid css class
- * @method removeInvalidHandleClass
- * @param {string} cssClass the class of the element(s) you wish to
+ * @param {String} cssClass the class of the element(s) you wish to
* re-enable
*/
removeInvalidHandleClass: function(cssClass) {
@@ -32239,9 +34213,8 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Checks the tag exclusion list to see if this click should be ignored
- * @method isValidHandleChild
* @param {HTMLElement} node the HTMLElement to evaluate
- * @return {boolean} true if this is a valid tag type, false if not
+ * @return {Boolean} true if this is a valid tag type, false if not
*/
isValidHandleChild: function(node) {
@@ -32266,9 +34239,8 @@ Ext.define('Ext.dd.DragDrop', {
},
/**
- * Create the array of horizontal tick marks if an interval was specified
+ * Creates the array of horizontal tick marks if an interval was specified
* in setXConstraint().
- * @method setXTicks
* @private
*/
setXTicks: function(iStartX, iTickSize) {
@@ -32295,9 +34267,8 @@ Ext.define('Ext.dd.DragDrop', {
},
/**
- * Create the array of vertical tick marks if an interval was specified in
+ * Creates the array of vertical tick marks if an interval was specified in
* setYConstraint().
- * @method setYTicks
* @private
*/
setYTicks: function(iStartY, iTickSize) {
@@ -32327,13 +34298,11 @@ Ext.define('Ext.dd.DragDrop', {
* 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
+ * @param {Number} iLeft the number of pixels the element can move to the left
+ * @param {Number} 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.
+ * @param {Number} iTickSize (optional) parameter for specifying that the
+ * element should move iTickSize pixels at a time.
*/
setXConstraint: function(iLeft, iRight, iTickSize) {
this.leftConstraint = iLeft;
@@ -32349,7 +34318,6 @@ Ext.define('Ext.dd.DragDrop', {
/**
* 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;
@@ -32359,7 +34327,6 @@ Ext.define('Ext.dd.DragDrop', {
/**
* Clears any tick interval defined for this instance
- * @method clearTicks
*/
clearTicks: function() {
this.xTicks = null;
@@ -32372,10 +34339,9 @@ Ext.define('Ext.dd.DragDrop', {
* 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
+ * @param {Number} iUp the number of pixels the element can move up
+ * @param {Number} iDown the number of pixels the element can move down
+ * @param {Number} iTickSize (optional) parameter for specifying that the
* element should move iTickSize pixels at a time.
*/
setYConstraint: function(iUp, iDown, iTickSize) {
@@ -32391,9 +34357,8 @@ Ext.define('Ext.dd.DragDrop', {
},
/**
- * resetConstraints must be called if you manually reposition a dd element.
- * @method resetConstraints
- * @param {boolean} maintainOffset
+ * Must be called if you manually reposition a dd element.
+ * @param {Boolean} maintainOffset
*/
resetConstraints: function() {
// Maintain offsets if necessary
@@ -32426,10 +34391,9 @@ Ext.define('Ext.dd.DragDrop', {
* 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
+ * @param {Number} val where we want to place the object
+ * @param {Number[]} tickArray sorted array of valid points
+ * @return {Number} the closest tick
* @private
*/
getTick: function(val, tickArray) {
@@ -32459,14 +34423,14 @@ Ext.define('Ext.dd.DragDrop', {
/**
* toString method
- * @method toString
- * @return {string} string representation of the dd obj
+ * @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:
@@ -32481,17 +34445,18 @@ Ext.define('Ext.dd.DragDrop', {
* 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'],
+
+ /**
+ * Creates new DD instance.
+ * @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
+ */
constructor: function(id, sGroup, config) {
if (id) {
this.init(id, sGroup, config);
@@ -32503,7 +34468,7 @@ Ext.define('Ext.dd.DD', {
* window when a drag and drop element is dragged near the viewport boundary.
* Defaults to true.
* @property scroll
- * @type boolean
+ * @type Boolean
*/
scroll: true,
@@ -32511,8 +34476,8 @@ Ext.define('Ext.dd.DD', {
* 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
+ * @param {Number} iPageX the X coordinate of the click
+ * @param {Number} iPageY the Y coordinate of the click
*/
autoOffset: function(iPageX, iPageY) {
var x = iPageX - this.startPageX;
@@ -32525,8 +34490,8 @@ Ext.define('Ext.dd.DD', {
* 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
+ * @param {Number} iDeltaX the distance from the left
+ * @param {Number} iDeltaY the distance from the top
*/
setDelta: function(iDeltaX, iDeltaY) {
this.deltaX = iDeltaX;
@@ -32539,8 +34504,8 @@ Ext.define('Ext.dd.DD', {
* 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
+ * @param {Number} iPageX the X coordinate of the mousedown or drag event
+ * @param {Number} 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
@@ -32557,14 +34522,14 @@ Ext.define('Ext.dd.DD', {
* 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
+ * @param {Number} iPageX the X coordinate of the mousedown or drag event
+ * @param {Number} 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,
+ EL = Ext.Element,
vpSize;
if (!this.deltaSetXY) {
@@ -32595,9 +34560,9 @@ Ext.define('Ext.dd.DD', {
* 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
+ * @param {Number} iPageX (optional) the current x position (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
+ * @param {Number} iPageY (optional) the current y position (this just makes it so we
* don't have to look it up again)
*/
cachePosition: function(iPageX, iPageY) {
@@ -32605,7 +34570,7 @@ Ext.define('Ext.dd.DD', {
this.lastPageX = iPageX;
this.lastPageY = iPageY;
} else {
- var aCoord = Ext.core.Element.getXY(this.getEl());
+ var aCoord = Ext.Element.getXY(this.getEl());
this.lastPageX = aCoord[0];
this.lastPageY = aCoord[1];
}
@@ -32615,20 +34580,20 @@ Ext.define('Ext.dd.DD', {
* 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
+ * @param {Number} x the drag element's x position
+ * @param {Number} y the drag element's y position
+ * @param {Number} h the height of the drag element
+ * @param {Number} 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();
+ var clientH = Ext.Element.getViewHeight();
// The client width
- var clientW = Ext.core.Element.getViewWidth();
+ var clientW = Ext.Element.getViewWidth();
// The amt scrolled down
var st = this.DDMInstance.getScrollTop();
@@ -32690,8 +34655,8 @@ Ext.define('Ext.dd.DD', {
* 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
+ * @param {Number} iPageX the X coordinate of the click
+ * @param {Number} iPageY the Y coordinate of the click
* @return an object that contains the coordinates (Object.x and Object.y)
* @private
*/
@@ -32794,6 +34759,7 @@ Ext.define('Ext.dd.DD', {
/**
* @class Ext.dd.DDProxy
+ * @extends Ext.dd.DD
* 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
@@ -32802,14 +34768,6 @@ Ext.define('Ext.dd.DD', {
* 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',
@@ -32817,13 +34775,22 @@ Ext.define('Ext.dd.DDProxy', {
statics: {
/**
* The default drag frame div id
- * @property Ext.dd.DDProxy.dragElId
- * @type String
* @static
*/
dragElId: "ygddfdiv"
},
+ /**
+ * Creates new DDProxy.
+ * @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
+ */
constructor: function(id, sGroup, config) {
if (id) {
this.init(id, sGroup, config);
@@ -32836,7 +34803,7 @@ Ext.define('Ext.dd.DDProxy', {
* 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
+ * @type Boolean
*/
resizeFrame: true,
@@ -32846,7 +34813,7 @@ Ext.define('Ext.dd.DDProxy', {
* 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
+ * @type Boolean
*/
centerFrame: false,
@@ -32904,8 +34871,8 @@ Ext.define('Ext.dd.DDProxy', {
* 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
+ * @param {Number} iPageX X click position
+ * @param {Number} iPageY Y click position
* @private
*/
showFrame: function(iPageX, iPageY) {
@@ -32997,9 +34964,6 @@ Ext.define('Ext.dd.DDProxy', {
* @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',
@@ -33011,34 +34975,40 @@ Ext.define('Ext.dd.DragSource', {
/**
* @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).
+ * interact with other drag drop objects in the same group.
*/
/**
- * @cfg {String} dropAllowed
- * The CSS class returned to the drag source when drop is allowed (defaults to "x-dd-drop-ok").
+ * @cfg {String} [dropAllowed="x-dd-drop-ok"]
+ * The CSS class returned to the drag source when drop is allowed.
*/
-
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").
+ * @cfg {String} [dropNotAllowed="x-dd-drop-nodrop"]
+ * The CSS class returned to the drag source when drop is not allowed.
*/
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.
+ * 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
+ * @cfg {String} repairHighlightColor
+ * The color to use when visually highlighting the drag source in the afterRepair
+ * method after a failed drop (defaults to light blue). The color must be a 6 digit hex value, without
* a preceding '#'.
*/
repairHighlightColor: 'c3daf9',
+ /**
+ * Creates new drag-source.
+ * @constructor
+ * @param {String/HTMLElement/Ext.Element} el The container element or ID of it.
+ * @param {Object} config (optional) Config object.
+ */
constructor: function(el, config) {
this.el = Ext.get(el);
if(!this.dragData){
@@ -33231,7 +35201,7 @@ Ext.define('Ext.dd.DragSource', {
* @param {Object} target The target DD
* @param {Event} e The event object
* @param {String} id The id of the dropped element
- * @method afterInvalidDrop
+ * @method afterValidDrop
*/
this.afterValidDrop(target, e, id);
}
@@ -33455,94 +35425,110 @@ Ext.define('Ext.layout.component.Dock', {
});
/**
- * @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.
+ * 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 #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
+ *
+ * A Panel may also contain {@link #bbar bottom} and {@link #tbar top} toolbars, along with separate {@link
+ * Ext.panel.Header header}, {@link #fbar footer} and body sections.
+ *
+ * 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:
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Hello',
+ * width: 200,
+ * html: 'World!
',
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * 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:
+ *
+ * @example
+ * var filterPanel = Ext.create('Ext.panel.Panel', {
+ * bodyPadding: 5, // Don't want content to crunch against the borders
+ * width: 300,
+ * title: 'Filters',
+ * items: [{
+ * xtype: 'datefield',
+ * fieldLabel: 'Start date'
+ * }, {
+ * xtype: 'datefield',
+ * fieldLabel: 'End date'
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * 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:
+ *
+ * @example
+ * var resultsPanel = Ext.create('Ext.panel.Panel', {
+ * title: 'Results',
+ * width: 600,
+ * height: 400,
+ * renderTo: Ext.getBody(),
+ * 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.
*/
Ext.define('Ext.panel.Panel', {
extend: 'Ext.panel.AbstractPanel',
@@ -33552,173 +35538,190 @@ Ext.define('Ext.panel.Panel', {
'Ext.util.KeyMap',
'Ext.panel.DD',
'Ext.XTemplate',
- 'Ext.layout.component.Dock'
+ 'Ext.layout.component.Dock',
+ 'Ext.util.Memento'
],
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'
).
+ * A CSS class to add to the panel's element after it has been 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.
+ * `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.
+ * Minimum width of all footer toolbar buttons in pixels. 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#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
).
+ * `true` to render the panel collapsed, `false` to render it expanded.
*/
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
).
+ * `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.
*/
collapseFirst: true,
/**
* @cfg {Boolean} hideCollapseTool
- * true
to hide the expand/collapse toggle button when {@link #collapsible} == true
,
- * false
to display it (defaults to false
).
+ * `true` to hide the expand/collapse toggle button when `{@link #collapsible} == true`, `false` to display it.
*/
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
)).
+ * `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 butto).
*/
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}.
- *
+ * **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 {Ext.Component/Object} 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 ).
+ * **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.
*/
floatable: true,
-
+
/**
- * @cfg {Mixed} overlapHeader
- * True to overlap the header in a panel over the framing of the panel itself. This is needed when frame:true (and is done automatically for you). Otherwise it is undefined.
- * If you manually add rounded corners to a panel header which does not have frame:true, this will need to be set to true.
+ * @cfg {Boolean} overlapHeader
+ * True to overlap the header in a panel over the framing of the panel itself. This is needed when frame:true (and
+ * is done automatically for you). Otherwise it is undefined. If you manually add rounded corners to a panel header
+ * which does not have frame:true, this will need to be set to 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).
+ * 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.
+ *
* 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'
.
+ * 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'.
+ * 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.
+ *
+ * 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.
+ * The action to take when the close header tool is clicked:
+ *
+ * - **`'{@link #destroy}'`** :
+ *
+ * {@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 {Object/Object[]} 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.
+ * @cfg {Boolean} preventHeader
+ * Prevent a Header from being created and shown.
*/
preventHeader: false,
/**
- * @cfg {String} headerPosition Specify as 'top'
, 'bottom'
, 'left'
or 'right'
. Defaults to 'top'
.
+ * @cfg {String} headerPosition
+ * Specify as `'top'`, `'bottom'`, `'left'` or `'right'`.
*/
headerPosition: 'top',
@@ -33735,90 +35738,151 @@ var panel = new Ext.panel.Panel({
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
- }
-}]
-
+ * @cfg {Object[]/Ext.panel.Tool[]} 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',
+ * tooltip: 'Refresh form Data',
+ * // hidden:true,
+ * handler: function(event, toolEl, panel){
+ * // refresh logic
+ * }
+ * },
+ * {
+ * type:'help',
+ * tooltip: 'Get Help',
+ * handler: function(event, toolEl, panel){
+ * // show help here
+ * }
+ * }]
+ */
+
+ /**
+ * @cfg {String} [title='']
+ * The title text to be used to display in the {@link Ext.panel.Header panel header}. When a
+ * `title` is specified the {@link Ext.panel.Header} will automatically be created and displayed unless
+ * {@link #preventHeader} is set to `true`.
*/
+ /**
+ * @cfg {String} iconCls
+ * CSS class for icon in header. Used for displaying an icon to the left of a title.
+ */
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.
- */
+
+ /**
+ * @event beforeclose
+ * Fires before the user closes the panel. Return false from any listener to stop the close event being
+ * fired
+ * @param {Ext.panel.Panel} panel The Panel object
+ */
+ 'beforeclose',
+
+ /**
+ * @event beforeexpand
+ * Fires before this panel is expanded. Return false to prevent the expand.
+ * @param {Ext.panel.Panel} p The Panel being expanded.
+ * @param {Boolean} animate True if the expand is animated, else false.
+ */
+ "beforeexpand",
+
+ /**
+ * @event beforecollapse
+ * Fires before this panel is collapsed. Return false to prevent the collapse.
+ * @param {Ext.panel.Panel} p The Panel being collapsed.
+ * @param {String} direction . The direction of the collapse. One of
+ *
+ * - Ext.Component.DIRECTION_TOP
+ * - Ext.Component.DIRECTION_RIGHT
+ * - Ext.Component.DIRECTION_BOTTOM
+ * - Ext.Component.DIRECTION_LEFT
+ *
+ * @param {Boolean} animate True if the collapse is animated, else false.
+ */
+ "beforecollapse",
+
+ /**
+ * @event expand
+ * Fires after this Panel has expanded.
+ * @param {Ext.panel.Panel} p The Panel that has been expanded.
+ */
+ "expand",
+
+ /**
+ * @event collapse
+ * Fires after this Panel hass collapsed.
+ * @param {Ext.panel.Panel} p The Panel that has been collapsed.
+ */
+ "collapse",
+
+ /**
+ * @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.
- */
+
+ /**
+ * @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'
);
+ // Save state on these two events.
+ this.addStateEvents('expand', 'collapse');
+
if (me.unstyled) {
me.setUI('plain');
}
if (me.frame) {
- me.setUI('default-framed');
+ me.setUI(me.ui + '-framed');
}
- me.callParent();
-
- me.collapseDirection = me.collapseDirection || me.headerPosition || Ext.Component.DIRECTION_TOP;
-
// Backwards compatibility
me.bridgeToolbars();
+
+ me.callParent();
+ me.collapseDirection = me.collapseDirection || me.headerPosition || Ext.Component.DIRECTION_TOP;
},
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);
},
@@ -33849,7 +35913,7 @@ tools:[{
},
/**
- * Set a title for the panel's header. See {@link Ext.panel.Header#title}.
+ * Set a title for the panel's header. See {@link Ext.panel.Header#title}.
* @param {String} newTitle
*/
setTitle: function(newTitle) {
@@ -33870,8 +35934,9 @@ tools:[{
},
/**
- * Set the iconCls for the panel's header. See {@link Ext.panel.Header#iconCls}.
- * @param {String} newIconCls
+ * Set the iconCls for the panel's header. See {@link Ext.panel.Header#iconCls}. It will fire the
+ * {@link #iconchange} event after completion.
+ * @param {String} newIconCls The new CSS class name
*/
setIconCls: function(newIconCls) {
var me = this,
@@ -33887,11 +35952,12 @@ tools:[{
bridgeToolbars: function() {
var me = this,
+ docked = [],
fbar,
fbarDefaults,
minButtonWidth = me.minButtonWidth;
- function initToolbar (toolbar, pos) {
+ function initToolbar (toolbar, pos, useButtonAlign) {
if (Ext.isArray(toolbar)) {
toolbar = {
xtype: 'toolbar',
@@ -33905,89 +35971,98 @@ tools:[{
if (pos == 'left' || pos == 'right') {
toolbar.vertical = true;
}
+
+ // Legacy support for buttonAlign (only used by buttons/fbar)
+ if (useButtonAlign) {
+ toolbar.layout = Ext.applyIf(toolbar.layout || {}, {
+ // default to 'end' (right-aligned) if me.buttonAlign is undefined or invalid
+ pack: { left:'start', center:'center' }[me.buttonAlign] || 'end'
+ });
+ }
return toolbar;
}
- // Backwards compatibility
+ // Short-hand toolbars (tbar, bbar and fbar plus new lbar and rbar):
/**
- * @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' }
- ]
- }]
+ * @cfg {String} buttonAlign
+ * The alignment of any buttons added to this panel. Valid values are 'right', 'left' and 'center' (defaults to
+ * 'right' for buttons/fbar, 'left' for other toolbar types).
+ *
+ * **NOTE:** The prefered way to specify toolbars is to use the dockedItems config. Instead of buttonAlign you
+ * would add the layout: { pack: 'start' | 'center' | 'end' } option to the dockedItem config.
+ */
- * @markdown
+ /**
+ * @cfg {Object/Object[]} tbar
+ * Convenience config. Short for 'Top Bar'.
+ *
+ * tbar: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'top',
+ * items: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
*/
if (me.tbar) {
- me.addDocked(initToolbar(me.tbar, 'top'));
+ docked.push(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
+ * @cfg {Object/Object[]} bbar
+ * Convenience config. Short for 'Bottom Bar'.
+ *
+ * bbar: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'bottom',
+ * items: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
*/
if (me.bbar) {
- me.addDocked(initToolbar(me.bbar, 'bottom'));
+ docked.push(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
+ * @cfg {Object/Object[]} buttons
+ * Convenience config used for adding buttons docked to the bottom of the panel. This is a
+ * synonym for the {@link #fbar} config.
+ *
+ * buttons: [
+ * { text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'bottom',
+ * ui: 'footer',
+ * 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.
*/
if (me.buttons) {
me.fbar = me.buttons;
@@ -33995,33 +36070,31 @@ each of the buttons in the buttons toolbar.
}
/**
- * @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
+ * @cfg {Object/Object[]} fbar
+ * Convenience config used for adding items to the bottom of the panel. Short for Footer Bar.
+ *
+ * fbar: [
+ * { type: 'button', text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'bottom',
+ * ui: 'footer',
+ * 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.
*/
if (me.fbar) {
- fbar = initToolbar(me.fbar, 'bottom');
+ fbar = initToolbar(me.fbar, 'bottom', true); // only we useButtonAlign
fbar.ui = 'footer';
// Apply the minButtonWidth config to buttons in the toolbar
@@ -34037,66 +36110,64 @@ each of the buttons in the fbar.
};
}
- fbar = me.addDocked(fbar)[0];
- fbar.insert(0, {
- flex: 1,
- xtype: 'component',
- focusable: false
- });
+ docked.push(fbar);
me.fbar = null;
}
/**
- * @cfg {Object/Array} lbar
- *
- * Convenience method. Short for 'Left Bar' (left-docked, vertical toolbar).
+ * @cfg {Object/Object[]} lbar
+ * Convenience config. Short for 'Left Bar' (left-docked, vertical toolbar).
*
- * lbar: [
- * { xtype: 'button', text: 'Button 1' }
- * ]
+ * lbar: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
*
* is equivalent to
*
- * dockedItems: [{
- * xtype: 'toolbar',
- * dock: 'left',
- * items: [
- * { xtype: 'button', text: 'Button 1' }
- * ]
- * }]
- *
- * @markdown
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'left',
+ * items: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
*/
if (me.lbar) {
- me.addDocked(initToolbar(me.lbar, 'left'));
+ docked.push(initToolbar(me.lbar, 'left'));
me.lbar = null;
}
/**
- * @cfg {Object/Array} rbar
- *
- * Convenience method. Short for 'Right Bar' (right-docked, vertical toolbar).
+ * @cfg {Object/Object[]} rbar
+ * Convenience config. Short for 'Right Bar' (right-docked, vertical toolbar).
*
- * rbar: [
- * { xtype: 'button', text: 'Button 1' }
- * ]
+ * rbar: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
*
* is equivalent to
*
- * dockedItems: [{
- * xtype: 'toolbar',
- * dock: 'right',
- * items: [
- * { xtype: 'button', text: 'Button 1' }
- * ]
- * }]
- *
- * @markdown
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'right',
+ * items: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
*/
if (me.rbar) {
- me.addDocked(initToolbar(me.rbar, 'right'));
+ docked.push(initToolbar(me.rbar, 'right'));
me.rbar = null;
}
+
+ if (me.dockedItems) {
+ if (!Ext.isArray(me.dockedItems)) {
+ me.dockedItems = [me.dockedItems];
+ }
+ me.dockedItems = me.dockedItems.concat(docked);
+ } else {
+ me.dockedItems = docked;
+ }
},
/**
@@ -34107,7 +36178,7 @@ each of the buttons in the fbar.
initTools: function() {
var me = this;
- me.tools = me.tools || [];
+ me.tools = me.tools ? Ext.Array.clone(me.tools) : [];
// Add a collapse tool unless configured to not show a collapse tool
// or to not even show a header.
@@ -34147,17 +36218,18 @@ each of the buttons in the fbar.
/**
* @private
+ * @template
* 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}.
+ * 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 also affected by the {@link #closeAction} setting. For more explicit control use
+ * {@link #destroy} and {@link #hide} methods.
*/
close: function() {
if (this.fireEvent('beforeclose', this) !== false) {
@@ -34182,35 +36254,28 @@ each of the buttons in the fbar.
// 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.
+ // Call to super after adding the header, to prevent an unnecessary re-layout
+ me.callParent(arguments);
+ },
+
+ afterRender: function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ // Instate the collapsed state after render. We need to wait for
+ // this moment so that we have established at least some of our size (from our
+ // configured dimensions or from content via the component layout)
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);
- };
- }
+ 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
+ * @param {Boolean} force True to force the header to be created
*/
updateHeader: function(force) {
var me = this,
@@ -34272,13 +36337,52 @@ each of the buttons in the fbar.
return this.body || this.frameBody || this.el;
},
+ // the overrides below allow for collapsed regions inside the border layout to be hidden
+
+ // inherit docs
+ isVisible: function(deep){
+ var me = this;
+ if (me.collapsed && me.placeholder) {
+ return me.placeholder.isVisible(deep);
+ }
+ return me.callParent(arguments);
+ },
+
+ // inherit docs
+ onHide: function(){
+ var me = this;
+ if (me.collapsed && me.placeholder) {
+ me.placeholder.hide();
+ } else {
+ me.callParent(arguments);
+ }
+ },
+
+ // inherit docs
+ onShow: function(){
+ var me = this;
+ if (me.collapsed && me.placeholder) {
+ // force hidden back to true, since this gets set by the layout
+ me.hidden = true;
+ me.placeholder.show();
+ } else {
+ me.callParent(arguments);
+ }
+ },
+
addTool: function(tool) {
- this.tools.push(tool);
- var header = this.header;
+ var me = this,
+ header = me.header;
+
+ if (Ext.isArray(tool)) {
+ Ext.each(tool, me.addTool, me);
+ return;
+ }
+ me.tools.push(tool);
if (header) {
header.addTool(tool);
}
- this.updateHeader();
+ me.updateHeader();
},
getOppositeDirection: function(d) {
@@ -34296,15 +36400,18 @@ each of the buttons in the fbar.
},
/**
- * 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
+ * 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 {String} 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
*/
@@ -34339,7 +36446,6 @@ each of the buttons in the fbar.
reExpanderOrientation,
reExpanderDock,
getDimension,
- setDimension,
collapseDimension;
if (!direction) {
@@ -34362,23 +36468,22 @@ each of the buttons in the fbar.
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
+ // Attempt to find a reExpander Component (docked in a horizontal orientation)
+ // Also, collect all other docked items which we must hide after collapse.
for (; i < dockedItemCount; i++) {
comp = dockedItems[i];
if (comp.isVisible()) {
- if (comp.isHeader && (!comp.dock || comp.dock == 'top' || comp.dock == 'bottom')) {
+ if (comp.isXType('header', true) && (!comp.dock || comp.dock == 'top' || comp.dock == 'bottom')) {
reExpander = comp;
} else {
me.hiddenDocked.push(comp);
}
+ } else if (comp === me.reExpander) {
+ reExpander = comp;
}
}
@@ -34390,15 +36495,12 @@ each of the buttons in the fbar.
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
+ // Attempt to find a reExpander Component (docked in a vecrtical orientation)
+ // Also, collect all other docked items which we must hide after collapse.
for (; i < dockedItemCount; i++) {
comp = dockedItems[i];
if (comp.isVisible()) {
@@ -34407,6 +36509,8 @@ each of the buttons in the fbar.
} else {
me.hiddenDocked.push(comp);
}
+ } else if (comp === me.reExpander) {
+ reExpander = comp;
}
}
@@ -34420,12 +36524,6 @@ each of the buttons in the fbar.
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();
@@ -34438,7 +36536,8 @@ each of the buttons in the fbar.
// }
// We found a header: Measure it to find the collapse-to size.
- if (reExpander) {
+ if (reExpander && reExpander.rendered) {
+
//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);
@@ -34447,13 +36546,13 @@ each of the buttons in the fbar.
}
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);
+ reExpander.removeClsWithUI(me.collapsedCls + '-' + reExpander.dock);
if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) {
reExpander.removeClsWithUI(me.collapsedCls + '-border-' + reExpander.dock);
}
@@ -34512,21 +36611,26 @@ each of the buttons in the fbar.
// Animate to the new size
anim.to[collapseDimension] = newSize;
+ // When we collapse a panel, the panel is in control of one dimension (depending on
+ // collapse direction) and sets that on the component. We must restore the user's
+ // original value (including non-existance) when we expand. Using this technique, we
+ // mimic setCalculatedSize for the dimension we do not control and setSize for the
+ // one we do (only while collapsed).
+ if (!me.collapseMemento) {
+ me.collapseMemento = new Ext.util.Memento(me);
+ }
+ me.collapseMemento.capture(['width', 'height', 'minWidth', 'minHeight', 'layoutManagedHeight', 'layoutManagedWidth']);
+
// 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;
+ me.suspendLayout = true;
if (animate) {
me.animate(anim);
} else {
- // EXTJSIV-1937 (would like to use setCalculateSize)
- // save width/height here, expand puts them back
- me.uncollapsedSize = { width: me.width, height: me.height };
-
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);
@@ -34541,10 +36645,24 @@ each of the buttons in the fbar.
i = 0,
l = me.hiddenDocked.length;
- me.minWidth = me.savedMinWidth;
- me.minHeight = me.savedMinHeight;
+ me.collapseMemento.restore(['minWidth', 'minHeight']);
+
+ // Now we can restore the dimension we don't control to its original state
+ // Leave the value in the memento so that it can be correctly restored
+ // if it is set by animation.
+ if (Ext.Component.VERTICAL_DIRECTION_Re.test(me.expandDirection)) {
+ me.layoutManagedHeight = 2;
+ me.collapseMemento.restore('width', false);
+ } else {
+ me.layoutManagedWidth = 2;
+ me.collapseMemento.restore('height', false);
+ }
+
+ // We must hide the body, otherwise it overlays docked items which come before
+ // it in the DOM order. Collapsing its dimension won't work - padding and borders keep a size.
+ me.saveScrollTop = me.body.dom.scrollTop;
+ me.body.setStyle('display', 'none');
- me.body.hide();
for (; i < l; i++) {
me.hiddenDocked[i].hide();
}
@@ -34553,9 +36671,18 @@ each of the buttons in the fbar.
me.reExpander.show();
}
me.collapsed = true;
+ me.suspendLayout = false;
if (!internal) {
- me.doComponentLayout();
+ if (me.ownerCt) {
+ // Because Component layouts only inform upstream containers if they have changed size,
+ // explicitly lay out the container now, because the lastComponentsize will have been set by the non-animated setCalculatedSize.
+ if (animated) {
+ me.ownerCt.layout.layout();
+ }
+ } else if (me.reExpander.temporary) {
+ me.doComponentLayout();
+ }
}
if (me.resizer) {
@@ -34577,36 +36704,24 @@ each of the buttons in the fbar.
},
/**
- * 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
+ * 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) {
+ var me = this;
+ if (!me.collapsed || me.fireEvent('beforeexpand', me, animate) === false) {
return false;
}
- // EXTJSIV-1937 (would like to use setCalculateSize)
- if (this.uncollapsedSize) {
- Ext.Object.each(this.uncollapsedSize, function (name, value) {
- if (Ext.isDefined(value)) {
- this[name] = value;
- } else {
- delete this[name];
- }
- }, this);
- delete this.uncollapsedSize;
- }
-
- var me = this,
- i = 0,
+ var i = 0,
l = me.hiddenDocked.length,
direction = me.expandDirection,
height = me.getHeight(),
width = me.getWidth(),
- pos, anim, satisfyJSLint;
+ pos, anim;
// Disable toggle tool during animated expand
if (animate && me.collapseTool) {
@@ -34637,12 +36752,13 @@ each of the buttons in the fbar.
me.collapseTool.setType('collapse-' + me.collapseDirection);
}
+ // Restore body display and scroll position
+ me.body.setStyle('display', '');
+ me.body.dom.scrollTop = me.saveScrollTop;
+
// 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) {
@@ -34664,8 +36780,12 @@ each of the buttons in the fbar.
if ((direction == Ext.Component.DIRECTION_TOP) || (direction == Ext.Component.DIRECTION_BOTTOM)) {
+ // Restore the collapsed dimension.
+ // Leave it in the memento, so that the final restoreAll can overwrite anything that animation does.
+ me.collapseMemento.restore('height', false);
+
// If autoHeight, measure the height now we have shown the body element.
- if (me.autoHeight) {
+ if (me.height === undefined) {
me.setCalculatedSize(me.width, null);
anim.to.height = me.getHeight();
@@ -34681,7 +36801,7 @@ each of the buttons in the fbar.
}
// Else, restore to saved height
else {
- anim.to.height = me.expandedSize;
+ anim.to.height = me.height;
}
// top needs animating upwards
@@ -34692,8 +36812,12 @@ each of the buttons in the fbar.
}
} else if ((direction == Ext.Component.DIRECTION_LEFT) || (direction == Ext.Component.DIRECTION_RIGHT)) {
+ // Restore the collapsed dimension.
+ // Leave it in the memento, so that the final restoreAll can overwrite anything that animation does.
+ me.collapseMemento.restore('width', false);
+
// If autoWidth, measure the width now we have shown the body element.
- if (me.autoWidth) {
+ if (me.width === undefined) {
me.setCalculatedSize(null, me.height);
anim.to.width = me.getWidth();
@@ -34709,7 +36833,7 @@ each of the buttons in the fbar.
}
// Else, restore to saved width
else {
- anim.to.width = me.expandedSize;
+ anim.to.width = me.width;
}
// left needs animating leftwards
@@ -34723,7 +36847,7 @@ each of the buttons in the fbar.
if (animate) {
me.animate(anim);
} else {
- me.setSize(anim.to.width, anim.to.height);
+ me.setCalculatedSize(anim.to.width, anim.to.height);
if (anim.to.x) {
me.setLeft(anim.to.x);
}
@@ -34738,7 +36862,6 @@ each of the buttons in the fbar.
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) {
@@ -34748,10 +36871,15 @@ each of the buttons in the fbar.
delete me.height;
}
- // Reinstate layout out after Panel has re-expanded
- delete me.suspendLayout;
+ // Restore width/height and dimension management flags to original values
+ if (me.collapseMemento) {
+ me.collapseMemento.restoreAll();
+ }
+
if (animated && me.ownerCt) {
- me.ownerCt.doLayout();
+ // IE 6 has an intermittent repaint issue in this case so give
+ // it a little extra time to catch up before laying out.
+ Ext.defer(me.ownerCt.doLayout, Ext.isIE6 ? 1 : 0, me);
}
if (me.resizer) {
@@ -34790,12 +36918,12 @@ each of the buttons in the fbar.
// 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
+ * @property {Ext.dd.DragSource} dd
+ * 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}.
*/
this.dd = Ext.create('Ext.panel.DD', this, Ext.isBoolean(this.draggable) ? null : this.draggable);
},
@@ -34803,10 +36931,10 @@ each of the buttons in the fbar.
// private - helper function for ghost
ghostTools : function() {
var tools = [],
- origTools = this.initialConfig.tools;
+ headerTools = this.header.query('tool[hidden=false]');
- if (origTools) {
- Ext.each(origTools, function(tool) {
+ if (headerTools.length) {
+ Ext.each(headerTools, 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
@@ -34815,8 +36943,7 @@ each of the buttons in the fbar.
type: tool.type
});
});
- }
- else {
+ } else {
tools = [{
type: 'placeholder'
}];
@@ -34828,23 +36955,19 @@ each of the buttons in the fbar.
ghost: function(cls) {
var me = this,
ghostPanel = me.ghostPanel,
- box = me.getBox();
+ box = me.getBox(),
+ header;
if (!ghostPanel) {
ghostPanel = Ext.create('Ext.panel.Panel', {
- renderTo: document.body,
+ renderTo: me.floating ? me.el.dom.parentNode : 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;
@@ -34855,6 +36978,19 @@ each of the buttons in the fbar.
} else {
ghostPanel.toFront();
}
+ header = ghostPanel.header;
+ // restore options
+ if (header) {
+ header.suspendLayout = true;
+ Ext.Array.forEach(header.query('tool'), function(tool){
+ header.remove(tool);
+ });
+ header.suspendLayout = false;
+ }
+ ghostPanel.addTool(me.ghostTools());
+ ghostPanel.setTitle(me.title);
+ ghostPanel.setIconCls(me.iconCls);
+
ghostPanel.el.show();
ghostPanel.setPosition(box.x, box.y);
ghostPanel.setSize(box.width, box.height);
@@ -34890,6 +37026,8 @@ each of the buttons in the fbar.
}
this.callParent([resizable]);
}
+}, function(){
+ this.prototype.animCollapse = Ext.enableFx;
});
/**
@@ -34975,9 +37113,6 @@ Ext.define('Ext.layout.component.Tip', {
* 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', {
@@ -34985,7 +37120,8 @@ Ext.define('Ext.tip.Tip', {
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 {Boolean} [closable=false]
+ * True to render a close tool button into the tooltip header.
*/
/**
* @cfg {Number} width
@@ -34993,33 +37129,32 @@ Ext.define('Ext.tip.Tip', {
* {@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).
+ * @cfg {Number} minWidth The minimum width of the tip in pixels.
*/
minWidth : 40,
/**
- * @cfg {Number} maxWidth The maximum width of the tip in pixels (defaults to 300). The maximum supported value is 500.
+ * @cfg {Number} maxWidth The maximum width of the tip in pixels. 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").
+ * for bottom-right shadow.
*/
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?").
+ * @cfg {String} defaultAlign
+ * Experimental . The default {@link Ext.Element#alignTo} anchor position value for this tip relative
+ * to its element of origin.
*/
defaultAlign : "tl-bl?",
/**
- * @cfg {Boolean} constrainPosition If true, then the tooltip will be automatically constrained to stay within
- * the browser viewport. Defaults to false.
+ * @cfg {Boolean} constrainPosition
+ * If true, then the tooltip will be automatically constrained to stay within the browser viewport.
*/
constrainPosition : true,
- /**
- * @inherited
- */
+ // @inherited
frame: false,
// private panel overrides
@@ -35034,15 +37169,35 @@ Ext.define('Ext.tip.Tip', {
focusOnToFront: false,
componentLayout: 'tip',
+ /**
+ * @cfg {String} closeAction
+ * The action to take when the close header tool is clicked:
+ *
+ * '{@link #destroy}'
:
+ * {@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}'
: Default
+ * {@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: 'hide',
ariaRole: 'tooltip',
initComponent: function() {
- this.callParent(arguments);
+ var me = this;
+
+ me.floating = Ext.apply({}, {shadow: me.shadow}, me.self.prototype.floating);
+ me.callParent(arguments);
// Or in the deprecated config. Floating.doConstrain only constrains if the constrain property is truthy.
- this.constrain = this.constrain || this.constrainPosition;
+ me.constrain = me.constrain || me.constrainPosition;
},
/**
@@ -35051,11 +37206,11 @@ Ext.define('Ext.tip.Tip', {
// Show the tip at x:50 and y:100
tip.showAt([50,100]);
- * @param {Array} xy An array containing the x and y coordinates
+ * @param {Number[]} xy An array containing the x and y coordinates
*/
showAt : function(xy){
var me = this;
- this.callParent();
+ this.callParent(arguments);
// Show may have been vetoed.
if (me.isVisible()) {
me.setPagePosition(xy[0], xy[1]);
@@ -35067,7 +37222,7 @@ tip.showAt([50,100]);
},
/**
- * Experimental . Shows this tip at a position relative to another element using a standard {@link Ext.core.Element#alignTo}
+ * Experimental . Shows this tip at a position relative to another element using a standard {@link Ext.Element#alignTo}
* anchor position value. Example usage:
*
// Show the tip at the default position ('tl-br?')
@@ -35076,8 +37231,8 @@ 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
+ * @param {String/HTMLElement/Ext.Element} el An HTMLElement, Ext.Element or string id of the target element to align to
+ * @param {String} [position] A valid {@link Ext.Element#alignTo} anchor position (defaults to 'tl-br?' or
* {@link #defaultAlign} if specified).
*/
showBy : function(el, pos) {
@@ -35095,7 +37250,7 @@ tip.showBy('my-el', 'tl-tr');
el: me.getDragEl(),
delegate: me.header.el,
constrain: me,
- constrainTo: me.el.dom.parentNode
+ constrainTo: me.el.getScopeParent()
};
// Important: Bypass Panel's initDraggable. Call direct to Component's implementation.
Ext.Component.prototype.initDraggable.call(me);
@@ -35107,86 +37262,106 @@ tip.showBy('my-el', 'tl-tr');
});
/**
- * @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
- *
+ *
+ * # 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
- *
+ *
+ * # 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.
+ *
+ * var store = Ext.create('Ext.data.ArrayStore', {
+ * fields: ['company', 'price', 'change'],
+ * data: [
+ * ['3m Co', 71.72, 0.02],
+ * ['Alcoa Inc', 29.01, 0.42],
+ * ['Altria Group Inc', 83.81, 0.28],
+ * ['American Express Company', 52.55, 0.01],
+ * ['American International Group, Inc.', 64.13, 0.31],
+ * ['AT&T Inc.', 31.61, -0.48]
+ * ]
+ * });
+ *
+ * var grid = Ext.create('Ext.grid.Panel', {
+ * title: 'Array Grid',
+ * store: store,
+ * columns: [
+ * {text: 'Company', flex: 1, dataIndex: 'company'},
+ * {text: 'Price', width: 75, dataIndex: 'price'},
+ * {text: 'Change', width: 75, dataIndex: 'change'}
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * grid.getView().on('render', function(view) {
+ * view.tip = Ext.create('Ext.tip.ToolTip', {
+ * // The overall target element.
+ * target: view.el,
+ * // Each grid row causes its own seperate show and hide.
+ * delegate: view.itemSelector,
+ * // Moving within the row should not hide the tip.
+ * trackMouse: true,
+ * // Render immediately so that tip.body can be referenced prior to the first show.
+ * renderTo: Ext.getBody(),
+ * 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
- *
+ *
+ * # 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
- *
+ *
+ * - {@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
+ *
+ * - {@link #autoHide}
+ * - {@link #showDelay}
+ * - {@link #hideDelay}
+ * - {@link #dismissDelay}
+ *
* @docauthor Jason Johnston
*/
Ext.define('Ext.tip.ToolTip', {
@@ -35194,101 +37369,87 @@ Ext.define('Ext.tip.ToolTip', {
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}
+ * @property {HTMLElement} triggerElement
+ * 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 {HTMLElement/Ext.Element/String} target
+ * The target element or string 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
+ * @cfg {Boolean} [autoHide=true]
+ * True to automatically hide the tooltip after the
+ * mouse exits the target element or after the `{@link #dismissDelay}`
+ * has expired if set. 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)
+ * @cfg {Number} showDelay
+ * Delay in milliseconds before the tooltip displays after the mouse enters the target element.
*/
showDelay: 500,
/**
- * @cfg {Number} hideDelay Delay in milliseconds after the mouse exits the
- * target element but before the tooltip actually hides (defaults to 200).
+ * @cfg {Number} hideDelay
+ * Delay in milliseconds after the mouse exits the target element but before the tooltip actually hides.
* 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
+ * @cfg {Number} dismissDelay
+ * Delay in milliseconds before the tooltip automatically hides. 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 {Number[]} [mouseOffset=[15,18]]
+ * An XY offset from the mouse position where the tooltip should be shown.
*/
/**
- * @cfg {Boolean} trackMouse True to have the tooltip follow the mouse as it
- * moves over the target element (defaults to false).
+ * @cfg {Boolean} trackMouse
+ * True to have the tooltip follow the mouse as it moves over the target element.
*/
trackMouse: false,
/**
- * @cfg {String} anchor If specified, indicates that the tip should be anchored to a
+ * @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.
+ * @cfg {Boolean} anchorToTarget
+ * True to anchor the tooltip to the target element, false to anchor it relative to the mouse coordinates.
+ * When `anchorToTarget` is true, use `{@link #defaultAlign}` to control tooltip alignment to the
+ * target element. When `anchorToTarget` is false, use `{@link #anchor}` 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
+ * @cfg {Number} anchorOffset
+ * A numeric pixel value used to offset the default position of the anchor arrow. 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);
- }
- }
- });
-});
- *
+ * @cfg {String} delegate
+ *
+ * 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.
+ *
+ * See the delegate example in class documentation of {@link Ext.tip.ToolTip}.
*/
// private
@@ -35321,12 +37482,12 @@ myGrid.on('render', function(grid) {
me.callParent(arguments);
zIndex = parseInt(me.el.getZIndex(), 10) || 0;
- me.anchorEl.setStyle('z-index', zIndex + 1).setVisibilityMode(Ext.core.Element.DISPLAY);
+ me.anchorEl.setStyle('z-index', zIndex + 1).setVisibilityMode(Ext.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
+ * @param {String/HTMLElement/Ext.Element} t The Element, HtmlElement, or ID of an element to bind to
*/
setTarget: function(target) {
var me = this,
@@ -35339,10 +37500,10 @@ myGrid.on('render', function(grid) {
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)
@@ -35370,7 +37531,7 @@ myGrid.on('render', function(grid) {
if (!me.hidden && me.trackMouse) {
xy = me.getTargetXY();
if (me.constrainPosition) {
- xy = me.el.adjustForConstraints(xy, me.el.dom.parentNode);
+ xy = me.el.adjustForConstraints(xy, me.el.getScopeParent());
}
me.setPagePosition(xy);
}
@@ -35395,8 +37556,8 @@ myGrid.on('render', function(grid) {
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,
+ dw = Ext.Element.getViewWidth() - 5,
+ dh = Ext.Element.getViewHeight() - 5,
de = document.documentElement,
bd = document.body,
scrollX = (de.scrollLeft || bd.scrollLeft || 0) + 5,
@@ -35794,20 +37955,17 @@ myGrid.on('render', function(grid) {
* @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
+ * {@link Ext.tip.QuickTipManager} instance. See the QuickTipManager documentation for additional usage details and examples.
* @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 {String/HTMLElement/Ext.Element} target The target HTMLElement, Ext.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).
+ * @cfg {Boolean} interceptTitles True to automatically use the element's DOM title value if available.
*/
interceptTitles : false,
@@ -35830,7 +37988,7 @@ Ext.define('Ext.tip.QuickTip', {
// private
initComponent : function(){
var me = this;
-
+
me.target = me.target || Ext.getDoc();
me.targets = me.targets || {};
me.callParent();
@@ -35854,7 +38012,7 @@ Ext.define('Ext.tip.QuickTip', {
i = 0,
len = configs.length,
target, j, targetLen;
-
+
for (; i < len; i++) {
config = configs[i];
target = config.target;
@@ -35872,20 +38030,20 @@ Ext.define('Ext.tip.QuickTip', {
/**
* 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.
+ * @param {String/HTMLElement/Ext.Element} el The element from which the quick tip is to be removed or ID of the element.
*/
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.
+ * @param {String/HTMLElement/Ext.Element} el The element that is the target of the tip or ID of the element.
*/
cancelShow: function(el){
var me = this,
activeTarget = me.activeTarget;
-
+
el = Ext.get(el).dom;
if (me.isVisible()) {
if (activeTarget && activeTarget.el == el) {
@@ -35895,26 +38053,36 @@ Ext.define('Ext.tip.QuickTip', {
me.clearTimer('show');
}
},
-
+
+ /**
+ * @private
+ * Reads the tip text from the closest node to the event target which contains the attribute we
+ * are configured to look for. Returns an object containing the text from the attribute, and the target element from
+ * which the text was read.
+ */
getTipCfg: function(e) {
var t = e.getTarget(),
- ttp,
+ titleText = t.title,
cfg;
-
- if(this.interceptTitles && t.title && Ext.isString(t.title)){
- ttp = t.title;
- t.qtip = ttp;
+
+ if (this.interceptTitles && titleText && Ext.isString(titleText)) {
+ t.qtip = titleText;
t.removeAttribute("title");
e.preventDefault();
- }
- else {
+ return {
+ text: titleText
+ };
+ }
+ else {
cfg = this.tagConfig;
t = e.getTarget('[' + cfg.namespace + cfg.attribute + ']');
if (t) {
- ttp = t.getAttribute(cfg.namespace + cfg.attribute);
+ return {
+ target: t,
+ text: t.getAttribute(cfg.namespace + cfg.attribute)
+ };
}
}
- return ttp;
},
// private
@@ -35924,9 +38092,9 @@ Ext.define('Ext.tip.QuickTip', {
elTarget,
cfg,
ns,
- ttp,
+ tipConfig,
autoHide;
-
+
if (me.disabled) {
return;
}
@@ -35939,13 +38107,13 @@ Ext.define('Ext.tip.QuickTip', {
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);
@@ -35968,21 +38136,28 @@ Ext.define('Ext.tip.QuickTip', {
elTarget = Ext.get(target);
cfg = me.tagConfig;
- ns = cfg.namespace;
- ttp = me.getTipCfg(e);
-
- if (ttp) {
+ ns = cfg.namespace;
+ tipConfig = me.getTipCfg(e);
+
+ if (tipConfig) {
+
+ // getTipCfg may look up the parentNode axis for a tip text attribute and will return the new target node.
+ // Change our target element to match that from which the tip text attribute was read.
+ if (tipConfig.target) {
+ target = tipConfig.target;
+ elTarget = Ext.get(target);
+ }
autoHide = elTarget.getAttribute(ns + cfg.hide);
-
+
me.activeTarget = {
el: target,
- text: ttp,
+ text: tipConfig.text,
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) {
@@ -35995,7 +38170,7 @@ Ext.define('Ext.tip.QuickTip', {
// 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;
@@ -36011,7 +38186,7 @@ Ext.define('Ext.tip.QuickTip', {
showAt : function(xy){
var me = this,
target = me.activeTarget;
-
+
if (target) {
if (!me.rendered) {
me.render(Ext.getBody());
@@ -36036,7 +38211,7 @@ Ext.define('Ext.tip.QuickTip', {
}
me.setWidth(target.width);
-
+
if (me.anchor) {
me.constrainPosition = false;
} else if (target.align) { // TODO: this doesn't seem to work consistently
@@ -36096,20 +38271,17 @@ Ext.define('Ext.tip.QuickTip', {
*
* 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
- *
+ * @example
* // 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',
@@ -36120,8 +38292,8 @@ Ext.define('Ext.tip.QuickTip', {
* },
* renderTo: Ext.getBody()
* });
- *
- *
+ *
+ *
* // Manually register a quick tip for a specific element
* Ext.tip.QuickTipManager.register({
* target: 'quickTipContainer',
@@ -36142,7 +38314,7 @@ Ext.define('Ext.tip.QuickTip', {
* - `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
*
@@ -36160,9 +38332,9 @@ Ext.define('Ext.tip.QuickTipManager', function() {
/**
* Initialize the global QuickTips instance and prepare any quick tips.
- * @param {Boolean} autoRender True to render the QuickTips container immediately to
+ * @param {Boolean} autoRender (optional) True to render the QuickTips container immediately to
* preload images. (Defaults to true)
- * @param {Object} config An optional config object for the created QuickTip. By
+ * @param {Object} config (optional) config object for the created QuickTip. By
* default, the {@link Ext.tip.QuickTip QuickTip} class is instantiated, but this can
* be changed by supplying an xtype property or a className property in this object.
* All other properties on this object are configuration for the created component.
@@ -36279,7 +38451,7 @@ Ext.define('Ext.tip.QuickTipManager', function() {
/**
* 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.
+ * @param {String/HTMLElement/Ext.Element} el The element from which the quick tip is to be removed or ID of the element.
*/
unregister : function(){
tip.unregister.apply(tip, arguments);
@@ -36295,12 +38467,9 @@ Ext.define('Ext.tip.QuickTipManager', function() {
};
}());
/**
- * @class Ext.app.Application
- * @extend Ext.app.Controller
- *
* 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() {
@@ -36311,56 +38480,57 @@ Ext.define('Ext.tip.QuickTipManager', function() {
* });
* }
* });
- *
+ *
* 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
- *
+ *
+ * # 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
+ * 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][mvc] -
+ * 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
+ * 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 .
- *
+ *
+ * For more information about writing Ext JS 4 applications, please see the
+ * [application architecture guide][mvc].
+ *
+ * [mvc]: #!/guide/application_architecture
+ *
* @docauthor Ed Spencer
- * @constructor
*/
Ext.define('Ext.app.Application', {
extend: 'Ext.app.Controller',
@@ -36386,27 +38556,30 @@ Ext.define('Ext.app.Application', {
scope: undefined,
/**
- * @cfg {Boolean} enableQuickTips True to automatically set up Ext.tip.QuickTip support (defaults to true)
+ * @cfg {Boolean} enableQuickTips True to automatically set up Ext.tip.QuickTip support.
*/
enableQuickTips: true,
/**
- * @cfg {String} defaultUrl When the app is first loaded, this url will be redirected to. Defaults to undefined
+ * @cfg {String} defaultUrl When the app is first loaded, this url will be redirected to.
*/
/**
* @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 True to automatically load and instantiate AppName.view.Viewport
- * before firing the launch function (defaults to false).
+ * before firing the launch function.
*/
autoCreateViewport: false,
+ /**
+ * Creates new Application.
+ * @param {Object} [config] Config object.
+ */
constructor: function(config) {
config = config || {};
Ext.apply(this, config);
@@ -36539,7 +38712,7 @@ Ext.define('Ext.app.Application', {
/**
* @class Ext.chart.Callout
- * @ignore
+ * A mixin providing callout functionality for Ext.chart.series.Series.
*/
Ext.define('Ext.chart.Callout', {
@@ -36771,7 +38944,7 @@ Ext.define('Ext.draw.CompositeSprite', {
});
},
- /** Add a Sprite to the Group */
+ // Inherit docs from MixedCollection
add: function(key, o) {
var result = this.callParent(arguments);
this.attachEvents(result);
@@ -36782,7 +38955,7 @@ Ext.define('Ext.draw.CompositeSprite', {
return this.callParent(arguments);
},
- /** Remove a Sprite from the Group */
+ // Inherit docs from MixedCollection
remove: function(o) {
var me = this;
@@ -36794,13 +38967,14 @@ Ext.define('Ext.draw.CompositeSprite', {
mouseout: me.onMouseOut,
click: me.onClick
});
- me.callParent(arguments);
+ return me.callParent(arguments);
},
/**
* Returns the group bounding box.
- * Behaves like {@link Ext.draw.Sprite} getBBox method.
- */
+ * Behaves like {@link Ext.draw.Sprite#getBBox} method.
+ * @return {Object} an object with x, y, width, and height properties.
+ */
getBBox: function() {
var i = 0,
sprite,
@@ -36834,10 +39008,11 @@ Ext.define('Ext.draw.CompositeSprite', {
},
/**
- * 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.
+ * 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.
+ * @param {Object} attrs Attributes to be changed on the sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.CompositeSprite} this
*/
setAttributes: function(attrs, redraw) {
var i = 0,
@@ -36853,6 +39028,8 @@ Ext.define('Ext.draw.CompositeSprite', {
/**
* Hides all sprites. If the first parameter of the method is true
* then a redraw will be forced for each sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.CompositeSprite} this
*/
hide: function(redraw) {
var i = 0,
@@ -36868,6 +39045,8 @@ Ext.define('Ext.draw.CompositeSprite', {
/**
* Shows all sprites. If the first parameter of the method is true
* then a redraw will be forced for each sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.CompositeSprite} this
*/
show: function(redraw) {
var i = 0,
@@ -36969,32 +39148,36 @@ Ext.define('Ext.draw.CompositeSprite', {
});
/**
- * @class Ext.layout.component.Draw
+ * @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.container.Container#layout layout} is configured.
*/
-Ext.define('Ext.layout.component.Draw', {
+Ext.define('Ext.layout.component.Auto', {
/* Begin Definitions */
- alias: 'layout.draw',
+ alias: 'layout.autocomponent',
- extend: 'Ext.layout.component.Auto',
+ extend: 'Ext.layout.component.Component',
/* End Definitions */
- type: 'draw',
+ type: 'autocomponent',
onLayout : function(width, height) {
- this.owner.surface.setSize(width, height);
- this.callParent(arguments);
+ this.setTargetSize(width, height);
}
});
/**
* @class Ext.chart.theme.Theme
- * @ignore
+ *
+ * Provides chart theming.
+ *
+ * Used as mixins by Ext.chart.Chart.
*/
Ext.define('Ext.chart.theme.Theme', {
@@ -37276,9 +39459,13 @@ function() {
* a handle to a mask instance from the chart object. The `chart.mask` element is a
* `Ext.Panel`.
*
- * @constructor
*/
Ext.define('Ext.chart.Mask', {
+ require: ['Ext.chart.MaskLayer'],
+ /**
+ * Creates new Mask.
+ * @param {Object} config (optional) Config object.
+ */
constructor: function(config) {
var me = this;
@@ -37421,12 +39608,7 @@ Ext.define('Ext.chart.Mask', {
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.updateBox(me.maskSelection);
me.mask.show();
me.maskSprite.setAttributes({
hidden: true
@@ -37465,57 +39647,76 @@ Ext.define('Ext.chart.Mask', {
* @class Ext.chart.Navigation
*
* Handles panning and zooming capabilities.
- *
- * @ignore
+ *
+ * Used as mixin by Ext.chart.Chart.
*/
Ext.define('Ext.chart.Navigation', {
constructor: function() {
this.originalStore = this.store;
},
-
- //filters the store to the specified interval(s)
+
+ /**
+ * Zooms the chart to the specified selection range.
+ * Can be used with a selection mask. For example:
+ *
+ * items: {
+ * xtype: 'chart',
+ * animate: true,
+ * store: store1,
+ * mask: 'horizontal',
+ * listeners: {
+ * select: {
+ * fn: function(me, selection) {
+ * me.setZoom(selection);
+ * me.mask.hide();
+ * }
+ * }
+ * }
+ * }
+ */
setZoom: function(zoomConfig) {
var me = this,
- store = me.substore || me.store,
+ axes = me.axes,
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);
+ xScale = 1 / bbox.width,
+ yScale = 1 / bbox.height,
+ zoomer = {
+ x : zoomConfig.x * xScale,
+ y : zoomConfig.y * yScale,
+ width : zoomConfig.width * xScale,
+ height : zoomConfig.height * yScale
+ };
+ axes.each(function(axis) {
+ var ends = axis.calcEnds();
+ if (axis.position == 'bottom' || axis.position == 'top') {
+ var from = (ends.to - ends.from) * zoomer.x + ends.from,
+ to = (ends.to - ends.from) * zoomer.width + from;
+ axis.minimum = from;
+ axis.maximum = to;
+ } else {
+ var to = (ends.to - ends.from) * (1 - zoomer.y) + ends.from,
+ from = to - (ends.to - ends.from) * zoomer.height;
+ axis.minimum = from;
+ axis.maximum = to;
}
- json.push(obj);
});
- me.store = me.substore = Ext.create('Ext.data.JsonStore', {
- fields: recFields,
- data: json
- });
- me.redraw(true);
+ me.redraw(false);
},
+ /**
+ * Restores the zoom to the original value. This can be used to reset
+ * the previous zoom state set by `setZoom`. For example:
+ *
+ * myChart.restoreZoom();
+ */
restoreZoom: function() {
this.store = this.substore = this.originalStore;
this.redraw(true);
}
-
+
});
+
/**
* @class Ext.chart.Shape
* @ignore
@@ -37624,9 +39825,6 @@ Ext.define('Ext.chart.Shape', {
}
});
/**
- * @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
@@ -37648,7 +39846,7 @@ Ext.define('Ext.chart.Shape', {
* The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.Sprite}
* class documentation.
*
- * ### Listeners
+ * # Listeners
*
* You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are:
*
@@ -37665,77 +39863,86 @@ Ext.define('Ext.chart.Shape', {
*
* drawComponent.surface.on({
* 'mousemove': function() {
- * console.log('moving the mouse over the surface');
+ * console.log('moving the mouse over the surface');
* }
* });
*
- * ## Example
+ * # Example
+ *
+ * var drawComponent = Ext.create('Ext.draw.Component', {
+ * width: 800,
+ * height: 600,
+ * renderTo: document.body
+ * }), surface = drawComponent.surface;
+ *
+ * surface.add([{
+ * type: 'circle',
+ * radius: 10,
+ * fill: '#f00',
+ * x: 10,
+ * y: 10,
+ * group: 'circles'
+ * }, {
+ * type: 'circle',
+ * radius: 10,
+ * fill: '#0f0',
+ * x: 50,
+ * y: 50,
+ * group: 'circles'
+ * }, {
+ * type: 'circle',
+ * radius: 10,
+ * fill: '#00f',
+ * x: 100,
+ * y: 100,
+ * group: 'circles'
+ * }, {
+ * type: 'rect',
+ * width: 20,
+ * height: 20,
+ * fill: '#f00',
+ * x: 10,
+ * y: 10,
+ * group: 'rectangles'
+ * }, {
+ * type: 'rect',
+ * width: 20,
+ * height: 20,
+ * fill: '#0f0',
+ * x: 50,
+ * y: 50,
+ * group: 'rectangles'
+ * }, {
+ * type: 'rect',
+ * width: 20,
+ * height: 20,
+ * fill: '#00f',
+ * x: 100,
+ * y: 100,
+ * group: 'rectangles'
+ * }]);
*
- * drawComponent.surface.add([
- * {
- * type: 'circle',
- * radius: 10,
- * fill: '#f00',
- * x: 10,
- * y: 10,
- * group: 'circles'
- * },
- * {
- * type: 'circle',
- * radius: 10,
- * fill: '#0f0',
- * x: 50,
- * y: 50,
- * group: 'circles'
- * },
- * {
- * type: 'circle',
- * radius: 10,
- * fill: '#00f',
- * x: 100,
- * y: 100,
- * group: 'circles'
- * },
- * {
- * type: 'rect',
- * radius: 10,
- * x: 10,
- * y: 10,
- * group: 'rectangles'
- * },
- * {
- * type: 'rect',
- * radius: 10,
- * x: 50,
- * y: 50,
- * group: 'rectangles'
- * },
- * {
- * type: 'rect',
- * radius: 10,
- * x: 100,
- * y: 100,
- * group: 'rectangles'
- * }
- * ]);
- *
* // Get references to my groups
- * my circles = surface.getGroup('circles');
- * my rectangles = surface.getGroup('rectangles');
- *
+ * circles = surface.getGroup('circles');
+ * rectangles = surface.getGroup('rectangles');
+ *
* // Animate the circles down
* circles.animate({
* duration: 1000,
- * translate: {
- * y: 200
+ * to: {
+ * translate: {
+ * y: 200
+ * }
* }
* });
- *
+ *
* // Animate the rectangles across
* rectangles.animate({
* duration: 1000,
- * translate: {
- * x: 200
+ * to: {
+ * translate: {
+ * x: 200
+ * }
* }
* });
*/
@@ -37754,11 +39961,12 @@ Ext.define('Ext.draw.Surface', {
statics: {
/**
- * Create and return a new concrete Surface instance appropriate for the current environment.
+ * Creates and returns 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']
.
+ * @param {String[]} enginePriority (Optional) order of implementations to use; the first one that is
+ * available in the current environment will be used. Defaults to `['Svg', 'Vml']`.
+ * @return {Object} The created Surface or false.
+ * @static
*/
create: function(config, enginePriority) {
enginePriority = enginePriority || ['Svg', 'Vml'];
@@ -37821,29 +40029,39 @@ Ext.define('Ext.draw.Surface', {
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}.
- */
+ /**
+ * @cfg {Number} height
+ * The height of this component in pixels (defaults to auto).
+ */
+ /**
+ * @cfg {Number} width
+ * The width of this component in pixels (defaults to auto).
+ */
+
container: undefined,
height: 352,
width: 512,
x: 0,
y: 0,
+ /**
+ * @private Flag indicating that the surface implementation requires sprites to be maintained
+ * in order of their zIndex. Impls that don't require this can set it to false.
+ */
+ orderSpritesByZIndex: true,
+
+
+ /**
+ * Creates new Surface.
+ * @param {Object} config (optional) Config object.
+ */
constructor: function(config) {
var me = this;
config = config || {};
Ext.apply(me, config);
me.domRef = Ext.getDoc().dom;
-
+
me.customAttributes = {};
me.addEvents(
@@ -37881,17 +40099,22 @@ Ext.define('Ext.draw.Surface', {
renderItems: Ext.emptyFn,
// @private
- setViewBox: Ext.emptyFn,
+ setViewBox: function (x, y, width, height) {
+ if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) {
+ this.viewBox = {x: x, y: y, width: width, height: height};
+ this.applyViewBox();
+ }
+ },
/**
* Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out.
*
* For example:
*
- * drawComponent.surface.addCls(sprite, 'x-visible');
- *
+ * 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
+ * @param {String/String[]} className The CSS class to add, or an array of classes
* @method
*/
addCls: Ext.emptyFn,
@@ -37901,10 +40124,10 @@ Ext.define('Ext.draw.Surface', {
*
* For example:
*
- * drawComponent.surface.removeCls(sprite, 'x-visible');
- *
+ * 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
+ * @param {String/String[]} className The CSS class to remove, or an array of classes
* @method
*/
removeCls: Ext.emptyFn,
@@ -37914,10 +40137,10 @@ Ext.define('Ext.draw.Surface', {
*
* For example:
*
- * drawComponent.surface.setStyle(sprite, {
- * 'cursor': 'pointer'
- * });
- *
+ * 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.
* @method
@@ -37941,7 +40164,7 @@ Ext.define('Ext.draw.Surface', {
this.add(items);
}
},
-
+
// @private
initBackground: function(config) {
var me = this,
@@ -37982,16 +40205,16 @@ Ext.define('Ext.draw.Surface', {
}
}
},
-
+
/**
* Sets the size of the surface. Accomodates the background (if any) to fit the new size too.
*
* For example:
*
- * drawComponent.surface.setSize(500, 500);
+ * 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.
*/
@@ -38003,6 +40226,7 @@ Ext.define('Ext.draw.Surface', {
hidden: false
}, true);
}
+ this.applyViewBox();
},
// @private
@@ -38011,7 +40235,7 @@ Ext.define('Ext.draw.Surface', {
attrs = {},
exclude = {},
sattr = sprite.attr;
- for (i in sattr) {
+ for (i in sattr) {
// Narrow down attributes to the main set
if (this.translateAttrs.hasOwnProperty(i)) {
// Translated attr
@@ -38063,7 +40287,7 @@ Ext.define('Ext.draw.Surface', {
onMouseLeave: Ext.emptyFn,
/**
- * Add a gradient definition to the Surface. Note that in some surface engines, adding
+ * Adds 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
@@ -38071,33 +40295,33 @@ Ext.define('Ext.draw.Surface', {
* 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'
- }
- }
- });
+ * - **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'
+ * }
+ * }
+ * });
*
* @method
*/
addGradient: Ext.emptyFn,
/**
- * Add a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be passed into this method.
+ * Adds a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be
+ * passed into this method.
*
* For example:
*
@@ -38109,7 +40333,7 @@ Ext.define('Ext.draw.Surface', {
* y: 100
* });
*
- */
+ */
add: function() {
var args = Array.prototype.slice.call(arguments),
sprite,
@@ -38127,7866 +40351,9082 @@ Ext.define('Ext.draw.Surface', {
results.push(item);
}
- return results;
- }
- sprite = this.prepareItems(args[0], true)[0];
- this.normalizeSpriteCollection(sprite);
- this.onAdd(sprite);
- return sprite;
+ return results;
+ }
+ sprite = this.prepareItems(args[0], true)[0];
+ this.insertByZIndex(sprite);
+ this.onAdd(sprite);
+ return sprite;
+ },
+
+ /**
+ * @private
+ * Inserts a given sprite into the correct position in the items collection, according to
+ * its zIndex. It will be inserted at the end of an existing series of sprites with the same or
+ * lower zIndex. By ensuring sprites are always ordered, this allows surface subclasses to render
+ * 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
+ */
+ insertByZIndex: function(sprite) {
+ var me = this,
+ sprites = me.items.items,
+ len = sprites.length,
+ ceil = Math.ceil,
+ zIndex = sprite.attr.zIndex,
+ idx = len,
+ high = idx - 1,
+ low = 0,
+ otherZIndex;
+
+ if (me.orderSpritesByZIndex && len && zIndex < sprites[high].attr.zIndex) {
+ // Find the target index via a binary search for speed
+ while (low <= high) {
+ idx = ceil((low + high) / 2);
+ otherZIndex = sprites[idx].attr.zIndex;
+ if (otherZIndex > zIndex) {
+ high = idx - 1;
+ }
+ else if (otherZIndex < zIndex) {
+ low = idx + 1;
+ }
+ else {
+ break;
+ }
+ }
+ // Step forward to the end of a sequence of the same or lower z-index
+ while (idx < len && sprites[idx].attr.zIndex <= zIndex) {
+ idx++;
+ }
+ }
+
+ me.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();
+ }
+ },
+
+ /**
+ * Removes 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();
+ }
+ }
+ },
+
+ /**
+ * Removes 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 Using the current viewBox property and the surface's width and height, calculate the
+ * appropriate viewBoxShift that will be applied as a persistent transform to all sprites.
+ */
+ applyViewBox: function() {
+ var me = this,
+ viewBox = me.viewBox,
+ width = me.width,
+ height = me.height,
+ viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
+ relativeHeight, relativeWidth, size;
+
+ if (viewBox && (width || height)) {
+ viewBoxX = viewBox.x;
+ viewBoxY = viewBox.y;
+ viewBoxWidth = viewBox.width;
+ viewBoxHeight = viewBox.height;
+ relativeHeight = height / viewBoxHeight;
+ relativeWidth = width / viewBoxWidth;
+
+ if (viewBoxWidth * relativeHeight < width) {
+ viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
+ }
+ if (viewBoxHeight * relativeWidth < height) {
+ viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
+ }
+
+ size = 1 / Math.min(viewBoxWidth, relativeHeight);
+
+ me.viewBoxShift = {
+ dx: -viewBoxX,
+ dy: -viewBoxY,
+ scale: size
+ };
+ }
+ },
+
+ transformToViewBox: function (x, y) {
+ if (this.viewBoxShift) {
+ var me = this, shift = me.viewBoxShift;
+ return [x * shift.scale - shift.dx, y * shift.scale - shift.dy];
+ } else {
+ return [x, y];
+ }
+ },
+
+ // @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.width / 2) || 0,
+ a.radiusY || (a.height / 2) || 0);
+ },
+
+ // @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.
+ * @method
+ */
+ 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.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.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.
+ *
+ * One way to create a draw component is:
+ *
+ * @example
+ * 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 {String[]} 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 {Object[]} 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
+ *
+ * ## 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
- * 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
+ *
+ * Create the Surface on initial render
*/
- normalizeSpriteCollection: function(sprite) {
- var items = this.items,
- zIndex = sprite.attr.zIndex,
- idx = items.indexOf(sprite);
+ onRender: function() {
+ var me = this,
+ viewBox = me.viewBox,
+ autoSize = me.autoSize,
+ bbox, items, width, height, x, y;
+ me.callParent(arguments);
- 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;
+ if (me.createSurface() !== false) {
+ 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();
+ }
}
- 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);
+ //@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)
}
- delete sprite.group;
+ }, true);
+ if (me.rendered) {
+ me.setSize(width, height);
+ me.surface.setSize(width, height);
}
- if (draggable) {
- sprite.initDraggable();
+ else {
+ me.surface.setSize(width, height);
}
+ me.el.setSize(width, height);
},
/**
- * 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:
+ * 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.remove(sprite);
- * //or...
- * sprite.remove();
- *
- * @param {Ext.draw.Sprite} sprite
- * @param {Boolean} destroySprite
- * @return {Number} the sprite's new index in the list
+ * drawComponent.surface.add(sprite);
*/
- 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();
- }
+ createSurface: function() {
+ var surface = Ext.draw.Surface.create(Ext.apply({}, {
+ width: this.width,
+ height: this.height,
+ renderTo: this.el
+ }, this.initialConfig));
+ if (!surface) {
+ // In case we cannot create a surface, return false so we can stop
+ return false;
+ }
+ 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')
+ });
},
+
/**
- * Remove all sprites from the surface, optionally destroying the sprites in the process.
- *
- * For example:
+ * @private
*
- * drawComponent.surface.removeAll();
- *
- * @param {Boolean} destroySprites Whether to destroy all sprites when removing them.
- * @return {Number} The sprite's new index in the list.
+ * Clean up the Surface instance on component destruction
*/
- removeAll: function(destroySprites) {
- var items = this.items.items,
- ln = items.length,
- i;
- for (i = ln - 1; i > -1; i--) {
- this.remove(items[i], destroySprites);
+ onDestroy: function() {
+ var surface = this.surface;
+ if (surface) {
+ surface.destroy();
}
- },
+ this.callParent(arguments);
+ }
- onRemove: Ext.emptyFn,
+});
- onDestroy: Ext.emptyFn,
+/**
+ * @class Ext.chart.LegendItem
+ * @extends Ext.draw.CompositeSprite
+ * A single item of a legend (marker plus label)
+ */
+Ext.define('Ext.chart.LegendItem', {
- // @private
- applyTransformations: function(sprite) {
- sprite.bbox.transform = 0;
- this.transform(sprite);
+ /* 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,
- dirty = false,
- attr = sprite.attr;
+ 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);
- if (attr.translation.x != null || attr.translation.y != null) {
- me.translate(sprite);
- dirty = true;
+ function getSeriesProp(name) {
+ var val = series[name];
+ return (Ext.isArray(val) ? val[idx] : val);
}
- if (attr.scaling.x != null || attr.scaling.y != null) {
- me.scale(sprite);
- dirty = true;
+
+ 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'
+ }
+ }));
+ }
}
- if (attr.rotation.degrees != null) {
- me.rotate(sprite);
- dirty = true;
+ // 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'
+ }
+ }));
}
- if (dirty) {
- sprite.bbox.transform = 0;
- this.transform(sprite);
- sprite.transformations = [];
+
+ 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
},
- // @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;
+ /**
+ * 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;
}
- sprite.transformations.push({
- type: "rotate",
- degrees: deg,
- x: centerX,
- y: centerY
- });
- },
+ 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);
+ }
+ }
+ }
+});
- // @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
- });
- },
+/**
+ * @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'
+ * },
+ *
+ * ## Example
+ *
+ * @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
+ * }
+ * }]
+ * });
+ */
+Ext.define('Ext.chart.Legend', {
- // @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;
+ /* Begin Definitions */
- 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
- });
- },
+ 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',
- // @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"]];
- },
+ /**
+ * @cfg {Number} itemSpacing
+ * Amount of space between legend items
+ */
+ itemSpacing: 10,
- // @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"]];
- },
+ /**
+ * @cfg {Number} padding
+ * Amount of padding between the legend box's border and its items
+ */
+ padding: 5,
// @private
- getPathpath: function (el) {
- return el.attr.path;
- },
-
+ width: 0,
// @private
- getPathcircle: function (el) {
- var a = el.attr;
- return this.ellipsePath(a.x, a.y, a.radius, a.radius);
- },
+ height: 0,
- // @private
- getPathellipse: function (el) {
- var a = el.attr;
- return this.ellipsePath(a.x, a.y,
- a.radiusX || (a.width / 2) || 0,
- a.radiusY || (a.height / 2) || 0);
- },
+ /**
+ * @cfg {Number} boxZIndex
+ * Sets the z-index for the legend. Defaults to 100.
+ */
+ boxZIndex: 100,
- // @private
- getPathrect: function (el) {
- var a = el.attr;
- return this.rectPath(a.x, a.y, a.width, a.height, a.r);
- },
+ /**
+ * Creates new Legend.
+ * @param {Object} config (optional) Config object.
+ */
+ 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);
- // @private
- getPathimage: function (el) {
- var a = el.attr;
- return this.rectPath(a.x || 0, a.y || 0, a.width, a.height);
+ // cache these here since they may get modified later on
+ me.origX = me.x;
+ me.origY = me.y;
},
- // @private
- getPathtext: function (el) {
- var bbox = this.getBBoxText(el);
- return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
- },
+ /**
+ * @private Create all the sprites for the legend
+ */
+ create: function() {
+ var me = this;
+ me.createBox();
+ me.createItems();
+ if (!me.created && me.isDisplayed()) {
+ me.created = true;
- createGroup: function(id) {
- var group = this.groups.get(id);
- if (!group) {
- group = Ext.create('Ext.draw.CompositeSprite', {
- surface: this
+ // 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();
+ });
});
- 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}.
+ * @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.
*/
- getGroup: function(id) {
- if (typeof id == "string") {
- var group = this.groups.get(id);
- if (!group) {
- group = this.createGroup(id);
+ 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();
}
- } else {
- group = id;
}
- return group;
- },
+ //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();
- // @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;
+ //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;
}
- return items;
+ me.height = mfloor((vertical ? totalHeight - spacingOffset * spacing : maxHeight) + (padding * 2));
+ me.itemHeight = maxHeight;
},
-
+
/**
- * 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.
- * @method
+ * @private Get the bounds for the legend's outer box
*/
- setText: Ext.emptyFn,
-
- //@private Creates an item and appends it to the surface. Called
- //as an internal method when calling `add`.
- createItem: Ext.emptyFn,
+ 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
+ };
+ },
/**
- * Retrieves the id of this component.
- * Will autogenerate an id if one has not already been set.
+ * @private Create the box around the legend items
*/
- getId: function() {
- return this.id || (this.id = Ext.id(null, 'ext-surface-'));
+ createBox: function() {
+ var me = this,
+ box;
+
+ if (me.boxSprite) {
+ me.boxSprite.destroy();
+ }
+
+ 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();
},
/**
- * 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();
+ * @private Update the position of all the legend's sprites to match its current x/y values
*/
- destroy: function() {
- delete this.domRef;
- this.removeAll();
+ 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.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:
+ * Charts provide a flexible way to achieve a wide range of data visualization capablitities.
+ * Each Chart gets its data directly from a {@link Ext.data.Store Store}, and automatically
+ * updates its display whenever data in the Store changes. In addition, the look and feel
+ * of a Chart can be customized using {@link Ext.chart.theme.Theme Theme}s.
*
- * var drawComponent = Ext.create('Ext.draw.Component', {
- * viewBox: false,
- * items: [{
- * type: 'circle',
- * fill: '#79BB3F',
- * radius: 100,
- * x: 100,
- * y: 100
- * }]
+ * ## Creating a Simple Chart
+ *
+ * Every Chart has three key parts - a {@link Ext.data.Store Store} that contains the data,
+ * an array of {@link Ext.chart.axis.Axis Axes} which define the boundaries of the Chart,
+ * and one or more {@link Ext.chart.series.Series Series} to handle the visual rendering of the data points.
+ *
+ * ### 1. Creating a Store
+ *
+ * The first step is to create a {@link Ext.data.Model Model} that represents the type of
+ * data that will be displayed in the Chart. For example the data for a chart that displays
+ * a weather forecast could be represented as a series of "WeatherPoint" data points with
+ * two fields - "temperature", and "date":
+ *
+ * Ext.define('WeatherPoint', {
+ * extend: 'Ext.data.Model',
+ * fields: ['temperature', 'date']
* });
- *
- * 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.
+ * Next a {@link Ext.data.Store Store} must be created. The store contains a collection of "WeatherPoint" Model instances.
+ * The data could be loaded dynamically, but for sake of ease this example uses inline data:
*
- * You can also add sprites by using the surface's add method:
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'WeatherPoint',
+ * data: [
+ * { temperature: 58, date: new Date(2011, 1, 1, 8) },
+ * { temperature: 63, date: new Date(2011, 1, 1, 9) },
+ * { temperature: 73, date: new Date(2011, 1, 1, 10) },
+ * { temperature: 78, date: new Date(2011, 1, 1, 11) },
+ * { temperature: 81, date: new Date(2011, 1, 1, 12) }
+ * ]
+ * });
*
- * drawComponent.surface.add({
- * type: 'circle',
- * fill: '#79BB3F',
- * radius: 100,
- * x: 100,
- * y: 100
+ * For additional information on Models and Stores please refer to the [Data Guide](#/guide/data).
+ *
+ * ### 2. Creating the Chart object
+ *
+ * Now that a Store has been created it can be used in a Chart:
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 400,
+ * height: 300,
+ * store: store
* });
- *
- * For more information on Sprites, the core elements added to a draw component's surface,
- * refer to the Ext.draw.Sprite documentation.
+ *
+ * That's all it takes to create a Chart instance that is backed by a Store.
+ * However, if the above code is run in a browser, a blank screen will be displayed.
+ * This is because the two pieces that are responsible for the visual display,
+ * the Chart's {@link #cfg-axes axes} and {@link #cfg-series series}, have not yet been defined.
+ *
+ * ### 3. Configuring the Axes
+ *
+ * {@link Ext.chart.axis.Axis Axes} are the lines that define the boundaries of the data points that a Chart can display.
+ * This example uses one of the most common Axes configurations - a horizontal "x" axis, and a vertical "y" axis:
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * ...
+ * axes: [
+ * {
+ * title: 'Temperature',
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['temperature'],
+ * minimum: 0,
+ * maximum: 100
+ * },
+ * {
+ * title: 'Time',
+ * type: 'Time',
+ * position: 'bottom',
+ * fields: ['date'],
+ * dateFormat: 'ga'
+ * }
+ * ]
+ * });
+ *
+ * The "Temperature" axis is a vertical {@link Ext.chart.axis.Numeric Numeric Axis} and is positioned on the left edge of the Chart.
+ * It represents the bounds of the data contained in the "WeatherPoint" Model's "temperature" field that was
+ * defined above. The minimum value for this axis is "0", and the maximum is "100".
+ *
+ * The horizontal axis is a {@link Ext.chart.axis.Time Time Axis} and is positioned on the bottom edge of the Chart.
+ * It represents the bounds of the data contained in the "WeatherPoint" Model's "date" field.
+ * The {@link Ext.chart.axis.Time#cfg-dateFormat dateFormat}
+ * configuration tells the Time Axis how to format it's labels.
+ *
+ * Here's what the Chart looks like now that it has its Axes configured:
+ *
+ * {@img Ext.chart.Chart/Ext.chart.Chart1.png Chart Axes}
+ *
+ * ### 4. Configuring the Series
+ *
+ * The final step in creating a simple Chart is to configure one or more {@link Ext.chart.series.Series Series}.
+ * Series are responsible for the visual representation of the data points contained in the Store.
+ * This example only has one Series:
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * ...
+ * axes: [
+ * ...
+ * ],
+ * series: [
+ * {
+ * type: 'line',
+ * xField: 'date',
+ * yField: 'temperature'
+ * }
+ * ]
+ * });
+ *
+ * This Series is a {@link Ext.chart.series.Line Line Series}, and it uses the "date" and "temperature" fields
+ * from the "WeatherPoint" Models in the Store to plot its data points:
+ *
+ * {@img Ext.chart.Chart/Ext.chart.Chart2.png Line Series}
+ *
+ * See the [Simple Chart Example](doc-resources/Ext.chart.Chart/examples/simple_chart/index.html) for a live demo.
+ *
+ * ## Themes
+ *
+ * The color scheme for a Chart can be easily changed using the {@link #cfg-theme theme} configuration option:
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * ...
+ * theme: 'Green',
+ * ...
+ * });
+ *
+ * {@img Ext.chart.Chart/Ext.chart.Chart3.png Green Theme}
+ *
+ * For more information on Charts please refer to the [Drawing and Charting Guide](#/guide/drawing_and_charting).
+ *
*/
-Ext.define('Ext.draw.Component', {
+Ext.define('Ext.chart.Chart', {
/* Begin Definitions */
- alias: 'widget.draw',
+ alias: 'widget.chart',
- extend: 'Ext.Component',
+ extend: 'Ext.draw.Component',
+
+ mixins: {
+ themeManager: 'Ext.chart.theme.Theme',
+ mask: 'Ext.chart.Mask',
+ navigation: 'Ext.chart.Navigation'
+ },
requires: [
- 'Ext.draw.Surface',
- 'Ext.layout.component.Draw'
+ 'Ext.util.MixedCollection',
+ 'Ext.data.StoreManager',
+ 'Ext.chart.Legend',
+ 'Ext.util.DelayedTask'
],
/* End Definitions */
+ // @private
+ viewBox: false,
+
/**
- * @cfg {Array} enginePriority
- * Defines the priority order for which Surface implementation to use. The first
- * one supported by the current environment will be used.
+ * @cfg {String} theme
+ * 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'.
*/
- enginePriority: ['Svg', 'Vml'],
- baseCls: Ext.baseCSSPrefix + 'surface',
+ /**
+ * @cfg {Boolean/Object} animate
+ * 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,
- componentLayout: 'draw',
+ /**
+ * @cfg {Boolean/Object} legend
+ * True for the default legend display or a legend config object. Defaults to false.
+ */
+ legend: false,
/**
- * @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.
+ * @cfg {Number} insetPadding
+ * The amount of inset padding in pixels for the chart. Defaults to 10.
*/
- viewBox: true,
+ insetPadding: 10,
/**
- * @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.
+ * @cfg {String[]} enginePriority
+ * Defines the priority order for which Surface implementation to use. The first one supported by the current
+ * environment will be used. Defaults to `['Svg', 'Vml']`.
*/
- autoSize: false,
-
+ enginePriority: ['Svg', 'Vml'],
+
/**
- * @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:
+ * @cfg {Object/Boolean} background
+ * 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
*
- *
- * 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);
-
-
+ * 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'
+ * }
+ * }
+ * }
+ * }
*/
-
- initComponent: function() {
- this.callParent(arguments);
-
- this.addEvents(
- 'mousedown',
- 'mouseup',
- 'mousemove',
- 'mouseenter',
- 'mouseleave',
- 'click'
- );
- },
+ background: false,
/**
- * @private
+ * @cfg {Object[]} gradients
+ * 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:
*
- * Create the Surface on initial render
+ * - **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);
*/
- onRender: function() {
- var me = this,
- viewBox = me.viewBox,
- autoSize = me.autoSize,
- bbox, items, width, height, x, y;
- me.callParent(arguments);
- me.createSurface();
+ /**
+ * @cfg {Ext.data.Store} store
+ * The store that supplies data to this chart.
+ */
- items = me.surface.items;
+ /**
+ * @cfg {Ext.chart.series.Series[]} series
+ * Array of {@link Ext.chart.series.Series Series} instances or config objects. For example:
+ *
+ * series: [{
+ * type: 'column',
+ * axis: 'left',
+ * listeners: {
+ * 'afterrender': function() {
+ * console('afterrender');
+ * }
+ * },
+ * xField: 'category',
+ * yField: 'data1'
+ * }]
+ */
+
+ /**
+ * @cfg {Ext.chart.axis.Axis[]} axes
+ * Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. For example:
+ *
+ * axes: [{
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['data1'],
+ * title: 'Number of Hits',
+ * minimum: 0,
+ * //one minor tick between two major ticks
+ * minorTickSteps: 1
+ * }, {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Month of the Year'
+ * }]
+ */
- 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);
+ constructor: function(config) {
+ var me = this,
+ defaultAnim;
+
+ config = Ext.apply({}, config);
+ 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 {
- // AutoSized
- me.autoSizeSurface();
+ config.animate = defaultAnim;
}
}
+ me.mixins.mask.constructor.call(me, config);
+ me.mixins.navigation.constructor.call(me, config);
+ me.callParent([config]);
+ },
+
+ getChartStore: function(){
+ return this.substore || this.store;
},
- //@private
- autoSizeSurface: function() {
+ initComponent: 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)
+ 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 {Ext.chart.Chart} this
+ */
+ 'beforerefresh',
+ /**
+ * @event refresh
+ * Fires after the chart data has been refreshed.
+ * @param {Ext.chart.Chart} this
+ */
+ 'refresh'
+ );
+ Ext.applyIf(me, {
+ zoom: {
+ width: 1,
+ height: 1,
+ x: 0,
+ y: 0
}
- }, true);
- if (me.rendered) {
- me.setSize(width, height);
- me.surface.setSize(width, height);
+ });
+ 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);
}
- else {
- me.surface.setSize(width, height);
+ 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);
}
- 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);
- };
+ if (me.legend !== false) {
+ me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend));
}
- surface.on({
- scope: this,
- mouseup: refire('mouseup'),
- mousedown: refire('mousedown'),
- mousemove: refire('mousemove'),
- mouseenter: refire('mouseenter'),
- mouseleave: refire('mouseleave'),
- click: refire('click')
+ 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);
+ },
/**
- * @private
- *
- * Clean up the Surface instance on component destruction
+ * Redraws the chart. If animations are set this will animate the chart too.
+ * @param {Boolean} resize (optional) flag which changes the default origin points of the chart for animations.
*/
- onDestroy: function() {
- var surface = this.surface;
- if (surface) {
- surface.destroy();
+ 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();
}
- this.callParent(arguments);
- }
-});
+ // Place axes properly, including influence from each other
+ me.alignAxes();
-/**
- * @class Ext.chart.LegendItem
- * @extends Ext.draw.CompositeSprite
- * A single item of a legend (marker plus label)
- * @constructor
- */
-Ext.define('Ext.chart.LegendItem', {
+ // Reposition legend based on new axis alignment
+ if (me.legend !== false) {
+ legend.updatePosition();
+ }
- /* Begin Definitions */
+ // Find the max gutter
+ me.getMaxGutter();
- extend: 'Ext.draw.CompositeSprite',
+ // Draw axes and series
+ me.resizing = !!resize;
- requires: ['Ext.chart.Shape'],
+ me.axes.each(me.drawAxis, me);
+ me.series.each(me.drawCharts, me);
+ me.resizing = false;
+ },
- /* End Definitions */
+ // @private set the store after rendering the chart.
+ afterRender: function() {
+ var ref,
+ me = this;
+ this.callParent();
- // Position of the item, relative to the upper-left corner of the legend box
- x: 0,
- y: 0,
- zIndex: 500,
+ if (me.categoryNames) {
+ me.setCategoryNames(me.categoryNames);
+ }
- constructor: function(config) {
- this.callParent(arguments);
- this.createLegend(config);
+ if (me.tipRenderer) {
+ ref = me.getFunctionRef(me.tipRenderer);
+ me.setTipRenderer(ref.fn, ref.scope);
+ }
+ me.bindStore(me.store, true);
+ me.refresh();
},
- /**
- * Creates all the individual sprites for this legend item
- */
- createLegend: function(config) {
+ // @private get x and y position of the mouse cursor.
+ getEventXY: function(e) {
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);
+ box = this.surface.getRegion(),
+ pageXY = e.getXY(),
+ x = pageXY[0] - box.left,
+ y = pageXY[1] - box.top;
+ return [x, y];
+ },
- 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')
- }));
+ // @private wrap the mouse down position to delegate the event to the series.
+ onClick: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item;
- // 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'
+ // 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);
}
- }));
- }
- }
- // 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);
- }
+ // @private wrap the mouse down position to delegate the event to the series.
+ onMouseDown: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item;
- me.on('mousedown', function() {
- if (!toggle) {
- series.hideAll();
- label.setAttributes({
- opacity: 0.5
- }, true);
- } else {
- series.showAll();
- label.setAttributes({
- opacity: 1
- }, true);
+ 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);
+ }
+ }
}
- 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) {
+ // @private wrap the mouse up event to delegate it to the series.
+ onMouseUp: function(e) {
var me = this,
- items = me.items,
- ln = items.length,
- i = 0,
+ position = me.getEventXY(e),
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);
- }
+
+ if (me.mask) {
+ me.mixins.mask.onMouseUp.call(me, e);
}
- }
-});
-/**
- * @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
+ // 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);
}
}
- }],
- 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,
+ }
+ }, me);
+ },
- /**
- * @cfg {String} boxFill
- * Fill style for the legend box
- */
- boxFill: '#FFF',
+ // @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;
- /**
- * @cfg {Number} itemSpacing
- * Amount of space between legend items
- */
- itemSpacing: 10,
+ 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;
- /**
- * @cfg {Number} padding
- * Amount of padding between the legend box's border and its items
- */
- padding: 5,
- // @private
- width: 0,
- // @private
- height: 0,
+ 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);
+ },
- /**
- * @cfg {Number} boxZIndex
- * Sets the z-index for the legend. Defaults to 100.
- */
- boxZIndex: 100,
+ // @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;
+ });
+ },
- constructor: function(config) {
+ // @private buffered refresh for when we update the store
+ delayRefresh: function() {
var me = this;
- if (config) {
- Ext.apply(me, config);
+ 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);
+ }
}
- 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
+ * Changes the data store bound to this chart and refreshes it.
+ * @param {Ext.data.Store} store The store to bind to this chart
*/
- create: function() {
+ bindStore: function(store, initial) {
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();
- });
+ if (!initial && me.store) {
+ if (store !== me.store && me.store.autoDestroy) {
+ me.store.destroyStore();
+ }
+ 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 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 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 Create the series markers and labels
+ * @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.
*/
- createItems: function() {
+ alignAxes: 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;
+ axes = me.axes,
+ legend = me.legend,
+ edges = ['top', 'right', 'bottom', 'left'],
+ chartBBox,
+ insetPadding = me.insetPadding,
+ insets = {
+ top: insetPadding,
+ right: insetPadding,
+ bottom: insetPadding,
+ left: insetPadding
+ };
- //remove all legend items
- if (len) {
- for (; i < len; i++) {
- items[i].destroy();
- }
+ function getAxis(edge) {
+ var i = axes.findIndex('position', edge);
+ return (i < 0) ? null : axes.getAt(i);
}
- //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;
+ // 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;
- 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);
+ // 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];
+ }
+ }
- // Collect cumulative dimensions
- totalWidth += width + spacing;
- totalHeight += height + spacing;
- maxWidth = mmax(maxWidth, width);
- maxHeight = mmax(maxHeight, height);
+ // 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;
- items.push(item);
- }, this);
+ // 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;
+ }
}
- }, me);
+ 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();
+ }
+ },
- // Store the collected dimensions for later
- me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2);
- if (vertical && items.length === 1) {
- spacingOffset = 1;
+ // @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');
}
- me.height = mfloor((vertical ? totalHeight - spacingOffset * spacing : maxHeight) + (padding * 2));
- me.itemHeight = maxHeight;
},
+ // @private remove gently.
+ destroy: function() {
+ Ext.destroy(this.surface);
+ this.bindStore(null);
+ this.callParent(arguments);
+ }
+});
+
+/**
+ * @class Ext.chart.Highlight
+ * A mixin providing highlight functionality for Ext.chart.series.Series.
+ */
+Ext.define('Ext.chart.Highlight', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.fx.Anim'],
+
+ /* End Definitions */
+
/**
- * @private Get the bounds for the legend's outer box
+ * 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.
*/
- 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
- };
+ 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'
+ };
+ }
+ }
},
/**
- * @private Create the box around the legend items
+ * Highlight the given series item.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
*/
- createBox: function() {
+ highlightItem: function(item) {
+ if (!item) {
+ return;
+ }
+
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();
+ 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.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;
+ sprite._endStyle = 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);
+ }
},
/**
- * @private Update the position of all the legend's sprites to match its current x/y values
+ * Un-highlight any existing highlights
*/
- updatePosition: function() {
+ unHighlightItem: function() {
+ if (!this.highlight || !this.items) {
+ return;
+ }
+
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;
+ 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;
}
- me.x = x;
- me.y = y;
+ 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._to = obj;
+ sprite._endStyle = obj;
+ sprite._anim = Ext.create('Ext.fx.Anim', {
+ target: sprite,
+ to: obj,
+ duration: 150
+ });
+ }
+ else {
+ sprite.setAttributes(obj, true);
+ }
+ delete sprite._highlighted;
+ //delete sprite._defaults;
+ }
+ }
+ },
- // 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);
+ 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.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... ]
- });
-
+ * @class Ext.chart.Label
+ *
+ * Labels is a mixin to the Series class. Labels methods are implemented
+ * in each of the Series (Pie, Bar, etc) for label creation and placement.
+ *
+ * The methods implemented by the Series are:
*
- * 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).
+ * - **`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
*
- * @xtype chart
+ * - **`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.Chart', {
+Ext.define('Ext.chart.Label', {
/* 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'
- ],
+ requires: ['Ext.draw.Color'],
/* 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.
+ * @cfg {Object} label
+ * Object with the following properties:
+ *
+ * - **display** : String
+ *
+ * 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'.
+ *
+ * - **color** : String
+ *
+ * The color of the label text.
+ * Default value: '#000' (black).
+ *
+ * - **contrast** : Boolean
+ *
+ * True to render the label in contrasting color with the backround.
+ * Default value: false.
+ *
+ * - **field** : String
+ *
+ * The name of the field to be displayed in the label.
+ * Default value: 'name'.
+ *
+ * - **minMargin** : Number
+ *
+ * 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.
+ *
+ * - **font** : String
+ *
+ * The font used for the labels.
+ * Default value: "11px Helvetica, sans-serif".
+ *
+ * - **orientation** : String
+ *
+ * Either "horizontal" or "vertical".
+ * Dafault value: "horizontal".
+ *
+ * - **renderer** : Function
+ *
+ * Optional function for formatting the label into a displayable value.
+ * Default value: function(v) { return v; }
*/
- legend: false,
- /**
- * @cfg {integer} insetPadding (optional) Set the amount of inset padding in pixels for the chart. Defaults to 10.
- */
- insetPadding: 10,
+ //@private a regex to parse url type colors.
+ colorStringRe: /url\s*\(\s*#([^\/)]+)\s*\)/,
- /**
- * @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'],
+ //@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;
+ }
+ });
- /**
- * @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'
+ if (me.label.display !== 'none') {
+ me.labelsGroup = me.chart.surface.getGroup(me.seriesId + '-labels');
}
-
+ },
- You can specify an image by using:
+ //@private a method to render all labels in the labelGroup
+ renderLabels: function() {
+ var me = this,
+ chart = me.chart,
+ gradients = chart.gradients,
+ items = me.items,
+ animate = chart.animate,
+ config = me.label,
+ display = config.display,
+ color = config.color,
+ field = [].concat(config.field),
+ group = me.labelsGroup,
+ groupLength = (group || 0) && group.length,
+ store = me.chart.store,
+ len = store.getCount(),
+ itemLength = (items || 0) && items.length,
+ ratio = itemLength / len,
+ gradientsCount = (gradients || 0) && gradients.length,
+ Color = Ext.draw.Color,
+ hides = [],
+ gradient, i, count, groupIndex, index, j, k, colorStopTotal, colorStopIndex, colorStop, item, label,
+ storeItem, sprite, spriteColor, spriteBrightness, labelColor, colorString;
-
- background: {
- image: 'http://path.to.image/'
+ if (display == 'none') {
+ return;
}
-
+ // no items displayed, hide all labels
+ if(itemLength == 0){
+ while(groupLength--)
+ hides.push(groupLength);
+ }else{
+ for (i = 0, count = 0, groupIndex = 0; i < len; i++) {
+ index = 0;
+ for (j = 0; j < ratio; j++) {
+ item = items[count];
+ label = group.getAt(groupIndex);
+ storeItem = store.getAt(i);
+ //check the excludes
+ while(this.__excludes && this.__excludes[index] && ratio > 1) {
+ if(field[j]){
+ hides.push(groupIndex);
+ }
+ index++;
- Also you can specify a gradient by using the gradient object syntax:
+ }
-
- background: {
- gradient: {
- id: 'gradientId',
- angle: 45,
- stops: {
- 0: {
- color: '#555'
+ if (!item && label) {
+ label.hide(true);
+ groupIndex++;
}
- 100: {
- color: '#ddd'
+
+ if (item && field[j]) {
+ if (!label) {
+ label = me.onCreateLabel(storeItem, item, i, display, j, index);
+ }
+ me.onPlaceLabel(label, storeItem, item, i, display, animate, j, index);
+ groupIndex++;
+
+ //set contrast
+ if (config.contrast && item.sprite) {
+ sprite = item.sprite;
+ //set the color string to the color to be set.
+ if (sprite._endStyle) {
+ colorString = sprite._endStyle.fill;
+ }
+ else if (sprite._to) {
+ colorString = sprite._to.fill;
+ }
+ else {
+ colorString = sprite.attr.fill;
+ }
+ colorString = colorString || 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;
+ }
+ if (label.isOutside) {
+ spriteBrightness = 1;
+ }
+ 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++;
+ index++;
}
}
}
-
- */
- 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:
+ me.hideLabels(hides);
+ },
+ hideLabels: function(hides){
+ var labelsGroup = this.labelsGroup,
+ hlen = hides.length;
+ while(hlen--)
+ labelsGroup.getAt(hides[hlen]).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'
+ );
+ },
-
- gradients: [{
- id: 'gradientId',
- angle: 45,
- stops: {
- 0: {
- color: '#555'
- },
- 100: {
- color: '#ddd'
- }
+ 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;
}
- }, {
- id: 'gradientId2',
- angle: 0,
- stops: {
- 0: {
- color: '#590'
- },
- 20: {
- color: '#599'
- },
- 100: {
- color: '#ddd'
- }
+
+ // 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', {
- Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example:
+ /* Begin Definitions */
-
- sprite.setAttributes({
- fill: 'url(#gradientId)'
- }, true);
-
+ extend: 'Ext.draw.Component',
- */
+ /* End Definitions */
+ spriteArray: false,
+ renderFirst: 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;
- }
+ this.callParent([config]);
+ if (config.sprites) {
+ this.spriteArray = [].concat(config.sprites);
+ delete config.sprites;
}
- me.mixins.mask.constructor.call(me, config);
- me.mixins.navigation.constructor.call(me, config);
- me.callParent([config]);
},
- initComponent: function() {
+ onRender: 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
+ 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);
}
- });
- 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
- });
- },
+/**
+ * @class Ext.chart.Tip
+ * Provides tips for Ext.chart.series.Series.
+ */
+Ext.define('Ext.chart.Tip', {
- // @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);
- },
+ /* Begin Definitions */
- /**
- * 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) {
+ requires: ['Ext.tip.ToolTip', 'Ext.chart.TipSurface'],
+
+ /* End Definitions */
+
+ constructor: function(config) {
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);
- });
+ 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);
+ me.chart.surface.on('mousemove', me.tooltip.onMouseMove, me.tooltip);
+ me.chart.surface.on('mouseleave', function() {
+ me.hideTip();
+ });
+ 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;
+ }
+ }
+ },
- // Create legend if not already created
- if (legend !== false) {
- legend.create();
+ 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];
+ }
}
-
- // Place axes properly, including influence from each other
- me.alignAxes();
-
- // Reposition legend based on new axis alignment
- if (me.legend !== false) {
- legend.updatePosition();
+ if (spriteTip) {
+ tipConfig.renderer.call(tooltip, item.storeItem, item, spriteTip.surface);
+ } else {
+ tipConfig.renderer.call(tooltip, item.storeItem, item);
}
-
- // 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;
+ tooltip.show();
+ tooltip.trackMouse = trackMouse;
},
- // @private set the store after rendering the chart.
- afterRender: function() {
- var ref,
- me = this;
- this.callParent();
-
- if (me.categoryNames) {
- me.setCategoryNames(me.categoryNames);
+ 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
+ * Base class for all axis classes.
+ * @private
+ */
+Ext.define('Ext.chart.axis.Abstract', {
- if (me.tipRenderer) {
- ref = me.getFunctionRef(me.tipRenderer);
- me.setTipRenderer(ref.fn, ref.scope);
- }
- me.bindStore(me.store, true);
- me.refresh();
- },
+ /* Begin Definitions */
- // @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];
- },
+ requires: ['Ext.chart.Chart'],
- // @private wrap the mouse down position to delegate the event to the series.
- onClick: function(e) {
- var me = this,
- position = me.getEventXY(e),
- item;
+ /* End Definitions */
- // 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);
- },
+ /**
+ * Creates new Axis.
+ * @param {Object} config (optional) Config options.
+ */
+ constructor: function(config) {
+ config = config || {};
- // @private wrap the mouse down position to delegate the event to the series.
- onMouseDown: function(e) {
var me = this,
- position = me.getEventXY(e),
- item;
+ pos = config.position || 'left';
- 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);
+ 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");
},
- // @private wrap the mouse up event to delegate it to the series.
- onMouseUp: function(e) {
- var me = this,
- position = me.getEventXY(e),
- item;
+ alignment: null,
+ grid: false,
+ steps: 10,
+ x: 0,
+ y: 0,
+ minValue: 0,
+ maxValue: 0,
- 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);
+ getId: function() {
+ return this.axisId || (this.axisId = Ext.id(null, 'ext-axis-'));
},
- // @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;
+ /*
+ 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,
- 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;
+ 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', {
- 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);
- },
+ /* Begin Definitions */
- // @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;
- });
- },
+ extend: 'Ext.chart.axis.Abstract',
- // @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);
- },
+ alternateClassName: 'Ext.chart.Axis',
- // @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);
- }
- }
- },
+ requires: ['Ext.draw.Draw'],
+
+ /* End Definitions */
/**
- * Changes the data store bound to this chart and refreshes it.
- * @param {Store} store The store to bind to this chart
+ * @cfg {Boolean/Object} grid
+ * The grid configuration enables you to set a background grid for an axis.
+ * If set to *true* on a vertical axis, vertical lines will be drawn.
+ * If set to *true* on a horizontal axis, horizontal lines will be drawn.
+ * If both are set, a proper grid with horizontal and vertical lines will be drawn.
+ *
+ * You can set specific options for the grid configuration for odd and/or even lines/rows.
+ * Since the rows being drawn are rectangle sprites, you can set to an odd or even property
+ * all styles that apply to {@link Ext.draw.Sprite}. For more information on all the style
+ * properties you can set please take a look at {@link Ext.draw.Sprite}. Some useful style properties are `opacity`, `fill`, `stroke`, `stroke-width`, etc.
+ *
+ * The possible values for a grid option are then *true*, *false*, or an object with `{ odd, even }` properties
+ * where each property contains a sprite style descriptor object that is defined in {@link Ext.draw.Sprite}.
+ *
+ * For example:
+ *
+ * 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
+ * }
+ * }
+ * }, {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Month of the Year',
+ * grid: true
+ * }]
+ *
*/
- 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);
- }
- },
+ /**
+ * @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.
+ */
/**
- * @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.
+ * @cfg {String} title
+ * The title for the Axis
*/
- 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);
- }
+ //@private force min/max values from store
+ forceMinMax: false,
- // 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;
+ /**
+ * @cfg {Number} dashSize
+ * The size of the dash marker. Default's 3.
+ */
+ dashSize: 3,
- // 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];
- }
- }
+ /**
+ * @cfg {String} position
+ * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`.
+ */
+ position: 'bottom',
- // 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;
+ // @private
+ skipFirst: false,
- // 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');
+ /**
+ * @cfg {Number} length
+ * Offset axis position. Default's 0.
+ */
+ length: 0,
- 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);
- });
- },
+ /**
+ * @cfg {Number} width
+ * Offset axis width. Default's 0.
+ */
+ width: 0,
- // @private initialize the series.
- initializeSeries: function(series, idx) {
+ majorTickSteps: false,
+
+ // @private
+ applyData: Ext.emptyFn,
+
+ getRange: function () {
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);
+ store = me.chart.getChartStore(),
+ fields = me.fields,
+ ln = fields.length,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ aggregate = false,
+ min = isNaN(me.minimum) ? Infinity : me.minimum,
+ max = isNaN(me.maximum) ? -Infinity : me.maximum,
+ total = 0, i, l, value, values, rec,
+ excludes = [],
+ series = me.chart.series.items;
+
+ //if one series is stacked I have to aggregate the values
+ //for the scale.
+ // TODO(zhangbei): the code below does not support series that stack on 1 side but non-stacked axis
+ // listed in axis config. For example, a Area series whose axis : ['left', 'bottom'].
+ // Assuming only stack on y-axis.
+ // CHANGED BY Nicolas: I removed the check `me.position == 'left'` and `me.position == 'right'` since
+ // it was constraining the minmax calculation to y-axis stacked
+ // visualizations.
+ 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);
}
- if (colorArrayStyle.length) {
- config.colorArrayStyle = colorArrayStyle;
+ 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);
}
}
- config.seriesIdx = idx;
+ });
+ if (!isFinite(max)) {
+ max = me.prevMax || 0;
}
- 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 (!isFinite(min)) {
+ min = me.prevMin || 0;
}
- if (series.initialize) {
- series.initialize();
+ //normalize min max for snapEnds.
+ if (min != max && (max != Math.floor(max))) {
+ max = Math.floor(max) + 1;
}
- },
-
- // @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');
+ if (!isNaN(me.minimum)) {
+ min = me.minimum;
+ }
+
+ if (!isNaN(me.maximum)) {
+ max = me.maximum;
}
- },
-
- // @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,
+ return {min: min, max: max};
+ },
- highlightCfg : null,
+ // @private creates a structure with start, end and step points.
+ calcEnds: function() {
+ var me = this,
+ fields = me.fields,
+ range = me.getRange(),
+ min = range.min,
+ max = range.max,
+ outfrom, outto, out;
- constructor: function(config) {
- if (config.highlight) {
- if (config.highlight !== true) { //is an object
- this.highlightCfg = Ext.apply({}, config.highlight);
+ out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ? (me.majorTickSteps +1) : me.steps);
+ outfrom = out.from;
+ outto = out.to;
+ if (me.forceMinMax) {
+ if (!isNaN(max)) {
+ out.to = max;
}
- else {
- this.highlightCfg = {
- fill: '#fdd',
- radius: 20,
- lineWidth: 5,
- stroke: '#f55'
- };
+ if (!isNaN(min)) {
+ out.from = min;
}
}
+ 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;
},
/**
- * Highlight the given series item.
- * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
+ * Renders the axis into the screen and updates its position.
*/
- highlightItem: function(item) {
- if (!item) {
- return;
- }
-
+ drawAxis: function (init) {
var me = this,
- sprite = item.sprite,
- opts = me.highlightCfg,
- surface = me.chart.surface,
- animate = me.chart.animate,
- p,
- from,
- to,
- pi;
+ 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 (!me.highlight || !sprite || sprite._highlighted) {
+ //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;
}
- if (sprite._anim) {
- sprite._anim.paused = true;
+
+ 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);
}
- 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];
- }
+ 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' || me.type == 'Time') {
+ 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);
}
- for (pi in opts[p]) {
- if (! (pi in to[p])) {
- to[p][pi] = opts[p][pi];
- }
+ }
+ 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);
+ }
}
- 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);
+ 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();
},
/**
- * Un-highlight any existing highlights
+ * Renders an horizontal and/or vertical grid into the Surface.
*/
- unHighlightItem: function() {
- if (!this.highlight || !this.items) {
- return;
- }
-
+ drawGrid: function() {
var me = this,
- items = me.items,
- len = items.length,
- opts = me.highlightCfg,
- animate = me.chart.animate,
- i = 0,
- obj,
- p,
- sprite;
+ 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 = [];
- for (; i < len; i++) {
- if (!items[i]) {
- continue;
- }
- sprite = items[i].sprite;
- if (sprite && sprite._highlighted) {
- if (sprite._anim) {
- sprite._anim.paused = true;
+ 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");
}
- 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];
- }
+ 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");
}
- if (animate) {
- sprite._anim = Ext.create('Ext.fx.Anim', {
- target: sprite,
- to: obj,
- duration: 150
- });
+ 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 {
- sprite.setAttributes(obj, true);
+ 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]);
}
- delete sprite._highlighted;
- //delete sprite._defaults;
}
}
- },
-
- cleanHighlights: function() {
- if (!this.highlight) {
- return;
+ 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);
+ }
}
-
- var group = this.group,
- markerGroup = this.markerGroup,
- i = 0,
- l;
- for (l = group.getCount(); i < l; i++) {
- delete group.getAt(i)._defaults;
+ 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);
+ }
}
- if (markerGroup) {
- for (l = markerGroup.getCount(); i < l; i++) {
- delete markerGroup.getAt(i)._defaults;
+ },
+
+ //@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();
}
}
- }
-});
-/**
- * @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', {
+ 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;
+ },
- /* Begin Definitions */
+ 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);
- requires: ['Ext.draw.Color'],
-
- /* End Definitions */
+ p2[0] = matrix.x.apply(matrix, p2p);
+ p2[1] = matrix.y.apply(matrix, p2p);
- /**
- * @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'.
- */
+ p3[0] = matrix.x.apply(matrix, p3p);
+ p3[1] = matrix.y.apply(matrix, p3p);
- /**
- * @cfg {String} color
- * The color of the label text.
- * Default value: '#000' (black).
- */
+ p4[0] = matrix.x.apply(matrix, p4p);
+ p4[1] = matrix.y.apply(matrix, p4p);
+ return [p1, p2, p3, p4];
+ },
- /**
- * @cfg {String} field
- * The name of the field to be displayed in the label.
- * Default value: 'name'.
- */
+ intersect: function(l1, l2) {
+ var r1 = this.rect2pointArray(l1),
+ r2 = this.rect2pointArray(l2);
+ return !!Ext.draw.Draw.intersect(r1, r2).length;
+ },
- /**
- * @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.
- */
+ 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;
- /**
- * @cfg {String} font
- * The font used for the labels.
- * Defautl value: "11px Helvetica, sans-serif".
- */
+ 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.floor(Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)));
- /**
- * @cfg {String} orientation
- * Either "horizontal" or "vertical".
- * Dafault value: "horizontal".
- */
+ 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);
+ }
- /**
- * @cfg {Function} renderer
- * Optional function for formatting the label into a displayable value.
- * Default value: function(v) { return v; }
- * @param v
- */
+ textLabel.setAttributes({
+ hidden: false,
+ x: x,
+ y: y
+ }, true);
- //@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;
+ // Skip label if there isn't available minimum space
+ if (i != 0 && (me.intersect(textLabel, prevLabel)
+ || me.intersect(textLabel, firstLabel))) {
+ textLabel.hide(true);
+ continue;
}
- });
- if (me.label.display !== 'none') {
- me.labelsGroup = me.chart.surface.getGroup(me.seriesId + '-labels');
+ prevLabel = textLabel;
}
+
+ return maxHeight;
},
- //@private a method to render all labels in the labelGroup
- renderLabels: function() {
+ drawVerticalLabels: 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, index, 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;
- }
+ 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;
- for (i = 0, count = 0; i < len; i++) {
- index = 0;
- for (j = 0; j < ratio; j++) {
- item = items[count];
- label = group.getAt(count);
- storeItem = store.getAt(i);
-
- //check the excludes
- while(this.__excludes && this.__excludes[index]) {
- index++;
- }
+ 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;
- if (!item && label) {
- label.hide(true);
+ 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);
}
-
- if (item && field[j]) {
- if (!label) {
- label = me.onCreateLabel(storeItem, item, i, display, j, index);
- }
- me.onPlaceLabel(label, storeItem, item, i, display, animate, j, index);
-
- //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);
- }
+ else if (i == 0 && axes.findIndex('position', 'bottom') == -1) {
+ y = me.y - floor(bbox.height / 2);
}
- count++;
- index++;
}
- }
- 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);
+ if (position == 'left') {
+ x = point[0] - bbox.width - me.dashSize - me.label.padding - 2;
}
- }
- }
-});
-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;
+ else {
+ x = point[0] + me.dashSize + me.label.padding + 2;
}
-
- // Set the constrainTo Region before we start dragging.
- if (me.constrain || me.constrainDelegate) {
- me.constrainTo = me.calculateConstrainRegion();
+ 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;
}
- };
- }
-});
-/**
- * @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;
+ prevLabel = textLabel;
}
+
+ return maxWidth;
},
- onRender: function() {
+ /**
+ * Renders the labels in the axes.
+ */
+ drawLabel: 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);
- }
- }
- }
-});
+ position = me.position,
+ labelGroup = me.labelGroup,
+ inflections = me.inflections,
+ maxWidth = 0,
+ maxHeight = 0,
+ ln, i;
-/**
- * @class Ext.chart.Tip
- * @ignore
- */
-Ext.define('Ext.chart.Tip', {
+ if (position == 'left' || position == 'right') {
+ maxWidth = me.drawVerticalLabels();
+ } else {
+ maxHeight = me.drawHorizontalLabels();
+ }
- /* Begin Definitions */
+ // Hide unused bars
+ ln = labelGroup.getCount();
+ i = inflections.length;
+ for (; i < ln; i++) {
+ labelGroup.getAt(i).hide(true);
+ }
- requires: ['Ext.tip.ToolTip', 'Ext.chart.TipSurface'],
+ 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);
+ }
+ },
- /* End Definitions */
+ // @private creates the elipsis for the text.
+ elipsis: function(sprite, text, desiredWidth, minWidth, center) {
+ var bbox,
+ x;
- 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);
+ 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);
}
- me.tooltip.add(tipSurface);
- me.spriteTip = tipSurface;
+ break;
}
}
+ return true;
},
- 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);
+ /**
+ * 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 {
- tipConfig.renderer.call(tooltip, item.storeItem, item);
+ base = {
+ type: 'text',
+ x: 0,
+ y: 0,
+ text: title
+ };
+ displaySprite = me.displaySprite = surface.add(Ext.apply(base, me.axisTitleStyle, me.labelTitle));
+ surface.renderItem(displaySprite);
}
- tooltip.show();
- tooltip.trackMouse = trackMouse;
- },
+ bbox = displaySprite.getBBox();
+ pad = me.dashSize + me.label.padding;
- hideTip: function(item) {
- var tooltip = this.tooltip;
- if (!tooltip) {
- return;
+ 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;
}
- clearTimeout(this.tipTimeout);
- this.tipTimeout = setTimeout(function() {
- tooltip.hide();
- }, 0);
+ 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.Abstract
- * @ignore
+ * @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:
+ *
+ * @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 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.Abstract', {
+Ext.define('Ext.chart.axis.Category', {
/* Begin Definitions */
- requires: ['Ext.chart.Chart'],
+ extend: 'Ext.chart.axis.Axis',
- /* End Definitions */
+ alternateClassName: 'Ext.chart.CategoryAxis',
- constructor: function(config) {
- config = config || {};
+ alias: 'axis.category',
- var me = this,
- pos = config.position || 'left';
+ /* End Definitions */
- 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");
- },
+ /**
+ * A list of category names to display along this axis.
+ * @property {String} categoryNames
+ */
+ categoryNames: null,
- alignment: null,
- grid: false,
- steps: 10,
- x: 0,
- y: 0,
- minValue: 0,
- maxValue: 0,
+ /**
+ * 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,
- getId: function() {
- return this.axisId || (this.axisId = Ext.id(null, 'ext-axis-'));
- },
+ // @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;
- /*
- 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,
+ this.labels = [];
+ store.each(function(record) {
+ for (i = 0; i < ln; i++) {
+ this.labels.push(record.get(fields[i]));
+ }
+ }, this);
+ },
- drawAxis: Ext.emptyFn,
- addDisplayAndLabels: Ext.emptyFn
+ // @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.Axis
+ * @class Ext.chart.axis.Gauge
* @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:
- *
+ *
+ * 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: '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.
+ * type: 'gauge',
+ * position: 'gauge',
+ * minimum: 0,
+ * maximum: 100,
+ * steps: 10,
+ * margin: 7
+ * }],
*/
-Ext.define('Ext.chart.axis.Axis', {
+Ext.define('Ext.chart.axis.Gauge', {
/* 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} minimum (required)
+ * The minimum value of the interval to be displayed in the axis.
*/
/**
- * @cfg {Number} minorTickSteps
- * The number of small ticks between two major ticks. Default is zero.
- */
-
- //@private force min/max values from store
- forceMinMax: false,
-
- /**
- * @cfg {Number} dashSize
- * The size of the dash marker. Default's 3.
+ * @cfg {Number} maximum (required)
+ * The maximum value of the interval to be displayed in the axis.
*/
- dashSize: 3,
-
+
/**
- * @cfg {String} position
- * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`.
+ * @cfg {Number} steps (required)
+ * The number of steps and tick marks to add to the interval.
*/
- position: 'bottom',
-
- // @private
- skipFirst: false,
-
+
/**
- * @cfg {Number} length
- * Offset axis position. Default's 0.
+ * @cfg {Number} [margin=10]
+ * The offset positioning of the tick marks and labels in pixels.
*/
- length: 0,
-
+
/**
- * @cfg {Number} width
- * Offset axis width. Default's 0.
+ * @cfg {String} title
+ * The title for the Axis.
*/
- width: 0,
-
- majorTickSteps: false,
- // @private
- applyData: Ext.emptyFn,
+ position: 'gauge',
- // @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;
+ alias: 'axis.gauge',
- //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;
+ 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;
}
- 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);
+
+ 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);
}
- 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);
+ } 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);
}
}
- });
- 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 (me.forceMinMax) {
- if (!isNaN(max)) {
- out.to = max;
- }
- if (!isNaN(min)) {
- out.from = min;
- }
- }
- 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;
+ this.sprites = sprites;
+ this.drawLabel();
+ if (this.title) {
+ this.drawTitle();
}
- if (me.adjustMinimumByMajorUnit) {
- out.from -= out.step;
+ },
+
+ 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
+ });
}
- me.prevMin = min == max? 0 : min;
- me.prevMax = max;
- return out;
+ 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);
},
/**
- * Renders the axis into the screen and updates it's position.
+ * Updates the {@link #title} of this axis.
+ * @param {String} title
*/
- 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;
- }
+ setTitle: function(title) {
+ this.title = title;
+ this.drawTitle();
+ },
- 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);
- }
+ 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);
}
}
- if (!me.axis) {
- me.axis = me.chart.surface.add(Ext.apply({
- type: 'path',
- path: path
- }, me.axisStyle));
+ 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);
+ }
}
- me.axis.setAttributes({
- path: path
- }, true);
- me.inflections = inflections;
- if (!init && me.grid) {
- me.drawGrid();
+ 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.
+ *
+ * @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.
+ */
+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,
+ hasLabel = !!(config.label && config.label.renderer),
+ label;
+
+ me.callParent([config]);
+ label = me.label;
+ if (me.roundToDecimal === false) {
+ return;
}
- me.axisBBox = me.axis.getBBox();
- me.drawLabel();
+ if (!hasLabel) {
+ label.renderer = function(v) {
+ return me.roundToDecimal(v, me.decimals);
+ };
+ }
+ },
+
+ roundToDecimal: function(v, dec) {
+ var val = Math.pow(10, dec || 0);
+ return Math.floor(v * val) / val;
},
/**
- * Renders an horizontal and/or vertical grid into the Surface.
+ * The minimum value drawn by the axis. If not set explicitly, the axis
+ * minimum will be calculated automatically.
+ *
+ * @property {Number} minimum
*/
- 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]);
- }
- }
+ minimum: NaN,
+
+ /**
+ * The maximum value drawn by the axis. If not set explicitly, the axis
+ * maximum will be calculated automatically.
+ *
+ * @property {Number} maximum
+ */
+ maximum: NaN,
+
+ /**
+ * The number of decimals to round the value to.
+ *
+ * @property {Number} decimals
+ */
+ decimals: 2,
+
+ /**
+ * The scaling algorithm to use on this axis. May be "linear" or
+ * "logarithmic". Currently only linear scale is implemented.
+ *
+ * @property {String} scale
+ * @private
+ */
+ scale: "linear",
+
+ /**
+ * Indicates the position of the axis relative to the chart
+ *
+ * @property {String} position
+ */
+ position: 'left',
+
+ /**
+ * Indicates whether to extend maximum beyond data's maximum to the nearest
+ * majorUnit.
+ *
+ * @property {Boolean} adjustMaximumByMajorUnit
+ */
+ adjustMaximumByMajorUnit: false,
+
+ /**
+ * Indicates whether to extend the minimum beyond data's minimum to the
+ * nearest majorUnit.
+ *
+ * @property {Boolean} adjustMinimumByMajorUnit
+ */
+ 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 (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,
+
+ 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
- }, odd || {}), true);
+ }, true);
+ sprites.push(sprite);
}
- if (evenPath.length) {
- if (!me.gridEven) {
- me.gridEven = surface.add({
- type: 'path',
- path: evenPath
- });
- }
- me.gridEven.setAttributes(Ext.apply({
- path: evenPath,
+ //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
- }, 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);
+ 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);
}
- else if (me.gridLines) {
- me.gridLines.hide(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();
},
- //@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;
+ 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;
}
- 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;
+ //get all rendered fields
+ chart.series.each(function(series) {
+ fields.push(series.yField);
+ xField = series.xField;
+ });
- 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];
+ //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);
}
- else if (i == last && axes.findIndex('position', 'right') == -1) {
- x = point[0] - bbox.width;
+ }
+ 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 (position == 'top') {
- y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2);
+ 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 {
- y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2);
+ }
+ 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);
+ }
}
-
- 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;
+ 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);
+ }
+ }
}
-
- prevLabel = textLabel;
}
-
- return maxHeight;
+ this.labelArray = labelArray;
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * 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.
+ *
+ * The store provides filtering and sorting support. This sorting/filtering can happen on the client side
+ * or can be completed on the server. This is controlled by the {@link Ext.data.Store#remoteSort remoteSort} and
+ * {@link Ext.data.Store#remoteFilter remoteFilter} config options. For more information see the {@link #sort} and
+ * {@link Ext.data.Store#filter filter} methods.
+ */
+Ext.define('Ext.data.AbstractStore', {
+ requires: ['Ext.util.MixedCollection', 'Ext.data.Operation', 'Ext.util.Filter'],
+
+ mixins: {
+ observable: 'Ext.util.Observable',
+ sortable: 'Ext.util.Sortable'
},
- 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);
+ statics: {
+ create: function(store){
+ if (!store.isStore) {
+ if (!store.type) {
+ store.type = 'store';
}
+ store = Ext.createByAlias('store.' + store.type, store);
}
- 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;
+ return store;
+ }
},
+
+ remoteSort : false,
+ remoteFilter: false,
/**
- * Renders the labels in the axes.
+ * @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.
*/
- 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();
- }
+ /**
+ * @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,
- // Hide unused bars
- ln = labelGroup.getCount();
- i = inflections.length;
- for (; i < ln; i++) {
- labelGroup.getAt(i).hide(true);
- }
+ /**
+ * @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,
- 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);
- }
- },
+ /**
+ * @property {String} batchUpdateMode
+ * 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.
+ */
+ batchUpdateMode: 'operation',
- // @private creates the elipsis for the text.
- elipsis: function(sprite, text, desiredWidth, minWidth, center) {
- var bbox,
- x;
+ /**
+ * @property {Boolean} filterOnLoad
+ * 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 Ext.data.Store#remoteFilter remoteFilter} is true
+ */
+ filterOnLoad: true,
- 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;
- },
+ /**
+ * @property {Boolean} sortOnLoad
+ * 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 Ext.data.Store#remoteSort remoteSort} is true
+ */
+ sortOnLoad: true,
/**
- * Updates the {@link #title} of this axis.
- * @param {String} title
+ * @property {Boolean} implicitModel
+ * 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.
+ * @private
*/
- setTitle: function(title) {
- this.title = title;
- this.drawLabel();
- },
+ implicitModel: false,
- // @private draws the title for the axis.
- drawTitle: function(maxWidth, maxHeight) {
+ /**
+ * @property {String} defaultProxyType
+ * The string type of the Proxy to create if none is specified. This defaults to creating a
+ * {@link Ext.data.proxy.Memory memory proxy}.
+ */
+ defaultProxyType: 'memory',
+
+ /**
+ * @property {Boolean} isDestroyed
+ * True if the Store has already been destroyed. If this is true, the reference to Store should be deleted
+ * as it will not function correctly any more.
+ */
+ isDestroyed: false,
+
+ isStore: true,
+
+ /**
+ * @cfg {String} storeId
+ * Unique identifier for this store. If present, this Store will be registered with the {@link Ext.data.StoreManager},
+ * making it easy to reuse elsewhere. Defaults to undefined.
+ */
+
+ /**
+ * @cfg {Object[]} fields
+ * This may be used in place of specifying a {@link #model} configuration. The fields should be a
+ * set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model}
+ * with these fields. In general this configuration option should be avoided, it exists for the purposes of
+ * backwards compatibility. For anything more complicated, such as specifying a particular id property or
+ * assocations, a {@link Ext.data.Model} should be defined and specified for the {@link #model}
+ * config.
+ */
+
+ /**
+ * @cfg {String} model
+ * Name of the {@link Ext.data.Model Model} associated with this store.
+ * The string is used as an argument for {@link Ext.ModelManager#getModel}.
+ */
+
+ sortRoot: 'data',
+
+ //documented above
+ constructor: function(config) {
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;
+ filters;
+
+ me.addEvents(
+ /**
+ * @event add
+ * Fired when a Model instance has been added to this Store
+ * @param {Ext.data.Store} store The store
+ * @param {Ext.data.Model[]} records The Model instances that were added
+ * @param {Number} index The index at which the instances were inserted
+ */
+ 'add',
- 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;
+ /**
+ * @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 Model instance has been updated
+ * @param {Ext.data.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',
- 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;
+ /**
+ * @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
+ * Fires before a request is made for a new data object. If the beforeload handler returns false the load
+ * action will be canceled.
+ * @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 {Ext.data.Model[]} records An array of records
+ * @param {Boolean} successful True if the operation was successful.
+ */
+ 'load',
+
+ /**
+ * @event write
+ * Fires whenever a successful write has been made via the configured {@link #proxy Proxy}
+ * @param {Ext.data.Store} store This Store
+ * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object that was used in
+ * the write
+ */
+ 'write',
+
+ /**
+ * @event beforesync
+ * Fired 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);
+ // don't use *config* anymore from here on... use *me* instead...
+
+ /**
+ * Temporary cache in which removed model instances are kept until successfully synchronised with a Proxy,
+ * at which point this is cleared.
+ * @private
+ * @property {Ext.data.Model[]} removed
+ */
+ me.removed = [];
+
+ me.mixins.observable.constructor.apply(me, arguments);
+ me.model = Ext.ModelManager.getModel(me.model);
+
+ /**
+ * @property {Object} modelDefaults
+ * @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;
}
- 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));
+
+ //
+ if (!me.model) {
+ if (Ext.isDefined(Ext.global.console)) {
+ Ext.global.console.warn('Store defined with no model. You may have mistyped the model name.');
}
- 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 .
- */
+ //
+
+ //ensures that the Proxy is instantiated correctly
+ me.setProxy(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);
+
+ /**
+ * @property {Ext.util.MixedCollection} filters
+ * The collection of {@link Ext.util.Filter Filters} currently applied to this Store
+ */
+ filters = me.decodeFilters(me.filters);
+ me.filters = Ext.create('Ext.util.MixedCollection');
+ me.filters.addAll(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;
+ },
-Ext.define('Ext.chart.axis.Category', {
+ /**
+ * Returns the proxy currently attached to this proxy instance
+ * @return {Ext.data.proxy.Proxy} The Proxy instance
+ */
+ getProxy: function() {
+ return this.proxy;
+ },
- /* Begin Definitions */
+ //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 || {};
- extend: 'Ext.chart.axis.Axis',
+ Ext.applyIf(options, {
+ action : 'create',
+ records: [instance]
+ });
- alternateClassName: 'Ext.chart.CategoryAxis',
+ operation = Ext.create('Ext.data.Operation', options);
- alias: 'axis.category',
+ me.proxy.create(operation, me.onProxyWrite, me);
+
+ return instance;
+ },
- /* End Definitions */
+ 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);
+ },
/**
- * A list of category names to display along this axis.
- *
- * @property categoryNames
- * @type Array
+ * @private
+ * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect
+ * the updates provided by the Proxy
*/
- categoryNames: null,
+ 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);
+ },
/**
- * 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
+ * @private
+ * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default just calls through
+ * to onProxyWrite.
*/
- calculateCategoryCount: false,
+ onBatchOperationComplete: function(batch, operation) {
+ return this.onProxyWrite(operation);
+ },
- // @private creates an array of labels to be used when rendering.
- setLabels: function() {
- var store = this.chart.store,
- fields = this.fields,
- ln = fields.length,
+ /**
+ * @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;
- this.labels = [];
- store.each(function(record) {
- for (i = 0; i < ln; i++) {
- this.labels.push(record.get(fields[i]));
- }
- }, this);
- },
+ me.suspendEvents();
- // @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
- };
- }
-});
+ for (i = 0; i < length; i++) {
+ me.onProxyWrite(operations[i]);
+ }
-/**
- * @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', {
+ me.resumeEvents();
- /* Begin Definitions */
+ me.fireEvent('datachanged', me);
+ },
- extend: 'Ext.chart.axis.Abstract',
+ onBatchException: function(batch, operation) {
+ // //decide what to do... could continue with the next operation
+ // batch.start();
+ //
+ // //or retry the last operation
+ // batch.retry();
+ },
- /* End Definitions */
-
/**
- * @cfg {Number} minimum (required) the minimum value of the interval to be displayed in the axis.
+ * @private
+ * Filter function for new records.
*/
+ filterNew: function(item) {
+ // only want phantom records that are valid
+ return item.phantom === true && item.isValid();
+ },
/**
- * @cfg {Number} maximum (required) the maximum value of the interval to be displayed in the axis.
+ * Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not
+ * yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one)
+ * @return {Ext.data.Model[]} The Model instances
*/
+ getNewRecords: function() {
+ return [];
+ },
/**
- * @cfg {Number} steps (required) the number of steps and tick marks to add to the interval.
+ * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy
+ * @return {Ext.data.Model[]} The updated Model instances
*/
+ getUpdatedRecords: function() {
+ return [];
+ },
/**
- * @cfg {Number} margin (optional) the offset positioning of the tick marks and labels in pixels. Default's 10.
+ * @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();
+ },
- position: 'gauge',
+ /**
+ * Returns any records that have been removed from the store but not yet destroyed on the proxy.
+ * @return {Ext.data.Model[]} The removed Model instances
+ */
+ getRemovedRecords: function() {
+ return this.removed;
+ },
- alias: 'axis.gauge',
+ filter: function(filters, value) {
- 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;
+ /**
+ * @private
+ * Normalizes an array of filter objects, ensuring that they are all Ext.util.Filter instances
+ * @param {Object[]} filters The filters array
+ * @return {Ext.util.Filter[]} Array of Ext.util.Filter objects
+ */
+ decodeFilters: function(filters) {
+ if (!Ext.isArray(filters)) {
+ if (filters === undefined) {
+ filters = [];
+ } else {
+ filters = [filters];
+ }
}
- 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);
+ 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;
}
- } 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);
+
+ //support a function to be passed as a filter definition
+ if (typeof config == 'function') {
+ config = {
+ filterFn: config
+ };
}
+
+ filters[i] = new Filter(config);
}
}
- 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);
+
+ return filters;
},
- /**
- * Updates the {@link #title} of this axis.
- * @param {String} title
- */
- setTitle: function(title) {
- this.title = title;
- this.drawTitle();
+ clearFilter: function(supressEvent) {
+
},
- 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; };
+ isFiltered: function() {
- 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 */
+ filterBy: function(fn, scope) {
- extend: 'Ext.chart.axis.Axis',
+ },
+
+ /**
+ * 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;
- alternateClassName: 'Ext.chart.NumericAxis',
+ if (toCreate.length > 0) {
+ options.create = toCreate;
+ needsSync = true;
+ }
+
+ if (toUpdate.length > 0) {
+ options.update = toUpdate;
+ needsSync = true;
+ }
- /* End Definitions */
+ if (toDestroy.length > 0) {
+ options.destroy = toDestroy;
+ needsSync = true;
+ }
- type: 'numeric',
+ if (needsSync && me.fireEvent('beforesync', options) !== false) {
+ me.proxy.batch(options, me.getBatchListeners());
+ }
+ },
- 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 );
+ /**
+ * @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 {
- label.renderer = function(v) {
- return me.roundToDecimal(v, me.decimals);
- };
+ listeners.complete = me.onBatchComplete;
}
+
+ return listeners;
},
-
- roundToDecimal: function(v, dec) {
- var val = Math.pow(10, dec || 0);
- return ((v * val) >> 0) / val;
+
+ //deprecated, will be removed in 5.0
+ save: function() {
+ return this.sync.apply(this, arguments);
},
-
- /**
- * 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
+ * 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
*/
- maximum: NaN,
+ 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;
+ },
/**
- * The number of decimals to round the value to.
- * Default's 2.
- *
- * @property decimals
- * @type Number
+ * @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
*/
- decimals: 2,
+ afterEdit : function(record) {
+ var me = this;
+
+ if (me.autoSync) {
+ me.sync();
+ }
+
+ me.fireEvent('update', me, record, Ext.data.Model.EDIT);
+ },
/**
- * The scaling algorithm to use on this axis. May be "linear" or
- * "logarithmic".
- *
- * @property scale
- * @type String
+ * @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
*/
- scale: "linear",
+ afterReject : function(record) {
+ this.fireEvent('update', this, record, Ext.data.Model.REJECT);
+ },
/**
- * Indicates the position of the axis relative to the chart
- *
- * @property position
- * @type String
+ * @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
*/
- position: 'left',
+ 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,
+
/**
- * Indicates whether to extend maximum beyond data's maximum to the nearest
- * majorUnit.
- *
- * @property adjustMaximumByMajorUnit
- * @type Boolean
+ * 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.
+ * @method
*/
- adjustMaximumByMajorUnit: false,
+ removeAll: Ext.emptyFn,
+ // individual substores should implement a "fast" remove
+ // and fire a clear event afterwards
/**
- * Indicates whether to extend the minimum beyond data's minimum to the
- * nearest majorUnit.
- *
- * @property adjustMinimumByMajorUnit
- * @type Boolean
+ * Returns true if the Store is currently performing a load operation
+ * @return {Boolean} True if the Store is currently loading
*/
- adjustMinimumByMajorUnit: false,
-
- // @private apply data.
- applyData: function() {
- this.callParent();
- return this.calcEnds();
- }
+ isLoading: function() {
+ return !!this.loading;
+ }
});
/**
- * @class Ext.chart.axis.Radial
- * @extends Ext.chart.axis.Abstract
- * @ignore
+ * @class Ext.util.Grouper
+ * @extends Ext.util.Sorter
+
+Represents a single grouper that can be applied to a Store. The grouper works
+in the same fashion as the {@link Ext.util.Sorter}.
+
+ * @markdown
*/
-Ext.define('Ext.chart.axis.Radial', {
+
+Ext.define('Ext.util.Grouper', {
/* Begin Definitions */
- extend: 'Ext.chart.axis.Abstract',
+ extend: 'Ext.util.Sorter',
/* 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;
- }
+ /**
+ * Returns the value for grouping to be used.
+ * @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'}
+ ]
+});
- 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);
- });
+var myStore = Ext.create('Ext.data.Store', {
+ model: 'User',
+ proxy: {
+ type: 'ajax',
+ url : '/users.json',
+ reader: {
+ type: 'json',
+ root: 'users'
}
- this.sprites = sprites;
-
- this.drawLabel();
},
+ autoLoad: true
+});
+
- 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;
+ * 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:
+ *
+
+Ext.create('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 = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ model: "User",
+ proxy: {
+ type: 'ajax',
+ url : 'users.json',
+ reader: {
+ type: 'json',
+ root: 'users'
}
-
- //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);
+ }
+});
+
+ *
+ * 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"
}
- }
+ ]
}
- 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);
- }
- }
- }
+ ]
+}
+
+ *
+ * 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 = Ext.create('Ext.data.Store', {
+ model: 'User',
+ sorters: [
+ {
+ property : 'age',
+ direction: 'DESC'
+ },
+ {
+ property : 'firstName',
+ direction: 'ASC'
}
- this.labelArray = labelArray;
- }
+ ],
+
+ 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
+Ext.create('Ext.data.Store', {
+ model: 'User',
+ storeId: 'usersStore'
});
-/**
- * @author Ed Spencer
- * @class Ext.data.AbstractStore
+
+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
+ *
*
- * 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,
+Ext.define('Ext.data.Store', {
+ extend: 'Ext.data.AbstractStore',
+
+ alias: 'store.store',
+
+ requires: ['Ext.data.StoreManager', '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. 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 {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.
+ * @cfg {Object[]/Ext.data.Model[]} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details.
*/
- 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.
+ * @property {String} groupField
+ * The 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.
*/
- autoSync: false,
+ groupField: undefined,
/**
- * 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
+ * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC"
+ * @property groupDir
* @type String
*/
- batchUpdateMode: 'operation',
+ groupDir: "ASC",
/**
- * 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
+ * @cfg {Number} pageSize
+ * The number of records considered to form a 'page'. This is used to power the built-in
+ * paging using the nextPage and previousPage functions. Defaults to 25.
*/
- filterOnLoad: true,
+ pageSize: 25,
/**
- * 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
+ * The page that the Store has most recently loaded (see {@link #loadPage})
+ * @property currentPage
+ * @type Number
*/
- sortOnLoad: true,
+ currentPage: 1,
/**
- * 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
+ * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage},
+ * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records, allowing
+ * large data sets to be loaded one page at a time but rendered all together.
*/
- implicitModel: false,
+ clearOnPageLoad: true,
/**
- * 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
+ * @property {Boolean} loading
+ * True if the Store is currently loading via its Proxy
+ * @private
*/
- defaultProxyType: 'memory',
+ loading: false,
/**
- * 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
+ * @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
*/
- isDestroyed: false,
-
- isStore: true,
+ sortOnFilter: 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 {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 {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.
+ * @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,
- sortRoot: 'data',
-
- //documented above
+ isStore: true,
+
+ onClassExtended: function(cls, data) {
+ var model = data.model;
+
+ if (typeof model == 'string') {
+ var onBeforeClassCreated = data.onBeforeClassCreated;
+
+ data.onBeforeClassCreated = function(cls, data) {
+ var me = this;
+
+ Ext.require(model, function() {
+ onBeforeClassCreated.call(me, cls, data);
+ });
+ };
+ }
+ },
+
+ /**
+ * Creates the store.
+ * @param {Object} config (optional) Config object
+ */
constructor: function(config) {
- var me = this,
- filters;
-
- 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',
+ // Clone the config so we don't modify the original config object
+ config = Ext.Object.merge({}, config);
- /**
- * @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',
+ var me = this,
+ groupers = config.groupers || me.groupers,
+ groupField = config.groupField || me.groupField,
+ proxy,
+ data;
- /**
- * @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',
+ if (config.buffered || me.buffered) {
+ me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) {
+ return record.index;
+ });
+ me.pendingRequests = [];
+ me.pagesRequested = [];
- /**
- * @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',
+ me.sortOnLoad = false;
+ me.filterOnLoad = false;
+ }
+ me.addEvents(
/**
- * @event load
- * Fires whenever the store reads data from a remote data source.
+ * @event beforeprefetch
+ * Fires before a prefetch occurs. Return false to cancel.
* @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
*/
- 'load',
-
+ 'beforeprefetch',
/**
- * @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
+ * @event groupchange
+ * Fired whenever the grouping in the grid changes
+ * @param {Ext.data.Store} store The store
+ * @param {Ext.util.Grouper[]} groupers The array of grouper objects
*/
- 'beforesync',
+ 'groupchange',
/**
- * @event clear
- * Fired after the {@link #removeAll} method is called.
+ * @event load
+ * Fires whenever records have been prefetched
* @param {Ext.data.Store} this
+ * @param {Ext.util.Grouper[]} records An array of records
+ * @param {Boolean} successful True if the operation was successful.
+ * @param {Ext.data.Operation} operation The associated operation
*/
- 'clear'
+ 'prefetch'
);
-
- Ext.apply(me, config);
- // don't use *config* anymore from here on... use *me* instead...
+ data = config.data || me.data;
/**
- * 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
+ * The MixedCollection that holds this store's local cache of records
+ * @property data
+ * @type Ext.util.MixedCollection
*/
- me.removed = [];
+ me.data = Ext.create('Ext.util.MixedCollection', false, function(record) {
+ return record.internalId;
+ });
- me.mixins.observable.constructor.apply(me, arguments);
- me.model = Ext.ModelManager.getModel(me.model);
+ if (data) {
+ me.inlineData = data;
+ delete config.data;
+ }
+
+ if (!groupers && groupField) {
+ groupers = [{
+ property : groupField,
+ direction: config.groupDir || me.groupDir
+ }];
+ }
+ delete config.groupers;
/**
- * @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.
+ * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store
+ * @property groupers
+ * @type Ext.util.MixedCollection
*/
- 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
- });
+ me.groupers = Ext.create('Ext.util.MixedCollection');
+ me.groupers.addAll(me.decodeGroupers(groupers));
- delete me.fields;
+ this.callParent([config]);
+ // don't use *config* anymore from here on... use *me* instead...
- me.implicitModel = true;
+ if (me.groupers.items.length) {
+ me.sort(me.groupers.items, 'prepend', false);
}
- //ensures that the Proxy is instantiated correctly
- me.setProxy(me.proxy || me.model.getProxy());
+ proxy = me.proxy;
+ data = me.inlineData;
- if (me.id && !me.storeId) {
- me.storeId = me.id;
- delete me.id;
+ 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);
}
+ },
- if (me.storeId) {
- Ext.data.StoreManager.register(me);
+ onBeforeSort: function() {
+ var groupers = this.groupers;
+ if (groupers.getCount() > 0) {
+ this.sort(groupers.items, 'prepend', false);
}
-
- 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
- */
- filters = me.decodeFilters(me.filters);
- me.filters = Ext.create('Ext.util.MixedCollection');
- me.filters.addAll(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
+ * @private
+ * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances
+ * @param {Object[]} groupers The groupers array
+ * @return {Ext.util.Grouper[]} Array of Ext.util.Grouper objects
*/
- 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
- };
+ decodeGroupers: function(groupers) {
+ if (!Ext.isArray(groupers)) {
+ if (groupers === undefined) {
+ groupers = [];
+ } else {
+ groupers = [groupers];
}
- Ext.applyIf(proxy, {
- model: me.model
- });
-
- proxy = Ext.createByAlias('proxy.' + proxy.type, proxy);
}
-
- me.proxy = proxy;
-
- return me.proxy;
+
+ 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;
},
/**
- * Returns the proxy currently attached to this proxy instance
- * @return {Ext.data.proxy.Proxy} The Proxy instance
+ * Group data in the store
+ * @param {String/Object[]} 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".
*/
- getProxy: function() {
- return this.proxy;
- },
-
- //saves any phantom records
- create: function(data, options) {
+ group: function(groupers, direction) {
var me = this,
- instance = Ext.ModelManager.create(Ext.applyIf(data, me.modelDefaults), me.model.modelName),
- operation;
-
- options = options || {};
+ hasNew = false,
+ grouper,
+ newGroupers;
- Ext.applyIf(options, {
- action : 'create',
- records: [instance]
- });
+ if (Ext.isArray(groupers)) {
+ newGroupers = groupers;
+ } else if (Ext.isObject(groupers)) {
+ newGroupers = [groupers];
+ } else if (Ext.isString(groupers)) {
+ grouper = me.groupers.get(groupers);
- operation = Ext.create('Ext.data.Operation', options);
+ if (!grouper) {
+ grouper = {
+ property : groupers,
+ direction: direction
+ };
+ newGroupers = [grouper];
+ } else if (direction === undefined) {
+ grouper.toggle();
+ } else {
+ grouper.setDirection(direction);
+ }
+ }
- me.proxy.create(operation, me.onProxyWrite, me);
-
- return instance;
- },
+ if (newGroupers && newGroupers.length) {
+ hasNew = true;
+ newGroupers = me.decodeGroupers(newGroupers);
+ me.groupers.clear();
+ me.groupers.addAll(newGroupers);
+ }
- read: function() {
- return this.load.apply(this, arguments);
+ if (me.remoteGroup) {
+ me.load({
+ scope: me,
+ callback: me.fireGroupChange
+ });
+ } else {
+ // need to explicitly force a sort if we have groupers
+ me.sort(null, null, null, hasNew);
+ me.fireGroupChange();
+ }
},
- onProxyRead: Ext.emptyFn,
-
- update: function(options) {
- var me = this,
- operation;
- options = options || {};
-
- Ext.applyIf(options, {
- action : 'update',
- records: me.getUpdatedRecords()
+ /**
+ * 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);
+ }
+ },
- operation = Ext.create('Ext.data.Operation', options);
-
- return me.proxy.update(operation, me.onProxyWrite, me);
+ /**
+ * 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
- * 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();
+ fireGroupChange: function(){
+ this.fireEvent('groupchange', this, this.groupers);
+ },
- 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;
- }
+ /**
+ * Returns an array 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 = Ext.create('Ext.data.Store', {
+ groupField: 'color',
+ groupDir : 'DESC'
+});
- 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]);
+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 {Object/Object[]} 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];
- //tells the attached proxy to destroy the given records
- destroy: function(options) {
- var me = this,
- operation;
-
- options = options || {};
+ if (group === undefined) {
+ group = {
+ name: groupStr,
+ children: []
+ };
- Ext.applyIf(options, {
- action : 'destroy',
- records: me.getRemovedRecords()
- });
+ groups.push(group);
+ pointers[groupStr] = group;
+ }
- operation = Ext.create('Ext.data.Operation', options);
+ group.children.push(record);
+ }
- return me.proxy.destroy(operation, me.onProxyWrite, me);
+ return requestGroupString ? pointers[requestGroupString] : groups;
},
/**
* @private
- * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default just calls through
- * to onProxyWrite.
+ * 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.
*/
- onBatchOperationComplete: function(batch, operation) {
- return this.onProxyWrite(operation);
+ 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
- * 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.
+ * 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 {Ext.data.Model[]} records The set or subset of records to group
+ * @param {Number} grouperIndex The grouper index to retrieve
+ * @return {Object[]} The grouped records
*/
- onBatchComplete: function(batch, operation) {
+ getGroupsForGrouperIndex: function(records, grouperIndex) {
var me = this,
- operations = batch.operations,
- length = operations.length,
+ groupers = me.groupers,
+ grouper = groupers.getAt(grouperIndex),
+ groups = me.getGroupsForGrouper(records, grouper),
+ length = groups.length,
i;
- me.suspendEvents();
+ 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++) {
- me.onProxyWrite(operations[i]);
+ groups[i].depth = grouperIndex;
}
- 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();
+ return groups;
},
/**
* @private
- * Filter function for new records.
+ * 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 {Object[]} The group data
*/
- filterNew: function(item) {
- // only want phantom records that are valid
- return item.phantom === true && item.isValid();
- },
+ getGroupData: function(sort) {
+ var me = this;
+ if (sort !== false) {
+ me.sort();
+ }
- /**
- * 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 [];
+ return me.getGroupsForGrouperIndex(me.data.items, 0);
},
/**
- * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy
- * @return {Array} The updated Model instances
+ * 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:
+
+Ext.create('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
*/
- getUpdatedRecords: function() {
- return [];
+ getGroupString: function(instance) {
+ var group = this.groupers.first();
+ if (group) {
+ return instance.get(group.property);
+ }
+ return '';
},
-
/**
- * @private
- * Filter function for updated records.
+ * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
+ * See also {@link #add}
.
+ * @param {Number} index The start index at which to insert the passed Records.
+ * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
*/
- filterUpdated: function(item) {
- // only want dirty records, not phantoms that are valid
- return item.dirty === true && item.phantom !== true && item.isValid();
- },
+ insert: function(index, records) {
+ var me = this,
+ sync = false,
+ i,
+ record,
+ len;
- /**
- * Returns any records that have been removed from the store but not yet destroyed on the proxy.
- * @return {Array} The removed Model instances
- */
- getRemovedRecords: function() {
- return this.removed;
- },
+ 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;
- filter: function(filters, value) {
+ 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();
+ }
},
/**
- * @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
+ * Adds Model instance to the Store. This method accepts either:
+ *
+ * - An array of Model instances or Model configuration objects.
+ * - Any number of Model instance or Model configuration object arguments.
+ *
+ * The new Model instances will be added at the end of the existing collection.
+ *
+ * Sample usage:
+ *
+ * myStore.add({some: 'data'}, {some: 'other data'});
+ *
+ * @param {Ext.data.Model[]/Ext.data.Model...} model An array of Model instances
+ * or Model configuration objects, or variable number of Model instance or config arguments.
+ * @return {Ext.data.Model[]} The model instances that were added
*/
- decodeFilters: function(filters) {
- if (!Ext.isArray(filters)) {
- if (filters === undefined) {
- filters = [];
- } else {
- filters = [filters];
- }
+ 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 length = filters.length,
- Filter = Ext.util.Filter,
- config, i;
-
- for (i = 0; i < length; i++) {
- config = filters[i];
+ var me = this,
+ i = 0,
+ length = records.length,
+ record;
- if (!(config instanceof Filter)) {
- Ext.apply(config, {
- root: 'data'
- });
+ 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;
+ }
- //support for 3.x style filters where a function can be defined as 'fn'
- if (config.fn) {
- config.filterFn = config.fn;
- }
+ me.insert(me.data.length, records);
- //support a function to be passed as a filter definition
- if (typeof config == 'function') {
- config = {
- filterFn: config
- };
- }
+ return records;
+ },
- filters[i] = new Filter(config);
- }
+ /**
+ * 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 filters;
+ return record;
},
- clearFilter: function(supressEvent) {
-
+ /**
+ * 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);
},
- isFiltered: function() {
+ /**
+ * 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/Ext.data.Model[]} 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;
- filterBy: function(fn, scope) {
+ for (; i < length; i++) {
+ record = records[i];
+ index = me.data.indexOf(record);
- },
-
- /**
- * 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 (me.snapshot) {
+ me.snapshot.remove(record);
+ }
- if (toCreate.length > 0) {
- options.create = toCreate;
- needsSync = true;
- }
+ if (index > -1) {
+ isPhantom = record.phantom === true;
+ if (!isMove && !isPhantom) {
+ // don't push phantom records onto removed
+ me.removed.push(record);
+ }
- if (toUpdate.length > 0) {
- options.update = toUpdate;
- needsSync = true;
- }
+ record.unjoin(me);
+ me.data.remove(record);
+ sync = sync || !isPhantom;
- if (toDestroy.length > 0) {
- options.destroy = toDestroy;
- needsSync = true;
+ me.fireEvent('remove', me, record, index);
+ }
}
- if (needsSync && me.fireEvent('beforesync', options) !== false) {
- me.proxy.batch(options, me.getBatchListeners());
+ me.fireEvent('datachanged', me);
+ if (!isMove && me.autoSync && sync) {
+ me.sync();
}
},
-
/**
- * @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
+ * Removes the model instance at the given index
+ * @param {Number} index The record index
*/
- getBatchListeners: function() {
- var me = this,
- listeners = {
- scope: me,
- exception: me.onBatchException
- };
+ removeAt: function(index) {
+ var record = this.getAt(index);
- if (me.batchUpdateMode == 'operation') {
- listeners.operationcomplete = me.onBatchOperationComplete;
- } else {
- listeners.complete = me.onBatchComplete;
+ if (record) {
+ this.remove(record);
}
-
- 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
+ * 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,
- operation;
+ var me = this;
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);
+ if (Ext.isFunction(options)) {
+ options = {
+ callback: options
+ };
}
-
- return me;
+
+ 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
- * 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
+ * Called internally when a Proxy has completed a load request
*/
- afterEdit : function(record) {
- var me = this;
-
- if (me.autoSync) {
- me.sync();
+ onProxyLoad: function(operation) {
+ var me = this,
+ resultSet = operation.getResultSet(),
+ records = operation.getRecords(),
+ successful = operation.wasSuccessful();
+
+ if (resultSet) {
+ me.totalCount = resultSet.total;
}
-
- me.fireEvent('update', me, record, Ext.data.Model.EDIT);
+
+ 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
- * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to..
- * @param {Ext.data.Model} record The model instance that was edited
+ * @param {Ext.data.Model[]} 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
*/
- afterReject : function(record) {
- this.fireEvent('update', this, record, Ext.data.Model.REJECT);
+ 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
- * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
- * @param {Ext.data.Model} record The model instance that was edited
+ * @param {Ext.data.Model[]} 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
*/
- 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;
+ onUpdateRecords: function(records, operation, success){
+ if (success) {
+ var i = 0,
+ length = records.length,
+ data = this.data,
+ snapshot = this.snapshot,
+ record;
- if (me.implicitModel) {
- Ext.destroy(me.model);
+ for (; i < length; ++i) {
+ record = records[i];
+ data.replace(record);
+ if (snapshot) {
+ snapshot.replace(record);
+ }
+ record.join(this);
}
}
},
-
- 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.
- * @method
- */
- 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
+ * Remove any records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} 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
*/
- isLoading: function() {
- return this.loading;
- }
-});
-
-/**
- * @class Ext.util.Grouper
- * @extends Ext.util.Sorter
- */
-
-Ext.define('Ext.util.Grouper', {
+ onDestroyRecords: function(records, operation, success){
+ if (success) {
+ var me = this,
+ i = 0,
+ length = records.length,
+ data = me.data,
+ snapshot = me.snapshot,
+ record;
- /* Begin Definitions */
+ for (; i < length; ++i) {
+ record = records[i];
+ record.unjoin(me);
+ data.remove(record);
+ if (snapshot) {
+ snapshot.remove(record);
+ }
+ }
+ me.removed = [];
+ }
+ },
- extend: 'Ext.util.Sorter',
+ //inherit docs
+ getNewRecords: function() {
+ return this.data.filterBy(this.filterNew).items;
+ },
- /* End Definitions */
+ //inherit docs
+ getUpdatedRecords: function() {
+ return this.data.filterBy(this.filterUpdated).items;
+ },
/**
- * Function description
- * @param {Ext.data.Model} instance The Model instance
- * @return {String} The group string for this model
+ * Filters the loaded set of records by a given set of filters.
+ *
+ * Filtering by single field:
+ *
+ * store.filter("email", /\.com$/);
+ *
+ * Using multiple filters:
+ *
+ * store.filter([
+ * {property: "email", value: /\.com$/},
+ * {filterFn: function(item) { return item.get("age") > 10; }}
+ * ]);
+ *
+ * Using Ext.util.Filter instances instead of config objects
+ * (note that we need to specify the {@link Ext.util.Filter#root root} config option in this case):
+ *
+ * store.filter([
+ * Ext.create('Ext.util.Filter', {property: "email", value: /\.com$/, root: 'data'}),
+ * Ext.create('Ext.util.Filter', {filterFn: function(item) { return item.get("age") > 10; }, root: 'data'})
+ * ]);
+ *
+ * @param {Object[]/Ext.util.Filter[]/String} filters The set of filters to apply to the data. These are stored internally on the store,
+ * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See
+ * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively,
+ * pass in a property string
+ * @param {String} value (optional) value to filter by (only if using a property string as the first argument)
*/
- 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'
+ filter: function(filters, value) {
+ if (Ext.isString(filters)) {
+ filters = {
+ property: filters,
+ value: value
+ };
}
- },
- 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'
- }
- ],
+ var me = this,
+ decoded = me.decodeFilters(filters),
+ i = 0,
+ doLocalSort = me.sortOnFilter && !me.remoteSort,
+ length = decoded.length;
- filters: [
- {
- property: 'firstName',
- value : /Ed/
+ for (; i < length; i++) {
+ me.filters.replace(decoded[i]);
}
- ]
-});
-
- *
- * 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',
+ 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);
- //other config goes here
-});
+ 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);
+ }
+ }
+ }
+ },
-new Ext.view.View({
- store: 'usersStore',
+ /**
+ * 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;
- //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',
+ me.filters.clear();
- alias: 'store.store',
+ if (me.remoteFilter) {
+ me.load();
+ } else if (me.isFiltered()) {
+ me.data = me.snapshot.clone();
+ delete me.snapshot;
- requires: ['Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'],
- uses: ['Ext.data.proxy.Memory'],
+ if (suppressEvent !== true) {
+ me.fireEvent('datachanged', me);
+ }
+ }
+ },
/**
- * @cfg {Boolean} remoteSort
- * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to false .
+ * Returns true if this store is currently filtered
+ * @return {Boolean}
*/
- remoteSort: false,
+ isFiltered: function() {
+ var snapshot = this.snapshot;
+ return !! snapshot && snapshot !== this.data;
+ },
/**
- * @cfg {Boolean} remoteFilter
- * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to false .
+ * Filter by a function. The specified function will be called for each
+ * Record in this Store. If the function returns true the Record is included,
+ * otherwise it is filtered out.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
*/
- remoteFilter: false,
-
+ 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);
+ },
+
/**
- * @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,
+ * Query the cached records in this Store using a filtering function. The specified function
+ * will be called with each record in this Store. If the function returns true the record is
+ * included in the results.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
+ * @return {Ext.util.MixedCollection} Returns an Ext.util.MixedCollection of the matched records
+ **/
+ queryBy: function(fn, scope) {
+ var me = this,
+ data = me.snapshot || me.data;
+ return data.filterBy(fn, scope || me);
+ },
/**
- * @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.
+ * Loads an array of data straight into the Store.
+ *
+ * Using this method is great if the data is in the correct format already (e.g. it doesn't need to be
+ * processed by a reader). If your data requires processing to decode the data structure, use a
+ * {@link Ext.data.proxy.Memory MemoryProxy} instead.
+ *
+ * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will be cast
+ * into model instances.
+ * @param {Boolean} [append=false] 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,
+ newData = [],
+ i,
+ record;
- /**
- * @cfg {Array} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details.
- */
+ //make sure each data element is an Ext.data.Model instance
+ for (i = 0; i < length; i++) {
+ record = data[i];
- /**
- * @cfg {String} model The {@link Ext.data.Model} associated with this store
- */
+ if (!(record instanceof Ext.data.Model)) {
+ record = Ext.ModelManager.create(record, model);
+ }
+ newData.push(record);
+ }
- /**
- * 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,
+ this.loadRecords(newData, {addRecords: append});
+ },
- /**
- * 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",
/**
- * @cfg {Number} pageSize
- * The number of records considered to form a 'page'. This is used to power the built-in
- * paging using the nextPage and previousPage functions. Defaults to 25.
- */
- pageSize: 25,
+ * Loads data via the bound Proxy's reader
+ *
+ * Use this method if you are attempting to load data and want to utilize the configured data reader.
+ *
+ * @param {Object[]} data The full JSON object you'd like to load into the Data store.
+ * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
+ * to remove the old ones first.
+ */
+ loadRawData : function(data, append) {
+ var me = this,
+ result = me.proxy.reader.read(data),
+ records = result.records;
+
+ if (result.success) {
+ me.loadRecords(records, { addRecords: append });
+ me.fireEvent('load', me, records, true);
+ }
+ },
- /**
- * 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.
+ * Loads an array of {@link 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 {Ext.data.Model[]} 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
*/
- clearOnPageLoad: true,
+ loadRecords: function(records, options) {
+ var me = this,
+ i = 0,
+ length = records.length;
- /**
- * True if the Store is currently loading via its Proxy
- * @property loading
- * @type Boolean
- * @private
- */
- loading: false,
+ options = options || {};
- /**
- * @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,
+ if (!options.addRecords) {
+ delete me.snapshot;
+ me.clearData();
+ }
- //documented above
- constructor: function(config) {
- config = config || {};
+ me.data.addAll(records);
- var me = this,
- groupers = config.groupers || me.groupers,
- groupField = config.groupField || me.groupField,
- 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;
+ //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);
}
-
- 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
+ /*
+ * 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.data = Ext.create('Ext.util.MixedCollection', false, function(record) {
- return record.internalId;
- });
+ me.suspendEvents();
- if (data) {
- me.inlineData = data;
- delete config.data;
- }
-
- if (!groupers && groupField) {
- groupers = [{
- property : groupField,
- direction: config.groupDir || me.groupDir
- }];
+ if (me.filterOnLoad && !me.remoteFilter) {
+ me.filter();
}
- 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]);
- // don't use *config* anymore from here on... use *me* instead...
-
- if (me.groupers.items.length) {
- me.sort(me.groupers.items, 'prepend', false);
+ if (me.sortOnLoad && !me.remoteSort) {
+ me.sort();
}
- proxy = me.proxy;
- data = me.inlineData;
+ me.resumeEvents();
+ me.fireEvent('datachanged', me, records);
+ },
- if (data) {
- if (proxy instanceof Ext.data.proxy.Memory) {
- proxy.data = data;
- me.read();
- } else {
- me.add.apply(me, data);
- }
+ // PAGING METHODS
+ /**
+ * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
+ * load operation, passing in calculated 'start' and 'limit' params
+ * @param {Number} page The number of the page to load
+ * @param {Object} options See options for {@link #load}
+ */
+ loadPage: function(page, options) {
+ var me = this;
+ options = Ext.apply({}, options);
- 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);
- }
+ me.currentPage = page;
+
+ me.read(Ext.applyIf(options, {
+ page: page,
+ start: (page - 1) * me.pageSize,
+ limit: me.pageSize,
+ addRecords: !me.clearOnPageLoad
+ }));
},
-
- onBeforeSort: function() {
- this.sort(this.groupers.items, 'prepend', false);
+
+ /**
+ * Loads the next 'page' in the current data set
+ * @param {Object} options See options for {@link #load}
+ */
+ nextPage: function(options) {
+ this.loadPage(this.currentPage + 1, options);
},
-
+
/**
- * @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
+ * Loads the previous 'page' in the current data set
+ * @param {Object} options See options for {@link #load}
*/
- decodeGroupers: function(groupers) {
- if (!Ext.isArray(groupers)) {
- if (groupers === undefined) {
- groupers = [];
- } else {
- groupers = [groupers];
- }
- }
+ previousPage: function(options) {
+ this.loadPage(this.currentPage - 1, options);
+ },
- var length = groupers.length,
- Grouper = Ext.util.Grouper,
- config, i;
+ // private
+ clearData: function() {
+ var me = this;
+ me.data.each(function(record) {
+ record.unjoin(me);
+ });
- for (i = 0; i < length; i++) {
- config = groupers[i];
+ me.data.clear();
+ },
- if (!(config instanceof Grouper)) {
- if (Ext.isString(config)) {
- config = {
- property: config
- };
- }
-
- Ext.applyIf(config, {
- root : 'data',
- direction: "ASC"
- });
+ // Buffering
+ /**
+ * Prefetches data into 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();
- //support for 3.x style sorters where a function can be defined as 'fn'
- if (config.fn) {
- config.sorterFn = config.fn;
- }
+ options = options || {};
- //support a function to be passed as a sorter definition
- if (typeof config == 'function') {
- config = {
- sorterFn: config
- };
- }
+ Ext.applyIf(options, {
+ action : 'read',
+ filters: me.filters.items,
+ sorters: me.sorters.items,
+ requestId: requestId
+ });
+ me.pendingRequests.push(requestId);
- groupers[i] = new Grouper(config);
- }
+ 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 groupers;
+ return me;
},
-
+
/**
- * 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".
+ * 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}
*/
- group: function(groupers, direction) {
+ prefetchPage: function(page, options) {
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);
+ pageSize = me.pageSize,
+ start = (page - 1) * me.pageSize,
+ end = start + pageSize;
- 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
+ // 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
});
- } else {
- me.sort();
- me.fireEvent('groupchange', me, me.groupers);
+
+ me.prefetch(options);
}
+
},
-
- /**
- * 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
+ * Returns a unique requestId to track requests.
* @private
*/
- fireGroupChange: function(){
- this.fireEvent('groupchange', this, this.groupers);
+ getRequestId: function() {
+ this.requestSeed = this.requestSeed || 1;
+ return this.requestSeed++;
},
/**
- * 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
+ * Called after the configured proxy completes a prefetch operation.
+ * @private
+ * @param {Ext.data.Operation} operation The operation that completed
*/
- getGroups: function(requestGroupString) {
- var records = this.data.items,
- length = records.length,
- groups = [],
- pointers = {},
- record,
- groupStr,
- group,
- i;
+ onProxyPrefetch: function(operation) {
+ var me = this,
+ resultSet = operation.getResultSet(),
+ records = operation.getRecords(),
- for (i = 0; i < length; i++) {
- record = records[i];
- groupStr = this.getGroupString(record);
- group = pointers[groupStr];
+ successful = operation.wasSuccessful();
- if (group === undefined) {
- group = {
- name: groupStr,
- children: []
- };
+ if (resultSet) {
+ me.totalCount = resultSet.total;
+ me.fireEvent('totalcountchange', me.totalCount);
+ }
- groups.push(group);
- pointers[groupStr] = group;
- }
+ if (successful) {
+ me.cacheRecords(records, operation);
+ }
+ Ext.Array.remove(me.pendingRequests, operation.requestId);
+ if (operation.page) {
+ Ext.Array.remove(me.pagesRequested, operation.page);
+ }
- group.children.push(record);
+ me.loading = false;
+ me.fireEvent('prefetch', me, records, successful, operation);
+
+ // HACK to support loadMask
+ if (operation.blocking) {
+ me.fireEvent('load', me, records, successful);
}
- return requestGroupString ? pointers[requestGroupString] : groups;
+ //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
- * 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.
+ * @param {Ext.data.Model[]} records The records to cache
+ * @param {Ext.data.Operation} The associated operation
*/
- getGroupsForGrouper: function(records, grouper) {
- var length = records.length,
- groups = [],
- oldValue,
- newValue,
- record,
- group,
- i;
+ cacheRecords: function(records, operation) {
+ var me = this,
+ i = 0,
+ length = records.length,
+ start = operation ? operation.start : 0;
- for (i = 0; i < length; i++) {
- record = records[i];
- newValue = grouper.getGroupString(record);
+ if (!Ext.isDefined(me.totalCount)) {
+ me.totalCount = records.length;
+ me.fireEvent('totalcountchange', me.totalCount);
+ }
- if (newValue !== oldValue) {
- group = {
- name: newValue,
- grouper: grouper,
- records: []
- };
- groups.push(group);
- }
+ for (; i < length; i++) {
+ // this is the true index, not the viewIndex
+ records[i].index = start + i;
+ }
- group.records.push(record);
+ 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;
- oldValue = newValue;
+ for (; i <= numRecordsToPurge; i++) {
+ me.prefetchData.removeAt(0);
}
-
- return groups;
},
/**
+ * Determines if the range has already been satisfied in the prefetchData.
* @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
+ * @param {Number} start The start index
+ * @param {Number} end The end index in the range
*/
- getGroupsForGrouperIndex: function(records, grouperIndex) {
+ rangeSatisfied: function(start, end) {
var me = this,
- groupers = me.groupers,
- grouper = groupers.getAt(grouperIndex),
- groups = me.getGroupsForGrouper(records, grouper),
- length = groups.length,
- i;
+ i = start,
+ satisfied = true;
- if (grouperIndex + 1 < groupers.length) {
- for (i = 0; i < length; i++) {
- groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1);
+ 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;
+ },
- for (i = 0; i < length; i++) {
- groups[i].depth = grouperIndex;
- }
-
- return groups;
+ /**
+ * 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
- * 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();
+ onGuaranteedRange: function() {
+ var me = this,
+ totalCount = me.getTotalCount(),
+ start = me.requestStart,
+ end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd,
+ range = [],
+ record,
+ i = start;
+
+ end = Math.max(0, end);
+
+ //
+ if (start > end) {
+ Ext.log({
+ level: 'warn',
+ msg: 'Start (' + start + ') was greater than end (' + end +
+ ') for the range of records requested (' + me.requestStart + '-' +
+ me.requestEnd + ')' + (this.storeId ? ' from store "' + this.storeId + '"' : '')
+ });
}
+ //
- return me.getGroupsForGrouperIndex(me.data.items, 0);
+ 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.log('Record with key "' + i + '" was not found and store said it was guaranteed');
+// }
+ //
+ if (record) {
+ 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 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
+ * Returns the number of pending requests out.
*/
- getGroupString: function(instance) {
- var group = this.groupers.first();
- if (group) {
- return instance.get(group.property);
+ hasPendingRequests: function() {
+ return this.pendingRequests.length;
+ },
+
+
+ // wait until all requests finish, until guaranteeing the range.
+ onWaitForGuarantee: function() {
+ if (!this.hasPendingRequests()) {
+ this.onGuaranteedRange();
}
- 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.
+ * 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.
*/
- insert: function(index, records) {
+ 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,
- sync = false,
- i,
- record,
- len;
+ i = start,
+ prefetchData = me.prefetchData,
+ range = [],
+ startLoaded = !!prefetchData.getByKey(start),
+ endLoaded = !!prefetchData.getByKey(end),
+ startPage = me.getPageFromRecordIndex(start),
+ endPage = me.getPageFromRecordIndex(end);
- 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);
+ me.cb = cb;
+ me.scope = scope;
- sync = sync || record.phantom === true;
+ 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();
}
+ },
- if (me.snapshot) {
- me.snapshot.addAll(records);
+ // 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;
+
+ 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);
}
+ },
- me.fireEvent('add', me, records, index);
- me.fireEvent('datachanged', me);
- if (me.autoSync && sync) {
- me.sync();
+ // 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);
}
},
/**
- * 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
+ * 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 (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
+ * @return {Number} The matched index or -1
*/
- 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);
- }
+ 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 (optional) 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,
- 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;
+ index = me.find.apply(me, arguments);
+ return index !== -1 ? me.getAt(index) : null;
},
/**
- * 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}
+ * 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=false] True if we don't care if the filter value is not the full value.
+ * @param {Boolean} [caseSensitive=false] True to create a case-sensitive regex.
+ * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters added to the regex).
+ * Ignored if anyMatch is true.
*/
- createModel: function(record) {
- if (!record.isModel) {
- record = Ext.ModelManager.create(record, this.model);
+ createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) {
+ if (Ext.isEmpty(value)) {
+ return false;
}
-
- return record;
+ value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch);
+ return function(r) {
+ return value.test(r.data[property]);
+ };
},
/**
- * 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.
+ * Finds the index of the first matching Record in this store by a specific field value.
+ * @param {String} fieldName The name of the Record field to test.
+ * @param {Object} value The value to match the field against.
+ * @param {Number} startIndex (optional) The index to start searching at
+ * @return {Number} The matched index or -1
*/
- each: function(fn, scope) {
- this.data.each(fn, scope);
+ findExact: function(property, value, start) {
+ return this.data.findIndexBy(function(rec) {
+ return rec.get(property) == value;
+ },
+ this, start);
},
/**
- * 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
+ * Find the index of the first matching Record in this Store by a function.
+ * If the function returns true it is considered a match.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {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
*/
- remove: function(records, /* private */ isMove) {
- if (!Ext.isArray(records)) {
- records = [records];
- }
+ findBy: function(fn, scope, start) {
+ return this.data.findIndexBy(fn, scope, start);
+ },
- /*
- * Pass the isMove parameter if we know we're going to be re-inserting this record
- */
- isMove = isMove === true;
+ /**
+ * 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 {Object[]} An array of the unique values
+ **/
+ collect: function(dataIndex, allowNull, bypassFilter) {
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);
- }
+ data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data;
- record.unjoin(me);
- me.data.remove(record);
- sync = sync || !isPhantom;
+ return data.collect(dataIndex, 'data', allowNull);
+ },
- me.fireEvent('remove', me, record, index);
- }
- }
+ /**
+ * 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;
+ },
- me.fireEvent('datachanged', me);
- if (!isMove && me.autoSync && sync) {
- me.sync();
- }
+ /**
+ * 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;
},
/**
- * Removes the model instance at the given index
- * @param {Number} index The record index
+ * 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.
*/
- removeAt: function(index) {
- var record = this.getAt(index);
+ getAt: function(index) {
+ return this.data.getAt(index);
+ },
- if (record) {
- this.remove(record);
- }
+ /**
+ * Returns a range of Records between specified indices.
+ * @param {Number} [startIndex=0] The starting index
+ * @param {Number} [endIndex] 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);
},
/**
- * 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.
+ * 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 null 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.
*/
- load: function(options) {
- var me = this;
-
- options = options || {};
+ indexOf: function(record) {
+ return this.data.indexOf(record);
+ },
- 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
- });
+ /**
+ * 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) {
+ var index = record.index;
+ if (index || index === 0) {
+ return index;
+ }
+ return this.indexOf(record);
+ },
- return me.callParent([options]);
+ /**
+ * 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.indexOf(this.getById(id));
},
/**
- * @private
- * Called internally when a Proxy has completed a load request
+ * Remove all items from the store.
+ * @param {Boolean} silent Prevent the `clear` event from being fired.
*/
- onProxyLoad: function(operation) {
- var me = this,
- resultSet = operation.getResultSet(),
- records = operation.getRecords(),
- successful = operation.wasSuccessful();
+ removeAll: function(silent) {
+ var me = this;
- if (resultSet) {
- me.totalCount = resultSet.total;
+ me.clearData();
+ if (me.snapshot) {
+ me.snapshot.clear();
}
-
- if (successful) {
- me.loadRecords(records, operation);
+ if (silent !== true) {
+ me.fireEvent('clear', me);
}
+ },
- 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());
+ /*
+ * Aggregation methods
+ */
- //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
+ * 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
*/
- 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;
+ first: function(grouped) {
+ var me = this;
- /*
- * 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);
- }
- }
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(function(records) {
+ return records.length ? records[0] : undefined;
+ }, me, true);
+ } else {
+ return me.data.first();
}
},
/**
- * 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
+ * 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
*/
- onUpdateRecords: function(records, operation, success){
- if (success) {
- var i = 0,
- length = records.length,
- data = this.data,
- snapshot = this.snapshot,
- record;
+ last: function(grouped) {
+ var me = this;
- for (; i < length; ++i) {
- record = records[i];
- data.replace(record);
- if (snapshot) {
- snapshot.replace(record);
- }
- record.join(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();
}
},
/**
- * 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
+ * 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
*/
- onDestroyRecords: function(records, operation, success){
- if (success) {
- var me = this,
- i = 0,
- length = records.length,
- data = me.data,
- snapshot = me.snapshot,
- record;
+ sum: function(field, grouped) {
+ var me = this;
- for (; i < length; ++i) {
- record = records[i];
- record.unjoin(me);
- data.remove(record);
- if (snapshot) {
- snapshot.remove(record);
- }
- }
- me.removed = [];
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getSum, me, true, [field]);
+ } else {
+ return me.getSum(me.data.items, field);
}
},
- //inherit docs
- getNewRecords: function() {
- return this.data.filterBy(this.filterNew).items;
- },
+ // @private, see sum
+ getSum: function(records, field) {
+ var total = 0,
+ i = 0,
+ len = records.length;
- //inherit docs
- getUpdatedRecords: function() {
- return this.data.filterBy(this.filterUpdated).items;
+ for (; i < len; ++i) {
+ total += records[i].get(field);
+ }
+
+ return total;
},
/**
- * 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)
+ * 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
*/
- 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]);
- }
+ count: function(grouped) {
+ var me = this;
- if (me.remoteFilter) {
- //the load function will pick up the new filters and request the filtered data from the proxy
- me.load();
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(function(records) {
+ return records.length;
+ }, me, true);
} 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);
- }
- }
+ return me.getCount();
}
},
/**
- * 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.
+ * 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 {Object} The minimum value, if no items exist, undefined.
*/
- clearFilter: function(suppressEvent) {
+ min: function(field, grouped) {
var me = this;
- me.filters.clear();
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getMin, me, true, [field]);
+ } else {
+ return me.getMin(me.data.items, field);
+ }
+ },
- if (me.remoteFilter) {
- me.load();
- } else if (me.isFiltered()) {
- me.data = me.snapshot.clone();
- delete me.snapshot;
+ // @private, see min
+ getMin: function(records, field){
+ var i = 1,
+ len = records.length,
+ value, min;
- if (suppressEvent !== true) {
- me.fireEvent('datachanged', me);
- }
+ if (len > 0) {
+ min = records[0].get(field);
}
- },
- /**
- * Returns true if this store is currently filtered
- * @return {Boolean}
- */
- isFiltered: function() {
- var snapshot = this.snapshot;
- return !! snapshot && snapshot !== this.data;
+ for (; i < len; ++i) {
+ value = records[i].get(field);
+ if (value < min) {
+ min = value;
+ }
+ }
+ return min;
},
/**
- * Filter by a function. The specified function will be called for each
- * Record in this Store. If the function returns true the Record is included,
- * otherwise it is filtered out.
- * @param {Function} fn The function to be called. It will be passed the following parameters:
- * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
+ * 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 {Object} The maximum value, if no items exist, undefined.
*/
- filterBy: function(fn, scope) {
+ max: function(field, grouped) {
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:
- * @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);
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getMax, me, true, [field]);
+ } else {
+ return me.getMax(me.data.items, field);
+ }
},
- /**
- * 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;
+ // @private, see max
+ getMax: function(records, field) {
+ var i = 1,
+ len = records.length,
+ value,
+ max;
- //make sure each data element is an Ext.data.Model instance
- for (i = 0; i < length; i++) {
- record = data[i];
+ if (len > 0) {
+ max = records[0].get(field);
+ }
- if (! (record instanceof Ext.data.Model)) {
- data[i] = Ext.ModelManager.create(record, model);
+ for (; i < len; ++i) {
+ value = records[i].get(field);
+ if (value > max) {
+ max = value;
}
}
-
- this.loadRecords(data, {addRecords: append});
+ return max;
},
/**
- * 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
+ * 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 {Object} The average value, if no items exist, 0.
*/
- loadRecords: function(records, options) {
- var me = this,
- i = 0,
- length = records.length;
-
- options = options || {};
-
-
- if (!options.addRecords) {
- delete me.snapshot;
- me.data.clear();
+ 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);
}
+ },
- 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;
+ // @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);
}
- records[i].join(me);
+ return sum / len;
}
+ return 0;
+ },
- /*
- * 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();
+ /**
+ * 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;
- if (me.filterOnLoad && !me.remoteFilter) {
- me.filter();
+ 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));
}
+ }
+}, function() {
+ // A dummy empty store with a fieldless Model defined in it.
+ // Just for binding to Views which are instantiated with no Store defined.
+ // They will be able to run and render fine, and be bound to a generated Store later.
+ Ext.regStore('ext-empty-store', {fields: [], proxy: 'proxy'});
+});
- if (me.sortOnLoad && !me.remoteSort) {
- me.sort();
- }
+/**
+ * @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',
- me.resumeEvents();
- me.fireEvent('datachanged', me, records);
+ proxy: {
+ type: 'ajax',
+ url: 'get-images.php',
+ reader: {
+ type: 'json',
+ root: 'images',
+ idProperty: 'name'
+ }
},
- // PAGING METHODS
+ //alternatively, a {@link Ext.data.Model} name can be given (see {@link Ext.data.Store} for an example)
+ fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
+});
+
+ *
+ * This store is configured to consume a returned object of the form:
+{
+ images: [
+ {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
+ {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
+ ]
+}
+
+ *
+ * An object literal of this form could also be used as the {@link #data} config option.
+ *
+ * @xtype jsonstore
+ */
+Ext.define('Ext.data.JsonStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.json',
+
/**
- * 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
+ * @cfg {Ext.data.DataReader} reader @hide
*/
- loadPage: function(page) {
- var me = this;
-
- me.currentPage = page;
+ constructor: function(config) {
+ config = config || {};
- me.read({
- page: page,
- start: (page - 1) * me.pageSize,
- limit: me.pageSize,
- addRecords: !me.clearOnPageLoad
+ Ext.applyIf(config, {
+ proxy: {
+ type : 'ajax',
+ reader: 'json',
+ writer: 'json'
+ }
});
- },
- /**
- * Loads the next 'page' in the current data set
- */
- nextPage: function() {
- this.loadPage(this.currentPage + 1);
- },
+ this.callParent([config]);
+ }
+});
- /**
- * Loads the previous 'page' in the current data set
- */
- previousPage: function() {
- this.loadPage(this.currentPage - 1);
- },
+/**
+ * @class Ext.chart.axis.Time
+ * @extends Ext.chart.axis.Numeric
+ *
+ * A type of axis whose units are measured in time values. Use this axis
+ * for listing dates that you will want to group or dynamically change.
+ * If you just want to display dates as categories then use the
+ * Category class for axis instead.
+ *
+ * For example:
+ *
+ * axes: [{
+ * type: 'Time',
+ * position: 'bottom',
+ * fields: 'date',
+ * title: 'Day',
+ * dateFormat: 'M d',
+ *
+ * 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 between `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.
+ *
+ */
+Ext.define('Ext.chart.axis.Time', {
- // private
- clearData: function() {
- this.data.each(function(record) {
- record.unjoin();
- });
+ /* Begin Definitions */
- 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();
+ extend: 'Ext.chart.axis.Numeric',
- options = options || {};
+ alternateClassName: 'Ext.chart.TimeAxis',
- Ext.applyIf(options, {
- action : 'read',
- filters: me.filters.items,
- sorters: me.sorters.items,
- requestId: requestId
- });
- me.pendingRequests.push(requestId);
+ alias: 'axis.time',
- operation = Ext.create('Ext.data.Operation', options);
+ requires: ['Ext.data.Store', 'Ext.data.JsonStore'],
+
+ /* End Definitions */
- // 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
+ * @cfg {String/Boolean} dateFormat
+ * Indicates the format the date will be rendered on.
+ * For example: 'M d' will render the dates as 'Jan 30', etc.
+ * For a list of possible format strings see {@link Ext.Date Date}
*/
- 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);
- }
-
- },
-
+ dateFormat: false,
+
/**
- * Returns a unique requestId to track requests.
- * @private
+ * @cfg {Date} fromDate The starting date for the time axis.
*/
- getRequestId: function() {
- this.requestSeed = this.requestSeed || 1;
- return this.requestSeed++;
- },
-
+ fromDate: false,
+
/**
- * Handles a success pre-fetch
- * @private
- * @param {Ext.data.Operation} operation The operation that completed
+ * @cfg {Date} toDate The ending date for the time axis.
*/
- 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);
- }
+ toDate: false,
- //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
+ * @cfg {Array/Boolean} step
+ * 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.).
+ * Defaults to `[Ext.Date.DAY, 1]`.
*/
- 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();
- }
-
- },
-
+ step: [Ext.Date.DAY, 1],
/**
- * Purge the least recently used records in the prefetch if the purgeCount
- * has been exceeded.
+ * @cfg {Boolean} constrain
+ * 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.
*/
- purgeRecords: function() {
- var me = this,
- prefetchCount = me.prefetchData.getCount(),
- purgeCount = me.purgePageCount * me.pageSize,
- numRecordsToPurge = prefetchCount - purgeCount - 1,
- i = 0;
+ constrain: false,
- for (; i <= numRecordsToPurge; i++) {
- me.prefetchData.removeAt(0);
- }
- },
+ // Avoid roundtoDecimal call in Numeric Axis's constructor
+ roundToDecimal: false,
- /**
- * 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;
+ constructor: function (config) {
+ var me = this, label, f, df;
+ me.callParent([config]);
+ label = me.label || {};
+ df = this.dateFormat;
+ if (df) {
+ if (label.renderer) {
+ f = label.renderer;
+ label.renderer = function(v) {
+ v = f(v);
+ return Ext.Date.format(new Date(f(v)), df);
+ };
+ } else {
+ label.renderer = function(v) {
+ return Ext.Date.format(new Date(v >> 0), df);
+ };
}
}
- 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() {
+
+ doConstrain: 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 + ")");
+ store = me.chart.store,
+ data = [],
+ series = me.chart.series.items,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ fields = me.fields,
+ ln = fields.length,
+ range = me.getRange(),
+ min = range.min, max = range.max, i, l, excludes = [],
+ value, values, rec, data = [];
+ for (i = 0, l = series.length; i < l; i++) {
+ excludes[i] = series[i].__excludes;
}
- //
-
- 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");
+ store.each(function(record) {
+ for (i = 0; i < ln; i++) {
+ if (excludes[i]) {
+ continue;
}
- //
- range.push(record);
- }
- me.fireEvent('guaranteedrange', range, start, end);
- if (me.cb) {
- me.cb.call(me.scope || me, range);
+ value = record.get(fields[i]);
+ if (+value < +min) return;
+ if (+value > +max) return;
}
- }
-
- me.unmask();
- },
-
- // hack to support loadmask
- mask: function() {
- this.masked = true;
- this.fireEvent('beforeload');
+ data.push(record);
+ })
+ me.chart.substore = Ext.create('Ext.data.JsonStore', { model: store.model, data: data });
},
-
- // hack to support loadmask
- unmask: function() {
- if (this.masked) {
- this.fireEvent('load');
+
+ // Before rendering, set current default step count to be number of records.
+ processView: function () {
+ var me = this;
+ if (me.fromDate) {
+ me.minimum = +me.fromDate;
}
- },
-
- /**
- * 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();
+ if (me.toDate) {
+ me.maximum = +me.toDate;
}
- },
-
- /**
- * 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"
- });
- }
+ if (me.constrain) {
+ me.doConstrain();
}
- //
-
- 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
- });
+ // @private modifies the store and creates the labels for the axes.
+ calcEnds: function() {
+ var me = this, range, step = me.step;
+ if (step) {
+ range = me.getRange();
+ range = Ext.draw.Draw.snapEndsByDateAndStep(new Date(range.min), new Date(range.max), Ext.isNumber(step) ? [Date.MILLI, step]: step);
+ if (me.minimum) {
+ range.from = me.minimum;
}
- // 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;
-
- 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);
+ if (me.maximum) {
+ range.to = me.maximum;
}
+ range.step = (range.to - range.from) / range.steps;
+ return range;
} else {
- me.callParent(arguments);
+ return 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);
- }
+
+/**
+ * @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 */
+
/**
- * 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
+ * @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');
+ * }
+ * },
*/
- 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
+ * @cfg {String} type
+ * The type of series. Set in subclasses.
*/
- findRecord: function() {
- var me = this,
- index = me.find.apply(me, arguments);
- return index !== -1 ? me.getAt(index) : null;
- },
+ type: 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.
+ * @cfg {String} title
+ * The human-readable name of the series.
*/
- 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]);
- };
- },
+ title: null,
/**
- * Finds the index of the first matching Record in this store by a specific field value.
- * @param {String} fieldName The name of the Record field to test.
- * @param {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
+ * @cfg {Boolean} showInLegend
+ * Whether to show this series in the legend.
*/
- findExact: function(property, value, start) {
- return this.data.findIndexBy(function(rec) {
- return rec.get(property) === value;
- },
- this, start);
- },
+ showInLegend: true,
/**
- * Find the index of the first matching Record in this Store by a function.
- * If the function returns true it is considered a match.
- * @param {Function} fn The function to be called. It will be passed the following parameters:
- * @param {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
+ * @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.
*/
- findBy: function(fn, scope, start) {
- return this.data.findIndexBy(fn, scope, start);
+ renderer: function(sprite, record, attributes, index, store) {
+ return attributes;
},
/**
- * 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;
+ * @cfg {Array} shadowAttributes
+ * An array with shadow attributes
+ */
+ shadowAttributes: null,
- return data.collect(dataIndex, 'data', allowNull);
- },
+ //@private triggerdrawlistener flag
+ triggerAfterDraw: false,
/**
- * 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.
+ * @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
*/
- getCount: function() {
- return this.data.length || 0;
- },
+ 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
+ });
+ },
+
/**
- * 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
+ * Iterate over each of the records for this series. The default implementation simply iterates
+ * through the entire data store, but individual series implementations can override this to
+ * provide custom handling, e.g. adding/removing records.
+ * @param {Function} fn The function to execute for each record.
+ * @param {Object} scope Scope for the fn.
*/
- getTotalCount: function() {
- return this.totalCount;
+ eachRecord: function(fn, scope) {
+ var chart = this.chart;
+ (chart.substore || chart.store).each(fn, scope);
},
/**
- * 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.
+ * Return the number of records being displayed in this series. Defaults to the number of
+ * records in the store; individual series implementations can override to provide custom handling.
*/
- getAt: function(index) {
- return this.data.getAt(index);
+ getRecordCount: function() {
+ var chart = this.chart,
+ store = chart.substore || chart.store;
+ return store ? store.getCount() : 0;
},
/**
- * 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
+ * Determines whether the series item at the given index has been excluded, i.e. toggled off in the legend.
+ * @param index
*/
- getRange: function(start, end) {
- return this.data.getRange(start, end);
+ isExcluded: function(index) {
+ var excludes = this.__excludes;
+ return !!(excludes && excludes[index]);
},
- /**
- * 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;
- });
+ // @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;
},
- /**
- * 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);
+ // @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];
+ },
- /**
- * 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);
+ // @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();
+ }
},
/**
- * 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.
+ * 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 the following:
+ * @return {Ext.chart.series.Series} return.series the Series object to which the item belongs
+ * @return {Object} return.value the value(s) of the item's data point
+ * @return {Array} return.point the x/y coordinates relative to the chart box of a single point
+ * for this data item, which can be used as e.g. a tooltip anchor point.
+ * @return {Ext.draw.Sprite} return.sprite the item's rendering Sprite.
*/
- indexOfId: function(id) {
- return this.data.indexOfKey(id);
+ 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;
+ },
+
/**
- * Remove all items from the store.
- * @param {Boolean} silent Prevent the `clear` event from being fired.
+ * Hides all the elements in the series.
*/
- removeAll: function(silent) {
- var me = this;
+ hideAll: function() {
+ var me = this,
+ items = me.items,
+ item, len, i, j, l, sprite, shadows;
- me.clearData();
- if (me.snapshot) {
- me.snapshot.clear();
- }
- if (silent !== true) {
- me.fireEvent('clear', me);
+ 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);
+ }
+
+ if (sprite && sprite.shadows) {
+ shadows = sprite.shadows;
+ for (j = 0, l = shadows.length; j < l; ++j) {
+ shadows[j].setAttributes({
+ hidden: true
+ }, true);
+ }
+ }
}
},
- /*
- * Aggregation methods
+ /**
+ * 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;
+ },
/**
- * 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
+ * Returns a string with the color to be used for the series legend item.
*/
- 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();
+ 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';
},
/**
- * 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
+ * Checks whether the data field should be visible in the legend
+ * @private
+ * @param {Number} index The index of the current item
*/
- 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();
+ visibleInLegend: function(index){
+ var excludes = this.__excludes;
+ if (excludes) {
+ return !excludes[index];
}
+ return !this.seriesIsHidden;
},
/**
- * 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
+ * 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
*/
- sum: function(field, grouped) {
- var me = this;
+ setTitle: function(index, title) {
+ var me = this,
+ oldTitle = me.title;
- if (grouped && me.isGrouped()) {
- return me.aggregate(me.getSum, me, true, [field]);
+ if (Ext.isString(index)) {
+ title = index;
+ index = 0;
+ }
+
+ if (Ext.isArray(oldTitle)) {
+ oldTitle[index] = title;
} else {
- return me.getSum(me.data.items, field);
+ me.title = title;
}
- },
- // @private, see sum
- getSum: function(records, field) {
- var total = 0,
- i = 0,
- len = records.length;
+ me.fireEvent('titlechange', title, index);
+ }
+});
- for (; i < len; ++i) {
- total += records[i].get(field);
- }
+/**
+ * @class Ext.chart.series.Cartesian
+ * @extends Ext.chart.series.Series
+ *
+ * Common base class for series implementations which plot values using x/y coordinates.
+ */
+Ext.define('Ext.chart.series.Cartesian', {
- return total;
- },
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Series',
+
+ alternateClassName: ['Ext.chart.CartesianSeries', 'Ext.chart.CartesianChart'],
+
+ /* End Definitions */
/**
- * 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
+ * The field used to access the x axis value from the items from the data
+ * source.
+ *
+ * @cfg xField
+ * @type String
*/
- count: function(grouped) {
- var me = this;
+ xField: null,
- if (grouped && me.isGrouped()) {
- return me.aggregate(function(records) {
- return records.length;
- }, me, true);
- } else {
- return me.getCount();
- }
- },
+ /**
+ * The field used to access the y-axis value from the items from the data
+ * source.
+ *
+ * @cfg yField
+ * @type String
+ */
+ yField: null,
/**
- * 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.
+ * @cfg {String} axis
+ * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
+ * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
+ * relative scale will be used.
*/
- min: function(field, grouped) {
- var me = this;
+ axis: 'left',
- if (grouped && me.isGrouped()) {
- return me.aggregate(me.getMin, me, true, [field]);
- } else {
- return me.getMin(me.data.items, field);
- }
- },
+ getLegendLabels: function() {
+ var me = this,
+ labels = [],
+ combinations = me.combinations;
- // @private, see min
- getMin: function(records, field){
- var i = 1,
- len = records.length,
- value, min;
+ Ext.each([].concat(me.yField), function(yField, i) {
+ var title = me.title;
+ // Use the 'title' config if present, otherwise use the raw yField name
+ labels.push((Ext.isArray(title) ? title[i] : title) || yField);
+ });
- if (len > 0) {
- min = records[0].get(field);
+ // Handle yFields combined via legend drag-drop
+ if (combinations) {
+ Ext.each(combinations, function(combo) {
+ var label0 = labels[combo[0]],
+ label1 = labels[combo[1]];
+ labels[combo[1]] = label0 + ' & ' + label1;
+ labels.splice(combo[0], 1);
+ });
}
- for (; i < len; ++i) {
- value = records[i].get(field);
- if (value < min) {
- min = value;
- }
- }
- return min;
+ return labels;
},
/**
- * 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.
+ * @protected Iterates over a given record's values for each of this series's yFields,
+ * executing a given function for each value. Any yFields that have been combined
+ * via legend drag-drop will be treated as a single value.
+ * @param {Ext.data.Model} record
+ * @param {Function} fn
+ * @param {Object} scope
*/
- max: function(field, grouped) {
- var me = this;
+ eachYValue: function(record, fn, scope) {
+ Ext.each(this.getYValueAccessors(), function(accessor, i) {
+ fn.call(scope, accessor(record), i);
+ });
+ },
- if (grouped && me.isGrouped()) {
- return me.aggregate(me.getMax, me, true, [field]);
- } else {
- return me.getMax(me.data.items, field);
- }
+ /**
+ * @protected Returns the number of yField values, taking into account fields combined
+ * via legend drag-drop.
+ * @return {Number}
+ */
+ getYValueCount: function() {
+ return this.getYValueAccessors().length;
},
- // @private, see max
- getMax: function(records, field) {
- var i = 1,
- len = records.length,
- value,
- max;
+ combine: function(index1, index2) {
+ var me = this,
+ accessors = me.getYValueAccessors(),
+ accessor1 = accessors[index1],
+ accessor2 = accessors[index2];
- if (len > 0) {
- max = records[0].get(field);
- }
+ // Combine the yValue accessors for the two indexes into a single accessor that returns their sum
+ accessors[index2] = function(record) {
+ return accessor1(record) + accessor2(record);
+ };
+ accessors.splice(index1, 1);
- for (; i < len; ++i) {
- value = records[i].get(field);
- if (value > max) {
- max = value;
- }
- }
- return max;
+ me.callParent([index1, index2]);
+ },
+
+ clearCombinations: function() {
+ // Clear combined accessors, they'll get regenerated on next call to getYValueAccessors
+ delete this.yValueAccessors;
+ this.callParent();
},
/**
- * 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.
+ * @protected Returns an array of functions, each of which returns the value of the yField
+ * corresponding to function's index in the array, for a given record (each function takes the
+ * record as its only argument.) If yFields have been combined by the user via legend drag-drop,
+ * this list of accessors will be kept in sync with those combinations.
+ * @return {Array} array of accessor functions
*/
- 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);
+ getYValueAccessors: function() {
+ var me = this,
+ accessors = me.yValueAccessors;
+ if (!accessors) {
+ accessors = me.yValueAccessors = [];
+ Ext.each([].concat(me.yField), function(yField) {
+ accessors.push(function(record) {
+ return record.get(yField);
+ });
+ });
}
+ return accessors;
},
- // @private, see average
- getAverage: function(records, field) {
- var i = 0,
- len = records.length,
- sum = 0;
+ /**
+ * Calculate the min and max values for this series's xField.
+ * @return {Array} [min, max]
+ */
+ getMinMaxXValues: function() {
+ var me = this,
+ min, max,
+ xField = me.xField;
- if (records.length > 0) {
- for (; i < len; ++i) {
- sum += records[i].get(field);
- }
- return sum / len;
+ if (me.getRecordCount() > 0) {
+ min = Infinity;
+ max = -min;
+ me.eachRecord(function(record) {
+ var xValue = record.get(xField);
+ if (xValue > max) {
+ max = xValue;
+ }
+ if (xValue < min) {
+ min = xValue;
+ }
+ });
+ } else {
+ min = max = 0;
}
- return 0;
+ return [min, max];
},
/**
- * 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.
+ * Calculate the min and max values for this series's yField(s). Takes into account yField
+ * combinations, exclusions, and stacking.
+ * @return {Array} [min, max]
*/
- aggregate: function(fn, scope, grouped, args) {
- args = args || [];
- if (grouped && this.isGrouped()) {
- var groups = this.getGroups(),
- i = 0,
- len = groups.length,
- out = {},
- group;
+ getMinMaxYValues: function() {
+ var me = this,
+ stacked = me.stacked,
+ min, max,
+ positiveTotal, negativeTotal;
- for (; i < len; ++i) {
- group = groups[i];
- out[group.name] = fn.apply(scope || this, [group.children].concat(args));
+ function eachYValueStacked(yValue, i) {
+ if (!me.isExcluded(i)) {
+ if (yValue < 0) {
+ negativeTotal += yValue;
+ } else {
+ positiveTotal += yValue;
+ }
}
- 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'
+ function eachYValue(yValue, i) {
+ if (!me.isExcluded(i)) {
+ if (yValue > max) {
+ max = yValue;
+ }
+ if (yValue < min) {
+ min = yValue;
+ }
+ }
+ }
- proxy: {
- type: 'ajax',
- url: 'get-images.php',
- reader: {
- type: 'json',
- root: 'images',
- idProperty: 'name'
+ if (me.getRecordCount() > 0) {
+ min = Infinity;
+ max = -min;
+ me.eachRecord(function(record) {
+ if (stacked) {
+ positiveTotal = 0;
+ negativeTotal = 0;
+ me.eachYValue(record, eachYValueStacked);
+ if (positiveTotal > max) {
+ max = positiveTotal;
+ }
+ if (negativeTotal < min) {
+ min = negativeTotal;
+ }
+ } else {
+ me.eachYValue(record, eachYValue);
+ }
+ });
+ } else {
+ min = max = 0;
}
+ return [min, max];
},
- //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',
+ getAxesForXAndYFields: function() {
+ var me = this,
+ axes = me.chart.axes,
+ axis = [].concat(me.axis),
+ xAxis, yAxis;
- /**
- * @cfg {Ext.data.DataReader} reader @hide
- */
- constructor: function(config) {
- config = config || {};
+ if (Ext.Array.indexOf(axis, 'top') > -1) {
+ xAxis = 'top';
+ } else if (Ext.Array.indexOf(axis, 'bottom') > -1) {
+ xAxis = 'bottom';
+ } else {
+ if (axes.get('top')) {
+ xAxis = 'top';
+ } else if (axes.get('bottom')) {
+ xAxis = 'bottom';
+ }
+ }
- Ext.applyIf(config, {
- proxy: {
- type : 'ajax',
- reader: 'json',
- writer: 'json'
+ if (Ext.Array.indexOf(axis, 'left') > -1) {
+ yAxis = 'left';
+ } else if (Ext.Array.indexOf(axis, 'right') > -1) {
+ yAxis = 'right';
+ } else {
+ if (axes.get('left')) {
+ yAxis = 'left';
+ } else if (axes.get('right')) {
+ yAxis = 'right';
}
- });
+ }
- this.callParent([config]);
+ return {
+ xAxis: xAxis,
+ yAxis: yAxis
+ };
}
+
+
});
/**
- * @class Ext.chart.axis.Time
- * @extends Ext.chart.axis.Axis
+ * @class Ext.chart.series.Area
+ * @extends Ext.chart.series.Cartesian
*
- * 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.
+ * 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:
*
- * For example:
+ * @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 }
+ * ]
+ * });
*
- * 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')
- * }]
+ * 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'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 between `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
+ * 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.axis.Time', {
+Ext.define('Ext.chart.series.Area', {
/* Begin Definitions */
- extend: 'Ext.chart.axis.Category',
-
- alternateClassName: 'Ext.chart.TimeAxis',
+ extend: 'Ext.chart.series.Cartesian',
- alias: 'axis.time',
+ alias: 'series.area',
- requires: ['Ext.data.Store', 'Ext.data.JsonStore'],
+ requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'],
/* 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],
-
+ type: 'area',
+
+ // @private Area charts are alyways stacked
+ stacked: true,
+
/**
- * 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
+ * @cfg {Object} style
+ * Append styling properties to this object for it to override theme properties.
*/
- 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();
+ 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 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];
+
+ // @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;
}
- for (; i < l; i++) {
- acum += list[i];
+ yRes.push(ySum);
+ //reset sum accumulators
+ xSum = 0;
+ for (j = 0, ySum = []; j < yCompLen; ++j) {
+ ySum[j] = 0;
}
- return acum;
- },
- 'max': function(list) {
- if (!list.length || etype(list[0]) != 'Number') {
- return list[0];
+ }
+ }
+ return {
+ x: xRes,
+ y: yRes
+ };
+ },
+
+ // @private Get chart and data boundaries
+ getBounds: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ 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;
}
- return Math.max.apply(Math, list);
- },
- 'min': function(list) {
- if (!list.length || etype(list[0]) != 'Number') {
- return list[0];
+ }
+ minX = mmin(minX, xValue);
+ maxX = mmax(maxX, xValue);
+ maxY = mmax(maxY, acumY);
+ yValues.push(yValue);
+ }, me);
+
+ xScale = bbox.width / ((maxX - minX) || 1);
+ yScale = bbox.height / ((maxY - minY) || 1);
+
+ 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.getChartStore(),
+ 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;
}
- 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];
+ 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]);
}
- for (; i < l; i++) {
- acum += list[i];
+ if (!items[areaIndex]) {
+ items[areaIndex] = {
+ pointsUp: [],
+ pointsDown: [],
+ series: me
+ };
}
- return acum / l;
+ items[areaIndex].pointsUp.push([x, y]);
}
- };
- })(),
-
- // @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);
- }
+ // Close the paths
+ for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
}
- //get aggregation record from hash
- if (key in aggStore) {
- obj = aggStore[key];
- } else {
- obj = aggStore[key] = {};
- aggKeys.push(key);
- dates.push(value);
+ 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');
}
- //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));
+ // 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');
}
- });
- //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;
+ prevAreaIndex = areaIndex;
+ }
+ return {
+ paths: paths,
+ areasLen: bounds.areasLen
+ };
},
-
- // @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();
- }
- },
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ 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;
- // @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
- };
- }
- });
+ me.unHighlightItem();
+ me.cleanHighlights();
+ if (!store || !store.getCount()) {
+ return;
+ }
-/**
- * @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', {
+ paths = me.getPaths();
- /* Begin Definitions */
+ if (!me.areas) {
+ me.areas = [];
+ }
- mixins: {
- observable: 'Ext.util.Observable',
- labels: 'Ext.chart.Label',
- highlights: 'Ext.chart.Highlight',
- tips: 'Ext.chart.Tip',
- callouts: 'Ext.chart.Callout'
+ 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();
},
- /* 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,
+ // @private
+ onAnimate: function(sprite, attr) {
+ sprite.show();
+ return this.callParent(arguments);
+ },
- /**
- * @cfg {Boolean} showInLegend
- * Whether to show this series in the legend.
- */
- showInLegend: true,
+ // @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);
- /**
- * @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;
+ 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 || {}));
},
- /**
- * @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);
+ // @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;
- me.addEvents({
- scope: me,
- itemmouseover: true,
- itemmouseout: true,
- itemmousedown: true,
- itemmouseup: true,
- mouseleave: true,
- afterdraw: true,
+ label.setAttributes({
+ text: format(storeItem.get(field[index])),
+ hidden: true
+ }, 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
- });
+ bb = label.getBBox();
+ width = bb.width / 2;
+ height = bb.height / 2;
- me.mixins.observable.constructor.call(me, config);
+ 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;
- me.on({
- scope: me,
- itemmouseover: me.onItemMouseOver,
- itemmouseout: me.onItemMouseOut,
- mouseleave: me.onMouseLeave
- });
+ 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 set the bbox and clipBox for the series
- setBBox: function(noGutter) {
+ // @private
+ onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
var me = this,
chart = me.chart,
- chartBBox = chart.chartBBox,
- gutterX = noGutter ? 0 : chart.maxGutter[0],
- gutterY = noGutter ? 0 : chart.maxGutter[1],
- clipBox, bbox;
+ 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;
- clipBox = {
- x: chartBBox.x,
- y: chartBBox.y,
- width: chartBBox.width,
- height: chartBBox.height
- };
- me.clipBox = clipBox;
+ //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]);
- 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;
- },
+ norm = Math.sqrt(1 + a * a);
+ dir = [1 / norm, a / norm];
+ normal = [-dir[1], dir[0]];
- // @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');
- }
- }
- }));
+ //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;
}
- },
-
- // @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);
- }
- }
- },
+ //position
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
- // @private wrapper for the itemmouseout event.
- onItemMouseOut: function(item) {
- var me = this;
- if (item.series === me) {
- me.unHighlightItem();
- if (me.tooltip) {
- me.hideTip(item);
- }
+ //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;
}
- },
- // @private wrapper for the mouseleave event.
- onMouseLeave: function() {
- var me = this;
- me.unHighlightItem();
- if (me.tooltip) {
- me.hideTip();
+ //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);
}
},
- /**
- * 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;
- }
+ isItemInPoint: function(x, y, item, i) {
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];
+ 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 null;
- },
-
- isItemInPoint: function(x, y, item, i) {
return false;
},
/**
- * Hides all the elements in the series.
+ * Highlight this entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
*/
- 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);
+ 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);
}
}
},
/**
- * 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.
+ * UnHighlight this entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
*/
- 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;
+ 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
+ }
+ });
}
- 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
+ * 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
*/
- visibleInLegend: function(index){
- var excludes = this.__excludes;
- if (excludes) {
- return !excludes[index];
+ highlightItem: function(item) {
+ var me = this,
+ points, path;
+ if (!item) {
+ this.highlightSeries();
+ return;
}
- return !this.seriesIsHidden;
+ 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);
},
/**
- * 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
+ * Un-highlights the specified item. If no item is provided it will un-highlight the entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint
*/
- setTitle: function(index, title) {
- var me = this,
- oldTitle = me.title;
-
- if (Ext.isString(index)) {
- title = index;
- index = 0;
+ unHighlightItem: function(item) {
+ if (!item) {
+ this.unHighlightSeries();
}
- if (Ext.isArray(oldTitle)) {
- oldTitle[index] = title;
- } else {
- me.title = title;
+ if (this.highlightSprite) {
+ this.highlightSprite.hide(true);
}
+ },
- 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,
+ // @private
+ hideAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = true;
+ this.areas[this._index].hide(true);
+ this.drawSeries();
+ }
+ },
- /**
- * The field used to access the y-axis value from the items from the data
- * source.
- *
- * @cfg yField
- * @type String
- */
- yField: null,
+ // @private
+ showAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = false;
+ this.areas[this._index].show(true);
+ this.drawSeries();
+ }
+ },
/**
- * Indicates which axis the series will bind to
- *
- * @property axis
- * @type String
+ * 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
*/
- axis: 'left'
+ getLegendColor: function(index) {
+ var me = this;
+ return me.colorArrayStyle[index % me.colorArrayStyle.length];
+ }
});
-
/**
* @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.
-
-
+ *
+ * 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:
+ *
+ * @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 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'],
@@ -45999,7 +49439,7 @@ Ext.define('Ext.chart.series.Area', {
stacked: true,
/**
- * @cfg {Object} style
+ * @cfg {Object} style
* Append styling properties to this object for it to override theme properties.
*/
style: {},
@@ -46075,7 +49515,7 @@ Ext.define('Ext.chart.series.Area', {
getBounds: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
areas = [].concat(me.yField),
areasLen = areas.length,
xValues = [],
@@ -46142,8 +49582,8 @@ Ext.define('Ext.chart.series.Area', {
yValues.push(yValue);
}, me);
- xScale = bbox.width / (maxX - minX);
- yScale = bbox.height / (maxY - minY);
+ xScale = bbox.width / ((maxX - minX) || 1);
+ yScale = bbox.height / ((maxY - minY) || 1);
ln = xValues.length;
if ((ln > bbox.width) && me.areas) {
@@ -46168,7 +49608,7 @@ Ext.define('Ext.chart.series.Area', {
getPaths: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
first = true,
bounds = me.getBounds(),
bbox = bounds.bbox,
@@ -46213,7 +49653,7 @@ Ext.define('Ext.chart.series.Area', {
items[areaIndex].pointsUp.push([x, y]);
}
}
-
+
// Close the paths
for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
// Excluded series
@@ -46255,7 +49695,7 @@ Ext.define('Ext.chart.series.Area', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
surface = chart.surface,
animate = chart.animate,
group = me.group,
@@ -46270,7 +49710,7 @@ Ext.define('Ext.chart.series.Area', {
if (!store || !store.getCount()) {
return;
}
-
+
paths = me.getPaths();
if (!me.areas) {
@@ -46296,7 +49736,7 @@ Ext.define('Ext.chart.series.Area', {
path = paths.paths[areaIndex];
if (animate) {
//Add renderer to line. There is not a unique record associated with this.
- rendererAttributes = me.renderer(areaElem, false, {
+ rendererAttributes = me.renderer(areaElem, false, {
path: path,
// 'clip-rect': me.clipBox,
fill: colorArrayStyle[areaIndex % colorArrayLength],
@@ -46307,7 +49747,7 @@ Ext.define('Ext.chart.series.Area', {
to: rendererAttributes
});
} else {
- rendererAttributes = me.renderer(areaElem, false, {
+ rendererAttributes = me.renderer(areaElem, false, {
path: path,
// 'clip-rect': me.clipBox,
hidden: false,
@@ -46356,16 +49796,16 @@ Ext.define('Ext.chart.series.Area', {
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;
@@ -46424,11 +49864,11 @@ Ext.define('Ext.chart.series.Area', {
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;
@@ -46441,13 +49881,13 @@ Ext.define('Ext.chart.series.Area', {
//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])) {
@@ -46460,13 +49900,13 @@ Ext.define('Ext.chart.series.Area', {
//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"]
@@ -46487,14 +49927,14 @@ Ext.define('Ext.chart.series.Area', {
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])) {
@@ -46645,19 +50085,18 @@ Ext.define('Ext.chart.series.Area', {
* 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}
- *
+ * @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}
+ * { '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,
@@ -46731,12 +50170,12 @@ Ext.define('Ext.chart.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
*/
@@ -46770,7 +50209,7 @@ Ext.define('Ext.chart.series.Bar', {
opacity: 0.8,
color: '#f00'
},
-
+
shadowAttributes: [{
"stroke-width": 6,
"stroke-opacity": 0.05,
@@ -46808,11 +50247,11 @@ Ext.define('Ext.chart.series.Bar', {
// @private sets the bar girth.
getBarGirth: function() {
var me = this,
- store = me.chart.store,
+ store = me.chart.getChartStore(),
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);
},
@@ -46828,7 +50267,7 @@ Ext.define('Ext.chart.series.Bar', {
getBounds: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
bars = [].concat(me.yField),
barsLen = bars.length,
groupBarsLen = barsLen,
@@ -46860,8 +50299,8 @@ Ext.define('Ext.chart.series.Bar', {
axis = chart.axes.get(me.axis);
if (axis) {
out = axis.calcEnds();
- minY = out.from || axis.prevMin;
- maxY = mmax(out.to || axis.prevMax, 0);
+ minY = out.from;
+ maxY = out.to;
}
}
@@ -46871,8 +50310,8 @@ Ext.define('Ext.chart.series.Bar', {
fields: [].concat(me.yField)
});
out = axis.calcEnds();
- minY = out.from || axis.prevMin;
- maxY = mmax(out.to || axis.prevMax, 0);
+ minY = out.from;
+ maxY = out.to;
}
if (!Ext.isNumber(minY)) {
@@ -46929,7 +50368,7 @@ Ext.define('Ext.chart.series.Bar', {
getPaths: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
bounds = me.bounds = me.getBounds(),
items = me.items = [],
gutter = me.gutter / 100,
@@ -46960,14 +50399,14 @@ Ext.define('Ext.chart.series.Bar', {
top = bounds.zero;
totalDim = 0;
totalNegDim = 0;
- hasShadow = false;
+ 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);
+ height = Math.round((yValue - mmax(bounds.minY, 0)) * bounds.scale);
barAttr = {
fill: colors[(barsLen > 1 ? j : 0) % colorLength]
};
@@ -47073,7 +50512,7 @@ Ext.define('Ext.chart.series.Bar', {
shadowGroups = me.shadowGroups,
shadowAttributes = me.shadowAttributes,
shadowGroupsLn = shadowGroups.length,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
column = me.column,
items = me.items,
shadows = [],
@@ -47131,7 +50570,7 @@ Ext.define('Ext.chart.series.Bar', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
surface = chart.surface,
animate = chart.animate,
stacked = me.stacked,
@@ -47143,11 +50582,11 @@ Ext.define('Ext.chart.series.Bar', {
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);
@@ -47221,7 +50660,7 @@ Ext.define('Ext.chart.series.Bar', {
}
me.renderLabels();
},
-
+
// @private handled when creating a label.
onCreateLabel: function(storeItem, item, i, display) {
var me = this,
@@ -47235,7 +50674,7 @@ Ext.define('Ext.chart.series.Bar', {
group: group
}, endLabelStyle || {}));
},
-
+
// @private callback used when placing a label.
onPlaceLabel: function(label, storeItem, item, i, display, animate, j, index) {
// Determine the label's final position. Starts with the configured preferred value but
@@ -47271,6 +50710,7 @@ Ext.define('Ext.chart.series.Bar', {
text: text
});
+ label.isOutside = false;
if (column) {
if (display == outside) {
if (height + offsetY + attr.height > (yValue >= 0 ? zero - chartBBox.y : chartBBox.y + chartBBox.height - zero)) {
@@ -47279,6 +50719,7 @@ Ext.define('Ext.chart.series.Bar', {
} else {
if (height + offsetY > attr.height) {
display = outside;
+ label.isOutside = true;
}
}
x = attr.x + groupBarWidth / 2;
@@ -47296,6 +50737,7 @@ Ext.define('Ext.chart.series.Bar', {
else {
if (width + offsetX > attr.width) {
display = outside;
+ label.isOutside = true;
}
}
x = display == insideStart ?
@@ -47388,14 +50830,14 @@ Ext.define('Ext.chart.series.Bar', {
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;
@@ -47425,63 +50867,67 @@ Ext.define('Ext.chart.series.Bar', {
});
}
},
-
+
/**
* 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];
+ var me = this,
+ colorLength = me.colorArrayStyle.length;
+
+ if (me.style && me.style.fill) {
+ return me.style.fill;
+ } else {
+ return me.colorArrayStyle[index % colorLength];
+ }
+ },
+
+ highlightItem: function(item) {
+ this.callParent(arguments);
+ this.renderLabels();
+ },
+
+ unHighlightItem: function() {
+ this.callParent(arguments);
+ this.renderLabels();
+ },
+
+ cleanHighlights: function() {
+ this.callParent(arguments);
+ this.renderLabels();
}
});
/**
* @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}
+ * 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:
*
- * ## Example
- *
+ * @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}
+ * { '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: [{
+ * axes: [
+ * {
* type: 'Numeric',
* position: 'left',
* fields: ['data1'],
@@ -47491,13 +50937,16 @@ Ext.define('Ext.chart.series.Bar', {
* title: 'Sample Values',
* grid: true,
* minimum: 0
- * }, {
+ * },
+ * {
* type: 'Category',
* position: 'bottom',
* fields: ['name'],
* title: 'Sample Metrics'
- * }],
- * series: [{
+ * }
+ * ],
+ * series: [
+ * {
* type: 'column',
* axis: 'left',
* highlight: true,
@@ -47519,11 +50968,13 @@ Ext.define('Ext.chart.series.Bar', {
* },
* 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.
+ *
+ * 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', {
@@ -47557,7 +51008,7 @@ Ext.define('Ext.chart.series.Column', {
* @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
+ * One is setting a store element into the Gauge and selecting the field to be used from that store. Another one is instantiating 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:
@@ -47607,10 +51058,9 @@ Ext.define('Ext.chart.series.Gauge', {
highlightDuration: 150,
/**
- * @cfg {String} angleField
+ * @cfg {String} angleField (required)
* The store record field name to be used for the pie angles.
* The values bound to this field name must be positive real numbers.
- * This parameter is required.
*/
angleField: false,
@@ -47621,7 +51071,7 @@ Ext.define('Ext.chart.series.Gauge', {
needle: false,
/**
- * @cfg {Boolean|Number} donut
+ * @cfg {Boolean/Number} donut
* Use the entire disk or just a fraction of it for the gauge. Default's false.
*/
donut: false,
@@ -47688,7 +51138,7 @@ Ext.define('Ext.chart.series.Gauge', {
//@private updates some onbefore render parameters.
initialize: function() {
var me = this,
- store = me.chart.substore || me.chart.store;
+ store = me.chart.getChartStore();
//Add yFields to be used in Legend.js
me.yField = [];
if (me.label.field) {
@@ -47789,7 +51239,7 @@ Ext.define('Ext.chart.series.Gauge', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
group = me.group,
animate = me.chart.animate,
axis = me.chart.axes.get(0),
@@ -48010,91 +51460,96 @@ Ext.define('Ext.chart.series.Gauge', {
/**
* @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
+ *
+ * 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
+ * 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}
- *
+ * @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}
+ * { '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': 4, 'data2': 4, '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
+ * axes: [
+ * {
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['data1', 'data2'],
+ * label: {
+ * renderer: Ext.util.Format.numberRenderer('0,0')
+ * },
+ * title: 'Sample Values',
+ * grid: true,
+ * minimum: 0
* },
- * axis: 'left',
- * xField: 'name',
- * yField: 'data1',
- * markerCfg: {
- * type: 'cross',
- * size: 4,
- * radius: 4,
- * 'stroke-width': 0
+ * {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics'
* }
- * }, {
- * type: 'line',
- * highlight: {
- * size: 7,
- * radius: 7
+ * ],
+ * series: [
+ * {
+ * type: 'line',
+ * highlight: {
+ * size: 7,
+ * radius: 7
+ * },
+ * axis: 'left',
+ * xField: 'name',
+ * yField: 'data1',
+ * markerConfig: {
+ * type: 'cross',
+ * size: 4,
+ * radius: 4,
+ * 'stroke-width': 0
+ * }
* },
- * axis: 'left',
- * fill: true,
- * xField: 'name',
- * yField: 'data3',
- * markerCfg: {
- * type: 'circle',
- * size: 4,
- * radius: 4,
- * 'stroke-width': 0
+ * {
+ * type: 'line',
+ * highlight: {
+ * size: 7,
+ * radius: 7
+ * },
+ * axis: 'left',
+ * fill: true,
+ * xField: 'name',
+ * yField: 'data2',
+ * markerConfig: {
+ * 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
+ *
+ * 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.
*
- * **Note:** In the series definition remember to explicitly set the axis to bind the
+ * **Note:** In the series definition remember to explicitly set the axis to bind the
* values of the line series to. This can be done by using the `axis` configuration property.
*/
Ext.define('Ext.chart.series.Line', {
@@ -48110,9 +51565,9 @@ Ext.define('Ext.chart.series.Line', {
/* End Definitions */
type: 'line',
-
+
alias: 'series.line',
-
+
/**
* @cfg {String} axis
* The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
@@ -48125,7 +51580,7 @@ Ext.define('Ext.chart.series.Line', {
* 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,
@@ -48147,28 +51602,52 @@ Ext.define('Ext.chart.series.Line', {
'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.
+ * An object containing style properties for the visualization lines and fill.
+ * These styles will override the theme styles. The following are valid style properties:
+ *
+ * - `stroke` - an rgb or hex color string for the background color of the line
+ * - `stroke-width` - the width of the stroke (integer)
+ * - `fill` - the background fill color string (hex or rgb), only works if {@link #fill} is `true`
+ * - `opacity` - the opacity of the line and the fill color (decimal)
+ *
+ * Example usage:
+ *
+ * style: {
+ * stroke: '#00ff00',
+ * 'stroke-width': 10,
+ * fill: '#80A080',
+ * opacity: 0.2
+ * }
*/
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.
+ * @cfg {Boolean/Number} smooth
+ * If set to `true` or a non-zero number, the line will be smoothed/rounded around its points; otherwise
+ * straight line segments will be drawn.
+ *
+ * A numeric value is interpreted as a divisor of the horizontal distance between consecutive points in
+ * the line; larger numbers result in sharper curves while smaller numbers result in smoother curves.
+ *
+ * If set to `true` then a default numeric value of 3 will be used. Defaults to `false`.
*/
smooth: false,
+ /**
+ * @private Default numeric smoothing value to be used when {@link #smooth} = true.
+ */
+ defaultSmoothness: 3,
+
/**
* @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.
+ * 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,
@@ -48213,12 +51692,12 @@ Ext.define('Ext.chart.series.Line', {
me.markerGroup = surface.getGroup(me.seriesId + '-markers');
}
if (shadow) {
- for (i = 0, l = this.shadowAttributes.length; i < l; i++) {
+ for (i = 0, l = me.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...
@@ -48229,7 +51708,7 @@ Ext.define('Ext.chart.series.Line', {
ySum = 0,
xRes = [xValues[0]],
yRes = [yValues[0]];
-
+
for (; i < len; ++i) {
xSum += xValues[i] || 0;
ySum += yValues[i] || 0;
@@ -48252,56 +51731,68 @@ Ext.define('Ext.chart.series.Line', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
- surface = chart.surface,
- chartBBox = chart.chartBBox,
+ chartAxes = chart.axes,
+ store = chart.getChartStore(),
+ storeCount = store.getCount(),
+ surface = me.chart.surface,
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,
+ shadowAttributes = me.shadowAttributes,
+ smooth = me.smooth,
lnsh = shadowGroups.length,
dummyPath = ["M"],
path = ["M"],
+ renderPath = ["M"],
+ smoothPath = ["M"],
markerIndex = chart.markerIndex,
axes = [].concat(me.axis),
- shadowGroup,
shadowBarAttr,
xValues = [],
+ xValueMap = {},
yValues = [],
- numericAxis = true,
- axisCount = 0,
+ yValueMap = {},
onbreak = false,
+ storeIndices = [],
markerStyle = me.markerStyle,
- seriesStyle = me.seriesStyle,
- seriesLabelStyle = me.seriesLabelStyle,
+ seriesStyle = me.style,
colorArrayStyle = me.colorArrayStyle,
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
- posHash = {
- 'left': 'right',
- 'right': 'left',
- 'top': 'bottom',
- 'bottom': 'top'
- },
- 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,
+ isNumber = Ext.isNumber,
+ seriesIdx = me.seriesIdx,
+ boundAxes = me.getAxesForXAndYFields(),
+ boundXAxis = boundAxes.xAxis,
+ boundYAxis = boundAxes.yAxis,
+ shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
+ x, y, prevX, prevY, firstX, 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, count;
-
- //if store is empty then there's nothing to draw.
- if (!store || !store.getCount()) {
+ endLineStyle, type, count, items;
+
+ if (me.fireEvent('beforedraw', me) === false) {
return;
}
-
+
+ //if store is empty or the series is excluded in the legend then there's nothing to draw.
+ if (!storeCount || me.seriesIsHidden) {
+ items = this.items;
+ if (items) {
+ for (i = 0, ln = items.length; i < ln; ++i) {
+ if (items[i].sprite) {
+ items[i].sprite.hide(true);
+ }
+ }
+ }
+ return;
+ }
+
//prepare style objects for line and markers
- endMarkerStyle = Ext.apply(markerStyle, me.markerConfig);
+ endMarkerStyle = Ext.apply(markerStyle || {}, me.markerConfig);
type = endMarkerStyle.type;
delete endMarkerStyle.type;
- endLineStyle = Ext.apply(seriesStyle, me.style);
+ endLineStyle = seriesStyle;
//if no stroke with is specified force it to 0.5 because this is
//about making *lines*
if (!endLineStyle['stroke-width']) {
@@ -48325,107 +51816,82 @@ Ext.define('Ext.chart.series.Line', {
}, true);
}
}
-
+
me.unHighlightItem();
me.cleanHighlights();
me.setBBox();
bbox = me.bbox;
-
me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];
-
- chart.axes.each(function(axis) {
- //only apply position calculations to axes that affect this series
- //this means the axis in the position referred by this series and also
- //the axis in the other coordinate for this series. For example: (left, top|bottom),
- //or (top, left|right), etc.
- if (axis.position == me.axis || axis.position != posHash[me.axis]) {
- axisCount++;
- if (axis.type != 'Numeric') {
- numericAxis = false;
- return;
+ for (i = 0, ln = axes.length; i < ln; i++) {
+ axis = chartAxes.get(axes[i]);
+ if (axis) {
+ ends = axis.calcEnds();
+ if (axis.position == 'top' || axis.position == 'bottom') {
+ minX = ends.from;
+ maxX = ends.to;
}
- numericAxis = (numericAxis && axis.type == 'Numeric');
- 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;
- }
+ else {
+ minY = ends.from;
+ maxY = ends.to;
}
}
- });
-
- //If there's only one axis specified for a series, then we set the default type of the other
- //axis to a category axis. So in this case numericAxis, which would be true if both axes affecting
- //the series are numeric should be false.
- if (numericAxis && axisCount == 1) {
- numericAxis = false;
}
-
// 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)) {
- if (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;
- } else if (numericAxis) {
- axis = Ext.create('Ext.chart.axis.Axis', {
- chart: chart,
- fields: [].concat(me.xField),
- forceMinMax: true
- }).calcEnds();
- minX = axis.from;
- maxX = axis.to;
- }
+ if (me.xField && !isNumber(minX) &&
+ (boundXAxis == 'bottom' || boundXAxis == 'top') &&
+ !chartAxes.get(boundXAxis)) {
+ 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)) {
- if (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;
- } else if (numericAxis) {
- axis = Ext.create('Ext.chart.axis.Axis', {
- chart: chart,
- fields: [].concat(me.yField),
- forceMinMax: true
- }).calcEnds();
- minY = axis.from;
- maxY = axis.to;
- }
+ if (me.yField && !isNumber(minY) &&
+ (boundYAxis == 'right' || boundYAxis == 'left') &&
+ !chartAxes.get(boundYAxis)) {
+ 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);
+ xScale = bbox.width / ((storeCount - 1) || 1);
}
else {
- xScale = bbox.width / (maxX - minX);
+ xScale = bbox.width / ((maxX - minX) || (storeCount -1) || 1);
}
if (isNaN(minY)) {
minY = 0;
- yScale = bbox.height / (store.getCount() - 1);
- }
+ yScale = bbox.height / ((storeCount - 1) || 1);
+ }
else {
- yScale = bbox.height / (maxY - minY);
+ yScale = bbox.height / ((maxY - minY) || (storeCount - 1) || 1);
}
-
- store.each(function(record, i) {
+
+ // Extract all x and y values from the store
+ me.eachRecord(function(record, i) {
xValue = record.get(me.xField);
+
+ // Ensure a value
+ if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)
+ //set as uniform distribution if the axis is a category axis.
+ || boundXAxis && chartAxes.get(boundXAxis) && chartAxes.get(boundXAxis).type == 'Category') {
+ if (xValue in xValueMap) {
+ xValue = xValueMap[xValue];
+ } else {
+ xValue = xValueMap[xValue] = i;
+ }
+ }
+
+ // Filter out values that don't fit within the pan/zoom buffer area
yValue = record.get(me.yField);
//skip undefined values
if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
@@ -48437,19 +51903,15 @@ Ext.define('Ext.chart.series.Line', {
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' && !numericAxis)) {
- xValue = i;
- }
- if (typeof yValue == 'string' || typeof yValue == 'object'
+ if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)
//set as uniform distribution if the axis is a category axis.
- || (me.axis != 'left' && me.axis != 'right' && !numericAxis)) {
+ || boundYAxis && chartAxes.get(boundYAxis) && chartAxes.get(boundYAxis).type == 'Category') {
yValue = i;
}
+ storeIndices.push(i);
xValues.push(xValue);
yValues.push(yValue);
- }, me);
+ });
ln = xValues.length;
if (ln > bbox.width) {
@@ -48478,11 +51940,12 @@ Ext.define('Ext.chart.series.Line', {
if (onbreak) {
onbreak = false;
path.push('M');
- }
+ }
path = path.concat([x, y]);
}
if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) {
firstY = y;
+ firstX = x;
}
// If this is the first line, create a dummypath to animate in from.
if (!me.line || chart.resizing) {
@@ -48517,15 +51980,16 @@ Ext.define('Ext.chart.series.Line', {
group: [group, markerGroup],
x: 0, y: 0,
translate: {
- x: prevX || x,
+ x: +(prevX || x),
y: prevY || (bbox.y + bbox.height / 2)
},
- value: '"' + xValue + ', ' + yValue + '"'
+ value: '"' + xValue + ', ' + yValue + '"',
+ zIndex: 4000
}, endMarkerStyle));
marker._to = {
translate: {
- x: x,
- y: y
+ x: +x,
+ y: +y
}
};
} else {
@@ -48536,7 +52000,8 @@ Ext.define('Ext.chart.series.Line', {
}, true);
marker._to = {
translate: {
- x: x, y: y
+ x: +x,
+ y: +y
}
};
}
@@ -48546,25 +52011,29 @@ Ext.define('Ext.chart.series.Line', {
value: [xValue, yValue],
point: [x, y],
sprite: marker,
- storeItem: store.getAt(i)
+ storeItem: store.getAt(storeIndices[i])
});
prevX = x;
prevY = y;
}
-
+
if (path.length <= 1) {
//nothing to be rendered
- return;
+ return;
}
-
+
if (me.smooth) {
- path = Ext.draw.Draw.smooth(path, 6);
+ smoothPath = Ext.draw.Draw.smooth(path, isNumber(smooth) ? smooth : me.defaultSmoothness);
}
-
+
+ renderPath = smooth ? smoothPath : path;
+
//Correct path if we're animating timeAxis intervals
if (chart.markerIndex && me.previousPath) {
fromPath = me.previousPath;
- fromPath.splice(1, 2);
+ if (!smooth) {
+ Ext.Array.erase(fromPath, 1, 2);
+ }
} else {
fromPath = path;
}
@@ -48577,9 +52046,15 @@ Ext.define('Ext.chart.series.Line', {
path: dummyPath,
stroke: endLineStyle.stroke || endLineStyle.fill
}, endLineStyle || {}));
+
+ if (enableShadows) {
+ me.line.setAttributes(Ext.apply({}, me.shadowOptions), true);
+ }
+
//unset fill here (there's always a default fill withing the themes).
me.line.setAttributes({
- fill: 'none'
+ fill: 'none',
+ zIndex: 3000
});
if (!endLineStyle.stroke && colorArrayLength) {
me.line.setAttributes({
@@ -48588,11 +52063,11 @@ Ext.define('Ext.chart.series.Line', {
}
if (enableShadows) {
//create shadows
- shadows = me.line.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({}, {
+ shadow = surface.add(Ext.apply({}, {
type: 'path',
group: shadowGroups[shindex]
}, shadowBarAttr));
@@ -48601,17 +52076,17 @@ Ext.define('Ext.chart.series.Line', {
}
}
if (me.fill) {
- fillPath = path.concat([
+ fillPath = renderPath.concat([
["L", x, bbox.y + bbox.height],
- ["L", bbox.x, bbox.y + bbox.height],
- ["L", bbox.x, firstY]
+ ["L", firstX, bbox.y + bbox.height],
+ ["L", firstX, firstY]
]);
if (!me.fillPath) {
me.fillPath = surface.add({
group: group,
type: 'path',
opacity: endLineStyle.opacity || 0.3,
- fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill,
+ fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength],
path: dummyPath
});
}
@@ -48621,12 +52096,13 @@ Ext.define('Ext.chart.series.Line', {
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);
+ rendererAttributes = me.renderer(line, false, { path: renderPath }, 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;
+ line.show(true);
if (chart.markerIndex && me.previousPath) {
me.animation = animation = me.onAnimate(line, {
to: rendererAttributes,
@@ -48643,24 +52119,27 @@ Ext.define('Ext.chart.series.Line', {
if (enableShadows) {
shadows = line.shadows;
for(j = 0; j < lnsh; j++) {
+ shadows[j].show(true);
if (chart.markerIndex && me.previousPath) {
me.onAnimate(shadows[j], {
- to: { path: path },
+ to: { path: renderPath },
from: { path: fromPath }
});
} else {
me.onAnimate(shadows[j], {
- to: { path: path }
+ to: { path: renderPath }
});
}
}
}
//animate fill path
if (fill) {
+ me.fillPath.show(true);
me.onAnimate(me.fillPath, {
to: Ext.apply({}, {
path: fillPath,
- fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill
+ fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength],
+ 'stroke-width': 0
}, endLineStyle || {})
});
}
@@ -48675,16 +52154,21 @@ Ext.define('Ext.chart.series.Line', {
me.onAnimate(item, {
to: Ext.apply(rendererAttributes, endMarkerStyle || {})
});
+ item.show(true);
}
- }
+ }
}
for(; count < markerCount; count++) {
item = markerGroup.getAt(count);
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);
+ rendererAttributes = me.renderer(me.line, false, { path: renderPath, hidden: false }, i, store);
Ext.apply(rendererAttributes, endLineStyle || {}, {
stroke: endLineStyle.stroke || endLineStyle.fill
});
@@ -48696,13 +52180,15 @@ Ext.define('Ext.chart.series.Line', {
shadows = me.line.shadows;
for(j = 0; j < lnsh; j++) {
shadows[j].setAttributes({
- path: path
+ path: renderPath,
+ hidden: false
}, true);
}
}
if (me.fill) {
me.fillPath.setAttributes({
- path: fillPath
+ path: fillPath,
+ hidden: false
}, true);
}
if (showMarkers) {
@@ -48713,8 +52199,9 @@ Ext.define('Ext.chart.series.Line', {
if (item) {
rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true);
+ item.show(true);
}
- }
+ }
}
for(; count < markerCount; count++) {
item = markerGroup.getAt(count);
@@ -48724,13 +52211,19 @@ Ext.define('Ext.chart.series.Line', {
}
if (chart.markerIndex) {
- path.splice(1, 0, path[1], path[2]);
+ if (me.smooth) {
+ Ext.Array.erase(path, 1, 2);
+ } else {
+ Ext.Array.splice(path, 1, 0, path[1], path[2]);
+ }
me.previousPath = path;
}
me.renderLabels();
me.renderCallouts();
+
+ me.fireEvent('draw', me);
},
-
+
// @private called when a label is to be created.
onCreateLabel: function(storeItem, item, i, display) {
var me = this,
@@ -48747,7 +52240,7 @@ Ext.define('Ext.chart.series.Line', {
'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,
@@ -48761,12 +52254,12 @@ Ext.define('Ext.chart.series.Line', {
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',
@@ -48783,7 +52276,7 @@ Ext.define('Ext.chart.series.Line', {
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();
@@ -48799,7 +52292,7 @@ Ext.define('Ext.chart.series.Line', {
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, {
@@ -48813,7 +52306,7 @@ Ext.define('Ext.chart.series.Line', {
x: x,
y: y
}, true);
- if (resizing) {
+ if (resizing && me.animation) {
me.animation.on('afteranimate', function() {
label.show(true);
});
@@ -48827,20 +52320,20 @@ Ext.define('Ext.chart.series.Line', {
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 (me.line && !me.highlighted) {
+ if (!('__strokeWidth' in me.line)) {
+ me.line.__strokeWidth = me.line.attr['stroke-width'] || 0;
}
- if (this.line.__anim) {
- this.line.__anim.paused = true;
+ if (me.line.__anim) {
+ me.line.__anim.paused = true;
}
- this.line.__anim = Ext.create('Ext.fx.Anim', {
- target: this.line,
+ me.line.__anim = Ext.create('Ext.fx.Anim', {
+ target: me.line,
to: {
- 'stroke-width': this.line.__strokeWidth + 3
+ 'stroke-width': me.line.__strokeWidth + 3
}
});
- this.highlighted = true;
+ me.highlighted = true;
}
},
@@ -48848,14 +52341,14 @@ Ext.define('Ext.chart.series.Line', {
unHighlightItem: function() {
var me = this;
me.callParent(arguments);
- if (this.line && this.highlighted) {
- this.line.__anim = Ext.create('Ext.fx.Anim', {
- target: this.line,
+ if (me.line && me.highlighted) {
+ me.line.__anim = Ext.create('Ext.fx.Anim', {
+ target: me.line,
to: {
- 'stroke-width': this.line.__strokeWidth
+ 'stroke-width': me.line.__strokeWidth
}
});
- this.highlighted = false;
+ me.highlighted = false;
}
},
@@ -48864,7 +52357,7 @@ Ext.define('Ext.chart.series.Line', {
if (!display) {
return;
}
-
+
var me = this,
chart = me.chart,
surface = chart.surface,
@@ -48896,11 +52389,11 @@ Ext.define('Ext.chart.series.Line', {
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) {
@@ -48920,7 +52413,7 @@ Ext.define('Ext.chart.series.Line', {
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])) {
@@ -48933,13 +52426,13 @@ Ext.define('Ext.chart.series.Line', {
//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, {
@@ -48966,7 +52459,7 @@ Ext.define('Ext.chart.series.Line', {
callout[p].show(true);
}
},
-
+
isItemInPoint: function(x, y, item, i) {
var me = this,
items = me.items,
@@ -48985,10 +52478,10 @@ Ext.define('Ext.chart.series.Line', {
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];
}
@@ -49001,22 +52494,22 @@ Ext.define('Ext.chart.series.Line', {
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);
+ Ext.chart.series.Cartesian.prototype.hideAll.call(me);
}
else {
- Ext.chart.series.Line.superclass.showAll.call(me);
+ Ext.chart.series.Cartesian.prototype.showAll.call(me);
}
if (me.line) {
me.line.setAttributes({
@@ -49038,43 +52531,43 @@ Ext.define('Ext.chart.series.Line', {
}, 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
+ *
+ * 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
+ * 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}
*
+ * @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}
+ * { '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,
+ * height: 350,
* animate: true,
* store: store,
* theme: 'Base:gradients',
@@ -49083,22 +52576,22 @@ Ext.define('Ext.chart.series.Line', {
* 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) + '%');
- * }
+ * 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
- * }
+ * segment: {
+ * margin: 20
+ * }
* },
* label: {
* field: 'name',
@@ -49106,16 +52599,18 @@ Ext.define('Ext.chart.series.Line', {
* 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.
- *
+ *
+ * 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', {
@@ -49129,7 +52624,7 @@ Ext.define('Ext.chart.series.Pie', {
/* End Definitions */
type: "pie",
-
+
alias: 'series.pie',
rad: Math.PI / 180,
@@ -49141,10 +52636,9 @@ Ext.define('Ext.chart.series.Pie', {
highlightDuration: 150,
/**
- * @cfg {String} angleField
+ * @cfg {String} angleField (required)
* The store record field name to be used for the pie angles.
* The values bound to this field name must be positive real numbers.
- * This parameter is required.
*/
angleField: false,
@@ -49152,12 +52646,11 @@ Ext.define('Ext.chart.series.Pie', {
* @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
+ * @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.
@@ -49174,13 +52667,13 @@ Ext.define('Ext.chart.series.Pie', {
* @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,
@@ -49195,7 +52688,7 @@ Ext.define('Ext.chart.series.Pie', {
}
}
});
- Ext.apply(me, config, {
+ Ext.apply(me, config, {
shadowAttributes: [{
"stroke-width": 6,
"stroke-opacity": 1,
@@ -49233,12 +52726,13 @@ Ext.define('Ext.chart.series.Pie', {
surface.customAttributes.segment = function(opt) {
return me.getSegment(opt);
};
+ me.__excludes = me.__excludes || [];
},
-
+
//@private updates some onbefore render parameters.
initialize: function() {
var me = this,
- store = me.chart.substore || me.chart.store;
+ store = me.chart.getChartStore();
//Add yFields to be used in Legend.js
me.yField = [];
if (me.label.field) {
@@ -49254,58 +52748,81 @@ Ext.define('Ext.chart.series.Pie', {
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,
+ x5 = 0, y5 = 0, x6 = 0, y6 = 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;
+ c1 = cos(a1), s1 = sin(a1),
+ c2 = cos(a2), s2 = sin(a2),
+ cm = cos(midAngle), sm = sin(midAngle),
+ flag = 0, hsqr2 = 0.7071067811865476; // sqrt(0.5)
- x += margin * cos(midAngle);
- y += margin * sin(midAngle);
-
- x1 = x + opt.startRho * cos(a1);
- y1 = y + opt.startRho * sin(a1);
+ if (a2 - a1 < delta) {
+ return {path: ""};
+ }
- x2 = x + opt.endRho * cos(a1);
- y2 = y + opt.endRho * sin(a1);
+ if (margin !== 0) {
+ x += margin * cm;
+ y += margin * sm;
+ }
- x3 = x + opt.startRho * cos(a2);
- y3 = y + opt.startRho * sin(a2);
+ x2 = x + opt.endRho * c1;
+ y2 = y + opt.endRho * s1;
- x4 = x + opt.endRho * cos(a2);
- y4 = y + opt.endRho * sin(a2);
+ x4 = x + opt.endRho * c2;
+ y4 = y + opt.endRho * s2;
- if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) {
- singleSlice = true;
+ if (Math.abs(x2 - x4) + Math.abs(y2 - y4) < delta) {
+ cm = hsqr2;
+ sm = -hsqr2;
+ flag = 1;
}
- //Solves mysterious clipping bug with IE
- if (singleSlice) {
+
+ x6 = x + opt.endRho * cm;
+ y6 = y + opt.endRho * sm;
+
+ // TODO(bei): It seems that the canvas engine cannot render half circle command correctly on IE.
+ // Better fix the VML engine for half circles.
+
+ if (opt.startRho !== 0) {
+ x1 = x + opt.startRho * c1;
+ y1 = y + opt.startRho * s1;
+
+ x3 = x + opt.startRho * c2;
+ y3 = y + opt.startRho * s2;
+
+ x5 = x + opt.startRho * cm;
+ y5 = y + opt.startRho * sm;
+
return {
path: [
- ["M", x1, y1],
- ["L", x2, y2],
- ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
- ["Z"]]
+ ["M", x2, y2],
+ ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
+ ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
+ ["L", x3, y3],
+ ["A", opt.startRho, opt.startRho, 0, flag, 0, x5, y5], ["L", x5, y5],
+ ["A", opt.startRho, opt.startRho, 0, 0, 0, x1, y1], ["L", x1, y1],
+ ["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"]]
+ ["M", x, y],
+ ["L", x2, y2],
+ ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
+ ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
+ ["L", x, y],
+ ["Z"]
+ ]
};
}
},
@@ -49320,11 +52837,10 @@ Ext.define('Ext.chart.series.Pie', {
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);
+ midAngle = -(startAngle + endAngle) * rad / 2,
+ r = (item.endRho + item.startRho) / 2,
+ xm = x + r * Math.cos(midAngle),
+ ym = y - r * Math.sin(midAngle);
item.middle = {
x: xm,
@@ -49337,7 +52853,7 @@ Ext.define('Ext.chart.series.Pie', {
*/
drawSeries: function() {
var me = this,
- store = me.chart.substore || me.chart.store,
+ store = me.chart.getChartStore(),
group = me.group,
animate = me.chart.animate,
field = me.angleField || me.field || me.xField,
@@ -49371,6 +52887,7 @@ Ext.define('Ext.chart.series.Pie', {
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
gutterX = chart.maxGutter[0],
gutterY = chart.maxGutter[1],
+ abs = Math.abs,
rendererAttributes,
shadowGroup,
shadowAttr,
@@ -49398,7 +52915,7 @@ Ext.define('Ext.chart.series.Pie', {
path,
p,
spriteOptions, bbox;
-
+
Ext.apply(seriesStyle, me.style || {});
me.setBBox();
@@ -49409,12 +52926,12 @@ Ext.define('Ext.chart.series.Pie', {
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();
@@ -49439,25 +52956,26 @@ Ext.define('Ext.chart.series.Pie', {
}
}, this);
+ totalField = totalField || 1;
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;
+ value = 0;
+ } else {
+ value = record.get(field);
+ if (first == 0) {
+ first = 1;
+ }
}
+
// First slice
- if (!i || first == 0) {
- angle = 360 - middleAngle;
- me.firstAngle = angle;
- middleAngle = angle - 360 * value / totalField / 2;
+ if (first == 1) {
+ first = 2;
+ me.firstAngle = angle = 360 * value / totalField / 2;
+ for (j = 0; j < i; j++) {
+ slices[j].startAngle = slices[j].endAngle = me.firstAngle;
+ }
}
+
endAngle = angle - 360 * value / totalField;
slice = {
series: me,
@@ -49473,20 +52991,11 @@ Ext.define('Ext.chart.series.Pie', {
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++) {
@@ -49501,7 +53010,8 @@ Ext.define('Ext.chart.series.Pie', {
rho: slice.rho,
startRho: rhoAcum + (deltaRho * donut / 100),
endRho: rhoAcum + deltaRho
- }
+ },
+ hidden: !slice.value && (slice.startAngle % 360) == (slice.endAngle % 360)
};
//create shadows
for (shindex = 0, shadows = []; shindex < lnsh; shindex++) {
@@ -49520,9 +53030,7 @@ Ext.define('Ext.chart.series.Pie', {
to: shadowAttr
});
} else {
- shadowAttr = me.renderer(shadow, store.getAt(i), Ext.apply(shadowAttr, {
- hidden: false
- }), i, store);
+ shadowAttr = me.renderer(shadow, store.getAt(i), shadowAttr, i, store);
shadow.setAttributes(shadowAttr, true);
}
shadows.push(shadow);
@@ -49533,10 +53041,6 @@ Ext.define('Ext.chart.series.Pie', {
}
//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);
@@ -49550,7 +53054,8 @@ Ext.define('Ext.chart.series.Pie', {
rho: slice.rho,
startRho: rhoAcum + (deltaRho * donut / 100),
endRho: rhoAcum + deltaRho
- }
+ },
+ hidden: (!slice.value && (slice.startAngle % 360) == (slice.endAngle % 360))
}, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
item = Ext.apply({},
rendererAttributes.segment, {
@@ -49601,7 +53106,7 @@ Ext.define('Ext.chart.series.Pie', {
rhoAcum += deltaRho;
}
}
-
+
// Hide unused bars
ln = group.getCount();
for (i = 0; i < ln; i++) {
@@ -49634,7 +53139,7 @@ Ext.define('Ext.chart.series.Pie', {
centerY = me.centerY,
middle = item.middle,
endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {});
-
+
return me.chart.surface.add(Ext.apply({
'type': 'text',
'text-anchor': 'middle',
@@ -49666,9 +53171,13 @@ Ext.define('Ext.chart.series.Pie', {
theta = Math.atan2(y, x || 1),
dg = theta * 180 / Math.PI,
prevDg;
-
+ if (this.__excludes && this.__excludes[i]) {
+ opt.hidden = true;
+ }
function fixAngle(a) {
- if (a < 0) a += 360;
+ if (a < 0) {
+ a += 360;
+ }
return a % 360;
}
@@ -49712,7 +53221,7 @@ Ext.define('Ext.chart.series.Pie', {
}
//ensure the object has zero translation
opt.translate = {
- x: 0, y: 0
+ x: 0, y: 0
};
if (animate && !resizing && (display != 'rotate' || prevDg != null)) {
me.onAnimate(label, {
@@ -49823,8 +53332,8 @@ Ext.define('Ext.chart.series.Pie', {
startAngle = item.startAngle,
endAngle = item.endAngle,
rho = Math.sqrt(dx * dx + dy * dy),
- angle = Math.atan2(y - cy, x - cx) / me.rad + 360;
-
+ angle = Math.atan2(y - cy, x - cx) / me.rad;
+
// normalize to the same range of angles created by drawSeries
if (angle > me.firstAngle) {
angle -= 360;
@@ -49832,7 +53341,7 @@ Ext.define('Ext.chart.series.Pie', {
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;
@@ -49858,7 +53367,7 @@ Ext.define('Ext.chart.series.Pie', {
this.drawSeries();
}
},
-
+
// @private shows all elements in the series.
showAll: function() {
if (!isNaN(this._index)) {
@@ -49875,13 +53384,13 @@ Ext.define('Ext.chart.series.Pie', {
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;
}
@@ -49914,7 +53423,7 @@ Ext.define('Ext.chart.series.Pie', {
if (Math.abs(y) < 1e-10) {
y = 0;
}
-
+
if (animate) {
label.stopAnimation();
label.animate({
@@ -49969,7 +53478,7 @@ Ext.define('Ext.chart.series.Pie', {
},
/**
- * un-highlights the specified item. If no item is provided it will un-highlight the entire series.
+ * 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() {
@@ -50056,14 +53565,14 @@ Ext.define('Ext.chart.series.Pie', {
}
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];
+ return (me.colorSet && me.colorSet[index % me.colorSet.length]) || me.colorArrayStyle[index % me.colorArrayStyle.length];
}
});
@@ -50071,25 +53580,25 @@ Ext.define('Ext.chart.series.Pie', {
/**
* @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
+ *
+ * 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
+ *
+ * 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}
*
+ * @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}
+ * { '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,
@@ -50112,7 +53621,7 @@ Ext.define('Ext.chart.series.Pie', {
* showMarkers: true,
* markerConfig: {
* radius: 5,
- * size: 5
+ * size: 5
* },
* style: {
* 'stroke-width': 2,
@@ -50146,13 +53655,15 @@ Ext.define('Ext.chart.series.Pie', {
* '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.
- *
+ *
+ * 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', {
@@ -50168,7 +53679,7 @@ Ext.define('Ext.chart.series.Radar', {
type: "radar",
alias: 'series.radar',
-
+
rad: Math.PI / 180,
showInLegend: false,
@@ -50178,7 +53689,7 @@ Ext.define('Ext.chart.series.Radar', {
* An object containing styles for overriding series styles from Theming.
*/
style: {},
-
+
constructor: function(config) {
this.callParent(arguments);
var me = this,
@@ -50194,7 +53705,7 @@ Ext.define('Ext.chart.series.Radar', {
*/
drawSeries: function() {
var me = this,
- store = me.chart.substore || me.chart.store,
+ store = me.chart.getChartStore(),
group = me.group,
sprite,
chart = me.chart,
@@ -50220,18 +53731,18 @@ Ext.define('Ext.chart.series.Radar', {
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();
@@ -50307,7 +53818,7 @@ Ext.define('Ext.chart.series.Radar', {
me.renderLabels();
me.renderCallouts();
},
-
+
// @private draws the markers for the lines (if any).
drawMarkers: function() {
var me = this,
@@ -50315,15 +53826,15 @@ Ext.define('Ext.chart.series.Radar', {
surface = chart.surface,
markerStyle = Ext.apply({}, me.markerStyle || {}),
endMarkerStyle = Ext.apply(markerStyle, me.markerConfig),
- items = me.items,
+ 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);
@@ -50368,7 +53879,7 @@ Ext.define('Ext.chart.series.Radar', {
}
}
},
-
+
isItemInPoint: function(x, y, item) {
var point,
tolerance = 10,
@@ -50387,7 +53898,7 @@ Ext.define('Ext.chart.series.Radar', {
centerY = me.centerY,
point = item.point,
endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config);
-
+
return me.chart.surface.add(Ext.apply({
'type': 'text',
'text-anchor': 'middle',
@@ -50419,14 +53930,14 @@ Ext.define('Ext.chart.series.Radar', {
hidden: true
},
true);
-
+
if (resizing) {
label.setAttributes({
x: centerX,
y: centerY
}, true);
}
-
+
if (animate) {
label.show(true);
me.onAnimate(label, {
@@ -50438,7 +53949,7 @@ Ext.define('Ext.chart.series.Radar', {
}
},
- // @private for toggling (show/hide) series.
+ // @private for toggling (show/hide) series.
toggleAll: function(show) {
var me = this,
i, ln, shadow, shadows;
@@ -50463,18 +53974,18 @@ Ext.define('Ext.chart.series.Radar', {
}
}
},
-
+
// @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,
@@ -50490,25 +54001,24 @@ Ext.define('Ext.chart.series.Radar', {
/**
* @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.
+ *
+ * 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
+ * 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}
- *
+ * @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}
+ * { '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,
@@ -50518,14 +54028,14 @@ Ext.define('Ext.chart.series.Radar', {
* store: store,
* axes: [{
* type: 'Numeric',
- * position: 'bottom',
- * fields: ['data1', 'data2', 'data3'],
+ * position: 'left',
+ * fields: ['data2', 'data3'],
* title: 'Sample Values',
* grid: true,
* minimum: 0
* }, {
* type: 'Category',
- * position: 'left',
+ * position: 'bottom',
* fields: ['name'],
* title: 'Sample Metrics'
* }],
@@ -50547,14 +54057,14 @@ Ext.define('Ext.chart.series.Radar', {
* 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
+ *
+ * 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', {
@@ -50574,11 +54084,18 @@ Ext.define('Ext.chart.series.Scatter', {
* @cfg {Object} markerConfig
* The display style for the scatter series markers.
*/
-
+
/**
- * @cfg {Object} style
+ * @cfg {Object} style
* Append styling properties to this object for it to override theme properties.
*/
+
+ /**
+ * @cfg {String/Array} axis
+ * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
+ * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
+ * relative scale will be used. If multiple axes are being used, they should both be specified in in the configuration.
+ */
constructor: function(config) {
this.callParent(arguments);
@@ -50614,14 +54131,14 @@ Ext.define('Ext.chart.series.Scatter', {
getBounds: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
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++) {
+ for (i = 0, ln = axes.length; i < ln; i++) {
axis = chart.axes.get(axes[i]);
if (axis) {
ends = axis.calcEnds();
@@ -50666,7 +54183,7 @@ Ext.define('Ext.chart.series.Scatter', {
minY = 0;
maxY = store.getCount() - 1;
yScale = bbox.height / (store.getCount() - 1);
- }
+ }
else {
yScale = bbox.height / (maxY - minY);
}
@@ -50685,7 +54202,7 @@ Ext.define('Ext.chart.series.Scatter', {
var me = this,
chart = me.chart,
enableShadows = chart.shadow,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
group = me.group,
bounds = me.bounds = me.getBounds(),
bbox = me.bbox,
@@ -50713,10 +54230,10 @@ Ext.define('Ext.chart.series.Scatter', {
return;
}
// Ensure a value
- if (typeof xValue == 'string' || typeof xValue == 'object') {
+ if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)) {
xValue = i;
}
- if (typeof yValue == 'string' || typeof yValue == 'object') {
+ if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)) {
yValue = i;
}
x = boxX + (xValue - minX) * xScale;
@@ -50842,7 +54359,7 @@ Ext.define('Ext.chart.series.Scatter', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
group = me.group,
enableShadows = chart.shadow,
shadowGroups = me.shadowGroups,
@@ -50889,23 +54406,28 @@ Ext.define('Ext.chart.series.Scatter', {
for (shindex = 0; shindex < lnsh; shindex++) {
shadowAttribute = Ext.apply({}, shadowAttributes[shindex]);
rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, {
+ hidden: false,
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);
+ rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
+ sprite._to = rendererAttributes;
sprite.setAttributes(rendererAttributes, true);
- //update shadows
+ //animate 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 = Ext.apply({}, shadowAttributes[shindex]);
+ rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, {
+ hidden: false,
+ translate: {
+ x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0),
+ y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0)
+ }
}, shadowAttribute), i, store);
shadows[shindex].setAttributes(rendererAttributes, true);
}
@@ -50921,7 +54443,7 @@ Ext.define('Ext.chart.series.Scatter', {
me.renderLabels();
me.renderCallouts();
},
-
+
// @private callback for when creating a label sprite.
onCreateLabel: function(storeItem, item, i, display) {
var me = this,
@@ -50929,7 +54451,7 @@ Ext.define('Ext.chart.series.Scatter', {
config = me.label,
endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle),
bbox = me.bbox;
-
+
return me.chart.surface.add(Ext.apply({
type: 'text',
group: group,
@@ -50937,7 +54459,7 @@ Ext.define('Ext.chart.series.Scatter', {
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,
@@ -50951,12 +54473,12 @@ Ext.define('Ext.chart.series.Scatter', {
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',
@@ -50973,7 +54495,7 @@ Ext.define('Ext.chart.series.Scatter', {
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();
@@ -51007,7 +54529,7 @@ Ext.define('Ext.chart.series.Scatter', {
y: y
}, true);
label.show(true);
- });
+ });
}
else {
label.show(true);
@@ -51023,8 +54545,8 @@ Ext.define('Ext.chart.series.Scatter', {
}
}
},
-
- // @private callback for when placing a callout sprite.
+
+ // @private callback for when placing a callout sprite.
onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
var me = this,
chart = me.chart,
@@ -51041,18 +54563,18 @@ Ext.define('Ext.chart.series.Scatter', {
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])) {
@@ -51061,17 +54583,17 @@ Ext.define('Ext.chart.series.Scatter', {
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, {
@@ -51299,58 +54821,46 @@ Ext.define('Ext.chart.theme.Base', {
/**
* @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}.
+ * 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.
+ * A store configuration would be something like:
*
- * *Note: Although not listed here, this class accepts all of the configuration options of
- * {@link Ext.data.reader.Array ArrayReader} .
+ * var store = Ext.create('Ext.data.ArrayStore', {
+ * // store configs
+ * autoDestroy: true,
+ * storeId: 'myStore',
+ * // reader configs
+ * idIndex: 0,
+ * fields: [
+ * 'company',
+ * {name: 'price', type: 'float'},
+ * {name: 'change', type: 'float'},
+ * {name: 'pctChange', type: 'float'},
+ * {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
+ * ]
+ * });
*
- * @constructor
- * @param {Object} config
- * @xtype arraystore
+ * 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:** This class accepts all of the configuration options of {@link Ext.data.reader.Array ArrayReader}.
*/
Ext.define('Ext.data.ArrayStore', {
extend: 'Ext.data.Store',
alias: 'store.array',
uses: ['Ext.data.reader.Array'],
- /**
- * @cfg {Ext.data.DataReader} reader @hide
- */
constructor: function(config) {
config = config || {};
@@ -51388,73 +54898,68 @@ Ext.define('Ext.data.ArrayStore', {
/**
* @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
+ * @property {Boolean} autoStart
+ * True to immediately start processing the batch as soon as it is constructed.
*/
autoStart: false,
-
+
/**
+ * @property {Number} current
* The index of the current operation being executed
- * @property current
- * @type Number
*/
current: -1,
-
+
/**
+ * @property {Number} total
* The total number of operations in this batch. Read only
- * @property total
- * @type Number
*/
total: 0,
-
+
/**
+ * @property {Boolean} isRunning
* True if the batch is currently running
- * @property isRunning
- * @type Boolean
*/
isRunning: false,
-
+
/**
+ * @property {Boolean} isComplete
* True if this batch has been executed completely
- * @property isComplete
- * @type Boolean
*/
isComplete: false,
-
+
/**
+ * @property {Boolean} hasException
* 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
+ * @property {Boolean} pauseOnException
+ * True to automatically pause the execution of the batch if any operation encounters an exception
*/
pauseOnException: true,
-
- constructor: function(config) {
+
+ /**
+ * Creates new Batch object.
+ * @param {Object} [config] Config object
+ */
+ constructor: function(config) {
var me = this;
-
+
me.addEvents(
/**
* @event complete
@@ -51463,7 +54968,7 @@ Ext.define('Ext.data.Batch', {
* @param {Object} operation The last operation that was executed
*/
'complete',
-
+
/**
* @event exception
* Fired when a operation encountered an exception
@@ -51471,7 +54976,7 @@ Ext.define('Ext.data.Batch', {
* @param {Object} operation The operation that encountered the exception
*/
'exception',
-
+
/**
* @event operationcomplete
* Fired when each operation of the batch completes
@@ -51480,29 +54985,28 @@ Ext.define('Ext.data.Batch', {
*/
'operationcomplete'
);
-
+
me.mixins.observable.constructor.call(me, config);
-
+
/**
* Ordered array of operations that will be executed by this batch
- * @property operations
- * @type Array
+ * @property {Ext.data.Operation[]} operations
*/
me.operations = [];
},
-
+
/**
* Adds a new operation to this batch
* @param {Object} operation The {@link Ext.data.Operation Operation} object
*/
add: function(operation) {
this.total++;
-
+
operation.setBatch(this);
-
+
this.operations.push(operation);
},
-
+
/**
* Kicks off the execution of the batch, continuing from the next operation if the previous
* operation encountered an exception, or if execution was paused
@@ -51510,10 +55014,10 @@ Ext.define('Ext.data.Batch', {
start: function() {
this.hasException = false;
this.isRunning = true;
-
+
this.runNextOperation();
},
-
+
/**
* @private
* Runs the next operation, relative to this.current.
@@ -51521,14 +55025,14 @@ Ext.define('Ext.data.Batch', {
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
@@ -51538,17 +55042,17 @@ Ext.define('Ext.data.Batch', {
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);
@@ -51563,9 +55067,9 @@ Ext.define('Ext.data.Batch', {
me.runNextOperation();
}
};
-
+
operation.setStarted();
-
+
me.proxy[operation.action](operation, onProxyReturn, me);
}
}
@@ -51575,117 +55079,110 @@ Ext.define('Ext.data.Batch', {
* @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:
+ * 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
+ * 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
+ * associations: [
+ * { type: 'belongsTo', model: 'Category' }
+ * ]
+ * });
+ *
+ * In the example above we have created models for Products and Categories, and linked them together
* by saying that each Product belongs to a Category. This automatically links each Product to a Category
- * based on the Product's category_id, and provides new functions on the Product model:
+ * based on the Product's category_id, and provides new functions on the Product model:
*
- * Generated getter function
+ * ## Generated getter function
*
- * The first function that is added to the owner model is a 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
+ * 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.
+ * callback when it has loaded.
*
- * The new getCategory function will also accept an object containing success, failure and callback properties
+ * 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:
+ * 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
-});
-
+ * 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
+ * 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.
+ * useful when the instance could not be loaded.
*
- * Generated setter function
+ * ## 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:
+ * 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
+ * // this call...
+ * product.setCategory(10);
*
- * 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:
+ * // is equivalent to this call:
+ * product.set('category_id', 10);
*
-
-Ext.define('Product', {
- fields: [...],
-
- associations: [
- {type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id'}
- ]
-});
-
+ * 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
+ * })
*
- * 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.
+ * ## 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',
@@ -51696,26 +55193,25 @@ Ext.define('Ext.data.BelongsToAssociation', {
* @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
-
- *
+ *
+ * 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
+ *
*/
/**
@@ -51727,18 +55223,16 @@ product.getOrder(); // Will make a call to the server asking for order_id 22
* @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'
-}]
- *
+ *
+ * associations: [{
+ * type: 'belongsTo',
+ * model: 'User'
+ * }]
*/
-
constructor: function(config) {
this.callParent(arguments);
@@ -51807,38 +55301,37 @@ associations: [{
return function(options, scope) {
options = options || {};
- var foreignKeyId = this.get(foreignKey),
- instance, callbackFn;
+ var model = this,
+ foreignKeyId = model.get(foreignKey),
+ instance,
+ args;
- if (this[instanceName] === undefined) {
+ if (model[instanceName] === undefined) {
instance = Ext.ModelManager.create({}, associatedName);
instance.set(primaryKey, foreignKeyId);
if (typeof options == 'function') {
options = {
callback: options,
- scope: scope || this
+ scope: scope || model
};
}
associatedModel.load(foreignKeyId, options);
+ model[instanceName] = associatedModel;
+ return associatedModel;
} else {
- instance = this[instanceName];
+ instance = model[instanceName];
+ args = [instance];
+ scope = scope || model;
//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);
- }
+ Ext.callback(options, scope, args);
+ Ext.callback(options.success, scope, args);
+ Ext.callback(options.failure, scope, args);
+ Ext.callback(options.callback, scope, args);
return instance;
}
@@ -51873,73 +55366,62 @@ Ext.define('Ext.data.BufferStore', {
}
});
/**
- * @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).
*
- * 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.
*
- * 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
*
- * Specification
+ * For additional information consult the [Ext.Direct Specification][1].
*
- * For additional information consult the
- * Ext.Direct Specification .
+ * # Providers
*
- * 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:
*
- * 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.
*
- * {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
- * {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
- * {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side
- * on the client.
- *
+ * A provider does not need to be invoked directly, providers are added via {@link Ext.direct.Manager}.{@link #addProvider}.
*
- * A provider does not need to be invoked directly, providers are added via
- * {@link Ext.direct.Manager}.{@link Ext.direct.Manager#add add}.
+ * # Router
*
- * 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.
*
- * 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
*
- * Server side events
+ * Custom events from the server may be handled by the client by adding listeners, for example:
+ *
+ * {"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
+ *
+ * // add a handler for a 'message' event sent by the server
+ * Ext.direct.Manager.on('message', function(e){
+ * out.append(String.format('{0}
', e.data));
+ * out.el.scrollTo('t', 100000, true);
+ * });
+ *
+ * [1]: http://sencha.com/products/extjs/extdirect
*
- * Custom events from the server may be handled by the client by adding
- * listeners, for example:
- *
-{"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
-
-// add a handler for a 'message' event sent by the server
-Ext.direct.Manager.on('message', function(e){
- out.append(String.format('<p><i>{0}</i></p>', e.data));
- out.el.scrollTo('t', 100000, true);
-});
- *
* @singleton
+ * @alternateClassName Ext.Direct
*/
-
Ext.define('Ext.direct.Manager', {
-
+
/* Begin Definitions */
singleton: true,
-
+
mixins: {
observable: 'Ext.util.Observable'
},
-
+
requires: ['Ext.util.MixedCollection'],
-
+
statics: {
exceptions: {
TRANSPORT: 'xhr',
@@ -51948,74 +55430,75 @@ Ext.define('Ext.direct.Manager', {
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.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.
+ * @param {Ext.direct.Event} e The 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.
+ /**
+ * 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 {Ext.direct.Provider/Object...} provider
+ * Accepts any number of Provider descriptions (an instance or config object for
+ * a Provider). Each Provider description instructs Ext.Directhow 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]);
@@ -52037,17 +55520,16 @@ Ext.direct.Manager.addProvider({
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.
+ * Retrieves a {@link Ext.direct.Provider provider} by the **{@link Ext.direct.Provider#id id}** specified when the
+ * provider is {@link #addProvider added}.
+ * @param {String/Ext.direct.Provider} id The id of the provider, or the provider instance.
*/
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.
@@ -52055,9 +55537,10 @@ Ext.direct.Manager.addProvider({
*/
removeProvider : function(provider){
var me = this,
- providers = me.providers,
- provider = provider.isProvider ? provider : providers.get(provider);
-
+ providers = me.providers;
+
+ provider = provider.isProvider ? provider : providers.get(provider);
+
if (provider) {
provider.un('data', me.onProviderData, me);
providers.remove(provider);
@@ -52065,9 +55548,9 @@ Ext.direct.Manager.addProvider({
}
return null;
},
-
+
/**
- * Add a transaction to the manager.
+ * Adds a transaction to the manager.
* @private
* @param {Ext.direct.Transaction} transaction The transaction to add
* @return {Ext.direct.Transaction} transaction
@@ -52078,7 +55561,7 @@ Ext.direct.Manager.addProvider({
},
/**
- * Remove a transaction from the manager.
+ * Removes a transaction from the manager.
* @private
* @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to remove
* @return {Ext.direct.Transaction} transaction
@@ -52098,12 +55581,12 @@ Ext.direct.Manager.addProvider({
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]);
@@ -52112,7 +55595,7 @@ Ext.direct.Manager.addProvider({
}
if (event.name && event.name != 'event' && event.name != 'exception') {
me.fireEvent(event.name, event);
- } else if (event.type == 'exception') {
+ } else if (event.status === false) {
me.fireEvent('exception', event);
}
me.fireEvent('event', event, provider);
@@ -52123,27 +55606,26 @@ Ext.direct.Manager.addProvider({
});
/**
- * @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 class is used to send requests to the server using {@link Ext.direct.Manager Ext.Direct}. When a
+ * request is made, the transport mechanism is handed off to the appropriate
+ * {@link Ext.direct.RemotingProvider Provider} to complete the call.
+ *
+ * # Specifying the function
+ *
* This proxy expects a Direct remoting method to be passed in order to be able to complete requests.
* This can be done by specifying the {@link #directFn} configuration. This will use the same direct
* method for all requests. Alternatively, you can provide an {@link #api} configuration. This
* allows you to specify a different remoting method for each CRUD action.
- *
- * ## Paramaters
+ *
+ * # Parameters
+ *
* This proxy provides options to help configure which parameters will be sent to the server.
* By specifying the {@link #paramsAsHash} option, it will send an object literal containing each
* of the passed parameters. The {@link #paramOrder} option can be used to specify the order in which
* the remoting method parameters are passed.
- *
- * ## Example Usage
- *
+ *
+ * # Example Usage
+ *
* Ext.define('User', {
* extend: 'Ext.data.Model',
* fields: ['firstName', 'lastName'],
@@ -52157,34 +55639,34 @@ Ext.direct.Manager.addProvider({
*/
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'
-
+
+ /**
+ * @cfg {String/String[]} paramOrder
+ * Defaults to undefined. A list of params to be executed server side. Specify the params in the order in
+ * which they must be executed on the server-side as either (1) an Array of String values, or (2) a String
+ * of params delimited by either whitespace, comma, or pipe. For example, any of the following would be
+ * acceptable:
+ *
+ * paramOrder: ['param1','param2','param3']
+ * paramOrder: 'param1 param2 param3'
+ * paramOrder: 'param1,param2,param3'
+ * paramOrder: 'param1|param2|param'
*/
paramOrder: undefined,
/**
* @cfg {Boolean} paramsAsHash
- * Send parameters as a collection of named arguments (defaults to true ). Providing a
- * {@link #paramOrder} nullifies this configuration.
+ * Send parameters as a collection of named arguments.
+ * Providing a {@link #paramOrder} nullifies this configuration.
*/
paramsAsHash: true,
@@ -52194,30 +55676,32 @@ paramOrder: 'param1|param2|param'
* 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
+ * @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
+ * @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(),
@@ -52229,21 +55713,21 @@ paramOrder: 'param1|param2|param'
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) {
@@ -52260,7 +55744,7 @@ paramOrder: 'param1|param2|param'
} else {
args.push(request.jsonData);
}
-
+
Ext.apply(request, {
args: args,
directFn: fn
@@ -52268,7 +55752,7 @@ paramOrder: 'param1|param2|param'
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
@@ -52276,25 +55760,25 @@ paramOrder: 'param1|param2|param'
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 '';
@@ -52302,39 +55786,28 @@ paramOrder: 'param1|param2|param'
});
/**
- * @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.
+ * Small helper class to create an {@link Ext.data.Store} configured with an {@link Ext.data.proxy.Direct}
+ * and {@link Ext.data.reader.Json} to make interacting with an {@link Ext.direct.Manager} server-side
+ * {@link Ext.direct.Provider Provider} easier. To create a different proxy/reader combination create a basic
+ * {@link Ext.data.Store} configured as needed.
*
- * *Note: Although they are not listed, this class inherits all of the config options of:
- *
- * {@link Ext.data.Store Store}
- *
+ * **Note:** Although they are not listed, this class inherits all of the config options of:
*
- *
- * {@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.Store Store}**
*
- * {@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}
- *
- *
+ * - **{@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 */
@@ -52345,8 +55818,8 @@ Ext.define('Ext.data.DirectStore', {
requires: ['Ext.data.proxy.Direct'],
/* End Definitions */
-
- constructor : function(config){
+
+ constructor : function(config){
config = Ext.apply({}, config);
if (!config.proxy) {
var proxy = {
@@ -52364,51 +55837,40 @@ Ext.define('Ext.data.DirectStore', {
});
/**
- * @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
+ * 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
+ * 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.
*/
-
Ext.define('Ext.util.Inflector', {
/* Begin Definitions */
@@ -52422,8 +55884,7 @@ Ext.define('Ext.util.Inflector', {
* 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
+ * @property {Array} plurals
*/
plurals: [
[(/(quiz)$/i), "$1zes" ],
@@ -52447,14 +55908,13 @@ Ext.define('Ext.util.Inflector', {
[(/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
+ * 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
+ * @property {Array} singulars
*/
singulars: [
[(/(quiz)zes$/i), "$1" ],
@@ -52483,12 +55943,11 @@ Ext.define('Ext.util.Inflector', {
[(/people$/i), "person" ],
[(/s$/i), "" ]
],
-
+
/**
* @private
* The registered uncountable words
- * @property uncountable
- * @type Array
+ * @property {String[]} uncountable
*/
uncountable: [
"sheep",
@@ -52505,7 +55964,7 @@ Ext.define('Ext.util.Inflector', {
"deer",
"means"
],
-
+
/**
* Adds a new singularization rule to the Inflector. See the intro docs for more information
* @param {RegExp} matcher The matcher regex
@@ -52514,7 +55973,7 @@ Ext.define('Ext.util.Inflector', {
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
@@ -52523,21 +55982,21 @@ Ext.define('Ext.util.Inflector', {
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
@@ -52560,19 +56019,19 @@ Ext.define('Ext.util.Inflector', {
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
@@ -52586,21 +56045,21 @@ Ext.define('Ext.util.Inflector', {
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
+ * 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
@@ -52608,9 +56067,9 @@ Ext.define('Ext.util.Inflector', {
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
+ * 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
@@ -52619,7 +56078,7 @@ Ext.define('Ext.util.Inflector', {
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";
@@ -52668,7 +56127,7 @@ Ext.define('Ext.util.Inflector', {
vita: 'vitae'
},
singular;
-
+
for (singular in irregulars) {
this.plural(singular, irregulars[singular]);
this.singular(irregulars[singular], singular);
@@ -52711,7 +56170,7 @@ Ext.define('User', {
*
//first, we load up a User with id of 1
-var user = Ext.ModelManager.create({id: 1, name: 'Ed'}, 'User');
+var user = Ext.create('User', {id: 1, name: 'Ed'});
//the user.products function was created automatically by the association and returns a {@link Ext.data.Store Store}
//the created store is automatically scoped to the set of Products for the User with id of 1
@@ -52767,7 +56226,7 @@ var store = new Search({query: 'Sencha Touch'}).tweets();
* equivalent to this:
*
-var store = new Ext.data.Store({
+var store = Ext.create('Ext.data.Store', {
model: 'Tweet',
filters: [
{
@@ -52963,22 +56422,21 @@ associations: [{
* @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
+ * requests for data cross domain. More information is available here .
*/
Ext.define('Ext.data.JsonP', {
-
+
/* Begin Definitions */
-
+
singleton: true,
-
+
statics: {
requestCount: 0,
requests: {}
},
-
+
/* End Definitions */
-
+
/**
* @property timeout
* @type Number
@@ -52986,21 +56444,21 @@ Ext.define('Ext.data.JsonP', {
* 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
+ * @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
@@ -53009,7 +56467,7 @@ Ext.define('Ext.data.JsonP', {
* 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
@@ -53029,7 +56487,7 @@ Ext.define('Ext.data.JsonP', {
* 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
+ *
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.
@@ -53038,32 +56496,33 @@ Ext.define('Ext.data.JsonP', {
*/
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 = options.callbackName || 'callback' + id,
- callbackKey = options.callbackKey || me.callbackKey,
- timeout = Ext.isDefined(options.timeout) ? options.timeout : me.timeout,
- params = Ext.apply({}, options.params),
+
+ var me = this,
+ disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching,
+ cacheParam = options.disableCachingParam || me.disableCachingParam,
+ id = ++me.statics().requestCount,
+ callbackName = options.callbackName || 'callback' + id,
+ callbackKey = options.callbackKey || me.callbackKey,
+ timeout = Ext.isDefined(options.timeout) ? options.timeout : me.timeout,
+ params = Ext.apply({}, options.params),
url = options.url,
- request,
+ name = Ext.isSandboxed ? Ext.getUniqueGlobalNamespace() : 'Ext',
+ request,
script;
-
- params[callbackKey] = 'Ext.data.JsonP.' + callbackName;
+
+ params[callbackKey] = name + '.data.JsonP.' + callbackName;
if (disableCaching) {
params[cacheParam] = new Date().getTime();
}
-
+
script = me.createScript(url, params);
-
+
me.statics().requests[id] = request = {
url: url,
params: params,
@@ -53075,17 +56534,17 @@ Ext.define('Ext.data.JsonP', {
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.
@@ -53094,7 +56553,7 @@ Ext.define('Ext.data.JsonP', {
abort: function(request){
var requests = this.statics().requests,
key;
-
+
if (request) {
if (!request.id) {
request = requests[request];
@@ -53108,7 +56567,7 @@ Ext.define('Ext.data.JsonP', {
}
}
},
-
+
/**
* Sets up error handling for the script
* @private
@@ -53117,7 +56576,7 @@ Ext.define('Ext.data.JsonP', {
setupErrorHandling: function(request){
request.script.onerror = Ext.bind(this.handleError, this, [request]);
},
-
+
/**
* Handles any aborts when loading the script
* @private
@@ -53127,7 +56586,7 @@ Ext.define('Ext.data.JsonP', {
request.errorType = 'abort';
this.handleResponse(null, request);
},
-
+
/**
* Handles any script errors when loading the script
* @private
@@ -53137,7 +56596,7 @@ Ext.define('Ext.data.JsonP', {
request.errorType = 'error';
this.handleResponse(null, request);
},
-
+
/**
* Cleans up anu script handling errors
* @private
@@ -53146,7 +56605,7 @@ Ext.define('Ext.data.JsonP', {
cleanupErrorHandling: function(request){
request.script.onerror = null;
},
-
+
/**
* Handle any script timeouts
* @private
@@ -53156,7 +56615,7 @@ Ext.define('Ext.data.JsonP', {
request.errorType = 'timeout';
this.handleResponse(null, request);
},
-
+
/**
* Handle a successful response
* @private
@@ -53164,9 +56623,9 @@ Ext.define('Ext.data.JsonP', {
* @param {Object} request The request
*/
handleResponse: function(result, request){
-
+
var success = true;
-
+
if (request.timeout) {
clearTimeout(request.timeout);
}
@@ -53174,7 +56633,7 @@ Ext.define('Ext.data.JsonP', {
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]);
@@ -53183,7 +56642,7 @@ Ext.define('Ext.data.JsonP', {
}
Ext.callback(request.callback, request.scope, [success, result, request.errorType]);
},
-
+
/**
* Create the script tag
* @private
@@ -53202,9 +56661,7 @@ Ext.define('Ext.data.JsonP', {
/**
* @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:
@@ -53235,8 +56692,6 @@ stcCallback({
* 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', {
@@ -53255,21 +56710,170 @@ Ext.define('Ext.data.JsonPStore', {
});
/**
- * @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
+ * This class is used as a set of methods that are applied to the prototype of a
+ * Model 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.
+ * not be used directly by the developer. This class also creates extra fields on the model if
+ * they do not exist, to help maintain the tree state and UI. These fields are documented as
+ * config options.
*/
Ext.define('Ext.data.NodeInterface', {
requires: ['Ext.data.Field'],
-
+
+ /**
+ * @cfg {String} parentId
+ * ID of parent node.
+ */
+
+ /**
+ * @cfg {Number} index
+ * The position of the node inside its parent. When parent has 4 children and the node is third amongst them,
+ * index will be 2.
+ */
+
+ /**
+ * @cfg {Number} depth
+ * The number of parents this node has. A root node has depth 0, a child of it depth 1, and so on...
+ */
+
+ /**
+ * @cfg {Boolean} [expanded=false]
+ * True if the node is expanded.
+ */
+
+ /**
+ * @cfg {Boolean} [expandable=false]
+ * Set to true to allow for expanding/collapsing of this node.
+ */
+
+ /**
+ * @cfg {Boolean} [checked=null]
+ * Set to true or false to show a checkbox alongside this node.
+ */
+
+ /**
+ * @cfg {Boolean} [leaf=false]
+ * Set to true to indicate that this child can have no children. The expand icon/arrow will then not be
+ * rendered for this node.
+ */
+
+ /**
+ * @cfg {String} cls
+ * CSS class to apply for this node.
+ */
+
+ /**
+ * @cfg {String} iconCls
+ * CSS class to apply for this node's icon.
+ */
+
+ /**
+ * @cfg {String} icon
+ * URL for this node's icon.
+ */
+
+ /**
+ * @cfg {Boolean} root
+ * True if this is the root node.
+ */
+
+ /**
+ * @cfg {Boolean} isLast
+ * True if this is the last node.
+ */
+
+ /**
+ * @cfg {Boolean} isFirst
+ * True if this is the first node.
+ */
+
+ /**
+ * @cfg {Boolean} [allowDrop=true]
+ * Set to false to deny dropping on this node.
+ */
+
+ /**
+ * @cfg {Boolean} [allowDrag=true]
+ * Set to false to deny dragging of this node.
+ */
+
+ /**
+ * @cfg {Boolean} [loaded=false]
+ * True if the node has finished loading.
+ */
+
+ /**
+ * @cfg {Boolean} [loading=false]
+ * True if the node is currently loading.
+ */
+
+ /**
+ * @cfg {String} href
+ * An URL for a link that's created when this config is specified.
+ */
+
+ /**
+ * @cfg {String} hrefTarget
+ * Target for link. Only applicable when {@link #href} also specified.
+ */
+
+ /**
+ * @cfg {String} qtip
+ * Tooltip text to show on this node.
+ */
+
+ /**
+ * @cfg {String} qtitle
+ * Tooltip title.
+ */
+
+ /**
+ * @cfg {String} text
+ * The text for to show on node label.
+ */
+
+ /**
+ * @cfg {Ext.data.NodeInterface[]} children
+ * Array of child nodes.
+ */
+
+
+ /**
+ * @property nextSibling
+ * A reference to this node's next sibling node. `null` if this node does not have a next sibling.
+ */
+
+ /**
+ * @property previousSibling
+ * A reference to this node's previous sibling node. `null` if this node does not have a previous sibling.
+ */
+
+ /**
+ * @property parentNode
+ * A reference to this node's parent node. `null` if this node is the root node.
+ */
+
+ /**
+ * @property lastChild
+ * A reference to this node's last child node. `null` if this node has no children.
+ */
+
+ /**
+ * @property firstChild
+ * A reference to this node's first child node. `null` if this node has no children.
+ */
+
+ /**
+ * @property childNodes
+ * An array of this nodes children. Array will be empty if this node has no chidren.
+ */
+
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.
+ * @param {Ext.data.Model} record The Record you want to decorate the prototype of.
* @static
*/
decorate: function(record) {
@@ -53280,51 +56884,46 @@ Ext.define('Ext.data.NodeInterface', {
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;
+ i, newField, len;
// 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}
+ {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: 'expandable', type: 'bool', defaultValue: true, persist: false},
+ {name: 'checked', type: 'auto', defaultValue: null},
+ {name: 'leaf', type: 'bool', defaultValue: false, persist: false},
+ {name: 'cls', type: 'string', defaultValue: null, persist: false},
+ {name: 'iconCls', type: 'string', defaultValue: null, persist: false},
+ {name: 'icon', 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;
- }
+ len = newFields.length;
+ // Set default values
+ for (i = 0; i < len; ++i) {
+ newField = newFields[i];
+ if (record.get(newField.name) === undefined) {
+ record.data[newField.name] = newField.defaultValue;
}
}
}
-
+
Ext.applyIf(record, {
firstChild: null,
lastChild: null,
@@ -53335,13 +56934,13 @@ Ext.define('Ext.data.NodeInterface', {
});
// 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 {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The newly appended node
* @param {Number} index The index of the newly appended node
*/
"append",
@@ -53349,17 +56948,17 @@ Ext.define('Ext.data.NodeInterface', {
/**
* @event remove
* Fires when a child node is removed
- * @param {Node} this This node
- * @param {Node} node The removed node
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The removed node
*/
"remove",
/**
* @event move
* Fires when this node is moved to a new location in the tree
- * @param {Node} this This node
- * @param {Node} oldParent The old parent of this node
- * @param {Node} newParent The new parent of this node
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} oldParent The old parent of this node
+ * @param {Ext.data.NodeInterface} newParent The new parent of this node
* @param {Number} index The index it was moved to
*/
"move",
@@ -53367,34 +56966,34 @@ Ext.define('Ext.data.NodeInterface', {
/**
* @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
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The child node inserted
+ * @param {Ext.data.NodeInterface} refNode The child node the node was inserted before
*/
"insert",
/**
* @event beforeappend
* Fires before a new child is appended, return false to cancel the append.
- * @param {Node} this This node
- * @param {Node} node The child node to be appended
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The child node to be appended
*/
"beforeappend",
/**
* @event beforeremove
* Fires before a child is removed, return false to cancel the remove.
- * @param {Node} this This node
- * @param {Node} node The child node to be removed
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The child node to be removed
*/
"beforeremove",
/**
* @event beforemove
* Fires before this node is moved to a new location in the tree. Return false to cancel the move.
- * @param {Node} this This node
- * @param {Node} oldParent The parent of this node
- * @param {Node} newParent The new parent this node is moving to
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} oldParent The parent of this node
+ * @param {Ext.data.NodeInterface} newParent The new parent this node is moving to
* @param {Number} index The index it is being moved to
*/
"beforemove",
@@ -53402,51 +57001,52 @@ Ext.define('Ext.data.NodeInterface', {
/**
* @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
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The child node to be inserted
+ * @param {Ext.data.NodeInterface} refNode The child node the node is being inserted before
*/
"beforeinsert",
-
+
/**
* @event expand
* Fires when this node is expanded.
- * @param {Node} this The expanding node
+ * @param {Ext.data.NodeInterface} this The expanding node
*/
"expand",
-
+
/**
* @event collapse
* Fires when this node is collapsed.
- * @param {Node} this The collapsing node
+ * @param {Ext.data.NodeInterface} this The collapsing node
*/
"collapse",
-
+
/**
* @event beforeexpand
* Fires before this node is expanded.
- * @param {Node} this The expanding node
+ * @param {Ext.data.NodeInterface} this The expanding node
*/
"beforeexpand",
-
+
/**
* @event beforecollapse
* Fires before this node is collapsed.
- * @param {Node} this The collapsing node
+ * @param {Ext.data.NodeInterface} this The collapsing node
*/
"beforecollapse",
-
+
/**
- * @event beforecollapse
- * Fires before this node is collapsed.
- * @param {Node} this The collapsing node
+ * @event sort
+ * Fires when this node's childNodes are sorted.
+ * @param {Ext.data.NodeInterface} this This node.
+ * @param {Ext.data.NodeInterface[]} childNodes The childNodes of this node.
*/
"sort"
]);
-
+
return record;
},
-
+
applyFields: function(modelClass, addFields) {
var modelPrototype = modelClass.prototype,
fields = modelPrototype.fields,
@@ -53454,20 +57054,20 @@ Ext.define('Ext.data.NodeInterface', {
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,
@@ -53483,7 +57083,7 @@ Ext.define('Ext.data.NodeInterface', {
// Make sure the node implements the node interface
return Ext.data.NodeInterface.decorate(node);
},
-
+
/**
* Returns true if this node is a leaf
* @return {Boolean}
@@ -53532,8 +57132,8 @@ Ext.define('Ext.data.NodeInterface', {
while (parent.parentNode) {
++depth;
parent = parent.parentNode;
- }
-
+ }
+
me.beginEdit();
me.set({
isFirst: isFirst,
@@ -53546,7 +57146,7 @@ Ext.define('Ext.data.NodeInterface', {
if (silent) {
me.commit();
}
-
+
for (i = 0; i < len; i++) {
children[i].updateInfo(silent);
}
@@ -53578,18 +57178,25 @@ Ext.define('Ext.data.NodeInterface', {
/**
* 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.
+ * node attribute is explicitly specified as true, otherwise returns false.
* @return {Boolean}
*/
isExpandable : function() {
- return this.get('expandable') || this.hasChildNodes();
+ var me = this;
+
+ if (me.get('expandable')) {
+ return !(me.isLeaf() || (me.isLoaded() && !me.hasChildNodes()));
+ }
+ return false;
},
/**
- * Insert node(s) as the last child node of this node.
- * If the node was previously a child node of another parent node, it will be removed from that node first.
- * @param {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
+ * Inserts node(s) as the last child node of this node.
+ *
+ * If the node was previously a child node of another parent node, it will be removed from that node first.
+ *
+ * @param {Ext.data.NodeInterface/Ext.data.NodeInterface[]} node The node or Array of nodes to append
+ * @return {Ext.data.NodeInterface} The appended node if single append, or null if an array was passed
*/
appendChild : function(node, suppressEvents, suppressNodeUpdate) {
var me = this,
@@ -53606,9 +57213,9 @@ Ext.define('Ext.data.NodeInterface', {
} else {
// Make sure it is a record
node = me.createNode(node);
-
+
if (suppressEvents !== true && me.fireEvent("beforeappend", me, node) === false) {
- return false;
+ return false;
}
index = me.childNodes.length;
@@ -53632,7 +57239,7 @@ Ext.define('Ext.data.NodeInterface', {
node.nextSibling = null;
me.setLastChild(node);
-
+
ps = me.childNodes[index - 1];
if (ps) {
node.previousSibling = ps;
@@ -53643,28 +57250,28 @@ Ext.define('Ext.data.NodeInterface', {
}
node.updateInfo(suppressNodeUpdate);
-
+
// As soon as we append a child to this node, we are loaded
if (!me.isLoaded()) {
- me.set('loaded', true);
+ 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
@@ -53676,20 +57283,20 @@ Ext.define('Ext.data.NodeInterface', {
/**
* 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
+ * @param {Ext.data.NodeInterface} node The node to remove
+ * @param {Boolean} [destroy=false] True to destroy the node upon removal.
+ * @return {Ext.data.NodeInterface} The removed node
*/
removeChild : function(node, destroy, suppressEvents, suppressNodeUpdate) {
var me = this,
index = me.indexOf(node);
-
+
if (index == -1 || (suppressEvents !== true && me.fireEvent("beforeremove", me, node) === false)) {
return false;
}
// remove it from childNodes collection
- me.childNodes.splice(index, 1);
+ Ext.Array.erase(me.childNodes, index, 1);
// update child refs
if (me.firstChild == node) {
@@ -53698,7 +57305,7 @@ Ext.define('Ext.data.NodeInterface', {
if (me.lastChild == node) {
me.setLastChild(node.previousSibling);
}
-
+
// update siblings
if (node.previousSibling) {
node.previousSibling.nextSibling = node.nextSibling;
@@ -53712,13 +57319,13 @@ Ext.define('Ext.data.NodeInterface', {
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 {
@@ -53730,10 +57337,10 @@ Ext.define('Ext.data.NodeInterface', {
/**
* 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.
+ * @param {String} [id] A new id, defaults to this Node's id.
+ * @param {Boolean} [deep=false] True to recursively copy all child Nodes into the new Node.
+ * False to copy without child Nodes.
+ * @return {Ext.data.NodeInterface} A copy of this Node.
*/
copy: function(newId, deep) {
var me = this,
@@ -53751,13 +57358,13 @@ Ext.define('Ext.data.NodeInterface', {
},
/**
- * Clear the node.
+ * Clears the node.
* @private
- * @param {Boolean} destroy True to destroy the node.
+ * @param {Boolean} [destroy=false] 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) {
@@ -53777,7 +57384,7 @@ Ext.define('Ext.data.NodeInterface', {
*/
var me = this,
options = me.destroyOptions;
-
+
if (silent === true) {
me.clear(true);
Ext.each(me.childNodes, function(n) {
@@ -53795,9 +57402,9 @@ Ext.define('Ext.data.NodeInterface', {
/**
* 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
+ * @param {Ext.data.NodeInterface} node The node to insert
+ * @param {Ext.data.NodeInterface} refNode The node to insert before (if null the node is appended)
+ * @return {Ext.data.NodeInterface} The inserted node
*/
insertBefore : function(node, refNode, suppressEvents) {
var me = this,
@@ -53805,11 +57412,11 @@ Ext.define('Ext.data.NodeInterface', {
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;
@@ -53817,11 +57424,11 @@ Ext.define('Ext.data.NodeInterface', {
// 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--;
@@ -53839,12 +57446,12 @@ Ext.define('Ext.data.NodeInterface', {
me.setFirstChild(node);
}
- me.childNodes.splice(refIndex, 0, node);
+ Ext.Array.splice(me.childNodes, refIndex, 0, node);
node.parentNode = me;
-
+
node.nextSibling = refNode;
refNode.previousSibling = node;
-
+
ps = me.childNodes[refIndex - 1];
if (ps) {
node.previousSibling = ps;
@@ -53853,12 +57460,12 @@ Ext.define('Ext.data.NodeInterface', {
} else {
node.previousSibling = null;
}
-
+
node.updateInfo();
-
+
if (!me.isLoaded()) {
- me.set('loaded', true);
- }
+ 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());
@@ -53869,18 +57476,18 @@ Ext.define('Ext.data.NodeInterface', {
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
- */
+ * @return {Ext.data.Model} The record you just inserted
+ */
insertChild: function(index, node) {
var sibling = this.childNodes[index];
if (sibling) {
@@ -53893,8 +57500,8 @@ Ext.define('Ext.data.NodeInterface', {
/**
* Removes this node from its parent
- * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false .
- * @return {Node} this
+ * @param {Boolean} [destroy=false] True to destroy the node upon removal.
+ * @return {Ext.data.NodeInterface} this
*/
remove : function(destroy, suppressEvents) {
var parentNode = this.parentNode;
@@ -53907,8 +57514,8 @@ Ext.define('Ext.data.NodeInterface', {
/**
* Removes all child nodes from this node.
- * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false .
- * @return {Node} this
+ * @param {Boolean} [destroy=false] 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)
+ * @param {Object} [scope] The scope (this reference) in which the function is executed. Defaults to the current Node.
+ * @param {Array} [args] The args to call the function with. Defaults to passing the current Node.
*/
bubble : function(fn, scope, args) {
var p = this;
@@ -53992,8 +57619,8 @@ Ext.define('Ext.data.NodeInterface', {
* 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)
+ * @param {Object} [scope] The scope (this reference) in which the function is executed. Defaults to the current Node.
+ * @param {Array} [args] The args to call the function with. Defaults to passing the current Node.
*/
cascadeBy : function(fn, scope, args) {
if (fn.apply(scope || this, args || [this]) !== false) {
@@ -54012,8 +57639,8 @@ Ext.define('Ext.data.NodeInterface', {
* 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)
+ * @param {Object} [scope] The scope (this reference) in which the function is executed. Defaults to the current Node in iteration.
+ * @param {Array} [args] The args to call the function with. Defaults to passing the current Node.
*/
eachChild : function(fn, scope, args) {
var childNodes = this.childNodes,
@@ -54030,9 +57657,9 @@ Ext.define('Ext.data.NodeInterface', {
/**
* 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
+ * @param {Object} value The value to search for
+ * @param {Boolean} [deep=false] True to search through nodes deeper than the immediate children
+ * @return {Ext.data.NodeInterface} The found child or null if none was found
*/
findChild : function(attribute, value, deep) {
return this.findChildBy(function() {
@@ -54041,11 +57668,11 @@ Ext.define('Ext.data.NodeInterface', {
},
/**
- * 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
+ * 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] The scope (this reference) in which the function is executed. Defaults to the Node being tested.
+ * @param {Boolean} [deep=false] True to search through nodes deeper than the immediate children
+ * @return {Ext.data.NodeInterface} The found child or null if none was found
*/
findChildBy : function(fn, scope, deep) {
var cs = this.childNodes,
@@ -54070,7 +57697,7 @@ Ext.define('Ext.data.NodeInterface', {
/**
* Returns true if this node is an ancestor (at any point) of the passed node.
- * @param {Node} node
+ * @param {Ext.data.NodeInterface} node
* @return {Boolean}
*/
contains : function(node) {
@@ -54079,7 +57706,7 @@ Ext.define('Ext.data.NodeInterface', {
/**
* Returns true if the passed node is an ancestor (at any point) of this node.
- * @param {Node} node
+ * @param {Ext.data.NodeInterface} node
* @return {Boolean}
*/
isAncestor : function(node) {
@@ -54096,21 +57723,21 @@ Ext.define('Ext.data.NodeInterface', {
/**
* 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.
+ * @param {Boolean} [recursive=false] True to apply this sort recursively
+ * @param {Boolean} [suppressEvent=false] 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();
@@ -54123,25 +57750,25 @@ Ext.define('Ext.data.NodeInterface', {
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');
},
@@ -54149,23 +57776,23 @@ Ext.define('Ext.data.NodeInterface', {
/**
* 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) {
@@ -54176,12 +57803,12 @@ Ext.define('Ext.data.NodeInterface', {
}
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
+ * @param {Boolean} [recursive=false] True to recursively expand all the children
+ * @param {Function} [callback] The function to execute once the expand completes
+ * @param {Object} [scope] The scope to run the callback in
*/
expand: function(recursive, callback, scope) {
var me = this;
@@ -54191,48 +57818,47 @@ Ext.define('Ext.data.NodeInterface', {
// 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]);
+ // If it's loaded, wait until it loads before proceeding
+ if (me.isLoading()) {
+ me.on('expand', function(){
+ me.expand(recursive, callback, scope);
+ }, me, {single: true});
+ } else {
+ // Now we check if this record is already expanding or expanded
+ if (!me.isExpanded()) {
+ // The TreeStore actually listens for the beforeexpand method and checks
+ // whether we have to asynchronously load the children from the server
+ // first. Thats why we pass a callback function to the event that the
+ // store can call once it has loaded and parsed all the children.
+ me.fireEvent('beforeexpand', me, function(){
+ 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);
+ } else if (recursive) {
+ // If it is is already expanded but we want to recursively expand then call expandChildren
+ 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 {
+ } else {
+ // If it's not then we fire the callback right away
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
+ * @param {Boolean} [recursive=false] True to recursively expand all the children
+ * @param {Function} [callback] The function to execute once all the children are expanded
+ * @param {Object} [scope] The scope to run the callback in
*/
expandChildren: function(recursive, callback, scope) {
var me = this,
@@ -54249,22 +57875,21 @@ Ext.define('Ext.data.NodeInterface', {
nodes[i].expand(recursive, function () {
expanding--;
if (callback && !expanding) {
- Ext.callback(callback, scope || me, me.childNodes);
+ Ext.callback(callback, scope || me, [me.childNodes]);
}
- });
+ });
}
}
-
+
if (!expanding && callback) {
- Ext.callback(callback, scope || me, me.childNodes);
- }
+ 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
+ * @param {Boolean} [recursive=false] True to recursively collapse all the children
+ * @param {Function} [callback] The function to execute once the collapse completes
+ * @param {Object} [scope] The scope to run the callback in
*/
collapse: function(recursive, callback, scope) {
var me = this;
@@ -54273,18 +57898,18 @@ Ext.define('Ext.data.NodeInterface', {
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('beforecollapse', me, function() {
+ me.set('expanded', false);
me.fireEvent('collapse', me, me.childNodes, false);
-
- // Call the collapseChildren method if recursive was set to true
+
+ // 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]);
+ Ext.callback(callback, scope || me, [me.childNodes]);
}
- }, me);
+ }, me);
}
// If it is is already collapsed but we want to recursively collapse then call collapseChildren
else if (recursive) {
@@ -54293,15 +57918,15 @@ Ext.define('Ext.data.NodeInterface', {
}
// If it's not then we fire the callback right away
else {
- Ext.callback(callback, scope || me, me.childNodes);
+ 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
+ * @param {Function} [recursive=false] True to recursively collapse all the children
+ * @param {Function} [callback] The function to execute once all the children are collapsed
+ * @param {Object} [scope] The scope to run the callback in
*/
collapseChildren: function(recursive, callback, scope) {
var me = this,
@@ -54318,14 +57943,14 @@ Ext.define('Ext.data.NodeInterface', {
nodes[i].collapse(recursive, function () {
collapsing--;
if (callback && !collapsing) {
- Ext.callback(callback, scope || me, me.childNodes);
+ Ext.callback(callback, scope || me, [me.childNodes]);
}
- });
+ });
}
}
-
+
if (!collapsing && callback) {
- Ext.callback(callback, scope || me, me.childNodes);
+ Ext.callback(callback, scope || me, [me.childNodes]);
}
}
};
@@ -54344,14 +57969,16 @@ Ext.define('Ext.data.NodeStore', {
requires: ['Ext.data.NodeInterface'],
/**
- * @cfg {Ext.data.Record} node The Record you want to bind this Store to. Note that
+ * @cfg {Ext.data.Model} 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
+ * @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
@@ -54361,7 +57988,8 @@ Ext.define('Ext.data.NodeStore', {
recursive: false,
/**
- * @cfg {Boolean} rootVisible false to not include the root node in this Stores collection (defaults to true )
+ * @cfg {Boolean} rootVisible
+ * False to not include the root node in this Stores collection.
*/
rootVisible: false,
@@ -54581,75 +58209,142 @@ Ext.define('Ext.data.NodeStore', {
});
/**
* @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.
+ * 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
+ * it does not contain any actual logic or perform the request itself.
*/
Ext.define('Ext.data.Request', {
/**
- * @cfg {String} action The name of the action this Request represents. Usually one of 'create', 'read', 'update' or 'destroy'
+ * @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.
+ * @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'
+ * @cfg {String} method
+ * The HTTP method to use on this Request. Should be one of 'GET', 'POST', 'PUT' or 'DELETE'.
*/
method: 'GET',
/**
- * @cfg {String} url The url to access on this Request
+ * @cfg {String} url
+ * The url to access on this Request
*/
url: undefined,
+ /**
+ * Creates the Request object.
+ * @param {Object} [config] Config object.
+ */
constructor: function(config) {
Ext.apply(this, config);
}
});
+/**
+ * @author Don Griffin
+ *
+ * This class is a sequential id generator. A simple use of this class would be like so:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'sequential'
+ * });
+ * // assign id's of 1, 2, 3, etc.
+ *
+ * An example of a configured generator would be:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * idgen: {
+ * type: 'sequential',
+ * prefix: 'ID_',
+ * seed: 1000
+ * }
+ * });
+ * // assign id's of ID_1000, ID_1001, ID_1002, etc.
+ *
+ */
+Ext.define('Ext.data.SequentialIdGenerator', {
+ extend: 'Ext.data.IdGenerator',
+ alias: 'idgen.sequential',
+
+ constructor: function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.parts = [ me.prefix, ''];
+ },
+
+ /**
+ * @cfg {String} prefix
+ * The string to place in front of the sequential number for each generated id. The
+ * default is blank.
+ */
+ prefix: '',
+
+ /**
+ * @cfg {Number} seed
+ * The number at which to start generating sequential id's. The default is 1.
+ */
+ seed: 1,
+
+ /**
+ * Generates and returns the next id.
+ * @return {String} The next id.
+ */
+ generate: function () {
+ var me = this,
+ parts = me.parts;
+
+ parts[1] = me.seed++;
+ return parts.join('');
+ }
+});
+
/**
* @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}.
+ * 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
+ * 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"
},
/**
+ * @property {Ext.data.NodeInterface}
* The root node for this tree
- * @type Node
*/
root: null,
-
+
+ /**
+ * Creates new Tree object.
+ * @param {Ext.data.NodeInterface} root (optional) The root node
+ */
constructor: function(root) {
var me = this;
+
- me.nodeHash = {};
me.mixins.observable.constructor.call(me);
-
+
if (root) {
me.setRootNode(root);
}
@@ -54670,119 +58365,84 @@ Ext.define('Ext.data.Tree', {
*/
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
+ * @alias Ext.data.NodeInterface#append
*/
"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
+ * @alias Ext.data.NodeInterface#remove
*/
"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
+ * @alias Ext.data.NodeInterface#move
*/
"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
+ * @alias Ext.data.NodeInterface#insert
*/
"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
+ * @alias Ext.data.NodeInterface#beforeappend
*/
"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
+ * @alias Ext.data.NodeInterface#beforeremove
*/
"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
+ * @alias Ext.data.NodeInterface#beforemove
*/
"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
+ * @alias Ext.data.NodeInterface#beforeinsert
*/
"beforeinsert",
/**
* @event expand
- * Fires when this node is expanded.
- * @param {Node} this The expanding node
+ * @alias Ext.data.NodeInterface#expand
*/
"expand",
/**
* @event collapse
- * Fires when this node is collapsed.
- * @param {Node} this The collapsing node
+ * @alias Ext.data.NodeInterface#collapse
*/
"collapse",
/**
* @event beforeexpand
- * Fires before this node is expanded.
- * @param {Node} this The expanding node
+ * @alias Ext.data.NodeInterface#beforeexpand
*/
"beforeexpand",
/**
* @event beforecollapse
- * Fires before this node is collapsed.
- * @param {Node} this The collapsing node
+ * @alias Ext.data.NodeInterface#beforecollapse
*/
"beforecollapse" ,
@@ -54793,7 +58453,7 @@ Ext.define('Ext.data.Tree', {
*/
"rootchange"
]);
-
+
node.on({
scope: me,
insert: me.onNodeInsert,
@@ -54801,24 +58461,25 @@ Ext.define('Ext.data.Tree', {
remove: me.onNodeRemove
});
- me.registerNode(node);
+ me.nodeHash = {};
+ 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.
+ * @return {Ext.data.NodeInterface[]} The flattened nodes.
*/
flatten: function(){
var nodes = [],
hash = this.nodeHash,
key;
-
+
for (key in hash) {
if (hash.hasOwnProperty(key)) {
nodes.push(hash[key]);
@@ -54826,7 +58487,7 @@ Ext.define('Ext.data.Tree', {
}
return nodes;
},
-
+
/**
* Fired when a node is inserted into the root or one of it's children
* @private
@@ -54834,9 +58495,9 @@ Ext.define('Ext.data.Tree', {
* @param {Ext.data.NodeInterface} node The inserted node
*/
onNodeInsert: function(parent, node) {
- this.registerNode(node);
+ this.registerNode(node, true);
},
-
+
/**
* Fired when a node is appended into the root or one of it's children
* @private
@@ -54844,9 +58505,9 @@ Ext.define('Ext.data.Tree', {
* @param {Ext.data.NodeInterface} node The appended node
*/
onNodeAppend: function(parent, node) {
- this.registerNode(node);
+ this.registerNode(node, true);
},
-
+
/**
* Fired when a node is removed from the root or one of it's children
* @private
@@ -54854,7 +58515,7 @@ Ext.define('Ext.data.Tree', {
* @param {Ext.data.NodeInterface} node The removed node
*/
onNodeRemove: function(parent, node) {
- this.unregisterNode(node);
+ this.unregisterNode(node, true);
},
/**
@@ -54870,20 +58531,32 @@ Ext.define('Ext.data.Tree', {
* Registers a node with the tree
* @private
* @param {Ext.data.NodeInterface} The node to register
+ * @param {Boolean} [includeChildren] True to unregister any child nodes
*/
- registerNode : function(node) {
+ registerNode : function(node, includeChildren) {
this.nodeHash[node.getId() || node.internalId] = node;
+ if (includeChildren === true) {
+ node.eachChild(function(child){
+ this.registerNode(child, true);
+ }, this);
+ }
},
/**
* Unregisters a node with the tree
* @private
* @param {Ext.data.NodeInterface} The node to unregister
+ * @param {Boolean} [includeChildren] True to unregister any child nodes
*/
- unregisterNode : function(node) {
+ unregisterNode : function(node, includeChildren) {
delete this.nodeHash[node.getId() || node.internalId];
+ if (includeChildren === true) {
+ node.eachChild(function(child){
+ this.unregisterNode(child, true);
+ }, this);
+ }
},
-
+
/**
* Sorts this tree
* @private
@@ -54893,7 +58566,7 @@ Ext.define('Ext.data.Tree', {
sort: function(sorterFn, recursive) {
this.getRootNode().sort(sorterFn, recursive);
},
-
+
/**
* Filters this tree
* @private
@@ -54905,21 +58578,21 @@ Ext.define('Ext.data.Tree', {
}
});
/**
- * @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
+ *
+ * # 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,
+ * The standard Tree fields will also be copied onto the Model for maintaining their state. These fields are listed
+ * in the {@link Ext.data.NodeInterface} documentation.
+ *
+ * # Reading Nested Data
+ *
+ * For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured with a root property,
* so the reader can find nested data for each node. If a root is not specified, it will default to
* 'children'.
*/
@@ -54929,14 +58602,33 @@ Ext.define('Ext.data.TreeStore', {
requires: ['Ext.data.Tree', 'Ext.data.NodeInterface', 'Ext.data.NodeStore'],
/**
- * @cfg {Boolean} clearOnLoad (optional) Default to true. Remove previously existing
- * child nodes before loading.
+ * @cfg {Ext.data.Model/Ext.data.NodeInterface/Object} root
+ * The root node for this store. For example:
+ *
+ * root: {
+ * expanded: true,
+ * text: "My Root",
+ * children: [
+ * { text: "Child 1", leaf: true },
+ * { text: "Child 2", expanded: true, children: [
+ * { text: "GrandChild", leaf: true }
+ * ] }
+ * ]
+ * }
+ *
+ * Setting the `root` config option is the same as calling {@link #setRootNode}.
+ */
+
+ /**
+ * @cfg {Boolean} clearOnLoad
+ * Remove previously existing child nodes before loading. Default to true.
*/
clearOnLoad : true,
/**
- * @cfg {String} nodeParam The name of the parameter sent to the server which contains
- * the identifier of the node. Defaults to 'node' .
+ * @cfg {String} nodeParam
+ * The name of the parameter sent to the server which contains the identifier of the node.
+ * Defaults to 'node'.
*/
nodeParam: 'node',
@@ -54945,7 +58637,7 @@ Ext.define('Ext.data.TreeStore', {
* 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.
@@ -54953,18 +58645,18 @@ Ext.define('Ext.data.TreeStore', {
defaultRootProperty: 'children',
/**
- * @cfg {Boolean} folderSort Set to true to automatically prepend a leaf sorter (defaults to undefined )
+ * @cfg {Boolean} folderSort
+ * Set to true to automatically prepend a leaf sorter. Defaults to `undefined`.
*/
folderSort: false,
-
+
constructor: function(config) {
- var me = this,
+ 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.
@@ -54975,160 +58667,118 @@ Ext.define('Ext.data.TreeStore', {
}
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
+ * @alias Ext.data.Tree#append
*/
"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
+ * @alias Ext.data.Tree#remove
*/
"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
+ * @alias Ext.data.Tree#move
*/
"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
+ * @alias Ext.data.Tree#insert
*/
"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
+ * @alias Ext.data.Tree#beforeappend
*/
"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
+ * @alias Ext.data.Tree#beforeremove
*/
"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
+ * @alias Ext.data.Tree#beforemove
*/
"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
+ * @alias Ext.data.Tree#beforeinsert
*/
"beforeinsert",
-
+
/**
* @event expand
- * Fires when this node is expanded.
- * @param {Node} this The expanding node
+ * @alias Ext.data.Tree#expand
*/
"expand",
-
+
/**
* @event collapse
- * Fires when this node is collapsed.
- * @param {Node} this The collapsing node
+ * @alias Ext.data.Tree#collapse
*/
"collapse",
-
+
/**
* @event beforeexpand
- * Fires before this node is expanded.
- * @param {Node} this The expanding node
+ * @alias Ext.data.Tree#beforeexpand
*/
"beforeexpand",
-
+
/**
* @event beforecollapse
- * Fires before this node is collapsed.
- * @param {Node} this The collapsing node
+ * @alias Ext.data.Tree#beforecollapse
*/
"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
+ * @alias Ext.data.Tree#rootchange
*/
"rootchange"
]);
-
+
+ me.tree.on({
+ scope: me,
+ remove: me.onNodeRemove,
+ // this event must follow the relay to beforeitemexpand to allow users to
+ // cancel the expand:
+ 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.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.
+ * @event sort
+ * Fires when this TreeStore is sorted.
+ * @param {Ext.data.NodeInterface} node The node that is sorted.
*/
- 'rootchange'
+ 'sort'
);
-
+
//
if (Ext.isDefined(me.nodeParameter)) {
if (Ext.isDefined(Ext.global.console)) {
@@ -55139,12 +58789,12 @@ Ext.define('Ext.data.TreeStore', {
}
//
},
-
+
// 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);
@@ -55164,17 +58814,17 @@ Ext.define('Ext.data.TreeStore', {
reader.buildExtractors(true);
}
},
-
+
// inherit docs
onBeforeSort: function() {
if (this.folderSort) {
this.sort({
property: 'leaf',
direction: 'ASC'
- }, 'prepend', false);
+ }, 'prepend', false);
}
},
-
+
/**
* Called before a node is expanded.
* @private
@@ -55197,10 +58847,10 @@ Ext.define('Ext.data.TreeStore', {
callback: function() {
Ext.callback(callback, scope || node, [node.childNodes]);
}
- });
+ });
}
},
-
+
//inherit docs
getNewRecords: function() {
return Ext.Array.filter(this.tree.flatten(), this.filterNew);
@@ -55210,7 +58860,7 @@ Ext.define('Ext.data.TreeStore', {
getUpdatedRecords: function() {
return Ext.Array.filter(this.tree.flatten(), this.filterUpdated);
},
-
+
/**
* Called before a node is collapsed.
* @private
@@ -55221,23 +58871,23 @@ Ext.define('Ext.data.TreeStore', {
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);
-
+
+ Ext.Array.remove(this.removed, node);
+
if (!node.isLeaf() && !node.isLoaded()) {
dataRoot = reader.getRoot(data);
if (dataRoot) {
@@ -55246,18 +58896,18 @@ Ext.define('Ext.data.TreeStore', {
}
}
},
-
+
/**
- * Sets the root node for this store
- * @param {Ext.data.Model/Ext.data.NodeInterface} root
+ * Sets the root node for this store. See also the {@link #root} config option.
+ * @param {Ext.data.Model/Ext.data.NodeInterface/Object} root
* @return {Ext.data.NodeInterface} The new root
*/
setRootNode: function(root) {
var me = this;
- root = root || {};
+ root = root || {};
if (!root.isNode) {
- // create a default rootNode and create internal data struct.
+ // create a default rootNode and create internal data struct.
Ext.applyIf(root, {
id: me.defaultRootId,
text: 'Root',
@@ -55270,20 +58920,20 @@ Ext.define('Ext.data.TreeStore', {
// 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()) {
+ if (!root.isLoaded() && (me.autoLoad === true || root.isExpanded())) {
me.load({
node: root
});
}
-
+
return root;
},
-
+
/**
* Returns the root node for this tree.
* @return {Ext.data.NodeInterface}
@@ -55302,7 +58952,7 @@ Ext.define('Ext.data.TreeStore', {
/**
* Loads the Store using its configured {@link #proxy}.
- * @param {Object} options Optional config object. This is passed into the {@link Ext.data.Operation Operation}
+ * @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.
@@ -55310,11 +58960,11 @@ Ext.define('Ext.data.TreeStore', {
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) {
@@ -55322,29 +58972,29 @@ Ext.define('Ext.data.TreeStore', {
expanded: true
});
}
-
+
if (me.clearOnLoad) {
- node.removeAll();
+ node.removeAll(true);
}
-
+
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
+ * @param {Ext.data.Model[]} records The records to add
*/
fillNode: function(node, records) {
var me = this,
@@ -55357,12 +59007,12 @@ Ext.define('Ext.data.TreeStore', {
sortCollection.sort(me.sorters.items);
records = sortCollection.items;
}
-
+
node.set('loaded', true);
for (; i < ln; i++) {
node.appendChild(records[i], undefined, true);
}
-
+
return records;
},
@@ -55373,21 +59023,32 @@ Ext.define('Ext.data.TreeStore', {
records = operation.getRecords(),
node = operation.node;
+ me.loading = false;
node.set('loading', false);
if (successful) {
records = me.fillNode(node, records);
}
+ // The load event has an extra node parameter
+ // (differing from the load event described in AbstractStore)
+ /**
+ * @event load
+ * Fires whenever the store reads data from a remote data source.
+ * @param {Ext.data.TreeStore} this
+ * @param {Ext.data.NodeInterface} node The node that was loaded.
+ * @param {Ext.data.Model[]} records An array of records.
+ * @param {Boolean} successful True if the operation was successful.
+ */
// 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.
+ * Creates any new records when a write is returned from the server.
* @private
- * @param {Array} records The array of new records
+ * @param {Ext.data.Model[]} 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
*/
@@ -55424,9 +59085,9 @@ Ext.define('Ext.data.TreeStore', {
},
/**
- * Update any records when a write is returned from the server.
+ * Updates any records when a write is returned from the server.
* @private
- * @param {Array} records The array of updated records
+ * @param {Ext.data.Model[]} 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
*/
@@ -55455,9 +59116,9 @@ Ext.define('Ext.data.TreeStore', {
},
/**
- * Remove any records when a write is returned from the server.
+ * Removes any records when a write is returned from the server.
* @private
- * @param {Array} records The array of removed records
+ * @param {Ext.data.Model[]} 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
*/
@@ -55482,10 +59143,227 @@ Ext.define('Ext.data.TreeStore', {
} else {
me.tree.sort(sorterFn, true);
me.fireEvent('datachanged', me);
- }
+ }
me.fireEvent('sort', me);
}
});
+
+/**
+ * @extend Ext.data.IdGenerator
+ * @author Don Griffin
+ *
+ * This class generates UUID's according to RFC 4122. This class has a default id property.
+ * This means that a single instance is shared unless the id property is overridden. Thus,
+ * two {@link Ext.data.Model} instances configured like the following share one generator:
+ *
+ * Ext.define('MyApp.data.MyModelX', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'uuid'
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelY', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'uuid'
+ * });
+ *
+ * This allows all models using this class to share a commonly configured instance.
+ *
+ * # Using Version 1 ("Sequential") UUID's
+ *
+ * If a server can provide a proper timestamp and a "cryptographic quality random number"
+ * (as described in RFC 4122), the shared instance can be configured as follows:
+ *
+ * Ext.data.IdGenerator.get('uuid').reconfigure({
+ * version: 1,
+ * clockSeq: clock, // 14 random bits
+ * salt: salt, // 48 secure random bits (the Node field)
+ * timestamp: ts // timestamp per Section 4.1.4
+ * });
+ *
+ * // or these values can be split into 32-bit chunks:
+ *
+ * Ext.data.IdGenerator.get('uuid').reconfigure({
+ * version: 1,
+ * clockSeq: clock,
+ * salt: { lo: saltLow32, hi: saltHigh32 },
+ * timestamp: { lo: timestampLow32, hi: timestamptHigh32 }
+ * });
+ *
+ * This approach improves the generator's uniqueness by providing a valid timestamp and
+ * higher quality random data. Version 1 UUID's should not be used unless this information
+ * can be provided by a server and care should be taken to avoid caching of this data.
+ *
+ * See http://www.ietf.org/rfc/rfc4122.txt for details.
+ */
+Ext.define('Ext.data.UuidGenerator', function () {
+ var twoPow14 = Math.pow(2, 14),
+ twoPow16 = Math.pow(2, 16),
+ twoPow28 = Math.pow(2, 28),
+ twoPow32 = Math.pow(2, 32);
+
+ function toHex (value, length) {
+ var ret = value.toString(16);
+ if (ret.length > length) {
+ ret = ret.substring(ret.length - length); // right-most digits
+ } else if (ret.length < length) {
+ ret = Ext.String.leftPad(ret, length, '0');
+ }
+ return ret;
+ }
+
+ function rand (lo, hi) {
+ var v = Math.random() * (hi - lo + 1);
+ return Math.floor(v) + lo;
+ }
+
+ function split (bignum) {
+ if (typeof(bignum) == 'number') {
+ var hi = Math.floor(bignum / twoPow32);
+ return {
+ lo: Math.floor(bignum - hi * twoPow32),
+ hi: hi
+ };
+ }
+ return bignum;
+ }
+
+ return {
+ extend: 'Ext.data.IdGenerator',
+
+ alias: 'idgen.uuid',
+
+ id: 'uuid', // shared by default
+
+ /**
+ * @property {Number/Object} salt
+ * When created, this value is a 48-bit number. For computation, this value is split
+ * into 32-bit parts and stored in an object with `hi` and `lo` properties.
+ */
+
+ /**
+ * @property {Number/Object} timestamp
+ * When created, this value is a 60-bit number. For computation, this value is split
+ * into 32-bit parts and stored in an object with `hi` and `lo` properties.
+ */
+
+ /**
+ * @cfg {Number} version
+ * The Version of UUID. Supported values are:
+ *
+ * * 1 : Time-based, "sequential" UUID.
+ * * 4 : Pseudo-random UUID.
+ *
+ * The default is 4.
+ */
+ version: 4,
+
+ constructor: function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.parts = [];
+ me.init();
+ },
+
+ generate: function () {
+ var me = this,
+ parts = me.parts,
+ ts = me.timestamp;
+
+ /*
+ The magic decoder ring (derived from RFC 4122 Section 4.2.2):
+
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | time_low |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | time_mid | ver | time_hi |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ |res| clock_hi | clock_low | salt 0 |M| salt 1 |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | salt (2-5) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+ time_mid clock_hi (low 6 bits)
+ time_low | time_hi |clock_lo
+ | | | || salt[0]
+ | | | || | salt[1..5]
+ v v v vv v v
+ 0badf00d-aced-1def-b123-dfad0badbeef
+ ^ ^ ^
+ version | multicast (low bit)
+ |
+ reserved (upper 2 bits)
+ */
+ parts[0] = toHex(ts.lo, 8);
+ parts[1] = toHex(ts.hi & 0xFFFF, 4);
+ parts[2] = toHex(((ts.hi >>> 16) & 0xFFF) | (me.version << 12), 4);
+ parts[3] = toHex(0x80 | ((me.clockSeq >>> 8) & 0x3F), 2) +
+ toHex(me.clockSeq & 0xFF, 2);
+ parts[4] = toHex(me.salt.hi, 4) + toHex(me.salt.lo, 8);
+
+ if (me.version == 4) {
+ me.init(); // just regenerate all the random values...
+ } else {
+ // sequentially increment the timestamp...
+ ++ts.lo;
+ if (ts.lo >= twoPow32) { // if (overflow)
+ ts.lo = 0;
+ ++ts.hi;
+ }
+ }
+
+ return parts.join('-').toLowerCase();
+ },
+
+ getRecId: function (rec) {
+ return rec.getId();
+ },
+
+ /**
+ * @private
+ */
+ init: function () {
+ var me = this,
+ salt, time;
+
+ if (me.version == 4) {
+ // See RFC 4122 (Secion 4.4)
+ // o If the state was unavailable (e.g., non-existent or corrupted),
+ // or the saved node ID is different than the current node ID,
+ // generate a random clock sequence value.
+ me.clockSeq = rand(0, twoPow14-1);
+
+ // we run this on every id generation...
+ salt = me.salt || (me.salt = {});
+ time = me.timestamp || (me.timestamp = {});
+
+ // See RFC 4122 (Secion 4.4)
+ salt.lo = rand(0, twoPow32-1);
+ salt.hi = rand(0, twoPow16-1);
+ time.lo = rand(0, twoPow32-1);
+ time.hi = rand(0, twoPow28-1);
+ } else {
+ // this is run only once per-instance
+ me.salt = split(me.salt);
+ me.timestamp = split(me.timestamp);
+
+ // Set multicast bit: "the least significant bit of the first octet of the
+ // node ID" (nodeId = salt for this implementation):
+ me.salt.hi |= 0x100;
+ }
+ },
+
+ /**
+ * Reconfigures this generator given new config properties.
+ */
+ reconfigure: function (config) {
+ Ext.apply(this, config);
+ this.init();
+ }
+ };
+}());
+
/**
* @author Ed Spencer
* @class Ext.data.XmlStore
@@ -55541,10 +59419,8 @@ var store = new Ext.data.XmlStore({
</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
+ *
Note: 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', {
@@ -55573,16 +59449,15 @@ Ext.define('Ext.data.XmlStore', {
/**
* @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.
+ *
+ * Base class for any client-side storage. Used as a superclass for {@link Ext.data.proxy.Memory Memory} and
+ * {@link Ext.data.proxy.WebStorage Web Storage} proxies. Do not use directly, use one of the subclasses instead.
+ * @private
*/
Ext.define('Ext.data.proxy.Client', {
extend: 'Ext.data.proxy.Proxy',
alternateClassName: 'Ext.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)
@@ -55595,154 +59470,130 @@ Ext.define('Ext.data.proxy.Client', {
});
/**
* @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.
+ * The JsonP proxy is useful when you need to load data from a domain other than the one your application is running on. If
+ * your application is running on http://domainA.com it cannot use {@link Ext.data.proxy.Ajax Ajax} to load its data
+ * from http://domainB.com because cross-domain ajax requests are prohibited by the browser.
*
- *
We can get around this using a 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:
+ * We can get around this using a JsonP proxy. JsonP proxy injects a `
*
- *
When we inject the tag above, the browser makes a request to that url and includes the response as if it was any
- * other type of JavaScript include. By passing a callback in the url above, we're telling domainB's server that we
- * want to be notified when the result comes in and that it should call our callback function with the data it sends
- * back. So long as the server formats the response to look like this, everything will work:
+ * 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"
- }
- ]
-});
-
+ * 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.
+ * 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:
+ * JsonP proxy takes care of all of this automatically. It formats the url you pass, adding the callback parameter
+ * automatically. It even creates a temporary callback function, waits for it to be called and then puts the data into
+ * the Proxy making it look just like you loaded it through a normal {@link Ext.data.proxy.Ajax AjaxProxy}. Here's how
+ * we might set that up:
*
-
-Ext.define('User', {
- extend: 'Ext.data.Model',
- fields: ['id', 'name', 'email']
-});
-
-var store = new Ext.data.Store({
- model: 'User',
- proxy: {
- type: 'jsonp',
- url : 'http://domainB.com/users'
- }
-});
-
-store.load();
-
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email']
+ * });
*
- *
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:
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'jsonp',
+ * url : 'http://domainB.com/users'
+ * }
+ * });
*
-
-<script src="http://domainB.com/users?callback=stcCallback001" id="stcScript001"></script>
-
+ * store.load();
*
- *
Customization
+ * That's all we need to do - JsonP proxy takes care of the rest. In this case the Proxy will have injected a script tag
+ * like this:
*
- *
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();
-
+ * # Customization
*
- *
Would inject a script tag like this:
+ * This script tag can be customized using the {@link #callbackKey} configuration. For example:
*
-
-<script src="http://domainB.com/users?theCallbackFunction=ABC001" id="injectedScript001"></script>
-
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'jsonp',
+ * url : 'http://domainB.com/users',
+ * callbackKey: 'theCallbackFunction'
+ * }
+ * });
*
- *
Implementing on the server side
+ * store.load();
*
- *
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:
+ * Would inject a script tag like this:
*
- *
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(");");
-}
-
+ * # Implementing on the server side
*
- *
PHP:
+ * 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:
*
-
-$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);
-}
-
+ * Java:
*
- *
ASP.net:
+ * 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(");");
+ * }
*
-
-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);
-
+ * 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',
@@ -55753,26 +59604,28 @@ Ext.define('Ext.data.proxy.JsonP', {
defaultWriterType: 'base',
/**
- * @cfg {String} callbackKey (Optional) See {@link Ext.data.JsonP#callbackKey}.
+ * @cfg {String} callbackKey
+ * 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'
+ * 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
+ * @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
+ * @event
* Fires when the server returns an exception
* @param {Ext.data.proxy.Proxy} this
* @param {Ext.data.Request} request The request that was sent
@@ -55785,7 +59638,7 @@ Ext.define('Ext.data.proxy.JsonP', {
/**
* @private
- * Performs the read request to the remote domain. JsonPProxy does not actually create an Ajax request,
+ * Performs the read request to the remote domain. JsonP proxy does not actually create an Ajax request,
* instead we write out a