Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / form / field / Trigger.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  * Provides a convenient wrapper for TextFields that adds a clickable trigger button (looks like a combobox by default).
17  * The trigger has no default action, so you must assign a function to implement the trigger click handler by overriding
18  * {@link #onTriggerClick}. You can create a Trigger field directly, as it renders exactly like a combobox for which you
19  * can provide a custom implementation.
20  *
21  * For example:
22  *
23  *     @example
24  *     Ext.define('Ext.ux.CustomTrigger', {
25  *         extend: 'Ext.form.field.Trigger',
26  *         alias: 'widget.customtrigger',
27  *
28  *         // override onTriggerClick
29  *         onTriggerClick: function() {
30  *             Ext.Msg.alert('Status', 'You clicked my trigger!');
31  *         }
32  *     });
33  *
34  *     Ext.create('Ext.form.FormPanel', {
35  *         title: 'Form with TriggerField',
36  *         bodyPadding: 5,
37  *         width: 350,
38  *         renderTo: Ext.getBody(),
39  *         items:[{
40  *             xtype: 'customtrigger',
41  *             fieldLabel: 'Sample Trigger',
42  *             emptyText: 'click the trigger',
43  *         }]
44  *     });
45  *
46  * However, in general you will most likely want to use Trigger as the base class for a reusable component.
47  * {@link Ext.form.field.Date} and {@link Ext.form.field.ComboBox} are perfect examples of this.
48  */
49 Ext.define('Ext.form.field.Trigger', {
50     extend:'Ext.form.field.Text',
51     alias: ['widget.triggerfield', 'widget.trigger'],
52     requires: ['Ext.DomHelper', 'Ext.util.ClickRepeater', 'Ext.layout.component.field.Trigger'],
53     alternateClassName: ['Ext.form.TriggerField', 'Ext.form.TwinTriggerField', 'Ext.form.Trigger'],
54
55     // note: {id} here is really {inputId}, but {cmpId} is available
56     fieldSubTpl: [
57         '<input id="{id}" type="{type}" ',
58             '<tpl if="name">name="{name}" </tpl>',
59             '<tpl if="size">size="{size}" </tpl>',
60             '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
61             'class="{fieldCls} {typeCls}" autocomplete="off" />',
62         '<div id="{cmpId}-triggerWrap" class="{triggerWrapCls}" role="presentation">',
63             '{triggerEl}',
64             '<div class="{clearCls}" role="presentation"></div>',
65         '</div>',
66         {
67             compiled: true,
68             disableFormats: true
69         }
70     ],
71
72     /**
73      * @cfg {String} triggerCls
74      * An additional CSS class used to style the trigger button. The trigger will always get the {@link #triggerBaseCls}
75      * by default and triggerCls will be **appended** if specified.
76      */
77
78     /**
79      * @cfg {String} [triggerBaseCls='x-form-trigger']
80      * The base CSS class that is always added to the trigger button. The {@link #triggerCls} will be appended in
81      * addition to this class.
82      */
83     triggerBaseCls: Ext.baseCSSPrefix + 'form-trigger',
84
85     /**
86      * @cfg {String} [triggerWrapCls='x-form-trigger-wrap']
87      * The CSS class that is added to the div wrapping the trigger button(s).
88      */
89     triggerWrapCls: Ext.baseCSSPrefix + 'form-trigger-wrap',
90
91     /**
92      * @cfg {Boolean} hideTrigger
93      * true to hide the trigger element and display only the base text field
94      */
95     hideTrigger: false,
96
97     /**
98      * @cfg {Boolean} editable
99      * false to prevent the user from typing text directly into the field; the field can only have its value set via an
100      * action invoked by the trigger.
101      */
102     editable: true,
103
104     /**
105      * @cfg {Boolean} readOnly
106      * true to prevent the user from changing the field, and hides the trigger. Supercedes the editable and hideTrigger
107      * options if the value is true.
108      */
109     readOnly: false,
110
111     /**
112      * @cfg {Boolean} [selectOnFocus=false]
113      * true to select any existing text in the field immediately on focus. Only applies when
114      * {@link #editable editable} = true
115      */
116
117     /**
118      * @cfg {Boolean} repeatTriggerClick
119      * true to attach a {@link Ext.util.ClickRepeater click repeater} to the trigger.
120      */
121     repeatTriggerClick: false,
122
123
124     /**
125      * @hide
126      * @method autoSize
127      */
128     autoSize: Ext.emptyFn,
129     // private
130     monitorTab: true,
131     // private
132     mimicing: false,
133     // private
134     triggerIndexRe: /trigger-index-(\d+)/,
135
136     componentLayout: 'triggerfield',
137
138     initComponent: function() {
139         this.wrapFocusCls = this.triggerWrapCls + '-focus';
140         this.callParent(arguments);
141     },
142
143     // private
144     onRender: function(ct, position) {
145         var me = this,
146             triggerCls,
147             triggerBaseCls = me.triggerBaseCls,
148             triggerWrapCls = me.triggerWrapCls,
149             triggerConfigs = [],
150             i;
151
152         // triggerCls is a synonym for trigger1Cls, so copy it.
153         // TODO this trigger<n>Cls API design doesn't feel clean, especially where it butts up against the
154         // single triggerCls config. Should rethink this, perhaps something more structured like a list of
155         // trigger config objects that hold cls, handler, etc.
156         if (!me.trigger1Cls) {
157             me.trigger1Cls = me.triggerCls;
158         }
159
160         // Create as many trigger elements as we have trigger<n>Cls configs, but always at least one
161         for (i = 0; (triggerCls = me['trigger' + (i + 1) + 'Cls']) || i < 1; i++) {
162             triggerConfigs.push({
163                 cls: [Ext.baseCSSPrefix + 'trigger-index-' + i, triggerBaseCls, triggerCls].join(' '),
164                 role: 'button'
165             });
166         }
167         triggerConfigs[i - 1].cls += ' ' + triggerBaseCls + '-last';
168
169         /**
170          * @property {Ext.Element} triggerWrap
171          * A reference to the div element wrapping the trigger button(s). Only set after the field has been rendered.
172          */
173         me.addChildEls('triggerWrap');
174
175         Ext.applyIf(me.subTplData, {
176             triggerWrapCls: triggerWrapCls,
177             triggerEl: Ext.DomHelper.markup(triggerConfigs),
178             clearCls: me.clearCls
179         });
180
181         me.callParent(arguments);
182
183         /**
184          * @property {Ext.CompositeElement} triggerEl
185          * A composite of all the trigger button elements. Only set after the field has been rendered.
186          */
187         me.triggerEl = Ext.select('.' + triggerBaseCls, true, me.triggerWrap.dom);
188
189         me.doc = Ext.getDoc();
190         me.initTrigger();
191     },
192
193     onEnable: function() {
194         this.callParent();
195         this.triggerWrap.unmask();
196     },
197     
198     onDisable: function() {
199         this.callParent();
200         this.triggerWrap.mask();
201     },
202     
203     afterRender: function() {
204         this.callParent();
205         this.updateEditState();
206         this.triggerEl.unselectable();
207     },
208
209     updateEditState: function() {
210         var me = this,
211             inputEl = me.inputEl,
212             triggerWrap = me.triggerWrap,
213             noeditCls = Ext.baseCSSPrefix + 'trigger-noedit',
214             displayed,
215             readOnly;
216
217         if (me.rendered) {
218             if (me.readOnly) {
219                 inputEl.addCls(noeditCls);
220                 readOnly = true;
221                 displayed = false;
222             } else {
223                 if (me.editable) {
224                     inputEl.removeCls(noeditCls);
225                     readOnly = false;
226                 } else {
227                     inputEl.addCls(noeditCls);
228                     readOnly = true;
229                 }
230                 displayed = !me.hideTrigger;
231             }
232
233             triggerWrap.setDisplayed(displayed);
234             inputEl.dom.readOnly = readOnly;
235             me.doComponentLayout();
236         }
237     },
238
239     /**
240      * Get the total width of the trigger button area. Only useful after the field has been rendered.
241      * @return {Number} The trigger width
242      */
243     getTriggerWidth: function() {
244         var me = this,
245             triggerWrap = me.triggerWrap,
246             totalTriggerWidth = 0;
247         if (triggerWrap && !me.hideTrigger && !me.readOnly) {
248             me.triggerEl.each(function(trigger) {
249                 totalTriggerWidth += trigger.getWidth();
250             });
251             totalTriggerWidth += me.triggerWrap.getFrameWidth('lr');
252         }
253         return totalTriggerWidth;
254     },
255
256     setHideTrigger: function(hideTrigger) {
257         if (hideTrigger != this.hideTrigger) {
258             this.hideTrigger = hideTrigger;
259             this.updateEditState();
260         }
261     },
262
263     /**
264      * Sets the editable state of this field. This method is the runtime equivalent of setting the 'editable' config
265      * option at config time.
266      * @param {Boolean} editable True to allow the user to directly edit the field text. If false is passed, the user
267      * will only be able to modify the field using the trigger. Will also add a click event to the text field which
268      * will call the trigger. 
269      */
270     setEditable: function(editable) {
271         if (editable != this.editable) {
272             this.editable = editable;
273             this.updateEditState();
274         }
275     },
276
277     /**
278      * Sets the read-only state of this field. This method is the runtime equivalent of setting the 'readOnly' config
279      * option at config time.
280      * @param {Boolean} readOnly True to prevent the user changing the field and explicitly hide the trigger. Setting
281      * this to true will superceed settings editable and hideTrigger. Setting this to false will defer back to editable
282      * and hideTrigger.
283      */
284     setReadOnly: function(readOnly) {
285         if (readOnly != this.readOnly) {
286             this.readOnly = readOnly;
287             this.updateEditState();
288         }
289     },
290
291     // private
292     initTrigger: function() {
293         var me = this,
294             triggerWrap = me.triggerWrap,
295             triggerEl = me.triggerEl;
296
297         if (me.repeatTriggerClick) {
298             me.triggerRepeater = Ext.create('Ext.util.ClickRepeater', triggerWrap, {
299                 preventDefault: true,
300                 handler: function(cr, e) {
301                     me.onTriggerWrapClick(e);
302                 }
303             });
304         } else {
305             me.mon(me.triggerWrap, 'click', me.onTriggerWrapClick, me);
306         }
307
308         triggerEl.addClsOnOver(me.triggerBaseCls + '-over');
309         triggerEl.each(function(el, c, i) {
310             el.addClsOnOver(me['trigger' + (i + 1) + 'Cls'] + '-over');
311         });
312         triggerEl.addClsOnClick(me.triggerBaseCls + '-click');
313         triggerEl.each(function(el, c, i) {
314             el.addClsOnClick(me['trigger' + (i + 1) + 'Cls'] + '-click');
315         });
316     },
317
318     // private
319     onDestroy: function() {
320         var me = this;
321         Ext.destroyMembers(me, 'triggerRepeater', 'triggerWrap', 'triggerEl');
322         delete me.doc;
323         me.callParent();
324     },
325
326     // private
327     onFocus: function() {
328         var me = this;
329         me.callParent();
330         if (!me.mimicing) {
331             me.bodyEl.addCls(me.wrapFocusCls);
332             me.mimicing = true;
333             me.mon(me.doc, 'mousedown', me.mimicBlur, me, {
334                 delay: 10
335             });
336             if (me.monitorTab) {
337                 me.on('specialkey', me.checkTab, me);
338             }
339         }
340     },
341
342     // private
343     checkTab: function(me, e) {
344         if (!this.ignoreMonitorTab && e.getKey() == e.TAB) {
345             this.triggerBlur();
346         }
347     },
348
349     // private
350     onBlur: Ext.emptyFn,
351
352     // private
353     mimicBlur: function(e) {
354         if (!this.isDestroyed && !this.bodyEl.contains(e.target) && this.validateBlur(e)) {
355             this.triggerBlur();
356         }
357     },
358
359     // private
360     triggerBlur: function() {
361         var me = this;
362         me.mimicing = false;
363         me.mun(me.doc, 'mousedown', me.mimicBlur, me);
364         if (me.monitorTab && me.inputEl) {
365             me.un('specialkey', me.checkTab, me);
366         }
367         Ext.form.field.Trigger.superclass.onBlur.call(me);
368         if (me.bodyEl) {
369             me.bodyEl.removeCls(me.wrapFocusCls);
370         }
371     },
372
373     beforeBlur: Ext.emptyFn,
374
375     // private
376     // This should be overridden by any subclass that needs to check whether or not the field can be blurred.
377     validateBlur: function(e) {
378         return true;
379     },
380
381     // private
382     // process clicks upon triggers.
383     // determine which trigger index, and dispatch to the appropriate click handler
384     onTriggerWrapClick: function(e) {
385         var me = this,
386             t = e && e.getTarget('.' + Ext.baseCSSPrefix + 'form-trigger', null),
387             match = t && t.className.match(me.triggerIndexRe),
388             idx,
389             triggerClickMethod;
390
391         if (match && !me.readOnly) {
392             idx = parseInt(match[1], 10);
393             triggerClickMethod = me['onTrigger' + (idx + 1) + 'Click'] || me.onTriggerClick;
394             if (triggerClickMethod) {
395                 triggerClickMethod.call(me, e);
396             }
397         }
398     },
399
400     /**
401      * @method onTriggerClick
402      * @protected
403      * The function that should handle the trigger's click event. This method does nothing by default until overridden
404      * by an implementing function. See Ext.form.field.ComboBox and Ext.form.field.Date for sample implementations.
405      * @param {Ext.EventObject} e
406      */
407     onTriggerClick: Ext.emptyFn
408
409     /**
410      * @cfg {Boolean} grow @hide
411      */
412     /**
413      * @cfg {Number} growMin @hide
414      */
415     /**
416      * @cfg {Number} growMax @hide
417      */
418 });
419