Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / button / Button.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @docauthor Robert Dougan <rob@sencha.com>
17  *
18  * Create simple buttons with this component. Customisations include {@link #iconAlign aligned}
19  * {@link #iconCls icons}, {@link #menu dropdown menus}, {@link #tooltip tooltips}
20  * and {@link #scale sizing options}. Specify a {@link #handler handler} to run code when
21  * a user clicks the button, or use {@link #listeners listeners} for other events such as
22  * {@link #mouseover mouseover}. Example usage:
23  *
24  *     @example
25  *     Ext.create('Ext.Button', {
26  *         text: 'Click me',
27  *         renderTo: Ext.getBody(),
28  *         handler: function() {
29  *             alert('You clicked the button!')
30  *         }
31  *     });
32  *
33  * The {@link #handler} configuration can also be updated dynamically using the {@link #setHandler}
34  * method.  Example usage:
35  *
36  *     @example
37  *     Ext.create('Ext.Button', {
38  *         text    : 'Dynamic Handler Button',
39  *         renderTo: Ext.getBody(),
40  *         handler : function() {
41  *             // this button will spit out a different number every time you click it.
42  *             // so firstly we must check if that number is already set:
43  *             if (this.clickCount) {
44  *                 // looks like the property is already set, so lets just add 1 to that number and alert the user
45  *                 this.clickCount++;
46  *                 alert('You have clicked the button "' + this.clickCount + '" times.\n\nTry clicking it again..');
47  *             } else {
48  *                 // if the clickCount property is not set, we will set it and alert the user
49  *                 this.clickCount = 1;
50  *                 alert('You just clicked the button for the first time!\n\nTry pressing it again..');
51  *             }
52  *         }
53  *     });
54  *
55  * A button within a container:
56  *
57  *     @example
58  *     Ext.create('Ext.Container', {
59  *         renderTo: Ext.getBody(),
60  *         items   : [
61  *             {
62  *                 xtype: 'button',
63  *                 text : 'My Button'
64  *             }
65  *         ]
66  *     });
67  *
68  * A useful option of Button is the {@link #scale} configuration. This configuration has three different options:
69  *
70  * - `'small'`
71  * - `'medium'`
72  * - `'large'`
73  *
74  * Example usage:
75  *
76  *     @example
77  *     Ext.create('Ext.Button', {
78  *         renderTo: document.body,
79  *         text    : 'Click me',
80  *         scale   : 'large'
81  *     });
82  *
83  * Buttons can also be toggled. To enable this, you simple set the {@link #enableToggle} property to `true`.
84  * Example usage:
85  *
86  *     @example
87  *     Ext.create('Ext.Button', {
88  *         renderTo: Ext.getBody(),
89  *         text: 'Click Me',
90  *         enableToggle: true
91  *     });
92  *
93  * You can assign a menu to a button by using the {@link #menu} configuration. This standard configuration
94  * can either be a reference to a {@link Ext.menu.Menu menu} object, a {@link Ext.menu.Menu menu} id or a
95  * {@link Ext.menu.Menu menu} config blob. When assigning a menu to a button, an arrow is automatically
96  * added to the button.  You can change the alignment of the arrow using the {@link #arrowAlign} configuration
97  * on button.  Example usage:
98  *
99  *     @example
100  *     Ext.create('Ext.Button', {
101  *         text      : 'Menu button',
102  *         renderTo  : Ext.getBody(),
103  *         arrowAlign: 'bottom',
104  *         menu      : [
105  *             {text: 'Item 1'},
106  *             {text: 'Item 2'},
107  *             {text: 'Item 3'},
108  *             {text: 'Item 4'}
109  *         ]
110  *     });
111  *
112  * Using listeners, you can easily listen to events fired by any component, using the {@link #listeners}
113  * configuration or using the {@link #addListener} method.  Button has a variety of different listeners:
114  *
115  * - `click`
116  * - `toggle`
117  * - `mouseover`
118  * - `mouseout`
119  * - `mouseshow`
120  * - `menuhide`
121  * - `menutriggerover`
122  * - `menutriggerout`
123  *
124  * Example usage:
125  *
126  *     @example
127  *     Ext.create('Ext.Button', {
128  *         text     : 'Button',
129  *         renderTo : Ext.getBody(),
130  *         listeners: {
131  *             click: function() {
132  *                 // this == the button, as we are in the local scope
133  *                 this.setText('I was clicked!');
134  *             },
135  *             mouseover: function() {
136  *                 // set a new config which says we moused over, if not already set
137  *                 if (!this.mousedOver) {
138  *                     this.mousedOver = true;
139  *                     alert('You moused over a button!\n\nI wont do this again.');
140  *                 }
141  *             }
142  *         }
143  *     });
144  */
145 Ext.define('Ext.button.Button', {
146
147     /* Begin Definitions */
148     alias: 'widget.button',
149     extend: 'Ext.Component',
150
151     requires: [
152         'Ext.menu.Manager',
153         'Ext.util.ClickRepeater',
154         'Ext.layout.component.Button',
155         'Ext.util.TextMetrics',
156         'Ext.util.KeyMap'
157     ],
158
159     alternateClassName: 'Ext.Button',
160     /* End Definitions */
161
162     isButton: true,
163     componentLayout: 'button',
164
165     /**
166      * @property {Boolean} hidden
167      * True if this button is hidden. Read-only.
168      */
169     hidden: false,
170
171     /**
172      * @property {Boolean} disabled
173      * True if this button is disabled. Read-only.
174      */
175     disabled: false,
176
177     /**
178      * @property {Boolean} pressed
179      * True if this button is pressed (only if enableToggle = true). Read-only.
180      */
181     pressed: false,
182
183     /**
184      * @cfg {String} text
185      * The button text to be used as innerHTML (html tags are accepted).
186      */
187
188     /**
189      * @cfg {String} icon
190      * The path to an image to display in the button (the image will be set as the background-image CSS property of the
191      * button by default, so if you want a mixed icon/text button, set cls:'x-btn-text-icon')
192      */
193
194     /**
195      * @cfg {Function} handler
196      * A function called when the button is clicked (can be used instead of click event).
197      * @cfg {Ext.button.Button} handler.button This button.
198      * @cfg {Ext.EventObject} handler.e The click event.
199      */
200
201     /**
202      * @cfg {Number} minWidth
203      * The minimum width for this button (used to give a set of buttons a common width).
204      * See also {@link Ext.panel.Panel}.{@link Ext.panel.Panel#minButtonWidth minButtonWidth}.
205      */
206
207     /**
208      * @cfg {String/Object} tooltip
209      * The tooltip for the button - can be a string to be used as innerHTML (html tags are accepted) or
210      * QuickTips config object.
211      */
212
213     /**
214      * @cfg {Boolean} [hidden=false]
215      * True to start hidden.
216      */
217
218     /**
219      * @cfg {Boolean} [disabled=true]
220      * True to start disabled.
221      */
222
223     /**
224      * @cfg {Boolean} [pressed=false]
225      * True to start pressed (only if enableToggle = true)
226      */
227
228     /**
229      * @cfg {String} toggleGroup
230      * The group this toggle button is a member of (only 1 per group can be pressed)
231      */
232
233     /**
234      * @cfg {Boolean/Object} [repeat=false]
235      * True to repeat fire the click event while the mouse is down. This can also be a
236      * {@link Ext.util.ClickRepeater ClickRepeater} config object.
237      */
238
239     /**
240      * @cfg {Number} tabIndex
241      * Set a DOM tabIndex for this button.
242      */
243
244     /**
245      * @cfg {Boolean} [allowDepress=true]
246      * False to not allow a pressed Button to be depressed. Only valid when {@link #enableToggle} is true.
247      */
248
249     /**
250      * @cfg {Boolean} [enableToggle=false]
251      * True to enable pressed/not pressed toggling.
252      */
253     enableToggle: false,
254
255     /**
256      * @cfg {Function} toggleHandler
257      * Function called when a Button with {@link #enableToggle} set to true is clicked.
258      * @cfg {Ext.button.Button} toggleHandler.button This button.
259      * @cfg {Boolean} toggleHandler.state The next state of the Button, true means pressed.
260      */
261
262     /**
263      * @cfg {Ext.menu.Menu/String/Object} menu
264      * Standard menu attribute consisting of a reference to a menu object, a menu id or a menu config blob.
265      */
266
267     /**
268      * @cfg {String} menuAlign
269      * The position to align the menu to (see {@link Ext.Element#alignTo} for more details).
270      */
271     menuAlign: 'tl-bl?',
272
273     /**
274      * @cfg {String} textAlign
275      * The text alignment for this button (center, left, right).
276      */
277     textAlign: 'center',
278
279     /**
280      * @cfg {String} overflowText
281      * If used in a {@link Ext.toolbar.Toolbar Toolbar}, the text to be used if this item is shown in the overflow menu.
282      * See also {@link Ext.toolbar.Item}.`{@link Ext.toolbar.Item#overflowText overflowText}`.
283      */
284
285     /**
286      * @cfg {String} iconCls
287      * A css class which sets a background image to be used as the icon for this button.
288      */
289
290     /**
291      * @cfg {String} type
292      * The type of `<input>` to create: submit, reset or button.
293      */
294     type: 'button',
295
296     /**
297      * @cfg {String} clickEvent
298      * The DOM event that will fire the handler of the button. This can be any valid event name (dblclick, contextmenu).
299      */
300     clickEvent: 'click',
301
302     /**
303      * @cfg {Boolean} preventDefault
304      * True to prevent the default action when the {@link #clickEvent} is processed.
305      */
306     preventDefault: true,
307
308     /**
309      * @cfg {Boolean} handleMouseEvents
310      * False to disable visual cues on mouseover, mouseout and mousedown.
311      */
312     handleMouseEvents: true,
313
314     /**
315      * @cfg {String} tooltipType
316      * The type of tooltip to use. Either 'qtip' for QuickTips or 'title' for title attribute.
317      */
318     tooltipType: 'qtip',
319
320     /**
321      * @cfg {String} [baseCls='x-btn']
322      * The base CSS class to add to all buttons.
323      */
324     baseCls: Ext.baseCSSPrefix + 'btn',
325
326     /**
327      * @cfg {String} pressedCls
328      * The CSS class to add to a button when it is in the pressed state.
329      */
330     pressedCls: 'pressed',
331
332     /**
333      * @cfg {String} overCls
334      * The CSS class to add to a button when it is in the over (hovered) state.
335      */
336     overCls: 'over',
337
338     /**
339      * @cfg {String} focusCls
340      * The CSS class to add to a button when it is in the focussed state.
341      */
342     focusCls: 'focus',
343
344     /**
345      * @cfg {String} menuActiveCls
346      * The CSS class to add to a button when it's menu is active.
347      */
348     menuActiveCls: 'menu-active',
349
350     /**
351      * @cfg {String} href
352      * The URL to visit when the button is clicked. Specifying this config is equivalent to specifying:
353      *
354      *     handler: function() { window.location = "http://www.sencha.com" }
355      */
356
357     /**
358      * @cfg {Object} baseParams
359      * An object literal of parameters to pass to the url when the {@link #href} property is specified.
360      */
361
362     /**
363      * @cfg {Object} params
364      * An object literal of parameters to pass to the url when the {@link #href} property is specified. Any params
365      * override {@link #baseParams}. New params can be set using the {@link #setParams} method.
366      */
367
368     ariaRole: 'button',
369
370     // inherited
371     renderTpl:
372         '<em id="{id}-btnWrap" class="{splitCls}">' +
373             '<tpl if="href">' +
374                 '<a id="{id}-btnEl" href="{href}" target="{target}"<tpl if="tabIndex"> tabIndex="{tabIndex}"</tpl> role="link">' +
375                     '<span id="{id}-btnInnerEl" class="{baseCls}-inner">' +
376                         '{text}' +
377                     '</span>' +
378                         '<span id="{id}-btnIconEl" class="{baseCls}-icon"></span>' +
379                 '</a>' +
380             '</tpl>' +
381             '<tpl if="!href">' +
382                 '<button id="{id}-btnEl" type="{type}" hidefocus="true"' +
383                     // the autocomplete="off" is required to prevent Firefox from remembering
384                     // the button's disabled state between page reloads.
385                     '<tpl if="tabIndex"> tabIndex="{tabIndex}"</tpl> role="button" autocomplete="off">' +
386                     '<span id="{id}-btnInnerEl" class="{baseCls}-inner" style="{innerSpanStyle}">' +
387                         '{text}' +
388                     '</span>' +
389                     '<span id="{id}-btnIconEl" class="{baseCls}-icon {iconCls}">&#160;</span>' +
390                 '</button>' +
391             '</tpl>' +
392         '</em>' ,
393
394     /**
395      * @cfg {String} scale
396      * The size of the Button. Three values are allowed:
397      *
398      * - 'small' - Results in the button element being 16px high.
399      * - 'medium' - Results in the button element being 24px high.
400      * - 'large' - Results in the button element being 32px high.
401      */
402     scale: 'small',
403
404     /**
405      * @private
406      * An array of allowed scales.
407      */
408     allowedScales: ['small', 'medium', 'large'],
409
410     /**
411      * @cfg {Object} scope
412      * The scope (**this** reference) in which the `{@link #handler}` and `{@link #toggleHandler}` is executed.
413      * Defaults to this Button.
414      */
415
416     /**
417      * @cfg {String} iconAlign
418      * The side of the Button box to render the icon. Four values are allowed:
419      *
420      * - 'top'
421      * - 'right'
422      * - 'bottom'
423      * - 'left'
424      */
425     iconAlign: 'left',
426
427     /**
428      * @cfg {String} arrowAlign
429      * The side of the Button box to render the arrow if the button has an associated {@link #menu}. Two
430      * values are allowed:
431      *
432      * - 'right'
433      * - 'bottom'
434      */
435     arrowAlign: 'right',
436
437     /**
438      * @cfg {String} arrowCls
439      * The className used for the inner arrow element if the button has a menu.
440      */
441     arrowCls: 'arrow',
442
443     /**
444      * @property {Ext.Template} template
445      * A {@link Ext.Template Template} used to create the Button's DOM structure.
446      *
447      * Instances, or subclasses which need a different DOM structure may provide a different template layout in
448      * conjunction with an implementation of {@link #getTemplateArgs}.
449      */
450
451     /**
452      * @cfg {String} cls
453      * A CSS class string to apply to the button's main element.
454      */
455
456     /**
457      * @property {Ext.menu.Menu} menu
458      * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the {@link #menu} config
459      * option.
460      */
461
462     /**
463      * @cfg {Boolean} autoWidth
464      * By default, if a width is not specified the button will attempt to stretch horizontally to fit its content. If
465      * the button is being managed by a width sizing layout (hbox, fit, anchor), set this to false to prevent the button
466      * from doing this automatic sizing.
467      */
468
469     maskOnDisable: false,
470
471     // inherit docs
472     initComponent: function() {
473         var me = this;
474         me.callParent(arguments);
475
476         me.addEvents(
477             /**
478              * @event click
479              * Fires when this button is clicked
480              * @param {Ext.button.Button} this
481              * @param {Event} e The click event
482              */
483             'click',
484
485             /**
486              * @event toggle
487              * Fires when the 'pressed' state of this button changes (only if enableToggle = true)
488              * @param {Ext.button.Button} this
489              * @param {Boolean} pressed
490              */
491             'toggle',
492
493             /**
494              * @event mouseover
495              * Fires when the mouse hovers over the button
496              * @param {Ext.button.Button} this
497              * @param {Event} e The event object
498              */
499             'mouseover',
500
501             /**
502              * @event mouseout
503              * Fires when the mouse exits the button
504              * @param {Ext.button.Button} this
505              * @param {Event} e The event object
506              */
507             'mouseout',
508
509             /**
510              * @event menushow
511              * If this button has a menu, this event fires when it is shown
512              * @param {Ext.button.Button} this
513              * @param {Ext.menu.Menu} menu
514              */
515             'menushow',
516
517             /**
518              * @event menuhide
519              * If this button has a menu, this event fires when it is hidden
520              * @param {Ext.button.Button} this
521              * @param {Ext.menu.Menu} menu
522              */
523             'menuhide',
524
525             /**
526              * @event menutriggerover
527              * If this button has a menu, this event fires when the mouse enters the menu triggering element
528              * @param {Ext.button.Button} this
529              * @param {Ext.menu.Menu} menu
530              * @param {Event} e
531              */
532             'menutriggerover',
533
534             /**
535              * @event menutriggerout
536              * If this button has a menu, this event fires when the mouse leaves the menu triggering element
537              * @param {Ext.button.Button} this
538              * @param {Ext.menu.Menu} menu
539              * @param {Event} e
540              */
541             'menutriggerout'
542         );
543
544         if (me.menu) {
545             // Flag that we'll have a splitCls
546             me.split = true;
547
548             // retrieve menu by id or instantiate instance if needed
549             me.menu = Ext.menu.Manager.get(me.menu);
550             me.menu.ownerCt = me;
551         }
552
553         // Accept url as a synonym for href
554         if (me.url) {
555             me.href = me.url;
556         }
557
558         // preventDefault defaults to false for links
559         if (me.href && !me.hasOwnProperty('preventDefault')) {
560             me.preventDefault = false;
561         }
562
563         if (Ext.isString(me.toggleGroup)) {
564             me.enableToggle = true;
565         }
566
567     },
568
569     // private
570     initAria: function() {
571         this.callParent();
572         var actionEl = this.getActionEl();
573         if (this.menu) {
574             actionEl.dom.setAttribute('aria-haspopup', true);
575         }
576     },
577
578     // inherit docs
579     getActionEl: function() {
580         return this.btnEl;
581     },
582
583     // inherit docs
584     getFocusEl: function() {
585         return this.btnEl;
586     },
587
588     // private
589     setButtonCls: function() {
590         var me = this,
591             cls = [],
592             btnIconEl = me.btnIconEl,
593             hide = 'x-hide-display';
594
595         if (me.useSetClass) {
596             if (!Ext.isEmpty(me.oldCls)) {
597                 me.removeClsWithUI(me.oldCls);
598                 me.removeClsWithUI(me.pressedCls);
599             }
600
601             // Check whether the button has an icon or not, and if it has an icon, what is th alignment
602             if (me.iconCls || me.icon) {
603                 if (me.text) {
604                     cls.push('icon-text-' + me.iconAlign);
605                 } else {
606                     cls.push('icon');
607                 }
608                 if (btnIconEl) {
609                     btnIconEl.removeCls(hide);
610                 }
611             } else {
612                 if (me.text) {
613                     cls.push('noicon');
614                 }
615                 if (btnIconEl) {
616                     btnIconEl.addCls(hide);
617                 }
618             }
619
620             me.oldCls = cls;
621             me.addClsWithUI(cls);
622             me.addClsWithUI(me.pressed ? me.pressedCls : null);
623         }
624     },
625
626     // private
627     onRender: function(ct, position) {
628         // classNames for the button
629         var me = this,
630             repeater, btn;
631
632         // Apply the renderData to the template args
633         Ext.applyIf(me.renderData, me.getTemplateArgs());
634
635         me.addChildEls('btnEl', 'btnWrap', 'btnInnerEl', 'btnIconEl');
636
637         if (me.scale) {
638             me.ui = me.ui + '-' + me.scale;
639         }
640
641         // Render internal structure
642         me.callParent(arguments);
643
644         // If it is a split button + has a toolip for the arrow
645         if (me.split && me.arrowTooltip) {
646             me.arrowEl.dom.setAttribute(me.getTipAttr(), me.arrowTooltip);
647         }
648
649         // Add listeners to the focus and blur events on the element
650         me.mon(me.btnEl, {
651             scope: me,
652             focus: me.onFocus,
653             blur : me.onBlur
654         });
655
656         // Set btn as a local variable for easy access
657         btn = me.el;
658
659         if (me.icon) {
660             me.setIcon(me.icon);
661         }
662
663         if (me.iconCls) {
664             me.setIconCls(me.iconCls);
665         }
666
667         if (me.tooltip) {
668             me.setTooltip(me.tooltip, true);
669         }
670
671         if (me.textAlign) {
672             me.setTextAlign(me.textAlign);
673         }
674
675         // Add the mouse events to the button
676         if (me.handleMouseEvents) {
677             me.mon(btn, {
678                 scope: me,
679                 mouseover: me.onMouseOver,
680                 mouseout: me.onMouseOut,
681                 mousedown: me.onMouseDown
682             });
683
684             if (me.split) {
685                 me.mon(btn, {
686                     mousemove: me.onMouseMove,
687                     scope: me
688                 });
689             }
690         }
691
692         // Check if the button has a menu
693         if (me.menu) {
694             me.mon(me.menu, {
695                 scope: me,
696                 show: me.onMenuShow,
697                 hide: me.onMenuHide
698             });
699
700             me.keyMap = Ext.create('Ext.util.KeyMap', me.el, {
701                 key: Ext.EventObject.DOWN,
702                 handler: me.onDownKey,
703                 scope: me
704             });
705         }
706
707         // Check if it is a repeat button
708         if (me.repeat) {
709             repeater = Ext.create('Ext.util.ClickRepeater', btn, Ext.isObject(me.repeat) ? me.repeat: {});
710             me.mon(repeater, 'click', me.onRepeatClick, me);
711         } else {
712             me.mon(btn, me.clickEvent, me.onClick, me);
713         }
714
715         // Register the button in the toggle manager
716         Ext.ButtonToggleManager.register(me);
717     },
718
719     /**
720      * This method returns an object which provides substitution parameters for the {@link #renderTpl XTemplate} used to
721      * create this Button's DOM structure.
722      *
723      * Instances or subclasses which use a different Template to create a different DOM structure may need to provide
724      * their own implementation of this method.
725      *
726      * @return {Object} Substitution data for a Template. The default implementation which provides data for the default
727      * {@link #template} returns an Object containing the following properties:
728      * @return {String} return.type The `<button>`'s {@link #type}
729      * @return {String} return.splitCls A CSS class to determine the presence and position of an arrow icon.
730      * (`'x-btn-arrow'` or `'x-btn-arrow-bottom'` or `''`)
731      * @return {String} return.cls A CSS class name applied to the Button's main `<tbody>` element which determines the
732      * button's scale and icon alignment.
733      * @return {String} return.text The {@link #text} to display ion the Button.
734      * @return {Number} return.tabIndex The tab index within the input flow.
735      */
736     getTemplateArgs: function() {
737         var me = this,
738             persistentPadding = me.getPersistentBtnPadding(),
739             innerSpanStyle = '';
740
741         // Create negative margin offsets to counteract persistent button padding if needed
742         if (Math.max.apply(Math, persistentPadding) > 0) {
743             innerSpanStyle = 'margin:' + Ext.Array.map(persistentPadding, function(pad) {
744                 return -pad + 'px';
745             }).join(' ');
746         }
747
748         return {
749             href     : me.getHref(),
750             target   : me.target || '_blank',
751             type     : me.type,
752             splitCls : me.getSplitCls(),
753             cls      : me.cls,
754             iconCls  : me.iconCls || '',
755             text     : me.text || '&#160;',
756             tabIndex : me.tabIndex,
757             innerSpanStyle: innerSpanStyle
758         };
759     },
760
761     /**
762      * @private
763      * If there is a configured href for this Button, returns the href with parameters appended.
764      * @returns The href string with parameters appended.
765      */
766     getHref: function() {
767         var me = this,
768             params = Ext.apply({}, me.baseParams);
769
770         // write baseParams first, then write any params
771         params = Ext.apply(params, me.params);
772         return me.href ? Ext.urlAppend(me.href, Ext.Object.toQueryString(params)) : false;
773     },
774
775     /**
776      * Sets the href of the link dynamically according to the params passed, and any {@link #baseParams} configured.
777      *
778      * **Only valid if the Button was originally configured with a {@link #href}**
779      *
780      * @param {Object} params Parameters to use in the href URL.
781      */
782     setParams: function(params) {
783         this.params = params;
784         this.btnEl.dom.href = this.getHref();
785     },
786
787     getSplitCls: function() {
788         var me = this;
789         return me.split ? (me.baseCls + '-' + me.arrowCls) + ' ' + (me.baseCls + '-' + me.arrowCls + '-' + me.arrowAlign) : '';
790     },
791
792     // private
793     afterRender: function() {
794         var me = this;
795         me.useSetClass = true;
796         me.setButtonCls();
797         me.doc = Ext.getDoc();
798         this.callParent(arguments);
799     },
800
801     /**
802      * Sets the CSS class that provides a background image to use as the button's icon. This method also changes the
803      * value of the {@link #iconCls} config internally.
804      * @param {String} cls The CSS class providing the icon image
805      * @return {Ext.button.Button} this
806      */
807     setIconCls: function(cls) {
808         var me = this,
809             btnIconEl = me.btnIconEl,
810             oldCls = me.iconCls;
811             
812         me.iconCls = cls;
813         if (btnIconEl) {
814             // Remove the previous iconCls from the button
815             btnIconEl.removeCls(oldCls);
816             btnIconEl.addCls(cls || '');
817             me.setButtonCls();
818         }
819         return me;
820     },
821
822     /**
823      * Sets the tooltip for this Button.
824      *
825      * @param {String/Object} tooltip This may be:
826      *
827      *   - **String** : A string to be used as innerHTML (html tags are accepted) to show in a tooltip
828      *   - **Object** : A configuration object for {@link Ext.tip.QuickTipManager#register}.
829      *
830      * @return {Ext.button.Button} this
831      */
832     setTooltip: function(tooltip, initial) {
833         var me = this;
834
835         if (me.rendered) {
836             if (!initial) {
837                 me.clearTip();
838             }
839             if (Ext.isObject(tooltip)) {
840                 Ext.tip.QuickTipManager.register(Ext.apply({
841                     target: me.btnEl.id
842                 },
843                 tooltip));
844                 me.tooltip = tooltip;
845             } else {
846                 me.btnEl.dom.setAttribute(me.getTipAttr(), tooltip);
847             }
848         } else {
849             me.tooltip = tooltip;
850         }
851         return me;
852     },
853
854     /**
855      * Sets the text alignment for this button.
856      * @param {String} align The new alignment of the button text. See {@link #textAlign}.
857      */
858     setTextAlign: function(align) {
859         var me = this,
860             btnEl = me.btnEl;
861
862         if (btnEl) {
863             btnEl.removeCls(me.baseCls + '-' + me.textAlign);
864             btnEl.addCls(me.baseCls + '-' + align);
865         }
866         me.textAlign = align;
867         return me;
868     },
869
870     getTipAttr: function(){
871         return this.tooltipType == 'qtip' ? 'data-qtip' : 'title';
872     },
873
874     // private
875     getRefItems: function(deep){
876         var menu = this.menu,
877             items;
878         
879         if (menu) {
880             items = menu.getRefItems(deep);
881             items.unshift(menu);
882         }
883         return items || [];
884     },
885
886     // private
887     clearTip: function() {
888         if (Ext.isObject(this.tooltip)) {
889             Ext.tip.QuickTipManager.unregister(this.btnEl);
890         }
891     },
892
893     // private
894     beforeDestroy: function() {
895         var me = this;
896         if (me.rendered) {
897             me.clearTip();
898         }
899         if (me.menu && me.destroyMenu !== false) {
900             Ext.destroy(me.menu);
901         }
902         Ext.destroy(me.btnInnerEl, me.repeater);
903         me.callParent();
904     },
905
906     // private
907     onDestroy: function() {
908         var me = this;
909         if (me.rendered) {
910             me.doc.un('mouseover', me.monitorMouseOver, me);
911             me.doc.un('mouseup', me.onMouseUp, me);
912             delete me.doc;
913             Ext.ButtonToggleManager.unregister(me);
914
915             Ext.destroy(me.keyMap);
916             delete me.keyMap;
917         }
918         me.callParent();
919     },
920
921     /**
922      * Assigns this Button's click handler
923      * @param {Function} handler The function to call when the button is clicked
924      * @param {Object} [scope] The scope (`this` reference) in which the handler function is executed.
925      * Defaults to this Button.
926      * @return {Ext.button.Button} this
927      */
928     setHandler: function(handler, scope) {
929         this.handler = handler;
930         this.scope = scope;
931         return this;
932     },
933
934     /**
935      * Sets this Button's text
936      * @param {String} text The button text
937      * @return {Ext.button.Button} this
938      */
939     setText: function(text) {
940         var me = this;
941         me.text = text;
942         if (me.el) {
943             me.btnInnerEl.update(text || '&#160;');
944             me.setButtonCls();
945         }
946         me.doComponentLayout();
947         return me;
948     },
949
950     /**
951      * Sets the background image (inline style) of the button. This method also changes the value of the {@link #icon}
952      * config internally.
953      * @param {String} icon The path to an image to display in the button
954      * @return {Ext.button.Button} this
955      */
956     setIcon: function(icon) {
957         var me = this,
958             iconEl = me.btnIconEl;
959             
960         me.icon = icon;
961         if (iconEl) {
962             iconEl.setStyle('background-image', icon ? 'url(' + icon + ')': '');
963             me.setButtonCls();
964         }
965         return me;
966     },
967
968     /**
969      * Gets the text for this Button
970      * @return {String} The button text
971      */
972     getText: function() {
973         return this.text;
974     },
975
976     /**
977      * If a state it passed, it becomes the pressed state otherwise the current state is toggled.
978      * @param {Boolean} [state] Force a particular state
979      * @param {Boolean} [suppressEvent=false] True to stop events being fired when calling this method.
980      * @return {Ext.button.Button} this
981      */
982     toggle: function(state, suppressEvent) {
983         var me = this;
984         state = state === undefined ? !me.pressed : !!state;
985         if (state !== me.pressed) {
986             if (me.rendered) {
987                 me[state ? 'addClsWithUI': 'removeClsWithUI'](me.pressedCls);
988             }
989             me.btnEl.dom.setAttribute('aria-pressed', state);
990             me.pressed = state;
991             if (!suppressEvent) {
992                 me.fireEvent('toggle', me, state);
993                 Ext.callback(me.toggleHandler, me.scope || me, [me, state]);
994             }
995         }
996         return me;
997     },
998     
999     maybeShowMenu: function(){
1000         var me = this;
1001         if (me.menu && !me.hasVisibleMenu() && !me.ignoreNextClick) {
1002             me.showMenu();
1003         }
1004     },
1005
1006     /**
1007      * Shows this button's menu (if it has one)
1008      */
1009     showMenu: function() {
1010         var me = this;
1011         if (me.rendered && me.menu) {
1012             if (me.tooltip && me.getTipAttr() != 'title') {
1013                 Ext.tip.QuickTipManager.getQuickTip().cancelShow(me.btnEl);
1014             }
1015             if (me.menu.isVisible()) {
1016                 me.menu.hide();
1017             }
1018
1019             me.menu.showBy(me.el, me.menuAlign);
1020         }
1021         return me;
1022     },
1023
1024     /**
1025      * Hides this button's menu (if it has one)
1026      */
1027     hideMenu: function() {
1028         if (this.hasVisibleMenu()) {
1029             this.menu.hide();
1030         }
1031         return this;
1032     },
1033
1034     /**
1035      * Returns true if the button has a menu and it is visible
1036      * @return {Boolean}
1037      */
1038     hasVisibleMenu: function() {
1039         var menu = this.menu;
1040         return menu && menu.rendered && menu.isVisible();
1041     },
1042
1043     // private
1044     onRepeatClick: function(repeat, e) {
1045         this.onClick(e);
1046     },
1047
1048     // private
1049     onClick: function(e) {
1050         var me = this;
1051         if (me.preventDefault || (me.disabled && me.getHref()) && e) {
1052             e.preventDefault();
1053         }
1054         if (e.button !== 0) {
1055             return;
1056         }
1057         if (!me.disabled) {
1058             me.doToggle();
1059             me.maybeShowMenu();
1060             me.fireHandler(e);
1061         }
1062     },
1063     
1064     fireHandler: function(e){
1065         var me = this,
1066             handler = me.handler;
1067             
1068         me.fireEvent('click', me, e);
1069         if (handler) {
1070             handler.call(me.scope || me, me, e);
1071         }
1072         me.onBlur();
1073     },
1074     
1075     doToggle: function(){
1076         var me = this;
1077         if (me.enableToggle && (me.allowDepress !== false || !me.pressed)) {
1078             me.toggle();
1079         }
1080     },
1081
1082     /**
1083      * @private mouseover handler called when a mouseover event occurs anywhere within the encapsulating element.
1084      * The targets are interrogated to see what is being entered from where.
1085      * @param e
1086      */
1087     onMouseOver: function(e) {
1088         var me = this;
1089         if (!me.disabled && !e.within(me.el, true, true)) {
1090             me.onMouseEnter(e);
1091         }
1092     },
1093
1094     /**
1095      * @private
1096      * mouseout handler called when a mouseout event occurs anywhere within the encapsulating element -
1097      * or the mouse leaves the encapsulating element.
1098      * The targets are interrogated to see what is being exited to where.
1099      * @param e
1100      */
1101     onMouseOut: function(e) {
1102         var me = this;
1103         if (!e.within(me.el, true, true)) {
1104             if (me.overMenuTrigger) {
1105                 me.onMenuTriggerOut(e);
1106             }
1107             me.onMouseLeave(e);
1108         }
1109     },
1110
1111     /**
1112      * @private
1113      * mousemove handler called when the mouse moves anywhere within the encapsulating element.
1114      * The position is checked to determine if the mouse is entering or leaving the trigger area. Using
1115      * mousemove to check this is more resource intensive than we'd like, but it is necessary because
1116      * the trigger area does not line up exactly with sub-elements so we don't always get mouseover/out
1117      * events when needed. In the future we should consider making the trigger a separate element that
1118      * is absolutely positioned and sized over the trigger area.
1119      */
1120     onMouseMove: function(e) {
1121         var me = this,
1122             el = me.el,
1123             over = me.overMenuTrigger,
1124             overlap, btnSize;
1125
1126         if (me.split) {
1127             if (me.arrowAlign === 'right') {
1128                 overlap = e.getX() - el.getX();
1129                 btnSize = el.getWidth();
1130             } else {
1131                 overlap = e.getY() - el.getY();
1132                 btnSize = el.getHeight();
1133             }
1134
1135             if (overlap > (btnSize - me.getTriggerSize())) {
1136                 if (!over) {
1137                     me.onMenuTriggerOver(e);
1138                 }
1139             } else {
1140                 if (over) {
1141                     me.onMenuTriggerOut(e);
1142                 }
1143             }
1144         }
1145     },
1146
1147     /**
1148      * @private
1149      * Measures the size of the trigger area for menu and split buttons. Will be a width for
1150      * a right-aligned trigger and a height for a bottom-aligned trigger. Cached after first measurement.
1151      */
1152     getTriggerSize: function() {
1153         var me = this,
1154             size = me.triggerSize,
1155             side, sideFirstLetter, undef;
1156
1157         if (size === undef) {
1158             side = me.arrowAlign;
1159             sideFirstLetter = side.charAt(0);
1160             size = me.triggerSize = me.el.getFrameWidth(sideFirstLetter) + me.btnWrap.getFrameWidth(sideFirstLetter) + (me.frameSize && me.frameSize[side] || 0);
1161         }
1162         return size;
1163     },
1164
1165     /**
1166      * @private
1167      * virtual mouseenter handler called when it is detected that the mouseout event
1168      * signified the mouse entering the encapsulating element.
1169      * @param e
1170      */
1171     onMouseEnter: function(e) {
1172         var me = this;
1173         me.addClsWithUI(me.overCls);
1174         me.fireEvent('mouseover', me, e);
1175     },
1176
1177     /**
1178      * @private
1179      * virtual mouseleave handler called when it is detected that the mouseover event
1180      * signified the mouse entering the encapsulating element.
1181      * @param e
1182      */
1183     onMouseLeave: function(e) {
1184         var me = this;
1185         me.removeClsWithUI(me.overCls);
1186         me.fireEvent('mouseout', me, e);
1187     },
1188
1189     /**
1190      * @private
1191      * virtual mouseenter handler called when it is detected that the mouseover event
1192      * signified the mouse entering the arrow area of the button - the <em>.
1193      * @param e
1194      */
1195     onMenuTriggerOver: function(e) {
1196         var me = this;
1197         me.overMenuTrigger = true;
1198         me.fireEvent('menutriggerover', me, me.menu, e);
1199     },
1200
1201     /**
1202      * @private
1203      * virtual mouseleave handler called when it is detected that the mouseout event
1204      * signified the mouse leaving the arrow area of the button - the <em>.
1205      * @param e
1206      */
1207     onMenuTriggerOut: function(e) {
1208         var me = this;
1209         delete me.overMenuTrigger;
1210         me.fireEvent('menutriggerout', me, me.menu, e);
1211     },
1212
1213     // inherit docs
1214     enable : function(silent) {
1215         var me = this;
1216
1217         me.callParent(arguments);
1218
1219         me.removeClsWithUI('disabled');
1220
1221         return me;
1222     },
1223
1224     // inherit docs
1225     disable : function(silent) {
1226         var me = this;
1227
1228         me.callParent(arguments);
1229
1230         me.addClsWithUI('disabled');
1231         me.removeClsWithUI(me.overCls);
1232
1233         return me;
1234     },
1235
1236     /**
1237      * Method to change the scale of the button. See {@link #scale} for allowed configurations.
1238      * @param {String} scale The scale to change to.
1239      */
1240     setScale: function(scale) {
1241         var me = this,
1242             ui = me.ui.replace('-' + me.scale, '');
1243
1244         //check if it is an allowed scale
1245         if (!Ext.Array.contains(me.allowedScales, scale)) {
1246             throw('#setScale: scale must be an allowed scale (' + me.allowedScales.join(', ') + ')');
1247         }
1248
1249         me.scale = scale;
1250         me.setUI(ui);
1251     },
1252
1253     // inherit docs
1254     setUI: function(ui) {
1255         var me = this;
1256
1257         //we need to append the scale to the UI, if not already done
1258         if (me.scale && !ui.match(me.scale)) {
1259             ui = ui + '-' + me.scale;
1260         }
1261
1262         me.callParent([ui]);
1263
1264         // Set all the state classNames, as they need to include the UI
1265         // me.disabledCls += ' ' + me.baseCls + '-' + me.ui + '-disabled';
1266     },
1267
1268     // private
1269     onFocus: function(e) {
1270         var me = this;
1271         if (!me.disabled) {
1272             me.addClsWithUI(me.focusCls);
1273         }
1274     },
1275
1276     // private
1277     onBlur: function(e) {
1278         var me = this;
1279         me.removeClsWithUI(me.focusCls);
1280     },
1281
1282     // private
1283     onMouseDown: function(e) {
1284         var me = this;
1285         if (!me.disabled && e.button === 0) {
1286             me.addClsWithUI(me.pressedCls);
1287             me.doc.on('mouseup', me.onMouseUp, me);
1288         }
1289     },
1290     // private
1291     onMouseUp: function(e) {
1292         var me = this;
1293         if (e.button === 0) {
1294             if (!me.pressed) {
1295                 me.removeClsWithUI(me.pressedCls);
1296             }
1297             me.doc.un('mouseup', me.onMouseUp, me);
1298         }
1299     },
1300     // private
1301     onMenuShow: function(e) {
1302         var me = this;
1303         me.ignoreNextClick = 0;
1304         me.addClsWithUI(me.menuActiveCls);
1305         me.fireEvent('menushow', me, me.menu);
1306     },
1307
1308     // private
1309     onMenuHide: function(e) {
1310         var me = this;
1311         me.removeClsWithUI(me.menuActiveCls);
1312         me.ignoreNextClick = Ext.defer(me.restoreClick, 250, me);
1313         me.fireEvent('menuhide', me, me.menu);
1314     },
1315
1316     // private
1317     restoreClick: function() {
1318         this.ignoreNextClick = 0;
1319     },
1320
1321     // private
1322     onDownKey: function() {
1323         var me = this;
1324
1325         if (!me.disabled) {
1326             if (me.menu) {
1327                 me.showMenu();
1328             }
1329         }
1330     },
1331
1332     /**
1333      * @private
1334      * Some browsers (notably Safari and older Chromes on Windows) add extra "padding" inside the button
1335      * element that cannot be removed. This method returns the size of that padding with a one-time detection.
1336      * @return {Number[]} [top, right, bottom, left]
1337      */
1338     getPersistentBtnPadding: function() {
1339         var cls = Ext.button.Button,
1340             padding = cls.persistentPadding,
1341             btn, leftTop, btnEl, btnInnerEl;
1342
1343         if (!padding) {
1344             padding = cls.persistentPadding = [0, 0, 0, 0]; //set early to prevent recursion
1345
1346             if (!Ext.isIE) { //short-circuit IE as it sometimes gives false positive for padding
1347                 // Create auto-size button offscreen and measure its insides
1348                 btn = Ext.create('Ext.button.Button', {
1349                     renderTo: Ext.getBody(),
1350                     text: 'test',
1351                     style: 'position:absolute;top:-999px;'
1352                 });
1353                 btnEl = btn.btnEl;
1354                 btnInnerEl = btn.btnInnerEl;
1355                 btnEl.setSize(null, null); //clear any hard dimensions on the button el to see what it does naturally
1356
1357                 leftTop = btnInnerEl.getOffsetsTo(btnEl);
1358                 padding[0] = leftTop[1];
1359                 padding[1] = btnEl.getWidth() - btnInnerEl.getWidth() - leftTop[0];
1360                 padding[2] = btnEl.getHeight() - btnInnerEl.getHeight() - leftTop[1];
1361                 padding[3] = leftTop[0];
1362
1363                 btn.destroy();
1364             }
1365         }
1366
1367         return padding;
1368     }
1369
1370 }, function() {
1371     var groups = {};
1372
1373     function toggleGroup(btn, state) {
1374         var g, i, l;
1375         if (state) {
1376             g = groups[btn.toggleGroup];
1377             for (i = 0, l = g.length; i < l; i++) {
1378                 if (g[i] !== btn) {
1379                     g[i].toggle(false);
1380                 }
1381             }
1382         }
1383     }
1384
1385     /**
1386      * Private utility class used by Button
1387      * @hide
1388      */
1389     Ext.ButtonToggleManager = {
1390         register: function(btn) {
1391             if (!btn.toggleGroup) {
1392                 return;
1393             }
1394             var group = groups[btn.toggleGroup];
1395             if (!group) {
1396                 group = groups[btn.toggleGroup] = [];
1397             }
1398             group.push(btn);
1399             btn.on('toggle', toggleGroup);
1400         },
1401
1402         unregister: function(btn) {
1403             if (!btn.toggleGroup) {
1404                 return;
1405             }
1406             var group = groups[btn.toggleGroup];
1407             if (group) {
1408                 Ext.Array.remove(group, btn);
1409                 btn.un('toggle', toggleGroup);
1410             }
1411         },
1412
1413         /**
1414          * Gets the pressed button in the passed group or null
1415          * @param {String} group
1416          * @return {Ext.button.Button}
1417          */
1418         getPressed: function(group) {
1419             var g = groups[group],
1420                 i = 0,
1421                 len;
1422             if (g) {
1423                 for (len = g.length; i < len; i++) {
1424                     if (g[i].pressed === true) {
1425                         return g[i];
1426                     }
1427                 }
1428             }
1429             return null;
1430         }
1431     };
1432 });
1433