3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.picker.Date
17 * @extends Ext.Component
18 * <p>A date picker. This class is used by the {@link Ext.form.field.Date} field to allow browsing and
19 * selection of valid dates in a popup next to the field, but may also be used with other components.</p>
20 * <p>Typically you will need to implement a handler function to be notified when the user chooses a color from the
21 * picker; you can register the handler using the {@link #select} event, or by implementing the {@link #handler}
23 * <p>By default the user will be allowed to pick any date; this can be changed by using the {@link #minDate},
24 * {@link #maxDate}, {@link #disabledDays}, {@link #disabledDatesRE}, and/or {@link #disabledDates} configs.</p>
25 * <p>All the string values documented below may be overridden by including an Ext locale file in your page.</p>
26 * <p>Example usage:</p>
27 * <pre><code>new Ext.panel.Panel({
28 title: 'Choose a future date:',
31 renderTo: Ext.getBody(),
35 handler: function(picker, date) {
36 // do something with the selected date
40 * {@img Ext.picker.Date/Ext.picker.Date.png Ext.picker.Date component}
43 Ext.define('Ext.picker.Date', {
44 extend: 'Ext.Component',
49 'Ext.util.ClickRepeater',
55 alias: 'widget.datepicker',
56 alternateClassName: 'Ext.DatePicker',
59 '<div class="{cls}" id="{id}" role="grid" title="{ariaTitle} {value:this.longDay}">',
60 '<div role="presentation" class="{baseCls}-header">',
61 '<div class="{baseCls}-prev"><a href="#" role="button" title="{prevText}"></a></div>',
62 '<div class="{baseCls}-month"></div>',
63 '<div class="{baseCls}-next"><a href="#" role="button" title="{nextText}"></a></div>',
65 '<table class="{baseCls}-inner" cellspacing="0" role="presentation">',
66 '<thead role="presentation"><tr role="presentation">',
67 '<tpl for="dayNames">',
68 '<th role="columnheader" title="{.}"><span>{.:this.firstInitial}</span></th>',
71 '<tbody role="presentation"><tr role="presentation">',
73 '{#:this.isEndOfWeek}',
74 '<td role="gridcell" id="{[Ext.id()]}">',
75 '<a role="presentation" href="#" hidefocus="on" class="{parent.baseCls}-date" tabIndex="1">',
76 '<em role="presentation"><span role="presentation"></span></em>',
82 '<tpl if="showToday">',
83 '<div role="presentation" class="{baseCls}-footer"></div>',
87 firstInitial: function(value) {
88 return value.substr(0,1);
90 isEndOfWeek: function(value) {
91 // convert from 1 based index to 0 based
92 // by decrementing value once.
94 var end = value % 7 === 0 && value !== 0;
95 return end ? '</tr><tr role="row">' : '';
97 longDay: function(value){
98 return Ext.Date.format(value, this.longDayFormat);
103 ariaTitle: 'Date Picker',
105 * @cfg {String} todayText
106 * The text to display on the button that selects the current date (defaults to <code>'Today'</code>)
110 * @cfg {Function} handler
111 * Optional. A function that will handle the select event of this picker.
112 * The handler is passed the following parameters:<div class="mdetail-params"><ul>
113 * <li><code>picker</code> : Ext.picker.Date <div class="sub-desc">This Date picker.</div></li>
114 * <li><code>date</code> : Date <div class="sub-desc">The selected date.</div></li>
118 * @cfg {Object} scope
119 * The scope (<code><b>this</b></code> reference) in which the <code>{@link #handler}</code>
120 * function will be called. Defaults to this DatePicker instance.
123 * @cfg {String} todayTip
124 * A string used to format the message for displaying in a tooltip over the button that
125 * selects the current date. Defaults to <code>'{0} (Spacebar)'</code> where
126 * the <code>{0}</code> token is replaced by today's date.
128 todayTip : '{0} (Spacebar)',
130 * @cfg {String} minText
131 * The error text to display if the minDate validation fails (defaults to <code>'This date is before the minimum date'</code>)
133 minText : 'This date is before the minimum date',
135 * @cfg {String} maxText
136 * The error text to display if the maxDate validation fails (defaults to <code>'This date is after the maximum date'</code>)
138 maxText : 'This date is after the maximum date',
140 * @cfg {String} format
141 * The default date format string which can be overriden for localization support. The format must be
142 * valid according to {@link Ext.Date#parse} (defaults to {@link Ext.Date#defaultFormat}).
145 * @cfg {String} disabledDaysText
146 * The tooltip to display when the date falls on a disabled day (defaults to <code>'Disabled'</code>)
148 disabledDaysText : 'Disabled',
150 * @cfg {String} disabledDatesText
151 * The tooltip text to display when the date falls on a disabled date (defaults to <code>'Disabled'</code>)
153 disabledDatesText : 'Disabled',
155 * @cfg {Array} monthNames
156 * An array of textual month names which can be overriden for localization support (defaults to Ext.Date.monthNames)
159 * @cfg {Array} dayNames
160 * An array of textual day names which can be overriden for localization support (defaults to Ext.Date.dayNames)
163 * @cfg {String} nextText
164 * The next month navigation button tooltip (defaults to <code>'Next Month (Control+Right)'</code>)
166 nextText : 'Next Month (Control+Right)',
168 * @cfg {String} prevText
169 * The previous month navigation button tooltip (defaults to <code>'Previous Month (Control+Left)'</code>)
171 prevText : 'Previous Month (Control+Left)',
173 * @cfg {String} monthYearText
174 * The header month selector tooltip (defaults to <code>'Choose a month (Control+Up/Down to move years)'</code>)
176 monthYearText : 'Choose a month (Control+Up/Down to move years)',
178 * @cfg {Number} startDay
179 * Day index at which the week should begin, 0-based (defaults to 0, which is Sunday)
183 * @cfg {Boolean} showToday
184 * False to hide the footer area containing the Today button and disable the keyboard handler for spacebar
185 * that selects the current date (defaults to <code>true</code>).
189 * @cfg {Date} minDate
190 * Minimum allowable date (JavaScript date object, defaults to null)
193 * @cfg {Date} maxDate
194 * Maximum allowable date (JavaScript date object, defaults to null)
197 * @cfg {Array} disabledDays
198 * An array of days to disable, 0-based. For example, [0, 6] disables Sunday and Saturday (defaults to null).
201 * @cfg {RegExp} disabledDatesRE
202 * JavaScript regular expression used to disable a pattern of dates (defaults to null). The {@link #disabledDates}
203 * config will generate this regex internally, but if you specify disabledDatesRE it will take precedence over the
204 * disabledDates value.
207 * @cfg {Array} disabledDates
208 * An array of 'dates' to disable, as strings. These strings will be used to build a dynamic regular
209 * expression so they are very powerful. Some examples:
211 * <li>['03/08/2003', '09/16/2003'] would disable those exact dates</li>
212 * <li>['03/08', '09/16'] would disable those days for every year</li>
213 * <li>['^03/08'] would only match the beginning (useful if you are using short years)</li>
214 * <li>['03/../2006'] would disable every day in March 2006</li>
215 * <li>['^03'] would disable every day in every March</li>
217 * Note that the format of the dates included in the array should exactly match the {@link #format} config.
218 * In order to support regular expressions, if you are using a date format that has '.' in it, you will have to
219 * escape the dot when restricting dates. For example: ['03\\.08\\.03'].
223 * @cfg {Boolean} disableAnim True to disable animations when showing the month picker. Defaults to <tt>false</tt>.
228 * @cfg {String} baseCls
229 * The base CSS class to apply to this components element (defaults to <tt>'x-datepicker'</tt>).
231 baseCls: Ext.baseCSSPrefix + 'datepicker',
234 * @cfg {String} selectedCls
235 * The class to apply to the selected cell. Defaults to <tt>'x-datepicker-selected'</tt>
239 * @cfg {String} disabledCellCls
240 * The class to apply to disabled cells. Defaults to <tt>'x-datepicker-disabled'</tt>
244 * @cfg {String} longDayFormat
245 * The format for displaying a date in a longer format. Defaults to <tt>'F d, Y'</tt>
247 longDayFormat: 'F d, Y',
250 * @cfg {Object} keyNavConfig Specifies optional custom key event handlers for the {@link Ext.util.KeyNav}
251 * attached to this date picker. Must conform to the config format recognized by the {@link Ext.util.KeyNav}
252 * constructor. Handlers specified in this object will replace default handlers of the same name.
256 * @cfg {Boolean} focusOnShow
257 * True to automatically focus the picker on show. Defaults to <tt>false</tt>.
262 // Set by other components to stop the picker focus being updated when the value changes.
267 // default value used to initialise each date in the DatePicker
268 // (note: 12 noon was chosen because it steers well clear of all DST timezone changes)
269 initHour: 12, // 24-hour format
273 // private, inherit docs
274 initComponent : function() {
276 clearTime = Ext.Date.clearTime;
278 me.selectedCls = me.baseCls + '-selected';
279 me.disabledCellCls = me.baseCls + '-disabled';
280 me.prevCls = me.baseCls + '-prevday';
281 me.activeCls = me.baseCls + '-active';
282 me.nextCls = me.baseCls + '-prevday';
283 me.todayCls = me.baseCls + '-today';
284 me.dayNames = me.dayNames.slice(me.startDay).concat(me.dayNames.slice(0, me.startDay));
287 me.value = me.value ?
288 clearTime(me.value, true) : clearTime(new Date());
293 * Fires when a date is selected
294 * @param {DatePicker} this DatePicker
295 * @param {Date} date The selected date
300 me.initDisabledDays();
303 // private, inherit docs
304 onRender : function(container, position){
306 * days array for looping through 6 full weeks (6 weeks * 7 days)
307 * Note that we explicitly force the size here so the template creates
308 * all the appropriate cells.
312 days = new Array(me.numDays),
313 today = Ext.Date.format(new Date(), me.format);
320 Ext.apply(me.renderData, {
321 dayNames: me.dayNames,
322 ariaTitle: me.ariaTitle,
324 showToday: me.showToday,
325 prevText: me.prevText,
326 nextText: me.nextText,
329 me.getTpl('renderTpl').longDayFormat = me.longDayFormat;
331 Ext.apply(me.renderSelectors, {
332 eventEl: 'table.' + me.baseCls + '-inner',
333 prevEl: '.' + me.baseCls + '-prev a',
334 nextEl: '.' + me.baseCls + '-next a',
335 middleBtnEl: '.' + me.baseCls + '-month',
336 footerEl: '.' + me.baseCls + '-footer'
339 this.callParent(arguments);
340 me.el.unselectable();
342 me.cells = me.eventEl.select('tbody td');
343 me.textNodes = me.eventEl.query('tbody td span');
345 me.monthBtn = Ext.create('Ext.button.Split', {
347 tooltip: me.monthYearText,
348 renderTo: me.middleBtnEl
350 //~ me.middleBtnEl.down('button').addCls(Ext.baseCSSPrefix + 'btn-arrow');
353 me.todayBtn = Ext.create('Ext.button.Button', {
354 renderTo: me.footerEl,
355 text: Ext.String.format(me.todayText, today),
356 tooltip: Ext.String.format(me.todayTip, today),
357 handler: me.selectToday,
362 // private, inherit docs
363 initEvents: function(){
370 me.prevRepeater = Ext.create('Ext.util.ClickRepeater', me.prevEl, {
371 handler: me.showPrevMonth,
373 preventDefault: true,
377 me.nextRepeater = Ext.create('Ext.util.ClickRepeater', me.nextEl, {
378 handler: me.showNextMonth,
384 me.keyNav = Ext.create('Ext.util.KeyNav', me.eventEl, Ext.apply({
386 'left' : function(e){
390 me.update(eDate.add(me.activeDate, day, -1));
394 'right' : function(e){
398 me.update(eDate.add(me.activeDate, day, 1));
406 me.update(eDate.add(me.activeDate, day, -7));
410 'down' : function(e){
414 me.update(eDate.add(me.activeDate, day, 7));
417 'pageUp' : me.showNextMonth,
418 'pageDown' : me.showPrevMonth,
419 'enter' : function(e){
423 }, me.keyNavConfig));
426 me.todayKeyListener = me.eventEl.addKeyListener(Ext.EventObject.SPACE, me.selectToday, me);
428 me.mon(me.eventEl, 'mousewheel', me.handleMouseWheel, me);
429 me.mon(me.eventEl, 'click', me.handleDateClick, me, {delegate: 'a.' + me.baseCls + '-date'});
430 me.mon(me.monthBtn, 'click', me.showMonthPicker, me);
431 me.mon(me.monthBtn, 'arrowclick', me.showMonthPicker, me);
436 * Setup the disabled dates regex based on config options
439 initDisabledDays : function(){
441 dd = me.disabledDates,
445 if(!me.disabledDatesRE && dd){
448 Ext.each(dd, function(d, i){
449 re += Ext.isDate(d) ? '^' + Ext.String.escapeRegex(Ext.Date.dateFormat(d, me.format)) + '$' : dd[i];
454 me.disabledDatesRE = new RegExp(re + ')');
459 * Replaces any existing disabled dates with new values and refreshes the DatePicker.
460 * @param {Array/RegExp} disabledDates An array of date strings (see the {@link #disabledDates} config
461 * for details on supported values), or a JavaScript regular expression used to disable a pattern of dates.
462 * @return {Ext.picker.Date} this
464 setDisabledDates : function(dd){
468 me.disabledDates = dd;
469 me.disabledDatesRE = null;
471 me.disabledDatesRE = dd;
473 me.initDisabledDays();
474 me.update(me.value, true);
479 * Replaces any existing disabled days (by index, 0-6) with new values and refreshes the DatePicker.
480 * @param {Array} disabledDays An array of disabled day indexes. See the {@link #disabledDays} config
481 * for details on supported values.
482 * @return {Ext.picker.Date} this
484 setDisabledDays : function(dd){
485 this.disabledDays = dd;
486 return this.update(this.value, true);
490 * Replaces any existing {@link #minDate} with the new value and refreshes the DatePicker.
491 * @param {Date} value The minimum date that can be selected
492 * @return {Ext.picker.Date} this
494 setMinDate : function(dt){
496 return this.update(this.value, true);
500 * Replaces any existing {@link #maxDate} with the new value and refreshes the DatePicker.
501 * @param {Date} value The maximum date that can be selected
502 * @return {Ext.picker.Date} this
504 setMaxDate : function(dt){
506 return this.update(this.value, true);
510 * Sets the value of the date field
511 * @param {Date} value The date to set
512 * @return {Ext.picker.Date} this
514 setValue : function(value){
515 this.value = Ext.Date.clearTime(value, true);
516 return this.update(this.value);
520 * Gets the current selected value of the date field
521 * @return {Date} The selected date
523 getValue : function(){
529 this.update(this.activeDate);
532 // private, inherit docs
533 onEnable: function(){
535 this.setDisabledStatus(false);
536 this.update(this.activeDate);
540 // private, inherit docs
541 onDisable : function(){
543 this.setDisabledStatus(true);
547 * Set the disabled state of various internal components
549 * @param {Boolean} disabled
551 setDisabledStatus : function(disabled){
554 me.keyNav.setDisabled(disabled);
555 me.prevRepeater.setDisabled(disabled);
556 me.nextRepeater.setDisabled(disabled);
558 me.todayKeyListener.setDisabled(disabled);
559 me.todayBtn.setDisabled(disabled);
564 * Get the current active date.
566 * @return {Date} The active date
568 getActive: function(){
569 return this.activeDate || me.value;
573 * Run any animation required to hide/show the month picker.
575 * @param {Boolean} isHide True if it's a hide operation
577 runAnimation: function(isHide){
579 target: this.monthPicker,
583 Ext.fx.Manager.run();
589 Ext.create('Ext.fx.Anim', options);
593 * Hides the month picker, if it's visible.
594 * @return {Ext.picker.Date} this
596 hideMonthPicker : function(){
598 picker = me.monthPicker;
601 if (me.disableAnim) {
604 this.runAnimation(true);
611 * Show the month picker
612 * @return {Ext.picker.Date} this
614 showMonthPicker : function(){
623 if (me.rendered && !me.disabled) {
625 picker = me.createMonthPicker();
627 picker.setSize(size);
628 picker.setValue(me.getActive());
630 if (me.disableAnim) {
631 picker.setPosition(-1, -1);
633 me.runAnimation(false);
640 * Create the month picker instance
642 * @return {Ext.picker.Month} picker
644 createMonthPicker: function(){
646 picker = me.monthPicker;
649 me.monthPicker = picker = Ext.create('Ext.picker.Month', {
653 small: me.showToday === false,
656 cancelclick: me.onCancelClick,
657 okclick: me.onOkClick,
658 yeardblclick: me.onOkClick,
659 monthdblclick: me.onOkClick
663 me.on('beforehide', me.hideMonthPicker, me);
669 * Respond to an ok click on the month picker
672 onOkClick: function(picker, value){
676 date = new Date(year, month, me.getActive().getDate());
678 if (date.getMonth() !== month) {
679 // 'fix' the JS rolling date conversion if needed
680 date = new Date(year, month, 1).getLastDateOfMonth();
683 me.hideMonthPicker();
687 * Respond to a cancel click on the month picker
690 onCancelClick: function(){
691 this.hideMonthPicker();
695 * Show the previous month.
696 * @return {Ext.picker.Date} this
698 showPrevMonth : function(e){
699 return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, -1));
703 * Show the next month.
704 * @return {Ext.picker.Date} this
706 showNextMonth : function(e){
707 return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, 1));
711 * Show the previous year.
712 * @return {Ext.picker.Date} this
714 showPrevYear : function(){
715 this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, -1));
719 * Show the next year.
720 * @return {Ext.picker.Date} this
722 showNextYear : function(){
723 this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, 1));
727 * Respond to the mouse wheel event
729 * @param {Ext.EventObject} e
731 handleMouseWheel : function(e){
734 var delta = e.getWheelDelta();
736 this.showPrevMonth();
737 } else if(delta < 0){
738 this.showNextMonth();
744 * Respond to a date being clicked in the picker
746 * @param {Ext.EventObject} e
747 * @param {HTMLElement} t
749 handleDateClick : function(e, t){
751 handler = me.handler;
754 if(!me.disabled && t.dateValue && !Ext.fly(t.parentNode).hasCls(me.disabledCellCls)){
755 me.cancelFocus = me.focusOnSelect === false;
756 me.setValue(new Date(t.dateValue));
757 delete me.cancelFocus;
758 me.fireEvent('select', me, me.value);
760 handler.call(me.scope || me, me, me.value);
762 // event handling is turned off on hide
763 // when we are using the picker in a field
764 // therefore onSelect comes AFTER the select
771 * Perform any post-select actions
774 onSelect: function() {
775 if (this.hideOnSelect) {
781 * Sets the current value to today.
782 * @return {Ext.picker.Date} this
784 selectToday : function(){
787 handler = me.handler;
789 if(btn && !btn.disabled){
790 me.setValue(Ext.Date.clearTime(new Date()));
791 me.fireEvent('select', me, me.value);
793 handler.call(me.scope || me, me, me.value);
801 * Update the selected cell
803 * @param {Date} date The new date
804 * @param {Date} active The active date
806 selectedUpdate: function(date, active){
810 cls = me.selectedCls;
812 cells.removeCls(cls);
813 cells.each(function(c){
814 if (c.dom.firstChild.dateValue == t) {
815 me.el.dom.setAttribute('aria-activedescendent', c.dom.id);
817 if(me.isVisible() && !me.cancelFocus){
818 Ext.fly(c.dom.firstChild).focus(50);
826 * Update the contents of the picker for a new month
828 * @param {Date} date The new date
829 * @param {Date} active The active date
831 fullUpdate: function(date, active){
833 cells = me.cells.elements,
834 textNodes = me.textNodes,
835 disabledCls = me.disabledCellCls,
839 visible = me.isVisible(),
840 sel = +eDate.clearTime(date, true),
841 today = +eDate.clearTime(new Date()),
842 min = me.minDate ? eDate.clearTime(me.minDate, true) : Number.NEGATIVE_INFINITY,
843 max = me.maxDate ? eDate.clearTime(me.maxDate, true) : Number.POSITIVE_INFINITY,
844 ddMatch = me.disabledDatesRE,
845 ddText = me.disabledDatesText,
846 ddays = me.disabledDays ? me.disabledDays.join('') : false,
847 ddaysText = me.disabledDaysText,
849 days = eDate.getDaysInMonth(date),
850 firstOfMonth = eDate.getFirstDateOfMonth(date),
851 startingPos = firstOfMonth.getDay() - me.startDay,
852 previousMonth = eDate.add(date, eDate.MONTH, -1),
853 longDayFormat = me.longDayFormat,
864 if (startingPos < 0) {
869 prevStart = eDate.getDaysInMonth(previousMonth) - startingPos;
870 current = new Date(previousMonth.getFullYear(), previousMonth.getMonth(), prevStart, me.initHour);
873 tempDate = eDate.clearTime(new Date());
874 disableToday = (tempDate < min || tempDate > max ||
875 (ddMatch && format && ddMatch.test(eDate.dateFormat(tempDate, format))) ||
876 (ddays && ddays.indexOf(tempDate.getDay()) != -1));
879 me.todayBtn.setDisabled(disableToday);
880 me.todayKeyListener.setDisabled(disableToday);
884 setCellClass = function(cell){
885 value = +eDate.clearTime(current, true);
886 cell.title = eDate.format(current, longDayFormat);
887 // store dateValue number as an expando
888 cell.firstChild.dateValue = value;
890 cell.className += ' ' + me.todayCls;
891 cell.title = me.todayText;
894 cell.className += ' ' + me.selectedCls;
895 me.el.dom.setAttribute('aria-activedescendant', cell.id);
896 if (visible && me.floating) {
897 Ext.fly(cell.firstChild).focus(50);
902 cell.className = disabledCls;
903 cell.title = me.minText;
907 cell.className = disabledCls;
908 cell.title = me.maxText;
912 if(ddays.indexOf(current.getDay()) != -1){
913 cell.title = ddaysText;
914 cell.className = disabledCls;
917 if(ddMatch && format){
918 formatValue = eDate.dateFormat(current, format);
919 if(ddMatch.test(formatValue)){
920 cell.title = ddText.replace('%0', formatValue);
921 cell.className = disabledCls;
926 for(; i < me.numDays; ++i) {
927 if (i < startingPos) {
928 html = (++prevStart);
930 } else if (i >= days) {
931 html = (++extraDays);
934 html = i - startingPos + 1;
937 textNodes[i].innerHTML = html;
938 cells[i].className = cls;
939 current.setDate(current.getDate() + 1);
940 setCellClass(cells[i]);
943 me.monthBtn.setText(me.monthNames[date.getMonth()] + ' ' + date.getFullYear());
947 * Update the contents of the picker
949 * @param {Date} date The new date
950 * @param {Boolean} forceRefresh True to force a full refresh
952 update : function(date, forceRefresh){
954 active = me.activeDate;
957 me.activeDate = date;
958 if(!forceRefresh && active && me.el && active.getMonth() == date.getMonth() && active.getFullYear() == date.getFullYear()){
959 me.selectedUpdate(date, active);
961 me.fullUpdate(date, active);
967 // private, inherit docs
968 beforeDestroy : function() {
982 delete me.cells.elements;
986 // private, inherit docs
988 this.callParent(arguments);
989 if (this.focusOnShow) {
995 // After dependencies have loaded:
997 var proto = this.prototype;
999 proto.monthNames = Ext.Date.monthNames;
1001 proto.dayNames = Ext.Date.dayNames;
1003 proto.format = Ext.Date.defaultFormat;