Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / form / field / Date.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  * @docauthor Jason Johnston <jason@sencha.com>
17  *
18  * Provides a date input field with a {@link Ext.picker.Date date picker} dropdown and automatic date
19  * validation.
20  *
21  * This field recognizes and uses the JavaScript Date object as its main {@link #value} type. In addition,
22  * it recognizes string values which are parsed according to the {@link #format} and/or {@link #altFormats}
23  * configs. These may be reconfigured to use date formats appropriate for the user's locale.
24  *
25  * The field may be limited to a certain range of dates by using the {@link #minValue}, {@link #maxValue},
26  * {@link #disabledDays}, and {@link #disabledDates} config parameters. These configurations will be used both
27  * in the field's validation, and in the date picker dropdown by preventing invalid dates from being selected.
28  *
29  * # Example usage
30  *
31  *     @example
32  *     Ext.create('Ext.form.Panel', {
33  *         renderTo: Ext.getBody(),
34  *         width: 300,
35  *         bodyPadding: 10,
36  *         title: 'Dates',
37  *         items: [{
38  *             xtype: 'datefield',
39  *             anchor: '100%',
40  *             fieldLabel: 'From',
41  *             name: 'from_date',
42  *             maxValue: new Date()  // limited to the current date or prior
43  *         }, {
44  *             xtype: 'datefield',
45  *             anchor: '100%',
46  *             fieldLabel: 'To',
47  *             name: 'to_date',
48  *             value: new Date()  // defaults to today
49  *         }]
50  *     });
51  *
52  * # Date Formats Examples
53  *
54  * This example shows a couple of different date format parsing scenarios. Both use custom date format
55  * configurations; the first one matches the configured `format` while the second matches the `altFormats`.
56  *
57  *     @example
58  *     Ext.create('Ext.form.Panel', {
59  *         renderTo: Ext.getBody(),
60  *         width: 300,
61  *         bodyPadding: 10,
62  *         title: 'Dates',
63  *         items: [{
64  *             xtype: 'datefield',
65  *             anchor: '100%',
66  *             fieldLabel: 'Date',
67  *             name: 'date',
68  *             // The value matches the format; will be parsed and displayed using that format.
69  *             format: 'm d Y',
70  *             value: '2 4 1978'
71  *         }, {
72  *             xtype: 'datefield',
73  *             anchor: '100%',
74  *             fieldLabel: 'Date',
75  *             name: 'date',
76  *             // The value does not match the format, but does match an altFormat; will be parsed
77  *             // using the altFormat and displayed using the format.
78  *             format: 'm d Y',
79  *             altFormats: 'm,d,Y|m.d.Y',
80  *             value: '2.4.1978'
81  *         }]
82  *     });
83  */
84 Ext.define('Ext.form.field.Date', {
85     extend:'Ext.form.field.Picker',
86     alias: 'widget.datefield',
87     requires: ['Ext.picker.Date'],
88     alternateClassName: ['Ext.form.DateField', 'Ext.form.Date'],
89
90     /**
91      * @cfg {String} format
92      * The default date format string which can be overriden for localization support. The format must be valid
93      * according to {@link Ext.Date#parse}.
94      */
95     format : "m/d/Y",
96     /**
97      * @cfg {String} altFormats
98      * Multiple date formats separated by "|" to try when parsing a user input value and it does not match the defined
99      * format.
100      */
101     altFormats : "m/d/Y|n/j/Y|n/j/y|m/j/y|n/d/y|m/j/Y|n/d/Y|m-d-y|m-d-Y|m/d|m-d|md|mdy|mdY|d|Y-m-d|n-j|n/j",
102     /**
103      * @cfg {String} disabledDaysText
104      * The tooltip to display when the date falls on a disabled day.
105      */
106     disabledDaysText : "Disabled",
107     /**
108      * @cfg {String} disabledDatesText
109      * The tooltip text to display when the date falls on a disabled date.
110      */
111     disabledDatesText : "Disabled",
112     /**
113      * @cfg {String} minText
114      * The error text to display when the date in the cell is before {@link #minValue}.
115      */
116     minText : "The date in this field must be equal to or after {0}",
117     /**
118      * @cfg {String} maxText
119      * The error text to display when the date in the cell is after {@link #maxValue}.
120      */
121     maxText : "The date in this field must be equal to or before {0}",
122     /**
123      * @cfg {String} invalidText
124      * The error text to display when the date in the field is invalid.
125      */
126     invalidText : "{0} is not a valid date - it must be in the format {1}",
127     /**
128      * @cfg {String} [triggerCls='x-form-date-trigger']
129      * An additional CSS class used to style the trigger button. The trigger will always get the class 'x-form-trigger'
130      * and triggerCls will be **appended** if specified (default class displays a calendar icon).
131      */
132     triggerCls : Ext.baseCSSPrefix + 'form-date-trigger',
133     /**
134      * @cfg {Boolean} showToday
135      * false to hide the footer area of the Date picker containing the Today button and disable the keyboard handler for
136      * spacebar that selects the current date.
137      */
138     showToday : true,
139     /**
140      * @cfg {Date/String} minValue
141      * The minimum allowed date. Can be either a Javascript date object or a string date in a valid format.
142      */
143     /**
144      * @cfg {Date/String} maxValue
145      * The maximum allowed date. Can be either a Javascript date object or a string date in a valid format.
146      */
147     /**
148      * @cfg {Number[]} disabledDays
149      * An array of days to disable, 0 based. Some examples:
150      *
151      *     // disable Sunday and Saturday:
152      *     disabledDays:  [0, 6]
153      *     // disable weekdays:
154      *     disabledDays: [1,2,3,4,5]
155      */
156     /**
157      * @cfg {String[]} disabledDates
158      * An array of "dates" to disable, as strings. These strings will be used to build a dynamic regular expression so
159      * they are very powerful. Some examples:
160      *
161      *     // disable these exact dates:
162      *     disabledDates: ["03/08/2003", "09/16/2003"]
163      *     // disable these days for every year:
164      *     disabledDates: ["03/08", "09/16"]
165      *     // only match the beginning (useful if you are using short years):
166      *     disabledDates: ["^03/08"]
167      *     // disable every day in March 2006:
168      *     disabledDates: ["03/../2006"]
169      *     // disable every day in every March:
170      *     disabledDates: ["^03"]
171      *
172      * Note that the format of the dates included in the array should exactly match the {@link #format} config. In order
173      * to support regular expressions, if you are using a {@link #format date format} that has "." in it, you will have
174      * to escape the dot when restricting dates. For example: `["03\\.08\\.03"]`.
175      */
176
177     /**
178      * @cfg {String} submitFormat
179      * The date format string which will be submitted to the server. The format must be valid according to {@link
180      * Ext.Date#parse} (defaults to {@link #format}).
181      */
182
183     // in the absence of a time value, a default value of 12 noon will be used
184     // (note: 12 noon was chosen because it steers well clear of all DST timezone changes)
185     initTime: '12', // 24 hour format
186
187     initTimeFormat: 'H',
188
189     matchFieldWidth: false,
190     /**
191      * @cfg {Number} startDay
192      * Day index at which the week should begin, 0-based (defaults to Sunday)
193      */
194     startDay: 0,
195
196     initComponent : function(){
197         var me = this,
198             isString = Ext.isString,
199             min, max;
200
201         min = me.minValue;
202         max = me.maxValue;
203         if(isString(min)){
204             me.minValue = me.parseDate(min);
205         }
206         if(isString(max)){
207             me.maxValue = me.parseDate(max);
208         }
209         me.disabledDatesRE = null;
210         me.initDisabledDays();
211
212         me.callParent();
213     },
214
215     initValue: function() {
216         var me = this,
217             value = me.value;
218
219         // If a String value was supplied, try to convert it to a proper Date
220         if (Ext.isString(value)) {
221             me.value = me.rawToValue(value);
222         }
223
224         me.callParent();
225     },
226
227     // private
228     initDisabledDays : function(){
229         if(this.disabledDates){
230             var dd = this.disabledDates,
231                 len = dd.length - 1,
232                 re = "(?:";
233
234             Ext.each(dd, function(d, i){
235                 re += Ext.isDate(d) ? '^' + Ext.String.escapeRegex(d.dateFormat(this.format)) + '$' : dd[i];
236                 if (i !== len) {
237                     re += '|';
238                 }
239             }, this);
240             this.disabledDatesRE = new RegExp(re + ')');
241         }
242     },
243
244     /**
245      * Replaces any existing disabled dates with new values and refreshes the Date picker.
246      * @param {String[]} disabledDates An array of date strings (see the {@link #disabledDates} config for details on
247      * supported values) used to disable a pattern of dates.
248      */
249     setDisabledDates : function(dd){
250         var me = this,
251             picker = me.picker;
252
253         me.disabledDates = dd;
254         me.initDisabledDays();
255         if (picker) {
256             picker.setDisabledDates(me.disabledDatesRE);
257         }
258     },
259
260     /**
261      * Replaces any existing disabled days (by index, 0-6) with new values and refreshes the Date picker.
262      * @param {Number[]} disabledDays An array of disabled day indexes. See the {@link #disabledDays} config for details on
263      * supported values.
264      */
265     setDisabledDays : function(dd){
266         var picker = this.picker;
267
268         this.disabledDays = dd;
269         if (picker) {
270             picker.setDisabledDays(dd);
271         }
272     },
273
274     /**
275      * Replaces any existing {@link #minValue} with the new value and refreshes the Date picker.
276      * @param {Date} value The minimum date that can be selected
277      */
278     setMinValue : function(dt){
279         var me = this,
280             picker = me.picker,
281             minValue = (Ext.isString(dt) ? me.parseDate(dt) : dt);
282
283         me.minValue = minValue;
284         if (picker) {
285             picker.minText = Ext.String.format(me.minText, me.formatDate(me.minValue));
286             picker.setMinDate(minValue);
287         }
288     },
289
290     /**
291      * Replaces any existing {@link #maxValue} with the new value and refreshes the Date picker.
292      * @param {Date} value The maximum date that can be selected
293      */
294     setMaxValue : function(dt){
295         var me = this,
296             picker = me.picker,
297             maxValue = (Ext.isString(dt) ? me.parseDate(dt) : dt);
298
299         me.maxValue = maxValue;
300         if (picker) {
301             picker.maxText = Ext.String.format(me.maxText, me.formatDate(me.maxValue));
302             picker.setMaxDate(maxValue);
303         }
304     },
305
306     /**
307      * Runs all of Date's validations and returns an array of any errors. Note that this first runs Text's validations,
308      * so the returned array is an amalgamation of all field errors. The additional validation checks are testing that
309      * the date format is valid, that the chosen date is within the min and max date constraints set, that the date
310      * chosen is not in the disabledDates regex and that the day chosed is not one of the disabledDays.
311      * @param {Object} [value] The value to get errors for (defaults to the current field value)
312      * @return {String[]} All validation errors for this field
313      */
314     getErrors: function(value) {
315         var me = this,
316             format = Ext.String.format,
317             clearTime = Ext.Date.clearTime,
318             errors = me.callParent(arguments),
319             disabledDays = me.disabledDays,
320             disabledDatesRE = me.disabledDatesRE,
321             minValue = me.minValue,
322             maxValue = me.maxValue,
323             len = disabledDays ? disabledDays.length : 0,
324             i = 0,
325             svalue,
326             fvalue,
327             day,
328             time;
329
330         value = me.formatDate(value || me.processRawValue(me.getRawValue()));
331
332         if (value === null || value.length < 1) { // if it's blank and textfield didn't flag it then it's valid
333              return errors;
334         }
335
336         svalue = value;
337         value = me.parseDate(value);
338         if (!value) {
339             errors.push(format(me.invalidText, svalue, me.format));
340             return errors;
341         }
342
343         time = value.getTime();
344         if (minValue && time < clearTime(minValue).getTime()) {
345             errors.push(format(me.minText, me.formatDate(minValue)));
346         }
347
348         if (maxValue && time > clearTime(maxValue).getTime()) {
349             errors.push(format(me.maxText, me.formatDate(maxValue)));
350         }
351
352         if (disabledDays) {
353             day = value.getDay();
354
355             for(; i < len; i++) {
356                 if (day === disabledDays[i]) {
357                     errors.push(me.disabledDaysText);
358                     break;
359                 }
360             }
361         }
362
363         fvalue = me.formatDate(value);
364         if (disabledDatesRE && disabledDatesRE.test(fvalue)) {
365             errors.push(format(me.disabledDatesText, fvalue));
366         }
367
368         return errors;
369     },
370
371     rawToValue: function(rawValue) {
372         return this.parseDate(rawValue) || rawValue || null;
373     },
374
375     valueToRaw: function(value) {
376         return this.formatDate(this.parseDate(value));
377     },
378
379     /**
380      * @method setValue
381      * Sets the value of the date field. You can pass a date object or any string that can be parsed into a valid date,
382      * using {@link #format} as the date format, according to the same rules as {@link Ext.Date#parse} (the default
383      * format used is "m/d/Y").
384      *
385      * Usage:
386      *
387      *     //All of these calls set the same date value (May 4, 2006)
388      *
389      *     //Pass a date object:
390      *     var dt = new Date('5/4/2006');
391      *     dateField.setValue(dt);
392      *
393      *     //Pass a date string (default format):
394      *     dateField.setValue('05/04/2006');
395      *
396      *     //Pass a date string (custom format):
397      *     dateField.format = 'Y-m-d';
398      *     dateField.setValue('2006-05-04');
399      *
400      * @param {String/Date} date The date or valid date string
401      * @return {Ext.form.field.Date} this
402      */
403
404     /**
405      * Attempts to parse a given string value using a given {@link Ext.Date#parse date format}.
406      * @param {String} value The value to attempt to parse
407      * @param {String} format A valid date format (see {@link Ext.Date#parse})
408      * @return {Date} The parsed Date object, or null if the value could not be successfully parsed.
409      */
410     safeParse : function(value, format) {
411         var me = this,
412             utilDate = Ext.Date,
413             parsedDate,
414             result = null;
415
416         if (utilDate.formatContainsHourInfo(format)) {
417             // if parse format contains hour information, no DST adjustment is necessary
418             result = utilDate.parse(value, format);
419         } else {
420             // set time to 12 noon, then clear the time
421             parsedDate = utilDate.parse(value + ' ' + me.initTime, format + ' ' + me.initTimeFormat);
422             if (parsedDate) {
423                 result = utilDate.clearTime(parsedDate);
424             }
425         }
426         return result;
427     },
428
429     // @private
430     getSubmitValue: function() {
431         var format = this.submitFormat || this.format,
432             value = this.getValue();
433
434         return value ? Ext.Date.format(value, format) : '';
435     },
436
437     /**
438      * @private
439      */
440     parseDate : function(value) {
441         if(!value || Ext.isDate(value)){
442             return value;
443         }
444
445         var me = this,
446             val = me.safeParse(value, me.format),
447             altFormats = me.altFormats,
448             altFormatsArray = me.altFormatsArray,
449             i = 0,
450             len;
451
452         if (!val && altFormats) {
453             altFormatsArray = altFormatsArray || altFormats.split('|');
454             len = altFormatsArray.length;
455             for (; i < len && !val; ++i) {
456                 val = me.safeParse(value, altFormatsArray[i]);
457             }
458         }
459         return val;
460     },
461
462     // private
463     formatDate : function(date){
464         return Ext.isDate(date) ? Ext.Date.dateFormat(date, this.format) : date;
465     },
466
467     createPicker: function() {
468         var me = this,
469             format = Ext.String.format;
470
471         return Ext.create('Ext.picker.Date', {
472             pickerField: me,
473             ownerCt: me.ownerCt,
474             renderTo: document.body,
475             floating: true,
476             hidden: true,
477             focusOnShow: true,
478             minDate: me.minValue,
479             maxDate: me.maxValue,
480             disabledDatesRE: me.disabledDatesRE,
481             disabledDatesText: me.disabledDatesText,
482             disabledDays: me.disabledDays,
483             disabledDaysText: me.disabledDaysText,
484             format: me.format,
485             showToday: me.showToday,
486             startDay: me.startDay,
487             minText: format(me.minText, me.formatDate(me.minValue)),
488             maxText: format(me.maxText, me.formatDate(me.maxValue)),
489             listeners: {
490                 scope: me,
491                 select: me.onSelect
492             },
493             keyNavConfig: {
494                 esc: function() {
495                     me.collapse();
496                 }
497             }
498         });
499     },
500
501     onSelect: function(m, d) {
502         var me = this;
503
504         me.setValue(d);
505         me.fireEvent('select', me, d);
506         me.collapse();
507     },
508
509     /**
510      * @private
511      * Sets the Date picker's value to match the current field value when expanding.
512      */
513     onExpand: function() {
514         var value = this.getValue();
515         this.picker.setValue(Ext.isDate(value) ? value : new Date());
516     },
517
518     /**
519      * @private
520      * Focuses the field when collapsing the Date picker.
521      */
522     onCollapse: function() {
523         this.focus(false, 60);
524     },
525
526     // private
527     beforeBlur : function(){
528         var me = this,
529             v = me.parseDate(me.getRawValue()),
530             focusTask = me.focusTask;
531
532         if (focusTask) {
533             focusTask.cancel();
534         }
535
536         if (v) {
537             me.setValue(v);
538         }
539     }
540
541     /**
542      * @hide
543      * @cfg {Boolean} grow
544      */
545     /**
546      * @hide
547      * @cfg {Number} growMin
548      */
549     /**
550      * @hide
551      * @cfg {Number} growMax
552      */
553     /**
554      * @hide
555      * @method autoSize
556      */
557 });
558