Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / form / field / Time.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.Time
17  * @extends Ext.form.field.Picker
18  * <p>Provides a time input field with a time dropdown and automatic time validation.</p>
19  * <p>This field recognizes and uses JavaScript Date objects as its main {@link #value} type (only the time
20  * portion of the date is used; the month/day/year are ignored). In addition, it recognizes string values which
21  * are parsed according to the {@link #format} and/or {@link #altFormats} configs. These may be reconfigured
22  * to use time formats appropriate for the user's locale.</p>
23  * <p>The field may be limited to a certain range of times by using the {@link #minValue} and {@link #maxValue}
24  * configs, and the interval between time options in the dropdown can be changed with the {@link #increment} config.</p>
25  * {@img Ext.form.Time/Ext.form.Time.png Ext.form.Time component}
26  * <p>Example usage:</p>
27  * <pre><code>
28 Ext.create('Ext.form.Panel', {
29     title: 'Time Card',
30     width: 300,
31     bodyPadding: 10,
32     renderTo: Ext.getBody(),        
33     items: [{
34         xtype: 'timefield',
35         name: 'in',
36         fieldLabel: 'Time In',
37         minValue: '6:00 AM',
38         maxValue: '8:00 PM',
39         increment: 30,
40         anchor: '100%'
41     }, {
42         xtype: 'timefield',
43         name: 'out',
44         fieldLabel: 'Time Out',
45         minValue: '6:00 AM',
46         maxValue: '8:00 PM',
47         increment: 30,
48         anchor: '100%'
49    }]
50 });
51 </code></pre>
52  */
53 Ext.define('Ext.form.field.Time', {
54     extend:'Ext.form.field.Picker',
55     alias: 'widget.timefield',
56     requires: ['Ext.form.field.Date', 'Ext.picker.Time', 'Ext.view.BoundListKeyNav', 'Ext.Date'],
57     alternateClassName: ['Ext.form.TimeField', 'Ext.form.Time'],
58
59     /**
60      * @cfg {String} triggerCls
61      * An additional CSS class used to style the trigger button.  The trigger will always get the
62      * {@link #triggerBaseCls} by default and <tt>triggerCls</tt> will be <b>appended</b> if specified.
63      * Defaults to <tt>'x-form-time-trigger'</tt> for the Time field trigger.
64      */
65     triggerCls: Ext.baseCSSPrefix + 'form-time-trigger',
66
67     /**
68      * @cfg {Date/String} minValue
69      * The minimum allowed time. Can be either a Javascript date object with a valid time value or a string
70      * time in a valid format -- see {@link #format} and {@link #altFormats} (defaults to undefined).
71      */
72
73     /**
74      * @cfg {Date/String} maxValue
75      * The maximum allowed time. Can be either a Javascript date object with a valid time value or a string
76      * time in a valid format -- see {@link #format} and {@link #altFormats} (defaults to undefined).
77      */
78
79     /**
80      * @cfg {String} minText
81      * The error text to display when the entered time is before {@link #minValue} (defaults to
82      * 'The time in this field must be equal to or after {0}').
83      */
84     minText : "The time in this field must be equal to or after {0}",
85
86     /**
87      * @cfg {String} maxText
88      * The error text to display when the entered time is after {@link #maxValue} (defaults to
89      * 'The time in this field must be equal to or before {0}').
90      */
91     maxText : "The time in this field must be equal to or before {0}",
92
93     /**
94      * @cfg {String} invalidText
95      * The error text to display when the time in the field is invalid (defaults to
96      * '{value} is not a valid time').
97      */
98     invalidText : "{0} is not a valid time",
99
100     /**
101      * @cfg {String} format
102      * The default time format string which can be overriden for localization support.  The format must be
103      * valid according to {@link Ext.Date#parse} (defaults to 'g:i A', e.g., '3:15 PM').  For 24-hour time
104      * format try 'H:i' instead.
105      */
106     format : "g:i A",
107
108     /**
109      * @cfg {String} submitFormat The date format string which will be submitted to the server.
110      * The format must be valid according to {@link Ext.Date#parse} (defaults to <tt>{@link #format}</tt>).
111      */
112
113     /**
114      * @cfg {String} altFormats
115      * Multiple date formats separated by "|" to try when parsing a user input value and it doesn't match the defined
116      * format (defaults to 'g:ia|g:iA|g:i a|g:i A|h:i|g:i|H:i|ga|ha|gA|h a|g a|g A|gi|hi|gia|hia|g|H|gi a|hi a|giA|hiA|gi A|hi A').
117      */
118     altFormats : "g:ia|g:iA|g:i a|g:i A|h:i|g:i|H:i|ga|ha|gA|h a|g a|g A|gi|hi|gia|hia|g|H|gi a|hi a|giA|hiA|gi A|hi A",
119
120     /**
121      * @cfg {Number} increment
122      * The number of minutes between each time value in the list (defaults to 15).
123      */
124     increment: 15,
125
126     /**
127      * @cfg {Number} pickerMaxHeight
128      * The maximum height of the {@link Ext.picker.Time} dropdown. Defaults to 300.
129      */
130     pickerMaxHeight: 300,
131
132     /**
133      * @cfg {Boolean} selectOnTab
134      * Whether the Tab key should select the currently highlighted item. Defaults to <tt>true</tt>.
135      */
136     selectOnTab: true,
137
138     /**
139      * @private
140      * This is the date to use when generating time values in the absence of either minValue
141      * or maxValue.  Using the current date causes DST issues on DST boundary dates, so this is an
142      * arbitrary "safe" date that can be any date aside from DST boundary dates.
143      */
144     initDate: '1/1/2008',
145     initDateFormat: 'j/n/Y',
146
147
148     initComponent: function() {
149         var me = this,
150             min = me.minValue,
151             max = me.maxValue;
152         if (min) {
153             me.setMinValue(min);
154         }
155         if (max) {
156             me.setMaxValue(max);
157         }
158         this.callParent();
159     },
160
161     initValue: function() {
162         var me = this,
163             value = me.value;
164
165         // If a String value was supplied, try to convert it to a proper Date object
166         if (Ext.isString(value)) {
167             me.value = me.rawToValue(value);
168         }
169
170         me.callParent();
171     },
172
173     /**
174      * Replaces any existing {@link #minValue} with the new time and refreshes the picker's range.
175      * @param {Date/String} value The minimum time that can be selected
176      */
177     setMinValue: function(value) {
178         var me = this,
179             picker = me.picker;
180         me.setLimit(value, true);
181         if (picker) {
182             picker.setMinValue(me.minValue);
183         }
184     },
185
186     /**
187      * Replaces any existing {@link #maxValue} with the new time and refreshes the picker's range.
188      * @param {Date/String} value The maximum time that can be selected
189      */
190     setMaxValue: function(value) {
191         var me = this,
192             picker = me.picker;
193         me.setLimit(value, false);
194         if (picker) {
195             picker.setMaxValue(me.maxValue);
196         }
197     },
198
199     /**
200      * @private
201      * Updates either the min or max value. Converts the user's value into a Date object whose
202      * year/month/day is set to the {@link #initDate} so that only the time fields are significant.
203      */
204     setLimit: function(value, isMin) {
205         var me = this,
206             d, val;
207         if (Ext.isString(value)) {
208             d = me.parseDate(value);
209         }
210         else if (Ext.isDate(value)) {
211             d = value;
212         }
213         if (d) {
214             val = Ext.Date.clearTime(new Date(me.initDate));
215             val.setHours(d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
216             me[isMin ? 'minValue' : 'maxValue'] = val;
217         }
218     },
219
220     rawToValue: function(rawValue) {
221         return this.parseDate(rawValue) || rawValue || null;
222     },
223
224     valueToRaw: function(value) {
225         return this.formatDate(this.parseDate(value));
226     },
227
228     /**
229      * Runs all of Time's validations and returns an array of any errors. Note that this first
230      * runs Text's validations, so the returned array is an amalgamation of all field errors.
231      * The additional validation checks are testing that the time format is valid, that the chosen
232      * time is within the {@link #minValue} and {@link #maxValue} constraints set.
233      * @param {Mixed} value The value to get errors for (defaults to the current field value)
234      * @return {Array} All validation errors for this field
235      */
236     getErrors: function(value) {
237         var me = this,
238             format = Ext.String.format,
239             errors = me.callParent(arguments),
240             minValue = me.minValue,
241             maxValue = me.maxValue,
242             date;
243
244         value = me.formatDate(value || me.processRawValue(me.getRawValue()));
245
246         if (value === null || value.length < 1) { // if it's blank and textfield didn't flag it then it's valid
247              return errors;
248         }
249
250         date = me.parseDate(value);
251         if (!date) {
252             errors.push(format(me.invalidText, value, me.format));
253             return errors;
254         }
255
256         if (minValue && date < minValue) {
257             errors.push(format(me.minText, me.formatDate(minValue)));
258         }
259
260         if (maxValue && date > maxValue) {
261             errors.push(format(me.maxText, me.formatDate(maxValue)));
262         }
263
264         return errors;
265     },
266
267     formatDate: function() {
268         return Ext.form.field.Date.prototype.formatDate.apply(this, arguments);
269     },
270
271     /**
272      * @private
273      * Parses an input value into a valid Date object.
274      * @param {String/Date} value
275      */
276     parseDate: function(value) {
277         if (!value || Ext.isDate(value)) {
278             return value;
279         }
280
281         var me = this,
282             val = me.safeParse(value, me.format),
283             altFormats = me.altFormats,
284             altFormatsArray = me.altFormatsArray,
285             i = 0,
286             len;
287
288         if (!val && altFormats) {
289             altFormatsArray = altFormatsArray || altFormats.split('|');
290             len = altFormatsArray.length;
291             for (; i < len && !val; ++i) {
292                 val = me.safeParse(value, altFormatsArray[i]);
293             }
294         }
295         return val;
296     },
297
298     safeParse: function(value, format){
299         var me = this,
300             utilDate = Ext.Date,
301             parsedDate,
302             result = null;
303
304         if (utilDate.formatContainsDateInfo(format)) {
305             // assume we've been given a full date
306             result = utilDate.parse(value, format);
307         } else {
308             // Use our initial safe date
309             parsedDate = utilDate.parse(me.initDate + ' ' + value, me.initDateFormat + ' ' + format);
310             if (parsedDate) {
311                 result = parsedDate;
312             }
313         }
314         return result;
315     },
316
317     // @private
318     getSubmitValue: function() {
319         var me = this,
320             format = me.submitFormat || me.format,
321             value = me.getValue();
322
323         return value ? Ext.Date.format(value, format) : null;
324     },
325
326     /**
327      * @private
328      * Creates the {@link Ext.picker.Time}
329      */
330     createPicker: function() {
331         var me = this,
332             picker = Ext.create('Ext.picker.Time', {
333                 selModel: {
334                     mode: 'SINGLE'
335                 },
336                 floating: true,
337                 hidden: true,
338                 minValue: me.minValue,
339                 maxValue: me.maxValue,
340                 increment: me.increment,
341                 format: me.format,
342                 ownerCt: this.ownerCt,
343                 renderTo: document.body,
344                 maxHeight: me.pickerMaxHeight,
345                 focusOnToFront: false
346             });
347
348         me.mon(picker.getSelectionModel(), {
349             selectionchange: me.onListSelect,
350             scope: me
351         });
352
353         return picker;
354     },
355
356     /**
357      * @private
358      * Enables the key nav for the Time picker when it is expanded.
359      * TODO this is largely the same logic as ComboBox, should factor out.
360      */
361     onExpand: function() {
362         var me = this,
363             keyNav = me.pickerKeyNav,
364             selectOnTab = me.selectOnTab,
365             picker = me.getPicker(),
366             lastSelected = picker.getSelectionModel().lastSelected,
367             itemNode;
368
369         if (!keyNav) {
370             keyNav = me.pickerKeyNav = Ext.create('Ext.view.BoundListKeyNav', this.inputEl, {
371                 boundList: picker,
372                 forceKeyDown: true,
373                 tab: function(e) {
374                     if (selectOnTab) {
375                         if(me.picker.highlightedItem) {
376                             this.selectHighlighted(e);
377                         } else {
378                             me.collapse();
379                         }
380                         me.triggerBlur();
381                     }
382                     // Tab key event is allowed to propagate to field
383                     return true;
384                 }
385             });
386             // stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab
387             if (selectOnTab) {
388                 me.ignoreMonitorTab = true;
389             }
390         }
391         Ext.defer(keyNav.enable, 1, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker
392
393         // Highlight the last selected item and scroll it into view
394         if (lastSelected) {
395             itemNode = picker.getNode(lastSelected);
396             if (itemNode) {
397                 picker.highlightItem(itemNode);
398                 picker.el.scrollChildIntoView(itemNode, false);
399             }
400         }
401     },
402
403     /**
404      * @private
405      * Disables the key nav for the Time picker when it is collapsed.
406      */
407     onCollapse: function() {
408         var me = this,
409             keyNav = me.pickerKeyNav;
410         if (keyNav) {
411             keyNav.disable();
412             me.ignoreMonitorTab = false;
413         }
414     },
415
416     /**
417      * @private
418      * Clears the highlighted item in the picker on change.
419      * This prevents the highlighted item from being selected instead of the custom typed in value when the tab key is pressed.
420      */
421     onChange: function() {
422         var me = this,
423             picker = me.picker;
424
425         me.callParent(arguments);
426         if(picker) {
427             picker.clearHighlight();
428         }
429     },
430
431     /**
432      * @private
433      * Handles a time being selected from the Time picker.
434      */
435     onListSelect: function(list, recordArray) {
436         var me = this,
437             record = recordArray[0],
438             val = record ? record.get('date') : null;
439         me.setValue(val);
440         me.fireEvent('select', me, val);
441         me.picker.clearHighlight();
442         me.collapse();
443         me.inputEl.focus();
444     }
445 });
446
447