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