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