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