/**
* 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',
alias: 'widget.monthpicker',
alternateClassName: 'Ext.MonthPicker',
requires: [
'Ext.XTemplate',
'Ext.util.ClickRepeater',
'Ext.Date',
'Ext.button.Button'
],
isMonthPicker: true,
/**
* @property focusable
* @inheritdoc
*/
focusable: true,
/**
* @cfg childEls
* @inheritdoc
*/
childEls: [
'bodyEl', 'prevEl', 'nextEl', 'monthEl', 'yearEl', 'buttons'
],
/* eslint-disable indent, max-len */
/**
* @cfg renderTpl
* @inheritdoc
*/
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>'
],
/* eslint-enable indent, max-len */
/**
* @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
* 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
* 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
/**
* @cfg alignOnScroll
* @inheritdoc
*/
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
*/
/**
* @method initComponent
* @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();
},
/**
* @method beforeRender
* @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();
},
/**
* @method afterRender
* @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();
}
}
}
});