Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / form / field / Picker.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  * An abstract class for fields that have a single trigger which opens a "picker" popup below the field, e.g. a combobox
17  * menu list or a date picker. It provides a base implementation for toggling the picker's visibility when the trigger
18  * is clicked, as well as keyboard navigation and some basic events. Sizing and alignment of the picker can be
19  * controlled via the {@link #matchFieldWidth} and {@link #pickerAlign}/{@link #pickerOffset} config properties
20  * respectively.
21  *
22  * You would not normally use this class directly, but instead use it as the parent class for a specific picker field
23  * implementation. Subclasses must implement the {@link #createPicker} method to create a picker component appropriate
24  * for the field.
25  */
26 Ext.define('Ext.form.field.Picker', {
27     extend: 'Ext.form.field.Trigger',
28     alias: 'widget.pickerfield',
29     alternateClassName: 'Ext.form.Picker',
30     requires: ['Ext.util.KeyNav'],
31
32     /**
33      * @cfg {Boolean} matchFieldWidth
34      * Whether the picker dropdown's width should be explicitly set to match the width of the field. Defaults to true.
35      */
36     matchFieldWidth: true,
37
38     /**
39      * @cfg {String} pickerAlign
40      * The {@link Ext.Element#alignTo alignment position} with which to align the picker. Defaults to "tl-bl?"
41      */
42     pickerAlign: 'tl-bl?',
43
44     /**
45      * @cfg {Number[]} pickerOffset
46      * An offset [x,y] to use in addition to the {@link #pickerAlign} when positioning the picker.
47      * Defaults to undefined.
48      */
49
50     /**
51      * @cfg {String} openCls
52      * A class to be added to the field's {@link #bodyEl} element when the picker is opened.
53      * Defaults to 'x-pickerfield-open'.
54      */
55     openCls: Ext.baseCSSPrefix + 'pickerfield-open',
56
57     /**
58      * @property {Boolean} isExpanded
59      * True if the picker is currently expanded, false if not.
60      */
61
62     /**
63      * @cfg {Boolean} editable
64      * False to prevent the user from typing text directly into the field; the field can only have its value set via
65      * selecting a value from the picker. In this state, the picker can also be opened by clicking directly on the input
66      * field itself.
67      */
68     editable: true,
69
70
71     initComponent: function() {
72         this.callParent();
73
74         // Custom events
75         this.addEvents(
76             /**
77              * @event expand
78              * Fires when the field's picker is expanded.
79              * @param {Ext.form.field.Picker} field This field instance
80              */
81             'expand',
82             /**
83              * @event collapse
84              * Fires when the field's picker is collapsed.
85              * @param {Ext.form.field.Picker} field This field instance
86              */
87             'collapse',
88             /**
89              * @event select
90              * Fires when a value is selected via the picker.
91              * @param {Ext.form.field.Picker} field This field instance
92              * @param {Object} value The value that was selected. The exact type of this value is dependent on
93              * the individual field and picker implementations.
94              */
95             'select'
96         );
97     },
98
99
100     initEvents: function() {
101         var me = this;
102         me.callParent();
103
104         // Add handlers for keys to expand/collapse the picker
105         me.keyNav = Ext.create('Ext.util.KeyNav', me.inputEl, {
106             down: function() {
107                 if (!me.isExpanded) {
108                     // Don't call expand() directly as there may be additional processing involved before
109                     // expanding, e.g. in the case of a ComboBox query.
110                     me.onTriggerClick();
111                 }
112             },
113             esc: me.collapse,
114             scope: me,
115             forceKeyDown: true
116         });
117
118         // Non-editable allows opening the picker by clicking the field
119         if (!me.editable) {
120             me.mon(me.inputEl, 'click', me.onTriggerClick, me);
121         }
122
123         // Disable native browser autocomplete
124         if (Ext.isGecko) {
125             me.inputEl.dom.setAttribute('autocomplete', 'off');
126         }
127     },
128
129
130     /**
131      * Expands this field's picker dropdown.
132      */
133     expand: function() {
134         var me = this,
135             bodyEl, picker, collapseIf;
136
137         if (me.rendered && !me.isExpanded && !me.isDestroyed) {
138             bodyEl = me.bodyEl;
139             picker = me.getPicker();
140             collapseIf = me.collapseIf;
141
142             // show the picker and set isExpanded flag
143             picker.show();
144             me.isExpanded = true;
145             me.alignPicker();
146             bodyEl.addCls(me.openCls);
147
148             // monitor clicking and mousewheel
149             me.mon(Ext.getDoc(), {
150                 mousewheel: collapseIf,
151                 mousedown: collapseIf,
152                 scope: me
153             });
154             Ext.EventManager.onWindowResize(me.alignPicker, me);
155             me.fireEvent('expand', me);
156             me.onExpand();
157         }
158     },
159
160     onExpand: Ext.emptyFn,
161
162     /**
163      * Aligns the picker to the input element
164      * @protected
165      */
166     alignPicker: function() {
167         var me = this,
168             picker;
169
170         if (me.isExpanded) {
171             picker = me.getPicker();
172             if (me.matchFieldWidth) {
173                 // Auto the height (it will be constrained by min and max width) unless there are no records to display.
174                 picker.setSize(me.bodyEl.getWidth(), picker.store && picker.store.getCount() ? null : 0);
175             }
176             if (picker.isFloating()) {
177                 me.doAlign();
178             }
179         }
180     },
181
182     /**
183      * Performs the alignment on the picker using the class defaults
184      * @private
185      */
186     doAlign: function(){
187         var me = this,
188             picker = me.picker,
189             aboveSfx = '-above',
190             isAbove;
191
192         me.picker.alignTo(me.inputEl, me.pickerAlign, me.pickerOffset);
193         // add the {openCls}-above class if the picker was aligned above
194         // the field due to hitting the bottom of the viewport
195         isAbove = picker.el.getY() < me.inputEl.getY();
196         me.bodyEl[isAbove ? 'addCls' : 'removeCls'](me.openCls + aboveSfx);
197         picker[isAbove ? 'addCls' : 'removeCls'](picker.baseCls + aboveSfx);
198     },
199
200     /**
201      * Collapses this field's picker dropdown.
202      */
203     collapse: function() {
204         if (this.isExpanded && !this.isDestroyed) {
205             var me = this,
206                 openCls = me.openCls,
207                 picker = me.picker,
208                 doc = Ext.getDoc(),
209                 collapseIf = me.collapseIf,
210                 aboveSfx = '-above';
211
212             // hide the picker and set isExpanded flag
213             picker.hide();
214             me.isExpanded = false;
215
216             // remove the openCls
217             me.bodyEl.removeCls([openCls, openCls + aboveSfx]);
218             picker.el.removeCls(picker.baseCls + aboveSfx);
219
220             // remove event listeners
221             doc.un('mousewheel', collapseIf, me);
222             doc.un('mousedown', collapseIf, me);
223             Ext.EventManager.removeResizeListener(me.alignPicker, me);
224             me.fireEvent('collapse', me);
225             me.onCollapse();
226         }
227     },
228
229     onCollapse: Ext.emptyFn,
230
231
232     /**
233      * @private
234      * Runs on mousewheel and mousedown of doc to check to see if we should collapse the picker
235      */
236     collapseIf: function(e) {
237         var me = this;
238         if (!me.isDestroyed && !e.within(me.bodyEl, false, true) && !e.within(me.picker.el, false, true)) {
239             me.collapse();
240         }
241     },
242
243     /**
244      * Returns a reference to the picker component for this field, creating it if necessary by
245      * calling {@link #createPicker}.
246      * @return {Ext.Component} The picker component
247      */
248     getPicker: function() {
249         var me = this;
250         return me.picker || (me.picker = me.createPicker());
251     },
252
253     /**
254      * @method
255      * Creates and returns the component to be used as this field's picker. Must be implemented by subclasses of Picker.
256      * The current field should also be passed as a configuration option to the picker component as the pickerField
257      * property.
258      */
259     createPicker: Ext.emptyFn,
260
261     /**
262      * Handles the trigger click; by default toggles between expanding and collapsing the picker component.
263      * @protected
264      */
265     onTriggerClick: function() {
266         var me = this;
267         if (!me.readOnly && !me.disabled) {
268             if (me.isExpanded) {
269                 me.collapse();
270             } else {
271                 me.expand();
272             }
273             me.inputEl.focus();
274         }
275     },
276
277     mimicBlur: function(e) {
278         var me = this,
279             picker = me.picker;
280         // ignore mousedown events within the picker element
281         if (!picker || !e.within(picker.el, false, true)) {
282             me.callParent(arguments);
283         }
284     },
285
286     onDestroy : function(){
287         var me = this,
288             picker = me.picker;
289
290         Ext.EventManager.removeResizeListener(me.alignPicker, me);
291         Ext.destroy(me.keyNav);
292         if (picker) {
293             delete picker.pickerField;
294             picker.destroy();
295         }
296         me.callParent();
297     }
298
299 });
300
301