Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / picker / Month.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  * @private
17  * @class Ext.picker.Month
18  * @extends Ext.Component
19  * <p>A month picker component. This class is used by the {@link Ext.picker.Date DatePicker} class
20  * to allow browsing and selection of year/months combinations.</p>
21  */
22 Ext.define('Ext.picker.Month', {
23     extend: 'Ext.Component',
24     requires: ['Ext.XTemplate', 'Ext.util.ClickRepeater', 'Ext.Date', 'Ext.button.Button'],
25     alias: 'widget.monthpicker',
26     alternateClassName: 'Ext.MonthPicker',
27
28     renderTpl: [
29         '<div class="{baseCls}-body">',
30           '<div class="{baseCls}-months">',
31               '<tpl for="months">',
32                   '<div class="{parent.baseCls}-item {parent.baseCls}-month"><a href="#" hidefocus="on">{.}</a></div>',
33               '</tpl>',
34           '</div>',
35           '<div class="{baseCls}-years">',
36               '<div class="{baseCls}-yearnav">',
37                   '<button class="{baseCls}-yearnav-prev"></button>',
38                   '<button class="{baseCls}-yearnav-next"></button>',
39               '</div>',
40               '<tpl for="years">',
41                   '<div class="{parent.baseCls}-item {parent.baseCls}-year"><a href="#" hidefocus="on">{.}</a></div>',
42               '</tpl>',
43           '</div>',
44         '</div>',
45         '<div class="' + Ext.baseCSSPrefix + 'clear"></div>',
46         '<tpl if="showButtons">',
47           '<div class="{baseCls}-buttons"></div>',
48         '</tpl>'
49     ],
50
51     /**
52      * @cfg {String} okText The text to display on the ok button. Defaults to <tt>'OK'</tt>
53      */
54     okText: 'OK',
55
56     /**
57      * @cfg {String} cancelText The text to display on the cancel button. Defaults to <tt>'Cancel'</tt>
58      */
59     cancelText: 'Cancel',
60
61     /**
62      * @cfg {String} baseCls The base CSS class to apply to the picker element. Defaults to <tt>'x-monthpicker'</tt>
63      */
64     baseCls: Ext.baseCSSPrefix + 'monthpicker',
65
66     /**
67      * @cfg {Boolean} showButtons True to show ok and cancel buttons below the picker. Defaults to <tt>true</tt>.
68      */
69     showButtons: true,
70
71     /**
72      * @cfg {String} selectedCls The class to be added to selected items in the picker. Defaults to
73      * <tt>'x-monthpicker-selected'</tt>
74      */
75
76     /**
77      * @cfg {Date/Array} value The default value to set. See {#setValue setValue}
78      */
79     width: 178,
80     
81     // used when attached to date picker which isnt showing buttons
82     smallCls: Ext.baseCSSPrefix + 'monthpicker-small',
83
84     // private
85     totalYears: 10,
86     yearOffset: 5, // 10 years in total, 2 per row
87     monthOffset: 6, // 12 months, 2 per row
88
89     // private, inherit docs
90     initComponent: function(){
91         var me = this;
92
93         me.selectedCls = me.baseCls + '-selected';
94         me.addEvents(
95             /**
96              * @event cancelclick
97              * Fires when the cancel button is pressed.
98              * @param {Ext.picker.Month} this
99              */
100             'cancelclick',
101
102             /**
103              * @event monthclick
104              * Fires when a month is clicked.
105              * @param {Ext.picker.Month} this
106              * @param {Array} value The current value
107              */
108             'monthclick',
109
110             /**
111              * @event monthdblclick
112              * Fires when a month is clicked.
113              * @param {Ext.picker.Month} this
114              * @param {Array} value The current value
115              */
116             'monthdblclick',
117
118             /**
119              * @event okclick
120              * Fires when the ok button is pressed.
121              * @param {Ext.picker.Month} this
122              * @param {Array} value The current value
123              */
124             'okclick',
125
126             /**
127              * @event select
128              * Fires when a month/year is selected.
129              * @param {Ext.picker.Month} this
130              * @param {Array} value The current value
131              */
132             'select',
133
134             /**
135              * @event yearclick
136              * Fires when a year is clicked.
137              * @param {Ext.picker.Month} this
138              * @param {Array} value The current value
139              */
140             'yearclick',
141
142             /**
143              * @event yeardblclick
144              * Fires when a year is clicked.
145              * @param {Ext.picker.Month} this
146              * @param {Array} value The current value
147              */
148             'yeardblclick'
149         );
150         if (me.small) {
151             me.addCls(me.smallCls);
152         }
153         me.setValue(me.value);
154         me.activeYear = me.getYear(new Date().getFullYear() - 4, -4);
155         this.callParent();
156     },
157
158     // private, inherit docs
159     onRender: function(ct, position){
160         var me = this,
161             i = 0,
162             months = [],
163             shortName = Ext.Date.getShortMonthName,
164             monthLen = me.monthOffset;
165
166         for (; i < monthLen; ++i) {
167             months.push(shortName(i), shortName(i + monthLen));
168         }
169
170         Ext.apply(me.renderData, {
171             months: months,
172             years: me.getYears(),
173             showButtons: me.showButtons
174         });
175
176         Ext.apply(me.renderSelectors, {
177             bodyEl: '.' + me.baseCls + '-body',
178             prevEl: '.' + me.baseCls + '-yearnav-prev',
179             nextEl: '.' + me.baseCls + '-yearnav-next',
180             buttonsEl: '.' + me.baseCls + '-buttons'
181         });
182         this.callParent([ct, position]);
183     },
184
185     // private, inherit docs
186     afterRender: function(){
187         var me = this,
188             body = me.bodyEl,
189             buttonsEl = me.buttonsEl;
190
191         me.callParent();
192
193         me.mon(body, 'click', me.onBodyClick, me);
194         me.mon(body, 'dblclick', me.onBodyClick, me);
195
196         // keep a reference to the year/month elements since we'll be re-using them
197         me.years = body.select('.' + me.baseCls + '-year a');
198         me.months = body.select('.' + me.baseCls + '-month a');
199
200         if (me.showButtons) {
201             me.okBtn = Ext.create('Ext.button.Button', {
202                 text: me.okText,
203                 renderTo: buttonsEl,
204                 handler: me.onOkClick,
205                 scope: me
206             });
207             me.cancelBtn = Ext.create('Ext.button.Button', {
208                 text: me.cancelText,
209                 renderTo: buttonsEl,
210                 handler: me.onCancelClick,
211                 scope: me
212             });
213         }
214
215         me.backRepeater = Ext.create('Ext.util.ClickRepeater', me.prevEl, {
216             handler: Ext.Function.bind(me.adjustYear, me, [-me.totalYears])
217         });
218
219         me.prevEl.addClsOnOver(me.baseCls + '-yearnav-prev-over');
220         me.nextRepeater = Ext.create('Ext.util.ClickRepeater', me.nextEl, {
221             handler: Ext.Function.bind(me.adjustYear, me, [me.totalYears])
222         });
223         me.nextEl.addClsOnOver(me.baseCls + '-yearnav-next-over');
224         me.updateBody();
225     },
226
227     /**
228      * Set the value for the picker.
229      * @param {Date/Array} value The value to set. It can be a Date object, where the month/year will be extracted, or
230      * it can be an array, with the month as the first index and the year as the second.
231      * @return {Ext.picker.Month} this
232      */
233     setValue: function(value){
234         var me = this,
235             active = me.activeYear,
236             offset = me.monthOffset,
237             year,
238             index;
239
240         if (!value) {
241             me.value = [null, null];
242         } else if (Ext.isDate(value)) {
243             me.value = [value.getMonth(), value.getFullYear()];
244         } else {
245             me.value = [value[0], value[1]];
246         }
247
248         if (me.rendered) {
249             year = me.value[1];
250             if (year !== null) {
251                 if ((year < active || year > active + me.yearOffset)) {
252                     me.activeYear = year - me.yearOffset + 1;
253                 }
254             }
255             me.updateBody();
256         }
257
258         return me;
259     },
260
261     /**
262      * Gets the selected value. It is returned as an array [month, year]. It may
263      * be a partial value, for example [null, 2010]. The month is returned as
264      * 0 based.
265      * @return {Array} The selected value
266      */
267     getValue: function(){
268         return this.value;
269     },
270
271     /**
272      * Checks whether the picker has a selection
273      * @return {Boolean} Returns true if both a month and year have been selected
274      */
275     hasSelection: function(){
276         var value = this.value;
277         return value[0] !== null && value[1] !== null;
278     },
279
280     /**
281      * Get an array of years to be pushed in the template. It is not in strict
282      * numerical order because we want to show them in columns.
283      * @private
284      * @return {Array} An array of years
285      */
286     getYears: function(){
287         var me = this,
288             offset = me.yearOffset,
289             start = me.activeYear, // put the "active" year on the left
290             end = start + offset,
291             i = start,
292             years = [];
293
294         for (; i < end; ++i) {
295             years.push(i, i + offset);
296         }
297
298         return years;
299     },
300
301     /**
302      * Update the years in the body based on any change
303      * @private
304      */
305     updateBody: function(){
306         var me = this,
307             years = me.years,
308             months = me.months,
309             yearNumbers = me.getYears(),
310             cls = me.selectedCls,
311             value = me.getYear(null),
312             month = me.value[0],
313             monthOffset = me.monthOffset,
314             year;
315
316         if (me.rendered) {
317             years.removeCls(cls);
318             months.removeCls(cls);
319             years.each(function(el, all, index){
320                 year = yearNumbers[index];
321                 el.dom.innerHTML = year;
322                 if (year == value) {
323                     el.dom.className = cls;
324                 }
325             });
326             if (month !== null) {
327                 if (month < monthOffset) {
328                     month = month * 2;
329                 } else {
330                     month = (month - monthOffset) * 2 + 1;
331                 }
332                 months.item(month).addCls(cls);
333             }
334         }
335     },
336
337     /**
338      * Gets the current year value, or the default.
339      * @private
340      * @param {Number} defaultValue The default value to use if the year is not defined.
341      * @param {Number} offset A number to offset the value by
342      * @return {Number} The year value
343      */
344     getYear: function(defaultValue, offset) {
345         var year = this.value[1];
346         offset = offset || 0;
347         return year === null ? defaultValue : year + offset;
348     },
349
350     /**
351      * React to clicks on the body
352      * @private
353      */
354     onBodyClick: function(e, t) {
355         var me = this,
356             isDouble = e.type == 'dblclick';
357
358         if (e.getTarget('.' + me.baseCls + '-month')) {
359             e.stopEvent();
360             me.onMonthClick(t, isDouble);
361         } else if (e.getTarget('.' + me.baseCls + '-year')) {
362             e.stopEvent();
363             me.onYearClick(t, isDouble);
364         }
365     },
366
367     /**
368      * Modify the year display by passing an offset.
369      * @param {Number} offset The offset to move by. If not specified, it defaults to 10.
370      */
371     adjustYear: function(offset){
372         if (typeof offset != 'number') {
373             offset = this.totalYears;
374         }
375         this.activeYear += offset;
376         this.updateBody();
377     },
378
379     /**
380      * React to the ok button being pressed
381      * @private
382      */
383     onOkClick: function(){
384         this.fireEvent('okclick', this, this.value);
385     },
386
387     /**
388      * React to the cancel button being pressed
389      * @private
390      */
391     onCancelClick: function(){
392         this.fireEvent('cancelclick', this);
393     },
394
395     /**
396      * React to a month being clicked
397      * @private
398      * @param {HTMLElement} target The element that was clicked
399      * @param {Boolean} isDouble True if the event was a doubleclick
400      */
401     onMonthClick: function(target, isDouble){
402         var me = this;
403         me.value[0] = me.resolveOffset(me.months.indexOf(target), me.monthOffset);
404         me.updateBody();
405         me.fireEvent('month' + (isDouble ? 'dbl' : '') + 'click', me, me.value);
406         me.fireEvent('select', me, me.value);
407     },
408
409     /**
410      * React to a year being clicked
411      * @private
412      * @param {HTMLElement} target The element that was clicked
413      * @param {Boolean} isDouble True if the event was a doubleclick
414      */
415     onYearClick: function(target, isDouble){
416         var me = this;
417         me.value[1] = me.activeYear + me.resolveOffset(me.years.indexOf(target), me.yearOffset);
418         me.updateBody();
419         me.fireEvent('year' + (isDouble ? 'dbl' : '') + 'click', me, me.value);
420         me.fireEvent('select', me, me.value);
421
422     },
423
424     /**
425      * Returns an offsetted number based on the position in the collection. Since our collections aren't
426      * numerically ordered, this function helps to normalize those differences.
427      * @private
428      * @param {Object} index
429      * @param {Object} offset
430      * @return {Number} The correctly offsetted number
431      */
432     resolveOffset: function(index, offset){
433         if (index % 2 === 0) {
434             return (index / 2);
435         } else {
436             return offset + Math.floor(index / 2);
437         }
438     },
439
440     // private, inherit docs
441     beforeDestroy: function(){
442         var me = this;
443         me.years = me.months = null;
444         Ext.destroyMembers('backRepeater', 'nextRepeater', 'okBtn', 'cancelBtn');
445         this.callParent();
446     }
447 });
448