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