Ext.require([ 'Ext.form.*', 'Ext.layout.container.Column', 'Ext.tab.Panel' ]); /** * @class Ext.aria.AriaController *

This class supplies support methods for integrating ARIA support into Components.

* */ Ext.define('Ext.aria.AriaController', { singleton: true, requires: ['Ext.AbstractComponent'], handlerRe: /^on(\w+)/, // This function is called as an interceptor of the target class's fireEvent method // It invokes the event's ariaHandler if present ariaFireEvent: function(evt){ var me = this, evtName = evt.toLowerCase(), args = Array.prototype.slice.call(arguments, 1), classFn = Ext.aria.AriaController.getClassAriaHandler(this.self.prototype, evtName), ariaFn = Ext.aria.AriaController[evtName], result; // Call user event handlers first. result = Ext.util.Observable.prototype.fireEvent.apply(me, arguments); if (result !== false) { // Then AriaController's event handler if (typeof ariaFn === 'function') { result = ariaFn.apply(me, args); } // Then class's configured ARIA event handler if (typeof classFn === 'function') { result = classFn.apply(me, args); } } return result; }, // Examine the passed class prototype for an aria_event_handler, and if not found, continue up the prototype chain. getClassAriaHandler: function(clsProto, evtName) { var curProto = clsProto, generatedHandlerName = 'generated_aria_' + evtName + '_handler', configuredHandlerName = 'aria_' + evtName + '_handler', fn, result = clsProto.hasOwnProperty(generatedHandlerName) ? clsProto[generatedHandlerName] : null; // If there is no generated ariaEventHandler for the class/event, we must generate one // which calls the cascade of event handlers for the inheritance chain if (!result) { var handlerStack = []; // Collect the stack of event handlers from each level in the inheritance tree while (curProto) { if (curProto.hasOwnProperty(configuredHandlerName)) { fn = curProto[configuredHandlerName]; if (fn) { handlerStack.unshift(fn); } } curProto = curProto.superclass; } // Generate a closure which captures the handler stack, and loops through it // to invoke them all. result = clsProto[generatedHandlerName] = (function(handlerStack) { var result = handlerStack.length ? function() { var i = 0, len = handlerStack.length; for (; i < len; i++) { handlerStack[i].apply(this, arguments); } } : Ext.emptyFn; return result; })(handlerStack); } return result; }, processClass: function(cls, props) { var me = this, clsProto = cls.prototype, propName, value, k; for (propName in props) { if (props.hasOwnProperty(propName)) { value = props[propName]; k = me.handlerRe.exec(propName); // Handle properties like onHide: function(){...} // Copy it into ariaHandlers.hide where it can be called // by the fireEvent interceptor if (k && (typeof value === 'function')) { propName = propName.toLowerCase(); // Hang the ariaHandlers on the class's prototype. clsProto['aria_' + k[1].toLowerCase() + '_handler'] = value; } else if (propName === 'role') { clsProto.ariaRole = value; } } } // Replace the class's fireEvent method with our code which intercepts relevant events, // performs known processing (such as setting the role on render), and calls any defined // ariaHandlers clsProto.fireEvent = me.ariaFireEvent; }, // ARIA event handler for the render event. render: function() { var me = this, p = me.pendingProps, len = p ? p.length : 0, i = 0; // Apply any pending ARIA updates from when applied before render for (; i < len; i++) { me.updateAria.apply(me, p[i]); } } }, function() { // Inject an updateAria method into AbstractComponent's prorotype Ext.override(Ext.AbstractComponent, { getElConfig: function() { var result = this.callOverridden(); result.role = this.ariaRole; return result; }, updateAria: function(el, props) { var me = this; // Queue the attributes up if not rendered. // They will be applied in the global render handler. if (!me.rendered) { if (!me.pendingProps) { me.pendingProps = []; } me.pendingProps.push(Array.prototype.slice.call(arguments)); return; } // The one argument form updates the actionEl if (arguments.length == 1) { props = el; el = this.getActionEl(); } // Ensure events are added if (!me.events.beforeariaupdate) { me.addEvents('beforeariaupdate', 'ariaupdate'); } if (me.fireEvent('beforeariaupdate', el, props) !== false) { Ext.fly(el).set(props); me.fireEvent('ariaupdate', el, props); } } }); Ext.core.Element.prototype.set = function(o, useSet) { var el = this.dom, attr, val; useSet = (useSet !== false) && !!el.setAttribute; for (attr in o) { if (o.hasOwnProperty(attr)) { val = o[attr]; if (attr == 'style') { DH.applyStyles(el, val); } else if (attr == 'cls') { el.className = val; } else if (useSet) { if (val === undefined) { el.removeAttribute(attr); } else { el.setAttribute(attr, val); } } else { el[attr] = val; } } } return this; }; }); Ext.onReady(function() { Ext.aria.AriaController.processClass(Ext.AbstractComponent, { onDisable: function() { this.updateAria(this.getActionEl(), { 'aria-disabled': true }) }, onEnable: function() { this.updateAria(this.getActionEl(), { 'aria-disabled': false }) }, onHide: function() { this.updateAria(this.getActionEl(), { 'aria-hidden': true }) }, onShow: function() { this.updateAria(this.getActionEl(), { 'aria-hidden': false }) } }); Ext.aria.AriaController.processClass(Ext.form.FieldSet, { onRender: function() { this.updateAria({ 'aria-expanded': !this.collapsed }); }, onExpand: function() { this.updateAria({ 'aria-expanded': true }); }, onCollapse: function() { this.updateAria({ 'aria-expanded': false }); } }); Ext.aria.AriaController.processClass(Ext.form.field.Base, { onRender: function() { if (this.labelEl) { this.updateAria(this.inputEl, { 'aria-labelledby': this.labelEl.id }); } }, onValidityChange: function(f, isValid) { this.updateAria(this.inputEl, { 'aria-invalid': !isValid, 'aria-describedby': isValid ? undefined : f.errorEl.id }); } }); Ext.aria.AriaController.processClass(Ext.form.field.Text, { role: 'textbox' }); Ext.aria.AriaController.processClass(Ext.form.field.Number, { role: 'spinbutton', onRender: function() { var me = this, props = {}; if (isFinite(me.minValue)) { props['aria-valuemin'] = me.minValue; } if (isFinite(me.maxValue)) { props['aria-valuemax'] = me.maxValue; } me.updateAria(me.inputEl, props); }, onChange: function(f) { var v = this.getValue(); this.updateAria(this.inputEl, { 'aria-valuenow': (this.isValid() && v !== null) ? v: undefined }); } }); // Picker fields are combobox type functionality Ext.aria.AriaController.processClass(Ext.form.field.Picker, function() { var listFns = { onListHighlight: function(node) { this.updateAria({ 'aria-activedescendant': node.id }); }, onListUnhighlight: function(node) { var n = this.getPicker().getSelectedNodes(); this.updateAria({ 'aria-activedescendant': n.length ? n[0].id : undefined }); }, onListSelectionChange: function(sm, selected) { if (selected.length) { var n = this.getPicker().getSelectedNodes(); this.updateAria({ 'aria-activedescendant': n[0].id }); } else { this.updateAria({ 'aria-activedescendant': undefined }); } } }; return { role: 'combobox', onRender: function() { this.updateAria(this.inputEl, { 'aria-autocomplete': 'inline' }); }, onExpand: function() { var me = this, picker = me.getPicker(); me.updateAria(me.inputEl, { 'aria-owns': picker.el.id, 'aria-expanded': true }); // On first expand, add listeners to the "picker" list to maintain our ARIA state if (!me.pickerListenersAdded) { picker.mon(picker, { highlight: listFns.onListHighlight, unhighlight: listFns.onListUnhighlight, selectionchange: listFns.onListSelectionChange, scope: me }); me.pickerListenersAdded = true; } }, onCollapse: function() { var p = this.getPicker(), n = p.getSelectedNodes ? p.getSelectedNodes() : [null]; this.updateAria({ 'aria-expanded': false, 'aria-activedescendant': n.length ? n[0].id: undefined }); } }; }()); Ext.aria.AriaController.processClass(Ext.view.BoundList, { role: 'listbox' }); Ext.QuickTips.init(); var bd = Ext.getBody(); /* * ================ Simple form ======================= */ bd.createChild({tag: 'h2', html: 'Form 1 - Very Simple'}); var simple = Ext.create('Ext.form.Panel', { url:'save-form.php', frame:true, title: 'Simple Form', bodyStyle:'padding:5px 5px 0', width: 350, fieldDefaults: { msgTarget: 'side', labelWidth: 75 }, defaultType: 'textfield', defaults: { anchor: '100%' }, items: [{ fieldLabel: 'First Name', name: 'first', allowBlank:false },{ fieldLabel: 'Last Name', name: 'last' },{ fieldLabel: 'Company', name: 'company' }, { fieldLabel: 'Email', name: 'email', vtype:'email' }, { fieldLabel: 'DOB', name: 'dob', xtype: 'datefield' }, { fieldLabel: 'Age', name: 'age', xtype: 'numberfield', minValue: 0, maxValue: 100 }, { xtype: 'timefield', fieldLabel: 'Time', name: 'time', minValue: '8:00am', maxValue: '6:00pm' }], buttons: [{ text: 'Save' },{ text: 'Cancel' }] }); simple.render(document.body); /* * ================ Form 2 ======================= */ bd.createChild({tag: 'h2', html: 'Form 2 - Adding fieldsets'}); var fsf = Ext.create('Ext.form.Panel', { url:'save-form.php', frame:true, title: 'Simple Form with FieldSets', bodyStyle:'padding:5px 5px 0', width: 350, fieldDefaults: { msgTarget: 'side', labelWidth: 75 }, defaults: { anchor: '100%' }, items: [{ xtype:'fieldset', checkboxToggle:true, title: 'User Information', defaultType: 'textfield', collapsed: true, layout: 'anchor', defaults: { anchor: '100%' }, items :[{ fieldLabel: 'First Name', name: 'first', allowBlank:false },{ fieldLabel: 'Last Name', name: 'last' },{ fieldLabel: 'Company', name: 'company' }, { fieldLabel: 'Email', name: 'email', vtype:'email' }] },{ xtype:'fieldset', title: 'Phone Number', collapsible: true, defaultType: 'textfield', layout: 'anchor', defaults: { anchor: '100%' }, items :[{ fieldLabel: 'Home', name: 'home', value: '(888) 555-1212' },{ fieldLabel: 'Business', name: 'business' },{ fieldLabel: 'Mobile', name: 'mobile' },{ fieldLabel: 'Fax', name: 'fax' }] }], buttons: [{ text: 'Save' },{ text: 'Cancel' }] }); fsf.render(document.body); /* * ================ Form 3 ======================= */ bd.createChild({tag: 'h2', html: 'Form 3 - A little more complex'}); var top = Ext.create('Ext.form.Panel', { frame:true, title: 'Multi Column, Nested Layouts and Anchoring', bodyStyle:'padding:5px 5px 0', width: 600, fieldDefaults: { labelAlign: 'top', msgTarget: 'side' }, items: [{ xtype: 'container', anchor: '100%', layout:'column', items:[{ xtype: 'container', columnWidth:.5, layout: 'anchor', items: [{ xtype:'textfield', fieldLabel: 'First Name', name: 'first', anchor:'96%' }, { xtype:'textfield', fieldLabel: 'Company', name: 'company', anchor:'96%' }] },{ xtype: 'container', columnWidth:.5, layout: 'anchor', items: [{ xtype:'textfield', fieldLabel: 'Last Name', name: 'last', anchor:'100%' },{ xtype:'textfield', fieldLabel: 'Email', name: 'email', vtype:'email', anchor:'100%' }] }] }, { xtype: 'htmleditor', name: 'bio', fieldLabel: 'Biography', height: 200, anchor: '100%' }], buttons: [{ text: 'Save' },{ text: 'Cancel' }] }); top.render(document.body); /* * ================ Form 4 ======================= */ bd.createChild({tag: 'h2', html: 'Form 4 - Forms can be a TabPanel...'}); var tabs = Ext.create('Ext.form.Panel', { width: 350, border: false, bodyBorder: false, fieldDefaults: { labelWidth: 75, msgTarget: 'side' }, defaults: { anchor: '100%' }, items: { xtype:'tabpanel', activeTab: 0, defaults:{ bodyStyle:'padding:10px' }, items:[{ title:'Personal Details', defaultType: 'textfield', items: [{ fieldLabel: 'First Name', name: 'first', allowBlank:false, value: 'Ed' },{ fieldLabel: 'Last Name', name: 'last', value: 'Spencer' },{ fieldLabel: 'Company', name: 'company', value: 'Ext JS' }, { fieldLabel: 'Email', name: 'email', vtype:'email' }] },{ title:'Phone Numbers', defaultType: 'textfield', items: [{ fieldLabel: 'Home', name: 'home', value: '(888) 555-1212' },{ fieldLabel: 'Business', name: 'business' },{ fieldLabel: 'Mobile', name: 'mobile' },{ fieldLabel: 'Fax', name: 'fax' }] }] }, buttons: [{ text: 'Save' },{ text: 'Cancel' }] }); tabs.render(document.body); /* * ================ Form 5 ======================= */ bd.createChild({tag: 'h2', html: 'Form 5 - ... and forms can contain TabPanel(s)'}); var tab2 = Ext.create('Ext.form.Panel', { title: 'Inner Tabs', bodyStyle:'padding:5px', width: 600, fieldDefaults: { labelAlign: 'top', msgTarget: 'side' }, defaults: { anchor: '100%' }, items: [{ layout:'column', border:false, items:[{ columnWidth:.5, border:false, layout: 'anchor', defaultType: 'textfield', items: [{ fieldLabel: 'First Name', name: 'first', anchor:'95%' }, { fieldLabel: 'Company', name: 'company', anchor:'95%' }] },{ columnWidth:.5, border:false, layout: 'anchor', defaultType: 'textfield', items: [{ fieldLabel: 'Last Name', name: 'last', anchor:'95%' },{ fieldLabel: 'Email', name: 'email', vtype:'email', anchor:'95%' }] }] },{ xtype:'tabpanel', plain:true, activeTab: 0, height:235, defaults:{bodyStyle:'padding:10px'}, items:[{ title:'Personal Details', defaults: {width: 230}, defaultType: 'textfield', items: [{ fieldLabel: 'First Name', name: 'first', allowBlank:false, value: 'Jamie' },{ fieldLabel: 'Last Name', name: 'last', value: 'Avins' },{ fieldLabel: 'Company', name: 'company', value: 'Ext JS' }, { fieldLabel: 'Email', name: 'email', vtype:'email' }] },{ title:'Phone Numbers', defaults: {width: 230}, defaultType: 'textfield', items: [{ fieldLabel: 'Home', name: 'home', value: '(888) 555-1212' },{ fieldLabel: 'Business', name: 'business' },{ fieldLabel: 'Mobile', name: 'mobile' },{ fieldLabel: 'Fax', name: 'fax' }] },{ cls: 'x-plain', title: 'Biography', layout: 'fit', items: { xtype: 'htmleditor', name: 'bio2', fieldLabel: 'Biography' } }] }], buttons: [{ text: 'Save' },{ text: 'Cancel' }] }); tab2.render(document.body); }); /** * @class Ext.picker.Date * @extends Ext.Component *

A date picker. This class is used by the {@link Ext.form.field.Date} field to allow browsing and * selection of valid dates in a popup next to the field, but may also be used with other components.

*

Typically you will need to implement a handler function to be notified when the user chooses a color from the * picker; you can register the handler using the {@link #select} event, or by implementing the {@link #handler} * method.

*

By default the user will be allowed to pick any date; this can be changed by using the {@link #minDate}, * {@link #maxDate}, {@link #disabledDays}, {@link #disabledDatesRE}, and/or {@link #disabledDates} configs.

*

All the string values documented below may be overridden by including an Ext locale file in your page.

*

Example usage:

*
new Ext.panel.Panel({
    title: 'Choose a future date:',
    width: 200,
    bodyPadding: 10,
    renderTo: Ext.getBody(),
    items: [{
        xtype: 'datepicker',
        minDate: new Date(),
        handler: function(picker, date) {
            // do something with the selected date
        }
    }]
});
* {@img Ext.picker.Date/Ext.picker.Date.png Ext.picker.Date component} * */ Ext.define('Ext.picker.NewDate', { extend: 'Ext.Container', requires: [ 'Ext.data.Model', 'Ext.view.BoundList', 'Ext.XTemplate', 'Ext.button.Button', 'Ext.button.Split', 'Ext.util.ClickRepeater', 'Ext.util.KeyNav', 'Ext.EventObject', 'Ext.fx.Manager', 'Ext.picker.Month' ], alias: 'widget.newdatepicker', alternateClassName: 'Ext.DatePicker', renderTpl: [ '
', '', '
', '', '', '', '
', { longDay: function(value){ return Ext.Date.format(value, this.longDayFormat); } } ], calendarTpl: [ '' + '' + '' + '' + '' + '' + '' + '' + '{#:this.isEndOfWeek}' + '' + '' + '' + '', { cellClass: function(value, data) { }, titleFormat: function(value) { return Ext.date.format(value, this.longDayFormat); }, firstInitial: function(value) { return value.substr(0,1); }, isEndOfWeek: function(value) { // convert from 1 based index to 0 based // by decrementing value once. value--; var end = value % 7 === 0 && value !== 0; return end ? '' : ''; } }], ariaTitle: 'Date Picker', /** * @cfg {String} todayText * The text to display on the button that selects the current date (defaults to 'Today') */ todayText : 'Today', /** * @cfg {Function} handler * Optional. A function that will handle the select event of this picker. * The handler is passed the following parameters:
*/ /** * @cfg {Object} scope * The scope (this reference) in which the {@link #handler} * function will be called. Defaults to this DatePicker instance. */ /** * @cfg {String} todayTip * A string used to format the message for displaying in a tooltip over the button that * selects the current date. Defaults to '{0} (Spacebar)' where * the {0} token is replaced by today's date. */ todayTip : '{0} (Spacebar)', /** * @cfg {String} minText * The error text to display if the minDate validation fails (defaults to 'This date is before the minimum date') */ minText : 'This date is before the minimum date', /** * @cfg {String} maxText * The error text to display if the maxDate validation fails (defaults to 'This date is after the maximum date') */ maxText : 'This date is after the maximum date', /** * @cfg {String} format * The default date format string which can be overriden for localization support. The format must be * valid according to {@link Ext.Date#parse} (defaults to {@link Ext.Date#defaultFormat}). */ /** * @cfg {String} disabledDaysText * The tooltip to display when the date falls on a disabled day (defaults to 'Disabled') */ disabledDaysText : 'Disabled', /** * @cfg {String} disabledDatesText * The tooltip text to display when the date falls on a disabled date (defaults to 'Disabled') */ disabledDatesText : 'Disabled', /** * @cfg {Array} monthNames * An array of textual month names which can be overriden for localization support (defaults to Ext.Date.monthNames) */ /** * @cfg {Array} dayNames * An array of textual day names which can be overriden for localization support (defaults to Ext.Date.dayNames) */ /** * @cfg {String} nextText * The next month navigation button tooltip (defaults to 'Next Month (Control+Right)') */ nextText : 'Next Month (Control+Right)', /** * @cfg {String} prevText * The previous month navigation button tooltip (defaults to 'Previous Month (Control+Left)') */ prevText : 'Previous Month (Control+Left)', /** * @cfg {String} monthYearText * The header month selector tooltip (defaults to 'Choose a month (Control+Up/Down to move years)') */ monthYearText : 'Choose a month (Control+Up/Down to move years)', /** * @cfg {Number} startDay * Day index at which the week should begin, 0-based (defaults to 0, which is Sunday) */ startDay : 0, /** * @cfg {Boolean} showToday * False to hide the footer area containing the Today button and disable the keyboard handler for spacebar * that selects the current date (defaults to true). */ showToday : true, /** * @cfg {Date} minDate * Minimum allowable date (JavaScript date object, defaults to null) */ /** * @cfg {Date} maxDate * Maximum allowable date (JavaScript date object, defaults to null) */ /** * @cfg {Array} disabledDays * An array of days to disable, 0-based. For example, [0, 6] disables Sunday and Saturday (defaults to null). */ /** * @cfg {RegExp} disabledDatesRE * JavaScript regular expression used to disable a pattern of dates (defaults to null). The {@link #disabledDates} * config will generate this regex internally, but if you specify disabledDatesRE it will take precedence over the * disabledDates value. */ /** * @cfg {Array} disabledDates * An array of 'dates' to disable, as strings. These strings will be used to build a dynamic regular * expression so they are very powerful. Some examples: * * Note that the format of the dates included in the array should exactly match the {@link #format} config. * In order to support regular expressions, if you are using a date format that has '.' in it, you will have to * escape the dot when restricting dates. For example: ['03\\.08\\.03']. */ /** * @cfg {Boolean} disableAnim True to disable animations when showing the month picker. Defaults to false. */ disableAnim: false, /** * @cfg {String} baseCls * The base CSS class to apply to this components element (defaults to 'x-datepicker'). */ baseCls: Ext.baseCSSPrefix + 'datepicker', /** * @cfg {String} selectedCls * The class to apply to the selected cell. Defaults to 'x-datepicker-selected' */ /** * @cfg {String} disabledCellCls * The class to apply to disabled cells. Defaults to 'x-datepicker-disabled' */ /** * @cfg {String} longDayFormat * The format for displaying a date in a longer format. Defaults to 'F d, Y' */ longDayFormat: 'F d, Y', /** * @cfg {Object} keyNavConfig Specifies optional custom key event handlers for the {@link Ext.util.KeyNav} * attached to this date picker. Must conform to the config format recognized by the {@link Ext.util.KeyNav} * constructor. Handlers specified in this object will replace default handlers of the same name. */ /** * @cfg {Boolean} focusOnShow * True to automatically focus the picker on show. Defaults to false. */ focusOnShow: false, // private // Set by other components to stop the picker focus being updated when the value changes. focusOnSelect: true, width: 178, // default value used to initialise each date in the DatePicker // (note: 12 noon was chosen because it steers well clear of all DST timezone changes) initHour: 12, // 24-hour format numDays: 42, // private, inherit docs initComponent : function() { var me = this, clearTime = Ext.Date.clearTime; me.selectedCls = me.baseCls + '-selected'; me.disabledCellCls = me.baseCls + '-disabled'; me.prevCls = me.baseCls + '-prevday'; me.activeCls = me.baseCls + '-active'; me.nextCls = me.baseCls + '-prevday'; me.todayCls = me.baseCls + '-today'; me.dayNames = me.dayNames.slice(me.startDay).concat(me.dayNames.slice(0, me.startDay)); this.callParent(); me.value = me.value ? clearTime(me.value, true) : clearTime(new Date()); me.addEvents( /** * @event select * Fires when a date is selected * @param {DatePicker} this DatePicker * @param {Date} date The selected date */ 'select' ); me.initDisabledDays(); }, // private, inherit docs onRender : function(container, position){ /* * days array for looping through 6 full weeks (6 weeks * 7 days) * Note that we explicitly force the size here so the template creates * all the appropriate cells. */ var me = this, days = new Array(me.numDays), today = Ext.Date.format(new Date(), me.format), calendarTpl = Ext.create('Ext.XTemplate', me.calendarTpl); Ext.apply(me.renderData, { dayNames: me.dayNames, ariaTitle: me.ariaTitle, value: me.value, showToday: me.showToday, prevText: me.prevText, nextText: me.nextText, days: days }); me.getTpl('renderTpl').longDayFormat = me.longDayFormat; me.addChildEls('calendarContainer', 'prevEl', 'nextEl', 'middleBtnEl', 'footerEl'); this.callParent(arguments); me.el.unselectable(); // The calendar is a BoundList with aen empty Store. // collectData returns the fields. calendarTpl.longDayFormat = me.longDayFormat; me.calendarView = Ext.create('Ext.view.BoundList', { store: new Ext.data.Store({fields:[]}), collectData: function() { return me.collectData(); }, renderTo: me.calendarContainer, tpl: calendarTpl, itemSelector: 'td', selectedItemCls: me.baseCls + '-selected' }); me.monthBtn = Ext.create('Ext.button.Split', { ownerCt: me, text: '', tooltip: me.monthYearText, renderTo: me.middleBtnEl }); //~ me.middleBtnEl.down('button').addCls(Ext.baseCSSPrefix + 'btn-arrow'); me.todayBtn = Ext.create('Ext.button.Button', { renderTo: me.footerEl, text: Ext.String.format(me.todayText, today), tooltip: Ext.String.format(me.todayTip, today), handler: me.selectToday, scope: me }); }, // Collect a data object for use by the calendar View collectData : function() { var me = this; return { today: new Date(), daynames: me.dayNames.slice(me.startDay).concat(me.dayNames.slice(0, me.startDay)), days: Ext.AbstractView.prototype.apply(me, arguments) }; }, // private, inherit docs initEvents: function(){ var me = this; this.callParent(); me.prevRepeater = Ext.create('Ext.util.ClickRepeater', me.prevEl, { handler: me.showPrevMonth, scope: me, preventDefault: true, stopDefault: true }); me.nextRepeater = Ext.create('Ext.util.ClickRepeater', me.nextEl, { handler: me.showNextMonth, scope: me, preventDefault:true, stopDefault:true }); me.keyNav = Ext.create('Ext.util.KeyNav', me.calendarView.el, Ext.apply({ scope: me, 'left' : function(e){ if(e.ctrlKey){ me.showPrevMonth(); } }, 'right' : function(e){ if(e.ctrlKey){ me.showNextMonth(); } }, 'up' : function(e){ if(e.ctrlKey){ me.showNextYear(); } }, 'down' : function(e){ if(e.ctrlKey){ me.showPrevYear(); } }, 'pageUp' : me.showNextMonth, 'pageDown' : me.showPrevMonth, 'enter' : function(e){ e.stopPropagation(); return true; } }, me.keyNavConfig)); if(me.showToday){ me.todayKeyListener = me.calendarView.el.addKeyListener(Ext.EventObject.SPACE, me.selectToday, me); } me.mon(me.calendarView.el, 'mousewheel', me.handleMouseWheel, me); me.mon(me.monthBtn, 'click', me.showMonthPicker, me); me.mon(me.monthBtn, 'arrowclick', me.showMonthPicker, me); me.update(me.value); }, /** * Setup the disabled dates regex based on config options * @private */ initDisabledDays : function(){ var me = this, dd = me.disabledDates, re = '(?:', len; if(!me.disabledDatesRE && dd){ len = dd.length - 1; Ext.each(dd, function(d, i){ re += Ext.isDate(d) ? '^' + Ext.String.escapeRegex(Ext.Date.dateFormat(d, me.format)) + '$' : dd[i]; if(i != len){ re += '|'; } }, me); me.disabledDatesRE = new RegExp(re + ')'); } }, /** * Replaces any existing disabled dates with new values and refreshes the DatePicker. * @param {Array/RegExp} disabledDates An array of date strings (see the {@link #disabledDates} config * for details on supported values), or a JavaScript regular expression used to disable a pattern of dates. * @return {Ext.picker.Date} this */ setDisabledDates : function(dd){ var me = this; if(Ext.isArray(dd)){ me.disabledDates = dd; me.disabledDatesRE = null; }else{ me.disabledDatesRE = dd; } me.initDisabledDays(); me.update(me.value, true); return me; }, /** * Replaces any existing disabled days (by index, 0-6) with new values and refreshes the DatePicker. * @param {Array} disabledDays An array of disabled day indexes. See the {@link #disabledDays} config * for details on supported values. * @return {Ext.picker.Date} this */ setDisabledDays : function(dd){ this.disabledDays = dd; return this.update(this.value, true); }, /** * Replaces any existing {@link #minDate} with the new value and refreshes the DatePicker. * @param {Date} value The minimum date that can be selected * @return {Ext.picker.Date} this */ setMinDate : function(dt){ this.minDate = dt; return this.update(this.value, true); }, /** * Replaces any existing {@link #maxDate} with the new value and refreshes the DatePicker. * @param {Date} value The maximum date that can be selected * @return {Ext.picker.Date} this */ setMaxDate : function(dt){ this.maxDate = dt; return this.update(this.value, true); }, /** * Sets the value of the date field * @param {Date} value The date to set * @return {Ext.picker.Date} this */ setValue : function(value){ this.value = Ext.Date.clearTime(value, true); return this.update(this.value); }, /** * Gets the current selected value of the date field * @return {Date} The selected date */ getValue : function(){ return this.value; }, // private focus : function(){ this.update(this.activeDate); }, // private, inherit docs onEnable: function(){ this.callParent(); this.setDisabledStatus(false); this.update(this.activeDate); }, // private, inherit docs onDisable : function(){ this.callParent(); this.setDisabledStatus(true); }, /** * Set the disabled state of various internal components * @private * @param {Boolean} disabled */ setDisabledStatus : function(disabled){ var me = this; me.keyNav.setDisabled(disabled); me.prevRepeater.setDisabled(disabled); me.nextRepeater.setDisabled(disabled); if (me.showToday) { me.todayKeyListener.setDisabled(disabled); me.todayBtn.setDisabled(disabled); } }, /** * Get the current active date. * @private * @return {Date} The active date */ getActive: function(){ return this.activeDate || this.value; }, /** * Run any animation required to hide/show the month picker. * @private * @param {Boolean} isHide True if it's a hide operation */ runAnimation: function(isHide){ var options = { duration: 200 }; if (isHide) { this.monthPicker.el.slideOut('t', options); } else { this.monthPicker.el.slideIn('t', options); } }, /** * Hides the month picker, if it's visible. * @return {Ext.picker.Date} this */ hideMonthPicker : function(){ var me = this, picker = me.monthPicker; if (picker) { if (me.disableAnim) { picker.hide(); } else { this.runAnimation(true); } } return me; }, /** * Show the month picker * @return {Ext.picker.Date} this */ showMonthPicker : function(){ var me = this, picker; if (me.rendered && !me.disabled) { picker = me.createMonthPicker(); picker.setValue(me.getActive()); picker.setSize(me.getSize()); picker.setPosition(-1, -1); if (me.disableAnim) { picker.show(); } else { me.runAnimation(false); } } return me; }, /** * Create the month picker instance * @private * @return {Ext.picker.Month} picker */ createMonthPicker: function(){ var me = this, picker = me.monthPicker; if (!picker) { me.monthPicker = picker = Ext.create('Ext.picker.Month', { renderTo: me.el, floating: true, shadow: false, small: me.showToday === false, listeners: { scope: me, cancelclick: me.onCancelClick, okclick: me.onOkClick, yeardblclick: me.onOkClick, monthdblclick: me.onOkClick } }); if (!me.disableAnim) { // hide the element if we're animating to prevent an initial flicker picker.el.setStyle('display', 'none'); } me.on('beforehide', me.hideMonthPicker, me); } return picker; }, /** * Respond to an ok click on the month picker * @private */ onOkClick: function(picker, value){ var me = this, month = value[0], year = value[1], date = new Date(year, month, me.getActive().getDate()); if (date.getMonth() !== month) { // 'fix' the JS rolling date conversion if needed date = new Date(year, month, 1).getLastDateOfMonth(); } me.update(date); me.hideMonthPicker(); }, /** * Respond to a cancel click on the month picker * @private */ onCancelClick: function(){ this.hideMonthPicker(); }, /** * Show the previous month. * @return {Ext.picker.Date} this */ showPrevMonth : function(e){ return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, -1)); }, /** * Show the next month. * @return {Ext.picker.Date} this */ showNextMonth : function(e){ return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, 1)); }, /** * Show the previous year. * @return {Ext.picker.Date} this */ showPrevYear : function(){ this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, -1)); }, /** * Show the next year. * @return {Ext.picker.Date} this */ showNextYear : function(){ this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, 1)); }, /** * Respond to the mouse wheel event * @private * @param {Ext.EventObject} e */ handleMouseWheel : function(e){ e.stopEvent(); if(!this.disabled){ var delta = e.getWheelDelta(); if(delta > 0){ this.showPrevMonth(); } else if(delta < 0){ this.showNextMonth(); } } }, /** * Respond to a date being clicked in the picker * @private * @param {Ext.EventObject} e * @param {HTMLElement} t */ handleDateClick : function(e, t){ var me = this, handler = me.handler; e.stopEvent(); if(!me.disabled && t.dateValue && !Ext.fly(t.parentNode).hasCls(me.disabledCellCls)){ me.cancelFocus = me.focusOnSelect === false; me.setValue(new Date(t.dateValue)); delete me.cancelFocus; me.fireEvent('select', me, me.value); if (handler) { handler.call(me.scope || me, me, me.value); } // event handling is turned off on hide // when we are using the picker in a field // therefore onSelect comes AFTER the select // event. me.onSelect(); } }, /** * Perform any post-select actions * @private */ onSelect: function() { if (this.hideOnSelect) { this.hide(); } }, /** * Sets the current value to today. * @return {Ext.picker.Date} this */ selectToday : function(){ var me = this, btn = me.todayBtn, handler = me.handler; if(btn && !btn.disabled){ me.setValue(Ext.Date.clearTime(new Date())); me.fireEvent('select', me, me.value); if (handler) { handler.call(me.scope || me, me, me.value); } me.onSelect(); } return me; }, /** * Update the selected cell * @private * @param {Date} date The new date * @param {Date} active The active date */ selectedUpdate: function(date, active){ var me = this, t = date.getTime(), cells = me.cells, cls = me.selectedCls; cells.removeCls(cls); cells.each(function(c){ if (c.dom.firstChild.dateValue == t) { me.el.dom.setAttribute('aria-activedescendent', c.dom.id); c.addCls(cls); if(me.isVisible() && !me.cancelFocus){ Ext.fly(c.dom.firstChild).focus(50); } return false; } }, this); }, /** * Update the contents of the picker for a new month * @private * @param {Date} date The new date * @param {Date} active The active date */ fullUpdate: function(date, active){ var me = this, cells = me.cells.elements, textNodes = me.textNodes, disabledCls = me.disabledCellCls, eDate = Ext.Date, i = 0, extraDays = 0, visible = me.isVisible(), sel = +eDate.clearTime(date, true), today = +eDate.clearTime(new Date()), min = me.minDate ? eDate.clearTime(me.minDate, true) : Number.NEGATIVE_INFINITY, max = me.maxDate ? eDate.clearTime(me.maxDate, true) : Number.POSITIVE_INFINITY, ddMatch = me.disabledDatesRE, ddText = me.disabledDatesText, ddays = me.disabledDays ? me.disabledDays.join('') : false, ddaysText = me.disabledDaysText, format = me.format, days = eDate.getDaysInMonth(date), firstOfMonth = eDate.getFirstDateOfMonth(date), startingPos = firstOfMonth.getDay() - me.startDay, previousMonth = eDate.add(date, eDate.MONTH, -1), longDayFormat = me.longDayFormat, prevStart, current, disableToday, tempDate, setCellClass, html, cls, formatValue, value; if (startingPos < 0) { startingPos += 7; } days += startingPos; prevStart = eDate.getDaysInMonth(previousMonth) - startingPos; current = new Date(previousMonth.getFullYear(), previousMonth.getMonth(), prevStart, me.initHour); if (me.showToday) { tempDate = eDate.clearTime(new Date()); disableToday = (tempDate < min || tempDate > max || (ddMatch && format && ddMatch.test(eDate.dateFormat(tempDate, format))) || (ddays && ddays.indexOf(tempDate.getDay()) != -1)); if (!me.disabled) { me.todayBtn.setDisabled(disableToday); me.todayKeyListener.setDisabled(disableToday); } } setCellClass = function(cell){ value = +eDate.clearTime(current, true); cell.title = eDate.format(current, longDayFormat); // store dateValue number as an expando cell.firstChild.dateValue = value; if(value == today){ cell.className += ' ' + me.todayCls; cell.title = me.todayText; } if(value == sel){ cell.className += ' ' + me.selectedCls; me.el.dom.setAttribute('aria-activedescendant', cell.id); if (visible && me.floating) { Ext.fly(cell.firstChild).focus(50); } } // disabling if(value < min) { cell.className = disabledCls; cell.title = me.minText; return; } if(value > max) { cell.className = disabledCls; cell.title = me.maxText; return; } if(ddays){ if(ddays.indexOf(current.getDay()) != -1){ cell.title = ddaysText; cell.className = disabledCls; } } if(ddMatch && format){ formatValue = eDate.dateFormat(current, format); if(ddMatch.test(formatValue)){ cell.title = ddText.replace('%0', formatValue); cell.className = disabledCls; } } }; for(; i < me.numDays; ++i) { if (i < startingPos) { html = (++prevStart); cls = me.prevCls; } else if (i >= days) { html = (++extraDays); cls = me.nextCls; } else { html = i - startingPos + 1; cls = me.activeCls; } textNodes[i].innerHTML = html; cells[i].className = cls; current.setDate(current.getDate() + 1); setCellClass(cells[i]); } me.monthBtn.setText(me.monthNames[date.getMonth()] + ' ' + date.getFullYear()); }, /** * Update the contents of the picker * @private * @param {Date} date The new date * @param {Boolean} forceRefresh True to force a full refresh */ update : function(date, forceRefresh){ var me = this, active = me.activeDate; if (me.rendered) { me.activeDate = date; if(!forceRefresh && active && me.el && active.getMonth() == date.getMonth() && active.getFullYear() == date.getFullYear()){ me.selectedUpdate(date, active); } else { me.fullUpdate(date, active); } } return me; }, // private, inherit docs beforeDestroy : function() { var me = this; if (me.rendered) { Ext.destroy( me.todayKeyListener, me.keyNav, me.monthPicker, me.monthBtn, me.nextRepeater, me.prevRepeater, me.todayBtn ); delete me.textNodes; delete me.cells.elements; } }, // private, inherit docs onShow: function() { this.callParent(arguments); if (this.focusOnShow) { this.focus(); } } }, // After dependencies have loaded: function() { var proto = this.prototype; proto.monthNames = Ext.Date.monthNames; proto.dayNames = Ext.Date.dayNames; proto.format = Ext.Date.defaultFormat; });