Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / picker / 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  * A date picker. This class is used by the Ext.form.field.Date field to allow browsing and selection of valid
17  * dates in a popup next to the field, but may also be used with other components.
18  *
19  * Typically you will need to implement a handler function to be notified when the user chooses a date from the picker;
20  * you can register the handler using the {@link #select} event, or by implementing the {@link #handler} method.
21  *
22  * By default the user will be allowed to pick any date; this can be changed by using the {@link #minDate},
23  * {@link #maxDate}, {@link #disabledDays}, {@link #disabledDatesRE}, and/or {@link #disabledDates} configs.
24  *
25  * All the string values documented below may be overridden by including an Ext locale file in your page.
26  *
27  *     @example
28  *     Ext.create('Ext.panel.Panel', {
29  *         title: 'Choose a future date:',
30  *         width: 200,
31  *         bodyPadding: 10,
32  *         renderTo: Ext.getBody(),
33  *         items: [{
34  *             xtype: 'datepicker',
35  *             minDate: new Date(),
36  *             handler: function(picker, date) {
37  *                 // do something with the selected date
38  *             }
39  *         }]
40  *     });
41  */
42 Ext.define('Ext.picker.Date', {
43     extend: 'Ext.Component',
44     requires: [
45         'Ext.XTemplate',
46         'Ext.button.Button',
47         'Ext.button.Split',
48         'Ext.util.ClickRepeater',
49         'Ext.util.KeyNav',
50         'Ext.EventObject',
51         'Ext.fx.Manager',
52         'Ext.picker.Month'
53     ],
54     alias: 'widget.datepicker',
55     alternateClassName: 'Ext.DatePicker',
56
57     renderTpl: [
58         '<div class="{cls}" id="{id}" role="grid" title="{ariaTitle} {value:this.longDay}">',
59             '<div role="presentation" class="{baseCls}-header">',
60                 '<div class="{baseCls}-prev"><a id="{id}-prevEl" href="#" role="button" title="{prevText}"></a></div>',
61                 '<div class="{baseCls}-month" id="{id}-middleBtnEl"></div>',
62                 '<div class="{baseCls}-next"><a id="{id}-nextEl" href="#" role="button" title="{nextText}"></a></div>',
63             '</div>',
64             '<table id="{id}-eventEl" class="{baseCls}-inner" cellspacing="0" role="presentation">',
65                 '<thead role="presentation"><tr role="presentation">',
66                     '<tpl for="dayNames">',
67                         '<th role="columnheader" title="{.}"><span>{.:this.firstInitial}</span></th>',
68                     '</tpl>',
69                 '</tr></thead>',
70                 '<tbody role="presentation"><tr role="presentation">',
71                     '<tpl for="days">',
72                         '{#:this.isEndOfWeek}',
73                         '<td role="gridcell" id="{[Ext.id()]}">',
74                             '<a role="presentation" href="#" hidefocus="on" class="{parent.baseCls}-date" tabIndex="1">',
75                                 '<em role="presentation"><span role="presentation"></span></em>',
76                             '</a>',
77                         '</td>',
78                     '</tpl>',
79                 '</tr></tbody>',
80             '</table>',
81             '<tpl if="showToday">',
82                 '<div id="{id}-footerEl" role="presentation" class="{baseCls}-footer"></div>',
83             '</tpl>',
84         '</div>',
85         {
86             firstInitial: function(value) {
87                 return value.substr(0,1);
88             },
89             isEndOfWeek: function(value) {
90                 // convert from 1 based index to 0 based
91                 // by decrementing value once.
92                 value--;
93                 var end = value % 7 === 0 && value !== 0;
94                 return end ? '</tr><tr role="row">' : '';
95             },
96             longDay: function(value){
97                 return Ext.Date.format(value, this.longDayFormat);
98             }
99         }
100     ],
101
102     ariaTitle: 'Date Picker',
103
104     /**
105      * @cfg {String} todayText
106      * The text to display on the button that selects the current date
107      */
108     todayText : 'Today',
109
110     /**
111      * @cfg {Function} handler
112      * Optional. A function that will handle the select event of this picker. The handler is passed the following
113      * parameters:
114      *
115      *   - `picker` : Ext.picker.Date
116      *
117      * This Date picker.
118      *
119      *   - `date` : Date
120      *
121      * The selected date.
122      */
123
124     /**
125      * @cfg {Object} scope
126      * The scope (`this` reference) in which the `{@link #handler}` function will be called. Defaults to this
127      * DatePicker instance.
128      */
129
130     /**
131      * @cfg {String} todayTip
132      * A string used to format the message for displaying in a tooltip over the button that selects the current date.
133      * The `{0}` token in string is replaced by today's date.
134      */
135     todayTip : '{0} (Spacebar)',
136
137     /**
138      * @cfg {String} minText
139      * The error text to display if the minDate validation fails.
140      */
141     minText : 'This date is before the minimum date',
142
143     /**
144      * @cfg {String} maxText
145      * The error text to display if the maxDate validation fails.
146      */
147     maxText : 'This date is after the maximum date',
148
149     /**
150      * @cfg {String} format
151      * The default date format string which can be overriden for localization support. The format must be valid
152      * according to {@link Ext.Date#parse} (defaults to {@link Ext.Date#defaultFormat}).
153      */
154
155     /**
156      * @cfg {String} disabledDaysText
157      * The tooltip to display when the date falls on a disabled day.
158      */
159     disabledDaysText : 'Disabled',
160
161     /**
162      * @cfg {String} disabledDatesText
163      * The tooltip text to display when the date falls on a disabled date.
164      */
165     disabledDatesText : 'Disabled',
166
167     /**
168      * @cfg {String[]} monthNames
169      * An array of textual month names which can be overriden for localization support (defaults to Ext.Date.monthNames)
170      */
171
172     /**
173      * @cfg {String[]} dayNames
174      * An array of textual day names which can be overriden for localization support (defaults to Ext.Date.dayNames)
175      */
176
177     /**
178      * @cfg {String} nextText
179      * The next month navigation button tooltip
180      */
181     nextText : 'Next Month (Control+Right)',
182
183     /**
184      * @cfg {String} prevText
185      * The previous month navigation button tooltip
186      */
187     prevText : 'Previous Month (Control+Left)',
188
189     /**
190      * @cfg {String} monthYearText
191      * The header month selector tooltip
192      */
193     monthYearText : 'Choose a month (Control+Up/Down to move years)',
194
195     /**
196      * @cfg {Number} startDay
197      * Day index at which the week should begin, 0-based (defaults to Sunday)
198      */
199     startDay : 0,
200
201     /**
202      * @cfg {Boolean} showToday
203      * False to hide the footer area containing the Today button and disable the keyboard handler for spacebar that
204      * selects the current date.
205      */
206     showToday : true,
207
208     /**
209      * @cfg {Date} [minDate=null]
210      * Minimum allowable date (JavaScript date object)
211      */
212
213     /**
214      * @cfg {Date} [maxDate=null]
215      * Maximum allowable date (JavaScript date object)
216      */
217
218     /**
219      * @cfg {Number[]} [disabledDays=null]
220      * An array of days to disable, 0-based. For example, [0, 6] disables Sunday and Saturday.
221      */
222
223     /**
224      * @cfg {RegExp} [disabledDatesRE=null]
225      * JavaScript regular expression used to disable a pattern of dates. The {@link #disabledDates}
226      * config will generate this regex internally, but if you specify disabledDatesRE it will take precedence over the
227      * disabledDates value.
228      */
229
230     /**
231      * @cfg {String[]} disabledDates
232      * An array of 'dates' to disable, as strings. These strings will be used to build a dynamic regular expression so
233      * they are very powerful. Some examples:
234      *
235      *   - ['03/08/2003', '09/16/2003'] would disable those exact dates
236      *   - ['03/08', '09/16'] would disable those days for every year
237      *   - ['^03/08'] would only match the beginning (useful if you are using short years)
238      *   - ['03/../2006'] would disable every day in March 2006
239      *   - ['^03'] would disable every day in every March
240      *
241      * Note that the format of the dates included in the array should exactly match the {@link #format} config. In order
242      * to support regular expressions, if you are using a date format that has '.' in it, you will have to escape the
243      * dot when restricting dates. For example: ['03\\.08\\.03'].
244      */
245
246     /**
247      * @cfg {Boolean} disableAnim
248      * True to disable animations when showing the month picker.
249      */
250     disableAnim: false,
251
252     /**
253      * @cfg {String} [baseCls='x-datepicker']
254      * The base CSS class to apply to this components element.
255      */
256     baseCls: Ext.baseCSSPrefix + 'datepicker',
257
258     /**
259      * @cfg {String} [selectedCls='x-datepicker-selected']
260      * The class to apply to the selected cell.
261      */
262
263     /**
264      * @cfg {String} [disabledCellCls='x-datepicker-disabled']
265      * The class to apply to disabled cells.
266      */
267
268     /**
269      * @cfg {String} longDayFormat
270      * The format for displaying a date in a longer format.
271      */
272     longDayFormat: 'F d, Y',
273
274     /**
275      * @cfg {Object} keyNavConfig
276      * Specifies optional custom key event handlers for the {@link Ext.util.KeyNav} attached to this date picker. Must
277      * conform to the config format recognized by the {@link Ext.util.KeyNav} constructor. Handlers specified in this
278      * object will replace default handlers of the same name.
279      */
280
281     /**
282      * @cfg {Boolean} focusOnShow
283      * True to automatically focus the picker on show.
284      */
285     focusOnShow: false,
286
287     // private
288     // Set by other components to stop the picker focus being updated when the value changes.
289     focusOnSelect: true,
290
291     width: 178,
292
293     // default value used to initialise each date in the DatePicker
294     // (note: 12 noon was chosen because it steers well clear of all DST timezone changes)
295     initHour: 12, // 24-hour format
296
297     numDays: 42,
298
299     // private, inherit docs
300     initComponent : function() {
301         var me = this,
302             clearTime = Ext.Date.clearTime;
303
304         me.selectedCls = me.baseCls + '-selected';
305         me.disabledCellCls = me.baseCls + '-disabled';
306         me.prevCls = me.baseCls + '-prevday';
307         me.activeCls = me.baseCls + '-active';
308         me.nextCls = me.baseCls + '-prevday';
309         me.todayCls = me.baseCls + '-today';
310         me.dayNames = me.dayNames.slice(me.startDay).concat(me.dayNames.slice(0, me.startDay));
311         this.callParent();
312
313         me.value = me.value ?
314                  clearTime(me.value, true) : clearTime(new Date());
315
316         me.addEvents(
317             /**
318              * @event select
319              * Fires when a date is selected
320              * @param {Ext.picker.Date} this DatePicker
321              * @param {Date} date The selected date
322              */
323             'select'
324         );
325
326         me.initDisabledDays();
327     },
328
329     // private, inherit docs
330     onRender : function(container, position){
331         /*
332          * days array for looping through 6 full weeks (6 weeks * 7 days)
333          * Note that we explicitly force the size here so the template creates
334          * all the appropriate cells.
335          */
336
337         var me = this,
338             days = new Array(me.numDays),
339             today = Ext.Date.format(new Date(), me.format);
340
341         Ext.applyIf(me, {
342             renderData: {}
343         });
344
345         Ext.apply(me.renderData, {
346             dayNames: me.dayNames,
347             ariaTitle: me.ariaTitle,
348             value: me.value,
349             showToday: me.showToday,
350             prevText: me.prevText,
351             nextText: me.nextText,
352             days: days
353         });
354         me.getTpl('renderTpl').longDayFormat = me.longDayFormat;
355
356         me.addChildEls('eventEl', 'prevEl', 'nextEl', 'middleBtnEl', 'footerEl');
357
358         this.callParent(arguments);
359         me.el.unselectable();
360
361         me.cells = me.eventEl.select('tbody td');
362         me.textNodes = me.eventEl.query('tbody td span');
363
364         me.monthBtn = Ext.create('Ext.button.Split', {
365             text: '',
366             tooltip: me.monthYearText,
367             renderTo: me.middleBtnEl
368         });
369         //~ me.middleBtnEl.down('button').addCls(Ext.baseCSSPrefix + 'btn-arrow');
370
371
372         me.todayBtn = Ext.create('Ext.button.Button', {
373             renderTo: me.footerEl,
374             text: Ext.String.format(me.todayText, today),
375             tooltip: Ext.String.format(me.todayTip, today),
376             handler: me.selectToday,
377             scope: me
378         });
379     },
380
381     // private, inherit docs
382     initEvents: function(){
383         var me = this,
384             eDate = Ext.Date,
385             day = eDate.DAY;
386
387         this.callParent();
388
389         me.prevRepeater = Ext.create('Ext.util.ClickRepeater', me.prevEl, {
390             handler: me.showPrevMonth,
391             scope: me,
392             preventDefault: true,
393             stopDefault: true
394         });
395
396         me.nextRepeater = Ext.create('Ext.util.ClickRepeater', me.nextEl, {
397             handler: me.showNextMonth,
398             scope: me,
399             preventDefault:true,
400             stopDefault:true
401         });
402
403         me.keyNav = Ext.create('Ext.util.KeyNav', me.eventEl, Ext.apply({
404             scope: me,
405             'left' : function(e){
406                 if(e.ctrlKey){
407                     me.showPrevMonth();
408                 }else{
409                     me.update(eDate.add(me.activeDate, day, -1));
410                 }
411             },
412
413             'right' : function(e){
414                 if(e.ctrlKey){
415                     me.showNextMonth();
416                 }else{
417                     me.update(eDate.add(me.activeDate, day, 1));
418                 }
419             },
420
421             'up' : function(e){
422                 if(e.ctrlKey){
423                     me.showNextYear();
424                 }else{
425                     me.update(eDate.add(me.activeDate, day, -7));
426                 }
427             },
428
429             'down' : function(e){
430                 if(e.ctrlKey){
431                     me.showPrevYear();
432                 }else{
433                     me.update(eDate.add(me.activeDate, day, 7));
434                 }
435             },
436             'pageUp' : me.showNextMonth,
437             'pageDown' : me.showPrevMonth,
438             'enter' : function(e){
439                 e.stopPropagation();
440                 return true;
441             }
442         }, me.keyNavConfig));
443
444         if(me.showToday){
445             me.todayKeyListener = me.eventEl.addKeyListener(Ext.EventObject.SPACE, me.selectToday,  me);
446         }
447         me.mon(me.eventEl, 'mousewheel', me.handleMouseWheel, me);
448         me.mon(me.eventEl, 'click', me.handleDateClick,  me, {delegate: 'a.' + me.baseCls + '-date'});
449         me.mon(me.monthBtn, 'click', me.showMonthPicker, me);
450         me.mon(me.monthBtn, 'arrowclick', me.showMonthPicker, me);
451         me.update(me.value);
452     },
453
454     /**
455      * Setup the disabled dates regex based on config options
456      * @private
457      */
458     initDisabledDays : function(){
459         var me = this,
460             dd = me.disabledDates,
461             re = '(?:',
462             len;
463
464         if(!me.disabledDatesRE && dd){
465                 len = dd.length - 1;
466
467             Ext.each(dd, function(d, i){
468                 re += Ext.isDate(d) ? '^' + Ext.String.escapeRegex(Ext.Date.dateFormat(d, me.format)) + '$' : dd[i];
469                 if(i != len){
470                     re += '|';
471                 }
472             }, me);
473             me.disabledDatesRE = new RegExp(re + ')');
474         }
475     },
476
477     /**
478      * Replaces any existing disabled dates with new values and refreshes the DatePicker.
479      * @param {String[]/RegExp} disabledDates An array of date strings (see the {@link #disabledDates} config for
480      * details on supported values), or a JavaScript regular expression used to disable a pattern of dates.
481      * @return {Ext.picker.Date} this
482      */
483     setDisabledDates : function(dd){
484         var me = this;
485
486         if(Ext.isArray(dd)){
487             me.disabledDates = dd;
488             me.disabledDatesRE = null;
489         }else{
490             me.disabledDatesRE = dd;
491         }
492         me.initDisabledDays();
493         me.update(me.value, true);
494         return me;
495     },
496
497     /**
498      * Replaces any existing disabled days (by index, 0-6) with new values and refreshes the DatePicker.
499      * @param {Number[]} disabledDays An array of disabled day indexes. See the {@link #disabledDays} config for details
500      * on supported values.
501      * @return {Ext.picker.Date} this
502      */
503     setDisabledDays : function(dd){
504         this.disabledDays = dd;
505         return this.update(this.value, true);
506     },
507
508     /**
509      * Replaces any existing {@link #minDate} with the new value and refreshes the DatePicker.
510      * @param {Date} value The minimum date that can be selected
511      * @return {Ext.picker.Date} this
512      */
513     setMinDate : function(dt){
514         this.minDate = dt;
515         return this.update(this.value, true);
516     },
517
518     /**
519      * Replaces any existing {@link #maxDate} with the new value and refreshes the DatePicker.
520      * @param {Date} value The maximum date that can be selected
521      * @return {Ext.picker.Date} this
522      */
523     setMaxDate : function(dt){
524         this.maxDate = dt;
525         return this.update(this.value, true);
526     },
527
528     /**
529      * Sets the value of the date field
530      * @param {Date} value The date to set
531      * @return {Ext.picker.Date} this
532      */
533     setValue : function(value){
534         this.value = Ext.Date.clearTime(value, true);
535         return this.update(this.value);
536     },
537
538     /**
539      * Gets the current selected value of the date field
540      * @return {Date} The selected date
541      */
542     getValue : function(){
543         return this.value;
544     },
545
546     // private
547     focus : function(){
548         this.update(this.activeDate);
549     },
550
551     // private, inherit docs
552     onEnable: function(){
553         this.callParent();
554         this.setDisabledStatus(false);
555         this.update(this.activeDate);
556
557     },
558
559     // private, inherit docs
560     onDisable : function(){
561         this.callParent();
562         this.setDisabledStatus(true);
563     },
564
565     /**
566      * Set the disabled state of various internal components
567      * @private
568      * @param {Boolean} disabled
569      */
570     setDisabledStatus : function(disabled){
571         var me = this;
572
573         me.keyNav.setDisabled(disabled);
574         me.prevRepeater.setDisabled(disabled);
575         me.nextRepeater.setDisabled(disabled);
576         if (me.showToday) {
577             me.todayKeyListener.setDisabled(disabled);
578             me.todayBtn.setDisabled(disabled);
579         }
580     },
581
582     /**
583      * Get the current active date.
584      * @private
585      * @return {Date} The active date
586      */
587     getActive: function(){
588         return this.activeDate || this.value;
589     },
590
591     /**
592      * Run any animation required to hide/show the month picker.
593      * @private
594      * @param {Boolean} isHide True if it's a hide operation
595      */
596     runAnimation: function(isHide){
597         var picker = this.monthPicker,
598             options = {
599                 duration: 200,
600                 callback: function(){
601                     if (isHide) {
602                         picker.hide();
603                     } else {
604                         picker.show();
605                     }
606                 }
607             };
608
609         if (isHide) {
610             picker.el.slideOut('t', options);
611         } else {
612             picker.el.slideIn('t', options);
613         }
614     },
615
616     /**
617      * Hides the month picker, if it's visible.
618      * @param {Boolean} [animate] Indicates whether to animate this action. If the animate
619      * parameter is not specified, the behavior will use {@link #disableAnim} to determine
620      * whether to animate or not.
621      * @return {Ext.picker.Date} this
622      */
623     hideMonthPicker : function(animate){
624         var me = this,
625             picker = me.monthPicker;
626
627         if (picker) {
628             if (me.shouldAnimate(animate)) {
629                 me.runAnimation(true);
630             } else {
631                 picker.hide();
632             }
633         }
634         return me;
635     },
636
637     /**
638      * Show the month picker
639      * @param {Boolean} [animate] Indicates whether to animate this action. If the animate
640      * parameter is not specified, the behavior will use {@link #disableAnim} to determine
641      * whether to animate or not.
642      * @return {Ext.picker.Date} this
643      */
644     showMonthPicker : function(animate){
645         var me = this,
646             picker;
647         
648         if (me.rendered && !me.disabled) {
649             picker = me.createMonthPicker();
650             picker.setValue(me.getActive());
651             picker.setSize(me.getSize());
652             picker.setPosition(-1, -1);
653             if (me.shouldAnimate(animate)) {
654                 me.runAnimation(false);
655             } else {
656                 picker.show();
657             }
658         }
659         return me;
660     },
661     
662     /**
663      * Checks whether a hide/show action should animate
664      * @private
665      * @param {Boolean} [animate] A possible animation value
666      * @return {Boolean} Whether to animate the action
667      */
668     shouldAnimate: function(animate){
669         return Ext.isDefined(animate) ? animate : !this.disableAnim;
670     },
671
672     /**
673      * Create the month picker instance
674      * @private
675      * @return {Ext.picker.Month} picker
676      */
677     createMonthPicker: function(){
678         var me = this,
679             picker = me.monthPicker;
680
681         if (!picker) {
682             me.monthPicker = picker = Ext.create('Ext.picker.Month', {
683                 renderTo: me.el,
684                 floating: true,
685                 shadow: false,
686                 small: me.showToday === false,
687                 listeners: {
688                     scope: me,
689                     cancelclick: me.onCancelClick,
690                     okclick: me.onOkClick,
691                     yeardblclick: me.onOkClick,
692                     monthdblclick: me.onOkClick
693                 }
694             });
695             if (!me.disableAnim) {
696                 // hide the element if we're animating to prevent an initial flicker
697                 picker.el.setStyle('display', 'none');
698             }
699             me.on('beforehide', Ext.Function.bind(me.hideMonthPicker, me, [false]));
700         }
701         return picker;
702     },
703
704     /**
705      * Respond to an ok click on the month picker
706      * @private
707      */
708     onOkClick: function(picker, value){
709         var me = this,
710             month = value[0],
711             year = value[1],
712             date = new Date(year, month, me.getActive().getDate());
713
714         if (date.getMonth() !== month) {
715             // 'fix' the JS rolling date conversion if needed
716             date = new Date(year, month, 1).getLastDateOfMonth();
717         }
718         me.update(date);
719         me.hideMonthPicker();
720     },
721
722     /**
723      * Respond to a cancel click on the month picker
724      * @private
725      */
726     onCancelClick: function(){
727         this.hideMonthPicker();
728     },
729
730     /**
731      * Show the previous month.
732      * @param {Object} e
733      * @return {Ext.picker.Date} this
734      */
735     showPrevMonth : function(e){
736         return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, -1));
737     },
738
739     /**
740      * Show the next month.
741      * @param {Object} e
742      * @return {Ext.picker.Date} this
743      */
744     showNextMonth : function(e){
745         return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, 1));
746     },
747
748     /**
749      * Show the previous year.
750      * @return {Ext.picker.Date} this
751      */
752     showPrevYear : function(){
753         this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, -1));
754     },
755
756     /**
757      * Show the next year.
758      * @return {Ext.picker.Date} this
759      */
760     showNextYear : function(){
761         this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, 1));
762     },
763
764     /**
765      * Respond to the mouse wheel event
766      * @private
767      * @param {Ext.EventObject} e
768      */
769     handleMouseWheel : function(e){
770         e.stopEvent();
771         if(!this.disabled){
772             var delta = e.getWheelDelta();
773             if(delta > 0){
774                 this.showPrevMonth();
775             } else if(delta < 0){
776                 this.showNextMonth();
777             }
778         }
779     },
780
781     /**
782      * Respond to a date being clicked in the picker
783      * @private
784      * @param {Ext.EventObject} e
785      * @param {HTMLElement} t
786      */
787     handleDateClick : function(e, t){
788         var me = this,
789             handler = me.handler;
790
791         e.stopEvent();
792         if(!me.disabled && t.dateValue && !Ext.fly(t.parentNode).hasCls(me.disabledCellCls)){
793             me.cancelFocus = me.focusOnSelect === false;
794             me.setValue(new Date(t.dateValue));
795             delete me.cancelFocus;
796             me.fireEvent('select', me, me.value);
797             if (handler) {
798                 handler.call(me.scope || me, me, me.value);
799             }
800             // event handling is turned off on hide
801             // when we are using the picker in a field
802             // therefore onSelect comes AFTER the select
803             // event.
804             me.onSelect();
805         }
806     },
807
808     /**
809      * Perform any post-select actions
810      * @private
811      */
812     onSelect: function() {
813         if (this.hideOnSelect) {
814              this.hide();
815          }
816     },
817
818     /**
819      * Sets the current value to today.
820      * @return {Ext.picker.Date} this
821      */
822     selectToday : function(){
823         var me = this,
824             btn = me.todayBtn,
825             handler = me.handler;
826
827         if(btn && !btn.disabled){
828             me.setValue(Ext.Date.clearTime(new Date()));
829             me.fireEvent('select', me, me.value);
830             if (handler) {
831                 handler.call(me.scope || me, me, me.value);
832             }
833             me.onSelect();
834         }
835         return me;
836     },
837
838     /**
839      * Update the selected cell
840      * @private
841      * @param {Date} date The new date
842      * @param {Date} active The active date
843      */
844     selectedUpdate: function(date, active){
845         var me = this,
846             t = date.getTime(),
847             cells = me.cells,
848             cls = me.selectedCls;
849
850         cells.removeCls(cls);
851         cells.each(function(c){
852             if (c.dom.firstChild.dateValue == t) {
853                 me.el.dom.setAttribute('aria-activedescendent', c.dom.id);
854                 c.addCls(cls);
855                 if(me.isVisible() && !me.cancelFocus){
856                     Ext.fly(c.dom.firstChild).focus(50);
857                 }
858                 return false;
859             }
860         }, this);
861     },
862
863     /**
864      * Update the contents of the picker for a new month
865      * @private
866      * @param {Date} date The new date
867      * @param {Date} active The active date
868      */
869     fullUpdate: function(date, active){
870         var me = this,
871             cells = me.cells.elements,
872             textNodes = me.textNodes,
873             disabledCls = me.disabledCellCls,
874             eDate = Ext.Date,
875             i = 0,
876             extraDays = 0,
877             visible = me.isVisible(),
878             sel = +eDate.clearTime(date, true),
879             today = +eDate.clearTime(new Date()),
880             min = me.minDate ? eDate.clearTime(me.minDate, true) : Number.NEGATIVE_INFINITY,
881             max = me.maxDate ? eDate.clearTime(me.maxDate, true) : Number.POSITIVE_INFINITY,
882             ddMatch = me.disabledDatesRE,
883             ddText = me.disabledDatesText,
884             ddays = me.disabledDays ? me.disabledDays.join('') : false,
885             ddaysText = me.disabledDaysText,
886             format = me.format,
887             days = eDate.getDaysInMonth(date),
888             firstOfMonth = eDate.getFirstDateOfMonth(date),
889             startingPos = firstOfMonth.getDay() - me.startDay,
890             previousMonth = eDate.add(date, eDate.MONTH, -1),
891             longDayFormat = me.longDayFormat,
892             prevStart,
893             current,
894             disableToday,
895             tempDate,
896             setCellClass,
897             html,
898             cls,
899             formatValue,
900             value;
901
902         if (startingPos < 0) {
903             startingPos += 7;
904         }
905
906         days += startingPos;
907         prevStart = eDate.getDaysInMonth(previousMonth) - startingPos;
908         current = new Date(previousMonth.getFullYear(), previousMonth.getMonth(), prevStart, me.initHour);
909
910         if (me.showToday) {
911             tempDate = eDate.clearTime(new Date());
912             disableToday = (tempDate < min || tempDate > max ||
913                 (ddMatch && format && ddMatch.test(eDate.dateFormat(tempDate, format))) ||
914                 (ddays && ddays.indexOf(tempDate.getDay()) != -1));
915
916             if (!me.disabled) {
917                 me.todayBtn.setDisabled(disableToday);
918                 me.todayKeyListener.setDisabled(disableToday);
919             }
920         }
921
922         setCellClass = function(cell){
923             value = +eDate.clearTime(current, true);
924             cell.title = eDate.format(current, longDayFormat);
925             // store dateValue number as an expando
926             cell.firstChild.dateValue = value;
927             if(value == today){
928                 cell.className += ' ' + me.todayCls;
929                 cell.title = me.todayText;
930             }
931             if(value == sel){
932                 cell.className += ' ' + me.selectedCls;
933                 me.el.dom.setAttribute('aria-activedescendant', cell.id);
934                 if (visible && me.floating) {
935                     Ext.fly(cell.firstChild).focus(50);
936                 }
937             }
938             // disabling
939             if(value < min) {
940                 cell.className = disabledCls;
941                 cell.title = me.minText;
942                 return;
943             }
944             if(value > max) {
945                 cell.className = disabledCls;
946                 cell.title = me.maxText;
947                 return;
948             }
949             if(ddays){
950                 if(ddays.indexOf(current.getDay()) != -1){
951                     cell.title = ddaysText;
952                     cell.className = disabledCls;
953                 }
954             }
955             if(ddMatch && format){
956                 formatValue = eDate.dateFormat(current, format);
957                 if(ddMatch.test(formatValue)){
958                     cell.title = ddText.replace('%0', formatValue);
959                     cell.className = disabledCls;
960                 }
961             }
962         };
963
964         for(; i < me.numDays; ++i) {
965             if (i < startingPos) {
966                 html = (++prevStart);
967                 cls = me.prevCls;
968             } else if (i >= days) {
969                 html = (++extraDays);
970                 cls = me.nextCls;
971             } else {
972                 html = i - startingPos + 1;
973                 cls = me.activeCls;
974             }
975             textNodes[i].innerHTML = html;
976             cells[i].className = cls;
977             current.setDate(current.getDate() + 1);
978             setCellClass(cells[i]);
979         }
980
981         me.monthBtn.setText(me.monthNames[date.getMonth()] + ' ' + date.getFullYear());
982     },
983
984     /**
985      * Update the contents of the picker
986      * @private
987      * @param {Date} date The new date
988      * @param {Boolean} forceRefresh True to force a full refresh
989      */
990     update : function(date, forceRefresh){
991         var me = this,
992             active = me.activeDate;
993
994         if (me.rendered) {
995             me.activeDate = date;
996             if(!forceRefresh && active && me.el && active.getMonth() == date.getMonth() && active.getFullYear() == date.getFullYear()){
997                 me.selectedUpdate(date, active);
998             } else {
999                 me.fullUpdate(date, active);
1000             }
1001         }
1002         return me;
1003     },
1004
1005     // private, inherit docs
1006     beforeDestroy : function() {
1007         var me = this;
1008
1009         if (me.rendered) {
1010             Ext.destroy(
1011                 me.todayKeyListener,
1012                 me.keyNav,
1013                 me.monthPicker,
1014                 me.monthBtn,
1015                 me.nextRepeater,
1016                 me.prevRepeater,
1017                 me.todayBtn
1018             );
1019             delete me.textNodes;
1020             delete me.cells.elements;
1021         }
1022         me.callParent();
1023     },
1024
1025     // private, inherit docs
1026     onShow: function() {
1027         this.callParent(arguments);
1028         if (this.focusOnShow) {
1029             this.focus();
1030         }
1031     }
1032 },
1033
1034 // After dependencies have loaded:
1035 function() {
1036     var proto = this.prototype;
1037
1038     proto.monthNames = Ext.Date.monthNames;
1039
1040     proto.dayNames = Ext.Date.dayNames;
1041
1042     proto.format = Ext.Date.defaultFormat;
1043 });
1044