macoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-services
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
16 KiB
561 lines
16 KiB
/** |
|
* 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' |
|
], |
|
|
|
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 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>' |
|
], |
|
|
|
//<locale> |
|
/** |
|
* @cfg {String} okText The text to display on the ok button. |
|
*/ |
|
okText: 'OK', |
|
//</locale> |
|
|
|
//<locale> |
|
/** |
|
* @cfg {String} cancelText The text to display on the cancel button. |
|
*/ |
|
cancelText: 'Cancel', |
|
//</locale> |
|
|
|
/** |
|
* @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, |
|
|
|
/** |
|
* @cfg {String} [selectedCls='x-monthpicker-selected'] The class to be added to selected items in the picker. |
|
*/ |
|
|
|
/** |
|
* @cfg {Date/Number[]} value The default value to set. See {@link #setValue} |
|
*/ |
|
|
|
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 |
|
|
|
/** |
|
* @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({ |
|
text: me.okText, |
|
handler: me.onOkClick, |
|
scope: me |
|
}); |
|
me.cancelBtn = new Ext.button.Button({ |
|
text: me.cancelText, |
|
handler: me.onCancelClick, |
|
scope: me |
|
}); |
|
} |
|
|
|
this.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(); |
|
|
|
me.mon(body, 'click', me.onBodyClick, me); |
|
me.mon(body, 'dblclick', me.onBodyClick, me); |
|
|
|
// 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; |
|
}, |
|
|
|
/** |
|
* 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); |
|
} |
|
}, |
|
|
|
/** |
|
* @inheritdoc |
|
* @private |
|
*/ |
|
beforeDestroy: function(){ |
|
var me = this; |
|
me.years = me.months = null; |
|
Ext.destroyMembers(me, 'backRepeater', 'nextRepeater', 'okBtn', 'cancelBtn'); |
|
me.callParent(); |
|
}, |
|
|
|
onDestroy: function() { |
|
Ext.destroyMembers(this, 'okBtn', '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(); |
|
} |
|
} |
|
} |
|
});
|
|
|