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 * 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.
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.
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.
25 * All the string values documented below may be overridden by including an Ext locale file in your page.
28 * Ext.create('Ext.panel.Panel', {
29 * title: 'Choose a future date:',
32 * renderTo: Ext.getBody(),
34 * xtype: 'datepicker',
35 * minDate: new Date(),
36 * handler: function(picker, date) {
37 * // do something with the selected date
42 Ext.define('Ext.picker.Date', {
43 extend: 'Ext.Component',
48 'Ext.util.ClickRepeater',
54 alias: 'widget.datepicker',
55 alternateClassName: 'Ext.DatePicker',
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>',
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>',
70 '<tbody role="presentation"><tr role="presentation">',
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>',
81 '<tpl if="showToday">',
82 '<div id="{id}-footerEl" role="presentation" class="{baseCls}-footer"></div>',
86 firstInitial: function(value) {
87 return value.substr(0,1);
89 isEndOfWeek: function(value) {
90 // convert from 1 based index to 0 based
91 // by decrementing value once.
93 var end = value % 7 === 0 && value !== 0;
94 return end ? '</tr><tr role="row">' : '';
96 longDay: function(value){
97 return Ext.Date.format(value, this.longDayFormat);
102 ariaTitle: 'Date Picker',
105 * @cfg {String} todayText
106 * The text to display on the button that selects the current date
111 * @cfg {Function} handler
112 * Optional. A function that will handle the select event of this picker. The handler is passed the following
115 * - `picker` : Ext.picker.Date
125 * @cfg {Object} scope
126 * The scope (`this` reference) in which the `{@link #handler}` function will be called. Defaults to this
127 * DatePicker instance.
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.
135 todayTip : '{0} (Spacebar)',
138 * @cfg {String} minText
139 * The error text to display if the minDate validation fails.
141 minText : 'This date is before the minimum date',
144 * @cfg {String} maxText
145 * The error text to display if the maxDate validation fails.
147 maxText : 'This date is after the maximum date',
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}).
156 * @cfg {String} disabledDaysText
157 * The tooltip to display when the date falls on a disabled day.
159 disabledDaysText : 'Disabled',
162 * @cfg {String} disabledDatesText
163 * The tooltip text to display when the date falls on a disabled date.
165 disabledDatesText : 'Disabled',
168 * @cfg {String[]} monthNames
169 * An array of textual month names which can be overriden for localization support (defaults to Ext.Date.monthNames)
173 * @cfg {String[]} dayNames
174 * An array of textual day names which can be overriden for localization support (defaults to Ext.Date.dayNames)
178 * @cfg {String} nextText
179 * The next month navigation button tooltip
181 nextText : 'Next Month (Control+Right)',
184 * @cfg {String} prevText
185 * The previous month navigation button tooltip
187 prevText : 'Previous Month (Control+Left)',
190 * @cfg {String} monthYearText
191 * The header month selector tooltip
193 monthYearText : 'Choose a month (Control+Up/Down to move years)',
196 * @cfg {Number} startDay
197 * Day index at which the week should begin, 0-based (defaults to Sunday)
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.
209 * @cfg {Date} [minDate=null]
210 * Minimum allowable date (JavaScript date object)
214 * @cfg {Date} [maxDate=null]
215 * Maximum allowable date (JavaScript date object)
219 * @cfg {Number[]} [disabledDays=null]
220 * An array of days to disable, 0-based. For example, [0, 6] disables Sunday and Saturday.
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.
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:
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
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'].
247 * @cfg {Boolean} disableAnim
248 * True to disable animations when showing the month picker.
253 * @cfg {String} [baseCls='x-datepicker']
254 * The base CSS class to apply to this components element.
256 baseCls: Ext.baseCSSPrefix + 'datepicker',
259 * @cfg {String} [selectedCls='x-datepicker-selected']
260 * The class to apply to the selected cell.
264 * @cfg {String} [disabledCellCls='x-datepicker-disabled']
265 * The class to apply to disabled cells.
269 * @cfg {String} longDayFormat
270 * The format for displaying a date in a longer format.
272 longDayFormat: 'F d, Y',
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.
282 * @cfg {Boolean} focusOnShow
283 * True to automatically focus the picker on show.
288 // Set by other components to stop the picker focus being updated when the value changes.
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
299 // private, inherit docs
300 initComponent : function() {
302 clearTime = Ext.Date.clearTime;
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));
313 me.value = me.value ?
314 clearTime(me.value, true) : clearTime(new Date());
319 * Fires when a date is selected
320 * @param {Ext.picker.Date} this DatePicker
321 * @param {Date} date The selected date
326 me.initDisabledDays();
329 // private, inherit docs
330 onRender : function(container, position){
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.
338 days = new Array(me.numDays),
339 today = Ext.Date.format(new Date(), me.format);
345 Ext.apply(me.renderData, {
346 dayNames: me.dayNames,
347 ariaTitle: me.ariaTitle,
349 showToday: me.showToday,
350 prevText: me.prevText,
351 nextText: me.nextText,
354 me.getTpl('renderTpl').longDayFormat = me.longDayFormat;
356 me.addChildEls('eventEl', 'prevEl', 'nextEl', 'middleBtnEl', 'footerEl');
358 this.callParent(arguments);
359 me.el.unselectable();
361 me.cells = me.eventEl.select('tbody td');
362 me.textNodes = me.eventEl.query('tbody td span');
364 me.monthBtn = Ext.create('Ext.button.Split', {
366 tooltip: me.monthYearText,
367 renderTo: me.middleBtnEl
369 //~ me.middleBtnEl.down('button').addCls(Ext.baseCSSPrefix + 'btn-arrow');
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,
381 // private, inherit docs
382 initEvents: function(){
389 me.prevRepeater = Ext.create('Ext.util.ClickRepeater', me.prevEl, {
390 handler: me.showPrevMonth,
392 preventDefault: true,
396 me.nextRepeater = Ext.create('Ext.util.ClickRepeater', me.nextEl, {
397 handler: me.showNextMonth,
403 me.keyNav = Ext.create('Ext.util.KeyNav', me.eventEl, Ext.apply({
405 'left' : function(e){
409 me.update(eDate.add(me.activeDate, day, -1));
413 'right' : function(e){
417 me.update(eDate.add(me.activeDate, day, 1));
425 me.update(eDate.add(me.activeDate, day, -7));
429 'down' : function(e){
433 me.update(eDate.add(me.activeDate, day, 7));
436 'pageUp' : me.showNextMonth,
437 'pageDown' : me.showPrevMonth,
438 'enter' : function(e){
442 }, me.keyNavConfig));
445 me.todayKeyListener = me.eventEl.addKeyListener(Ext.EventObject.SPACE, me.selectToday, me);
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);
455 * Setup the disabled dates regex based on config options
458 initDisabledDays : function(){
460 dd = me.disabledDates,
464 if(!me.disabledDatesRE && dd){
467 Ext.each(dd, function(d, i){
468 re += Ext.isDate(d) ? '^' + Ext.String.escapeRegex(Ext.Date.dateFormat(d, me.format)) + '$' : dd[i];
473 me.disabledDatesRE = new RegExp(re + ')');
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
483 setDisabledDates : function(dd){
487 me.disabledDates = dd;
488 me.disabledDatesRE = null;
490 me.disabledDatesRE = dd;
492 me.initDisabledDays();
493 me.update(me.value, true);
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
503 setDisabledDays : function(dd){
504 this.disabledDays = dd;
505 return this.update(this.value, true);
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
513 setMinDate : function(dt){
515 return this.update(this.value, true);
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
523 setMaxDate : function(dt){
525 return this.update(this.value, true);
529 * Sets the value of the date field
530 * @param {Date} value The date to set
531 * @return {Ext.picker.Date} this
533 setValue : function(value){
534 this.value = Ext.Date.clearTime(value, true);
535 return this.update(this.value);
539 * Gets the current selected value of the date field
540 * @return {Date} The selected date
542 getValue : function(){
548 this.update(this.activeDate);
551 // private, inherit docs
552 onEnable: function(){
554 this.setDisabledStatus(false);
555 this.update(this.activeDate);
559 // private, inherit docs
560 onDisable : function(){
562 this.setDisabledStatus(true);
566 * Set the disabled state of various internal components
568 * @param {Boolean} disabled
570 setDisabledStatus : function(disabled){
573 me.keyNav.setDisabled(disabled);
574 me.prevRepeater.setDisabled(disabled);
575 me.nextRepeater.setDisabled(disabled);
577 me.todayKeyListener.setDisabled(disabled);
578 me.todayBtn.setDisabled(disabled);
583 * Get the current active date.
585 * @return {Date} The active date
587 getActive: function(){
588 return this.activeDate || this.value;
592 * Run any animation required to hide/show the month picker.
594 * @param {Boolean} isHide True if it's a hide operation
596 runAnimation: function(isHide){
597 var picker = this.monthPicker,
600 callback: function(){
610 picker.el.slideOut('t', options);
612 picker.el.slideIn('t', options);
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
623 hideMonthPicker : function(animate){
625 picker = me.monthPicker;
628 if (me.shouldAnimate(animate)) {
629 me.runAnimation(true);
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
644 showMonthPicker : function(animate){
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);
663 * Checks whether a hide/show action should animate
665 * @param {Boolean} [animate] A possible animation value
666 * @return {Boolean} Whether to animate the action
668 shouldAnimate: function(animate){
669 return Ext.isDefined(animate) ? animate : !this.disableAnim;
673 * Create the month picker instance
675 * @return {Ext.picker.Month} picker
677 createMonthPicker: function(){
679 picker = me.monthPicker;
682 me.monthPicker = picker = Ext.create('Ext.picker.Month', {
686 small: me.showToday === false,
689 cancelclick: me.onCancelClick,
690 okclick: me.onOkClick,
691 yeardblclick: me.onOkClick,
692 monthdblclick: me.onOkClick
695 if (!me.disableAnim) {
696 // hide the element if we're animating to prevent an initial flicker
697 picker.el.setStyle('display', 'none');
699 me.on('beforehide', Ext.Function.bind(me.hideMonthPicker, me, [false]));
705 * Respond to an ok click on the month picker
708 onOkClick: function(picker, value){
712 date = new Date(year, month, me.getActive().getDate());
714 if (date.getMonth() !== month) {
715 // 'fix' the JS rolling date conversion if needed
716 date = new Date(year, month, 1).getLastDateOfMonth();
719 me.hideMonthPicker();
723 * Respond to a cancel click on the month picker
726 onCancelClick: function(){
727 this.hideMonthPicker();
731 * Show the previous month.
733 * @return {Ext.picker.Date} this
735 showPrevMonth : function(e){
736 return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, -1));
740 * Show the next month.
742 * @return {Ext.picker.Date} this
744 showNextMonth : function(e){
745 return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, 1));
749 * Show the previous year.
750 * @return {Ext.picker.Date} this
752 showPrevYear : function(){
753 this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, -1));
757 * Show the next year.
758 * @return {Ext.picker.Date} this
760 showNextYear : function(){
761 this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, 1));
765 * Respond to the mouse wheel event
767 * @param {Ext.EventObject} e
769 handleMouseWheel : function(e){
772 var delta = e.getWheelDelta();
774 this.showPrevMonth();
775 } else if(delta < 0){
776 this.showNextMonth();
782 * Respond to a date being clicked in the picker
784 * @param {Ext.EventObject} e
785 * @param {HTMLElement} t
787 handleDateClick : function(e, t){
789 handler = me.handler;
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);
798 handler.call(me.scope || me, me, me.value);
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
809 * Perform any post-select actions
812 onSelect: function() {
813 if (this.hideOnSelect) {
819 * Sets the current value to today.
820 * @return {Ext.picker.Date} this
822 selectToday : function(){
825 handler = me.handler;
827 if(btn && !btn.disabled){
828 me.setValue(Ext.Date.clearTime(new Date()));
829 me.fireEvent('select', me, me.value);
831 handler.call(me.scope || me, me, me.value);
839 * Update the selected cell
841 * @param {Date} date The new date
842 * @param {Date} active The active date
844 selectedUpdate: function(date, active){
848 cls = me.selectedCls;
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);
855 if(me.isVisible() && !me.cancelFocus){
856 Ext.fly(c.dom.firstChild).focus(50);
864 * Update the contents of the picker for a new month
866 * @param {Date} date The new date
867 * @param {Date} active The active date
869 fullUpdate: function(date, active){
871 cells = me.cells.elements,
872 textNodes = me.textNodes,
873 disabledCls = me.disabledCellCls,
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,
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,
902 if (startingPos < 0) {
907 prevStart = eDate.getDaysInMonth(previousMonth) - startingPos;
908 current = new Date(previousMonth.getFullYear(), previousMonth.getMonth(), prevStart, me.initHour);
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));
917 me.todayBtn.setDisabled(disableToday);
918 me.todayKeyListener.setDisabled(disableToday);
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;
928 cell.className += ' ' + me.todayCls;
929 cell.title = me.todayText;
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);
940 cell.className = disabledCls;
941 cell.title = me.minText;
945 cell.className = disabledCls;
946 cell.title = me.maxText;
950 if(ddays.indexOf(current.getDay()) != -1){
951 cell.title = ddaysText;
952 cell.className = disabledCls;
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;
964 for(; i < me.numDays; ++i) {
965 if (i < startingPos) {
966 html = (++prevStart);
968 } else if (i >= days) {
969 html = (++extraDays);
972 html = i - startingPos + 1;
975 textNodes[i].innerHTML = html;
976 cells[i].className = cls;
977 current.setDate(current.getDate() + 1);
978 setCellClass(cells[i]);
981 me.monthBtn.setText(me.monthNames[date.getMonth()] + ' ' + date.getFullYear());
985 * Update the contents of the picker
987 * @param {Date} date The new date
988 * @param {Boolean} forceRefresh True to force a full refresh
990 update : function(date, forceRefresh){
992 active = me.activeDate;
995 me.activeDate = date;
996 if(!forceRefresh && active && me.el && active.getMonth() == date.getMonth() && active.getFullYear() == date.getFullYear()){
997 me.selectedUpdate(date, active);
999 me.fullUpdate(date, active);
1005 // private, inherit docs
1006 beforeDestroy : function() {
1011 me.todayKeyListener,
1019 delete me.textNodes;
1020 delete me.cells.elements;
1025 // private, inherit docs
1026 onShow: function() {
1027 this.callParent(arguments);
1028 if (this.focusOnShow) {
1034 // After dependencies have loaded:
1036 var proto = this.prototype;
1038 proto.monthNames = Ext.Date.monthNames;
1040 proto.dayNames = Ext.Date.dayNames;
1042 proto.format = Ext.Date.defaultFormat;