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