/** * A month / year picker component. This class is used by the * {@link Ext.picker.Date Date picker} to allow browsing and selection of year and * months combinations, but may also be used as a standalone component. * * @example * Ext.create({ * xtype: 'monthpicker', * renderTo: document.body, * value: new Date(), * onSelect: function() { * Ext.Msg.alert('Selected', this.getValue()); * }, * listeners: { * okclick: 'onSelect', * monthdblclick: 'onSelect', * yeardblclick: 'onSelect', * cancelclick: function () { * this.setValue(new Date()); * } * } * }); */Ext.define('Ext.picker.Month', { extend: 'Ext.Component', requires: [ 'Ext.XTemplate', 'Ext.util.ClickRepeater', 'Ext.Date', 'Ext.button.Button' ], alias: 'widget.monthpicker', alternateClassName: 'Ext.MonthPicker', isMonthPicker: true, focusable: true, childEls: [ 'bodyEl', 'prevEl', 'nextEl', 'monthEl', 'yearEl', 'buttons' ], renderTpl: [ '<div id="{id}-bodyEl" data-ref="bodyEl" class="{baseCls}-body">', '<div id="{id}-monthEl" data-ref="monthEl" class="{baseCls}-months">', '<tpl for="months">', '<div class="{parent.baseCls}-item {parent.baseCls}-month">', '<a style="{parent.monthStyle}" role="button" hidefocus="on" class="{parent.baseCls}-item-inner">{.}</a>', '</div>', '</tpl>', '</div>', '<div id="{id}-yearEl" data-ref="yearEl" class="{baseCls}-years">', '<div class="{baseCls}-yearnav">', '<div class="{baseCls}-yearnav-button-ct">', '<a id="{id}-prevEl" data-ref="prevEl" class="{baseCls}-yearnav-button {baseCls}-yearnav-prev" hidefocus="on" role="button"></a>', '</div>', '<div class="{baseCls}-yearnav-button-ct">', '<a id="{id}-nextEl" data-ref="nextEl" class="{baseCls}-yearnav-button {baseCls}-yearnav-next" hidefocus="on" role="button"></a>', '</div>', '</div>', '<tpl for="years">', '<div class="{parent.baseCls}-item {parent.baseCls}-year">', '<a hidefocus="on" class="{parent.baseCls}-item-inner" role="button">{.}</a>', '</div>', '</tpl>', '</div>', '<div class="' + Ext.baseCSSPrefix + 'clear"></div>', '<tpl if="showButtons">', '<div id="{id}-buttons" data-ref="buttons" class="{baseCls}-buttons">{%', 'var me=values.$comp, okBtn=me.okBtn, cancelBtn=me.cancelBtn;', 'okBtn.ownerLayout = cancelBtn.ownerLayout = me.componentLayout;', 'okBtn.ownerCt = cancelBtn.ownerCt = me;', 'Ext.DomHelper.generateMarkup(okBtn.getRenderTree(), out);', 'Ext.DomHelper.generateMarkup(cancelBtn.getRenderTree(), out);', '%}</div>', '</tpl>', '</div>' ], /** * @cfg {String} okText * The text to display on the OK button. * @locale */ okText: 'OK', /** * @cfg {String} cancelText * The text to display on the Cancel button. * @locale */ cancelText: 'Cancel', /** * @cfg {String} [baseCls='x-monthpicker'] * The base CSS class to apply to the picker element. */ baseCls: Ext.baseCSSPrefix + 'monthpicker', /** * @cfg {Boolean} showButtons * True to show ok and cancel buttons below the picker. */ showButtons: true, /** * @property {String} [selectedCls='x-monthpicker-selected'] * The class to be added to selected items in the picker. * @readonly */ /** * @cfg {Date/Number[]} value * The default value to set. See {@link #setValue} */ /** * @cfg {String} footerButtonUI * The {@link Ext.button.Button#ui} to use for the month picker's footer buttons. */ footerButtonUI: 'default', measureWidth: 35, measureMaxHeight: 20, // used when attached to date picker which isnt showing buttons smallCls: Ext.baseCSSPrefix + 'monthpicker-small', /** * @private */ totalYears: 10, yearOffset: 5, // 10 years in total, 2 per row monthOffset: 6, // 12 months, 2 per row alignOnScroll: false, /** * @event cancelclick * Fires when the cancel button is pressed. * @param {Ext.picker.Month} this */ /** * @event monthclick * Fires when a month is clicked. * @param {Ext.picker.Month} this * @param {Array} value The current value */ /** * @event monthdblclick * Fires when a month is clicked. * @param {Ext.picker.Month} this * @param {Array} value The current value */ /** * @event okclick * Fires when the ok button is pressed. * @param {Ext.picker.Month} this * @param {Array} value The current value */ /** * @event select * Fires when a month/year is selected. * @param {Ext.picker.Month} this * @param {Array} value The current value */ /** * @event yearclick * Fires when a year is clicked. * @param {Ext.picker.Month} this * @param {Array} value The current value */ /** * @event yeardblclick * Fires when a year is clicked. * @param {Ext.picker.Month} this * @param {Array} value The current value */ /** * @inheritdoc * @private */ initComponent: function(){ var me = this; me.selectedCls = me.baseCls + '-selected'; if (me.small) { me.addCls(me.smallCls); } me.setValue(me.value); me.activeYear = me.getYear(new Date().getFullYear() - 4, -4); if (me.showButtons) { me.okBtn = new Ext.button.Button({ ui: me.footerButtonUI, text: me.okText, handler: me.onOkClick, scope: me }); me.cancelBtn = new Ext.button.Button({ ui: me.footerButtonUI, text: me.cancelText, handler: me.onCancelClick, scope: me }); } me.callParent(); }, /** * @inheritdoc * @private */ beforeRender: function(){ var me = this, i = 0, months = [], shortName = Ext.Date.getShortMonthName, monthLen = me.monthOffset, margin = me.monthMargin, style = ''; if (me.padding && !me.width) { me.cacheWidth(); } me.callParent(); for (; i < monthLen; ++i) { months.push(shortName(i), shortName(i + monthLen)); } if (Ext.isDefined(margin)) { style = 'margin: 0 ' + margin + 'px;'; } Ext.apply(me.renderData, { months: months, years: me.getYears(), showButtons: me.showButtons, monthStyle: style }); }, cacheWidth: function() { var me = this, padding = me.parseBox(me.padding), widthEl = Ext.getBody().createChild({ cls: me.baseCls + ' ' + me.borderBoxCls, style: 'position:absolute;top:-1000px;left:-1000px;', html: ' ' // required for opera 11.64 to measure a width }); me.self.prototype.width = widthEl.getWidth() + padding.left + padding.right; widthEl.destroy(); }, /** * @inheritdoc * @private */ afterRender: function(){ var me = this, body = me.bodyEl; me.callParent(); // Month picker is not focusable and essentially is pointer only thing. // Clicking on it will focus the document body, which may disrupt the state // of the floating parent such as Date picker or a menu, and cause it to hide. // To work around that, we stop the mousedown events completely. if (me.up('[floating=true]')) { me.el.on('mousedown', me.onElClick, me, {translate: false}); } body.on({ scope: me, click: 'onBodyClick', dblclick: 'onBodyClick' }); // keep a reference to the year/month elements since we'll be re-using them me.years = body.select('.' + me.baseCls + '-year a'); me.months = body.select('.' + me.baseCls + '-month a'); me.backRepeater = new Ext.util.ClickRepeater(me.prevEl, { handler: Ext.Function.bind(me.adjustYear, me, [-me.totalYears]) }); me.prevEl.addClsOnOver(me.baseCls + '-yearnav-prev-over'); me.nextRepeater = new Ext.util.ClickRepeater(me.nextEl, { handler: Ext.Function.bind(me.adjustYear, me, [me.totalYears]) }); me.nextEl.addClsOnOver(me.baseCls + '-yearnav-next-over'); me.updateBody(); if (!Ext.isDefined(me.monthMargin)) { Ext.picker.Month.prototype.monthMargin = me.calculateMonthMargin(); } }, calculateMonthMargin: function(){ // We use this method for locales where the short month name // may be longer than we see in English. For example in the // zh_TW locale the month ends up spanning lines, so we loosen // the margins to get some extra space var me = this, months = me.months, first = months.first(), itemMargin = first.getMargin('l'); while (itemMargin && me.getLargest() > me.measureMaxHeight) { --itemMargin; months.setStyle('margin', '0 ' + itemMargin + 'px'); } return itemMargin; }, getLargest: function(months){ var largest = 0; this.months.each(function(item){ var h = item.getHeight(); if (h > largest) { largest = h; } }); return largest; }, /** * Set the value for the picker. * @param {Date/Number[]} value The value to set. It can be a Date object, where the month/year will be extracted, or * it can be an array, with the month as the first index and the year as the second. * @return {Ext.picker.Month} this */ setValue: function(value){ var me = this, active = me.activeYear, year; if (!value) { me.value = [null, null]; } else if (Ext.isDate(value)) { me.value = [value.getMonth(), value.getFullYear()]; } else { me.value = [value[0], value[1]]; } if (me.rendered) { year = me.value[1]; if (year !== null) { if ((year < active || year > active + me.yearOffset)) { me.activeYear = year - me.yearOffset + 1; } } me.updateBody(); } return me; }, /** * Gets the selected value. It is returned as an array [month, year]. It may * be a partial value, for example [null, 2010]. The month is returned as * 0 based. * @return {Number[]} The selected value */ getValue: function(){ return this.value; }, /** * Checks whether the picker has a selection * @return {Boolean} Returns true if both a month and year have been selected */ hasSelection: function(){ var value = this.value; return value[0] !== null && value[1] !== null; }, /** * Get an array of years to be pushed in the template. It is not in strict * numerical order because we want to show them in columns. * @private * @return {Number[]} An array of years */ getYears: function(){ var me = this, offset = me.yearOffset, start = me.activeYear, // put the "active" year on the left end = start + offset, i = start, years = []; for (; i < end; ++i) { years.push(i, i + offset); } return years; }, /** * Update the years in the body based on any change * @private */ updateBody: function(){ var me = this, years = me.years, months = me.months, yearNumbers = me.getYears(), cls = me.selectedCls, value = me.getYear(null), month = me.value[0], monthOffset = me.monthOffset, year, yearItems, y, yLen, el; if (me.rendered) { years.removeCls(cls); months.removeCls(cls); yearItems = years.elements; yLen = yearItems.length; for (y = 0; y < yLen; y++) { el = Ext.fly(yearItems[y]); year = yearNumbers[y]; el.dom.innerHTML = year; if (year === value) { el.addCls(cls); } } if (month !== null) { if (month < monthOffset) { month = month * 2; } else { month = (month - monthOffset) * 2 + 1; } months.item(month).addCls(cls); } } }, /** * Gets the current year value, or the default. * @private * @param {Number} defaultValue The default value to use if the year is not defined. * @param {Number} offset A number to offset the value by * @return {Number} The year value */ getYear: function(defaultValue, offset) { var year = this.value[1]; offset = offset || 0; return year === null ? defaultValue : year + offset; }, onElClick: function(e) { e.stopEvent(); }, /** * React to clicks on the body * @private */ onBodyClick: function(e, t) { var me = this, isDouble = e.type === 'dblclick'; if (e.getTarget('.' + me.baseCls + '-month')) { e.stopEvent(); me.onMonthClick(t, isDouble); } else if (e.getTarget('.' + me.baseCls + '-year')) { e.stopEvent(); me.onYearClick(t, isDouble); } }, /** * Modify the year display by passing an offset. * @param {Number} [offset=10] The offset to move by. */ adjustYear: function(offset){ if (typeof offset !== 'number') { offset = this.totalYears; } this.activeYear += offset; this.updateBody(); }, /** * React to the ok button being pressed * @private */ onOkClick: function(){ this.fireEvent('okclick', this, this.value); }, /** * React to the cancel button being pressed * @private */ onCancelClick: function(){ this.fireEvent('cancelclick', this); }, /** * React to a month being clicked * @private * @param {HTMLElement} target The element that was clicked * @param {Boolean} isDouble True if the event was a doubleclick */ onMonthClick: function(target, isDouble){ var me = this; me.value[0] = me.resolveOffset(me.months.indexOf(target), me.monthOffset); me.updateBody(); me.fireEvent('month' + (isDouble ? 'dbl' : '') + 'click', me, me.value); me.fireEvent('select', me, me.value); }, /** * React to a year being clicked * @private * @param {HTMLElement} target The element that was clicked * @param {Boolean} isDouble True if the event was a doubleclick */ onYearClick: function(target, isDouble){ var me = this; me.value[1] = me.activeYear + me.resolveOffset(me.years.indexOf(target), me.yearOffset); me.updateBody(); me.fireEvent('year' + (isDouble ? 'dbl' : '') + 'click', me, me.value); me.fireEvent('select', me, me.value); }, /** * Returns an offsetted number based on the position in the collection. Since our collections aren't * numerically ordered, this function helps to normalize those differences. * @private * @param {Object} index * @param {Object} offset * @return {Number} The correctly offsetted number */ resolveOffset: function(index, offset){ if (index % 2 === 0) { return (index / 2); } else { return offset + Math.floor(index / 2); } }, doDestroy: function() { Ext.destroy(this.backRepeater, this.nextRepeater, this.okBtn, this.cancelBtn); this.callParent(); }, privates: { // Do the job of a container layout at this point even though we are not a Container. // TODO: Refactor as a Container. finishRenderChildren: function () { var me = this; this.callParent(arguments); if (this.showButtons) { me.okBtn.finishRender(); me.cancelBtn.finishRender(); } } }});