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