]> git.proxmox.com Git - extjs.git/blob - extjs/classic/classic/src/picker/Month.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / picker / Month.js
1 /**
2 * A month / year picker component. This class is used by the
3 * {@link Ext.picker.Date Date picker} to allow browsing and selection of year and
4 * months combinations, but may also be used as a standalone component.
5 *
6 * @example
7 * Ext.create({
8 * xtype: 'monthpicker',
9 * renderTo: document.body,
10 * value: new Date(),
11 * onSelect: function() {
12 * Ext.Msg.alert('Selected', this.getValue());
13 * },
14 * listeners: {
15 * okclick: 'onSelect',
16 * monthdblclick: 'onSelect',
17 * yeardblclick: 'onSelect',
18 * cancelclick: function () {
19 * this.setValue(new Date());
20 * }
21 * }
22 * });
23 */
24 Ext.define('Ext.picker.Month', {
25 extend: 'Ext.Component',
26 requires: [
27 'Ext.XTemplate',
28 'Ext.util.ClickRepeater',
29 'Ext.Date',
30 'Ext.button.Button'
31 ],
32 alias: 'widget.monthpicker',
33 alternateClassName: 'Ext.MonthPicker',
34
35 isMonthPicker: true,
36
37 focusable: true,
38
39 childEls: [
40 'bodyEl', 'prevEl', 'nextEl', 'monthEl', 'yearEl'
41 ],
42
43 renderTpl: [
44 '<div id="{id}-bodyEl" data-ref="bodyEl" class="{baseCls}-body">',
45 '<div id="{id}-monthEl" data-ref="monthEl" class="{baseCls}-months">',
46 '<tpl for="months">',
47 '<div class="{parent.baseCls}-item {parent.baseCls}-month">',
48 '<a style="{parent.monthStyle}" role="button" hidefocus="on" class="{parent.baseCls}-item-inner">{.}</a>',
49 '</div>',
50 '</tpl>',
51 '</div>',
52 '<div id="{id}-yearEl" data-ref="yearEl" class="{baseCls}-years">',
53 '<div class="{baseCls}-yearnav">',
54 '<div class="{baseCls}-yearnav-button-ct">',
55 '<a id="{id}-prevEl" data-ref="prevEl" class="{baseCls}-yearnav-button {baseCls}-yearnav-prev" hidefocus="on" role="button"></a>',
56 '</div>',
57 '<div class="{baseCls}-yearnav-button-ct">',
58 '<a id="{id}-nextEl" data-ref="nextEl" class="{baseCls}-yearnav-button {baseCls}-yearnav-next" hidefocus="on" role="button"></a>',
59 '</div>',
60 '</div>',
61 '<tpl for="years">',
62 '<div class="{parent.baseCls}-item {parent.baseCls}-year">',
63 '<a hidefocus="on" class="{parent.baseCls}-item-inner" role="button">{.}</a>',
64 '</div>',
65 '</tpl>',
66 '</div>',
67 '<div class="' + Ext.baseCSSPrefix + 'clear"></div>',
68 '<tpl if="showButtons">',
69 '<div class="{baseCls}-buttons">{%',
70 'var me=values.$comp, okBtn=me.okBtn, cancelBtn=me.cancelBtn;',
71 'okBtn.ownerLayout = cancelBtn.ownerLayout = me.componentLayout;',
72 'okBtn.ownerCt = cancelBtn.ownerCt = me;',
73 'Ext.DomHelper.generateMarkup(okBtn.getRenderTree(), out);',
74 'Ext.DomHelper.generateMarkup(cancelBtn.getRenderTree(), out);',
75 '%}</div>',
76 '</tpl>',
77 '</div>'
78 ],
79
80 //<locale>
81 /**
82 * @cfg {String} okText The text to display on the ok button.
83 */
84 okText: 'OK',
85 //</locale>
86
87 //<locale>
88 /**
89 * @cfg {String} cancelText The text to display on the cancel button.
90 */
91 cancelText: 'Cancel',
92 //</locale>
93
94 /**
95 * @cfg {String} [baseCls='x-monthpicker']
96 * The base CSS class to apply to the picker element.
97 */
98 baseCls: Ext.baseCSSPrefix + 'monthpicker',
99
100 /**
101 * @cfg {Boolean} showButtons True to show ok and cancel buttons below the picker.
102 */
103 showButtons: true,
104
105 /**
106 * @cfg {String} [selectedCls='x-monthpicker-selected'] The class to be added to selected items in the picker.
107 */
108
109 /**
110 * @cfg {Date/Number[]} value The default value to set. See {@link #setValue}
111 */
112
113 /**
114 * @cfg {String}
115 * The {@link Ext.button.Button#ui} to use for the month picker's footer buttons.
116 */
117 footerButtonUI: 'default',
118
119 measureWidth: 35,
120 measureMaxHeight: 20,
121
122 // used when attached to date picker which isnt showing buttons
123 smallCls: Ext.baseCSSPrefix + 'monthpicker-small',
124
125 /**
126 * @private
127 */
128 totalYears: 10,
129 yearOffset: 5, // 10 years in total, 2 per row
130 monthOffset: 6, // 12 months, 2 per row
131
132 /**
133 * @event cancelclick
134 * Fires when the cancel button is pressed.
135 * @param {Ext.picker.Month} this
136 */
137
138 /**
139 * @event monthclick
140 * Fires when a month is clicked.
141 * @param {Ext.picker.Month} this
142 * @param {Array} value The current value
143 */
144
145 /**
146 * @event monthdblclick
147 * Fires when a month is clicked.
148 * @param {Ext.picker.Month} this
149 * @param {Array} value The current value
150 */
151
152 /**
153 * @event okclick
154 * Fires when the ok button is pressed.
155 * @param {Ext.picker.Month} this
156 * @param {Array} value The current value
157 */
158
159 /**
160 * @event select
161 * Fires when a month/year is selected.
162 * @param {Ext.picker.Month} this
163 * @param {Array} value The current value
164 */
165
166 /**
167 * @event yearclick
168 * Fires when a year is clicked.
169 * @param {Ext.picker.Month} this
170 * @param {Array} value The current value
171 */
172
173 /**
174 * @event yeardblclick
175 * Fires when a year is clicked.
176 * @param {Ext.picker.Month} this
177 * @param {Array} value The current value
178 */
179
180 /**
181 * @inheritdoc
182 * @private
183 */
184 initComponent: function(){
185 var me = this;
186
187 me.selectedCls = me.baseCls + '-selected';
188
189 if (me.small) {
190 me.addCls(me.smallCls);
191 }
192 me.setValue(me.value);
193 me.activeYear = me.getYear(new Date().getFullYear() - 4, -4);
194
195 if (me.showButtons) {
196 me.okBtn = new Ext.button.Button({
197 ui: me.footerButtonUI,
198 text: me.okText,
199 handler: me.onOkClick,
200 scope: me
201 });
202 me.cancelBtn = new Ext.button.Button({
203 ui: me.footerButtonUI,
204 text: me.cancelText,
205 handler: me.onCancelClick,
206 scope: me
207 });
208 }
209
210 this.callParent();
211 },
212
213 /**
214 * @inheritdoc
215 * @private
216 */
217 beforeRender: function(){
218 var me = this,
219 i = 0,
220 months = [],
221 shortName = Ext.Date.getShortMonthName,
222 monthLen = me.monthOffset,
223 margin = me.monthMargin,
224 style = '';
225
226 if (me.padding && !me.width) {
227 me.cacheWidth();
228 }
229
230 me.callParent();
231
232 for (; i < monthLen; ++i) {
233 months.push(shortName(i), shortName(i + monthLen));
234 }
235
236 if (Ext.isDefined(margin)) {
237 style = 'margin: 0 ' + margin + 'px;';
238 }
239
240 Ext.apply(me.renderData, {
241 months: months,
242 years: me.getYears(),
243 showButtons: me.showButtons,
244 monthStyle: style
245 });
246 },
247
248 cacheWidth: function() {
249 var me = this,
250 padding = me.parseBox(me.padding),
251 widthEl = Ext.getBody().createChild({
252 cls: me.baseCls + ' ' + me.borderBoxCls,
253 style: 'position:absolute;top:-1000px;left:-1000px;',
254 html: '&nbsp;' // required for opera 11.64 to measure a width
255 });
256
257 me.self.prototype.width = widthEl.getWidth() + padding.left + padding.right;
258 widthEl.destroy();
259 },
260
261 /**
262 * @inheritdoc
263 * @private
264 */
265 afterRender: function(){
266 var me = this,
267 body = me.bodyEl;
268
269 me.callParent();
270
271 // Month picker is not focusable and essentially is pointer only thing.
272 // Clicking on it will focus the document body, which may disrupt the state
273 // of the floating parent such as Date picker or a menu, and cause it to hide.
274 // To work around that, we stop the mousedown events completely.
275 if (me.up('[floating=true]')) {
276 me.el.on('mousedown', me.onElClick, me);
277 }
278
279 me.mon(body, 'click', me.onBodyClick, me);
280 me.mon(body, 'dblclick', me.onBodyClick, me);
281
282 // keep a reference to the year/month elements since we'll be re-using them
283 me.years = body.select('.' + me.baseCls + '-year a');
284 me.months = body.select('.' + me.baseCls + '-month a');
285
286 me.backRepeater = new Ext.util.ClickRepeater(me.prevEl, {
287 handler: Ext.Function.bind(me.adjustYear, me, [-me.totalYears])
288 });
289
290 me.prevEl.addClsOnOver(me.baseCls + '-yearnav-prev-over');
291 me.nextRepeater = new Ext.util.ClickRepeater(me.nextEl, {
292 handler: Ext.Function.bind(me.adjustYear, me, [me.totalYears])
293 });
294 me.nextEl.addClsOnOver(me.baseCls + '-yearnav-next-over');
295 me.updateBody();
296
297 if (!Ext.isDefined(me.monthMargin)) {
298 Ext.picker.Month.prototype.monthMargin = me.calculateMonthMargin();
299 }
300 },
301
302 calculateMonthMargin: function(){
303 // We use this method for locales where the short month name
304 // may be longer than we see in English. For example in the
305 // zh_TW locale the month ends up spanning lines, so we loosen
306 // the margins to get some extra space
307 var me = this,
308 months = me.months,
309 first = months.first(),
310 itemMargin = first.getMargin('l');
311
312 while (itemMargin && me.getLargest() > me.measureMaxHeight) {
313 --itemMargin;
314 months.setStyle('margin', '0 ' + itemMargin + 'px');
315 }
316 return itemMargin;
317 },
318
319 getLargest: function(months){
320 var largest = 0;
321 this.months.each(function(item){
322 var h = item.getHeight();
323 if (h > largest) {
324 largest = h;
325 }
326 });
327 return largest;
328
329 },
330
331 /**
332 * Set the value for the picker.
333 * @param {Date/Number[]} value The value to set. It can be a Date object, where the month/year will be extracted, or
334 * it can be an array, with the month as the first index and the year as the second.
335 * @return {Ext.picker.Month} this
336 */
337 setValue: function(value){
338 var me = this,
339 active = me.activeYear,
340 year;
341
342 if (!value) {
343 me.value = [null, null];
344 } else if (Ext.isDate(value)) {
345 me.value = [value.getMonth(), value.getFullYear()];
346 } else {
347 me.value = [value[0], value[1]];
348 }
349
350 if (me.rendered) {
351 year = me.value[1];
352 if (year !== null) {
353 if ((year < active || year > active + me.yearOffset)) {
354 me.activeYear = year - me.yearOffset + 1;
355 }
356 }
357 me.updateBody();
358 }
359
360 return me;
361 },
362
363 /**
364 * Gets the selected value. It is returned as an array [month, year]. It may
365 * be a partial value, for example [null, 2010]. The month is returned as
366 * 0 based.
367 * @return {Number[]} The selected value
368 */
369 getValue: function(){
370 return this.value;
371 },
372
373 /**
374 * Checks whether the picker has a selection
375 * @return {Boolean} Returns true if both a month and year have been selected
376 */
377 hasSelection: function(){
378 var value = this.value;
379 return value[0] !== null && value[1] !== null;
380 },
381
382 /**
383 * Get an array of years to be pushed in the template. It is not in strict
384 * numerical order because we want to show them in columns.
385 * @private
386 * @return {Number[]} An array of years
387 */
388 getYears: function(){
389 var me = this,
390 offset = me.yearOffset,
391 start = me.activeYear, // put the "active" year on the left
392 end = start + offset,
393 i = start,
394 years = [];
395
396 for (; i < end; ++i) {
397 years.push(i, i + offset);
398 }
399
400 return years;
401 },
402
403 /**
404 * Update the years in the body based on any change
405 * @private
406 */
407 updateBody: function(){
408 var me = this,
409 years = me.years,
410 months = me.months,
411 yearNumbers = me.getYears(),
412 cls = me.selectedCls,
413 value = me.getYear(null),
414 month = me.value[0],
415 monthOffset = me.monthOffset,
416 year,
417 yearItems, y, yLen, el;
418
419 if (me.rendered) {
420 years.removeCls(cls);
421 months.removeCls(cls);
422
423 yearItems = years.elements;
424 yLen = yearItems.length;
425
426 for (y = 0; y < yLen; y++) {
427 el = Ext.fly(yearItems[y]);
428
429 year = yearNumbers[y];
430 el.dom.innerHTML = year;
431 if (year === value) {
432 el.addCls(cls);
433 }
434 }
435 if (month !== null) {
436 if (month < monthOffset) {
437 month = month * 2;
438 } else {
439 month = (month - monthOffset) * 2 + 1;
440 }
441 months.item(month).addCls(cls);
442 }
443 }
444 },
445
446 /**
447 * Gets the current year value, or the default.
448 * @private
449 * @param {Number} defaultValue The default value to use if the year is not defined.
450 * @param {Number} offset A number to offset the value by
451 * @return {Number} The year value
452 */
453 getYear: function(defaultValue, offset) {
454 var year = this.value[1];
455 offset = offset || 0;
456 return year === null ? defaultValue : year + offset;
457 },
458
459 onElClick: function(e) {
460 e.stopEvent();
461 },
462
463 /**
464 * React to clicks on the body
465 * @private
466 */
467 onBodyClick: function(e, t) {
468 var me = this,
469 isDouble = e.type === 'dblclick';
470
471 if (e.getTarget('.' + me.baseCls + '-month')) {
472 e.stopEvent();
473 me.onMonthClick(t, isDouble);
474 } else if (e.getTarget('.' + me.baseCls + '-year')) {
475 e.stopEvent();
476 me.onYearClick(t, isDouble);
477 }
478 },
479
480 /**
481 * Modify the year display by passing an offset.
482 * @param {Number} [offset=10] The offset to move by.
483 */
484 adjustYear: function(offset){
485 if (typeof offset !== 'number') {
486 offset = this.totalYears;
487 }
488 this.activeYear += offset;
489 this.updateBody();
490 },
491
492 /**
493 * React to the ok button being pressed
494 * @private
495 */
496 onOkClick: function(){
497 this.fireEvent('okclick', this, this.value);
498 },
499
500 /**
501 * React to the cancel button being pressed
502 * @private
503 */
504 onCancelClick: function(){
505 this.fireEvent('cancelclick', this);
506 },
507
508 /**
509 * React to a month being clicked
510 * @private
511 * @param {HTMLElement} target The element that was clicked
512 * @param {Boolean} isDouble True if the event was a doubleclick
513 */
514 onMonthClick: function(target, isDouble){
515 var me = this;
516 me.value[0] = me.resolveOffset(me.months.indexOf(target), me.monthOffset);
517 me.updateBody();
518 me.fireEvent('month' + (isDouble ? 'dbl' : '') + 'click', me, me.value);
519 me.fireEvent('select', me, me.value);
520 },
521
522 /**
523 * React to a year being clicked
524 * @private
525 * @param {HTMLElement} target The element that was clicked
526 * @param {Boolean} isDouble True if the event was a doubleclick
527 */
528 onYearClick: function(target, isDouble){
529 var me = this;
530 me.value[1] = me.activeYear + me.resolveOffset(me.years.indexOf(target), me.yearOffset);
531 me.updateBody();
532 me.fireEvent('year' + (isDouble ? 'dbl' : '') + 'click', me, me.value);
533 me.fireEvent('select', me, me.value);
534
535 },
536
537 /**
538 * Returns an offsetted number based on the position in the collection. Since our collections aren't
539 * numerically ordered, this function helps to normalize those differences.
540 * @private
541 * @param {Object} index
542 * @param {Object} offset
543 * @return {Number} The correctly offsetted number
544 */
545 resolveOffset: function(index, offset){
546 if (index % 2 === 0) {
547 return (index / 2);
548 } else {
549 return offset + Math.floor(index / 2);
550 }
551 },
552
553 /**
554 * @inheritdoc
555 * @private
556 */
557 beforeDestroy: function(){
558 var me = this;
559 me.years = me.months = null;
560 Ext.destroyMembers(me, 'backRepeater', 'nextRepeater', 'okBtn', 'cancelBtn');
561 me.callParent();
562 },
563
564 onDestroy: function() {
565 Ext.destroyMembers(this, 'okBtn', 'cancelBtn');
566 this.callParent();
567 },
568
569 privates: {
570 // Do the job of a container layout at this point even though we are not a Container.
571 // TODO: Refactor as a Container.
572 finishRenderChildren: function () {
573 var me = this;
574
575 this.callParent(arguments);
576
577 if (this.showButtons) {
578 me.okBtn.finishRender();
579 me.cancelBtn.finishRender();
580 }
581 }
582 }
583 });