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