X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/form/field/Picker.js diff --git a/src/form/field/Picker.js b/src/form/field/Picker.js new file mode 100644 index 00000000..6a5f7f91 --- /dev/null +++ b/src/form/field/Picker.js @@ -0,0 +1,276 @@ +/** + * @class Ext.form.field.Picker + * @extends Ext.form.field.Trigger + *

An abstract class for fields that have a single trigger which opens a "picker" popup below + * the field, e.g. a combobox menu list or a date picker. It provides a base implementation for + * toggling the picker's visibility when the trigger is clicked, as well as keyboard navigation + * and some basic events. Sizing and alignment of the picker can be controlled via the {@link #matchFieldWidth} + * and {@link #pickerAlign}/{@link #pickerOffset} config properties respectively.

+ *

You would not normally use this class directly, but instead use it as the parent class for + * a specific picker field implementation. Subclasses must implement the {@link #createPicker} method + * to create a picker component appropriate for the field.

+ * + * @xtype pickerfield + * @constructor + * Create a new picker field + * @param {Object} config + */ +Ext.define('Ext.form.field.Picker', { + extend: 'Ext.form.field.Trigger', + alias: 'widget.pickerfield', + alternateClassName: 'Ext.form.Picker', + requires: ['Ext.util.KeyNav'], + + /** + * @cfg {Boolean} matchFieldWidth + * Whether the picker dropdown's width should be explicitly set to match the width of the field. + * Defaults to true. + */ + matchFieldWidth: true, + + /** + * @cfg {String} pickerAlign + * The {@link Ext.core.Element#alignTo alignment position} with which to align the picker. Defaults + * to "tl-bl?" + */ + pickerAlign: 'tl-bl?', + + /** + * @cfg {Array} pickerOffset + * An offset [x,y] to use in addition to the {@link #pickerAlign} when positioning the picker. + * Defaults to undefined. + */ + + /** + * @cfg {String} openCls + * A class to be added to the field's {@link #bodyEl} element when the picker is opened. Defaults + * to 'x-pickerfield-open'. + */ + openCls: Ext.baseCSSPrefix + 'pickerfield-open', + + /** + * @property isExpanded + * @type Boolean + * True if the picker is currently expanded, false if not. + */ + + /** + * @cfg {Boolean} editable false to prevent the user from typing text directly into the field; + * the field can only have its value set via selecting a value from the picker. In this state, the picker + * can also be opened by clicking directly on the input field itself. + * (defaults to true). + */ + editable: true, + + + initComponent: function() { + this.callParent(); + + // Custom events + this.addEvents( + /** + * @event expand + * Fires when the field's picker is expanded. + * @param {Ext.form.field.Picker} field This field instance + */ + 'expand', + /** + * @event collapse + * Fires when the field's picker is collapsed. + * @param {Ext.form.field.Picker} field This field instance + */ + 'collapse', + /** + * @event select + * Fires when a value is selected via the picker. + * @param {Ext.form.field.Picker} field This field instance + * @param {Mixed} value The value that was selected. The exact type of this value is dependent on + * the individual field and picker implementations. + */ + 'select' + ); + }, + + + initEvents: function() { + var me = this; + me.callParent(); + + // Add handlers for keys to expand/collapse the picker + me.keyNav = Ext.create('Ext.util.KeyNav', me.inputEl, { + down: function() { + if (!me.isExpanded) { + // Don't call expand() directly as there may be additional processing involved before + // expanding, e.g. in the case of a ComboBox query. + me.onTriggerClick(); + } + }, + esc: me.collapse, + scope: me, + forceKeyDown: true + }); + + // Non-editable allows opening the picker by clicking the field + if (!me.editable) { + me.mon(me.inputEl, 'click', me.onTriggerClick, me); + } + + // Disable native browser autocomplete + if (Ext.isGecko) { + me.inputEl.dom.setAttribute('autocomplete', 'off'); + } + }, + + + /** + * Expand this field's picker dropdown. + */ + expand: function() { + var me = this, + bodyEl, picker, collapseIf; + + if (me.rendered && !me.isExpanded && !me.isDestroyed) { + bodyEl = me.bodyEl; + picker = me.getPicker(); + collapseIf = me.collapseIf; + + // show the picker and set isExpanded flag + picker.show(); + me.isExpanded = true; + me.alignPicker(); + bodyEl.addCls(me.openCls); + + // monitor clicking and mousewheel + me.mon(Ext.getDoc(), { + mousewheel: collapseIf, + mousedown: collapseIf, + scope: me + }); + + me.fireEvent('expand', me); + me.onExpand(); + } + }, + + onExpand: Ext.emptyFn, + + /** + * @protected + * Aligns the picker to the + */ + alignPicker: function() { + var me = this, + picker, isAbove, + aboveSfx = '-above'; + + if (this.isExpanded) { + picker = me.getPicker(); + if (me.matchFieldWidth) { + // Auto the height (it will be constrained by min and max width) unless there are no records to display. + picker.setSize(me.bodyEl.getWidth(), picker.store && picker.store.getCount() ? null : 0); + } + if (picker.isFloating()) { + picker.alignTo(me.inputEl, me.pickerAlign, me.pickerOffset); + + // add the {openCls}-above class if the picker was aligned above + // the field due to hitting the bottom of the viewport + isAbove = picker.el.getY() < me.inputEl.getY(); + me.bodyEl[isAbove ? 'addCls' : 'removeCls'](me.openCls + aboveSfx); + picker.el[isAbove ? 'addCls' : 'removeCls'](picker.baseCls + aboveSfx); + } + } + }, + + /** + * Collapse this field's picker dropdown. + */ + collapse: function() { + if (this.isExpanded && !this.isDestroyed) { + var me = this, + openCls = me.openCls, + picker = me.picker, + doc = Ext.getDoc(), + collapseIf = me.collapseIf, + aboveSfx = '-above'; + + // hide the picker and set isExpanded flag + picker.hide(); + me.isExpanded = false; + + // remove the openCls + me.bodyEl.removeCls([openCls, openCls + aboveSfx]); + picker.el.removeCls(picker.baseCls + aboveSfx); + + // remove event listeners + doc.un('mousewheel', collapseIf, me); + doc.un('mousedown', collapseIf, me); + + me.fireEvent('collapse', me); + me.onCollapse(); + } + }, + + onCollapse: Ext.emptyFn, + + + /** + * @private + * Runs on mousewheel and mousedown of doc to check to see if we should collapse the picker + */ + collapseIf: function(e) { + var me = this; + if (!me.isDestroyed && !e.within(me.bodyEl, false, true) && !e.within(me.picker.el, false, true)) { + me.collapse(); + } + }, + + /** + * Return a reference to the picker component for this field, creating it if necessary by + * calling {@link #createPicker}. + * @return {Ext.Component} The picker component + */ + getPicker: function() { + var me = this; + return me.picker || (me.picker = me.createPicker()); + }, + + /** + * Create and return the component to be used as this field's picker. Must be implemented + * by subclasses of Picker. + * @return {Ext.Component} The picker component + */ + createPicker: Ext.emptyFn, + + /** + * Handles the trigger click; by default toggles between expanding and collapsing the + * picker component. + */ + onTriggerClick: function() { + var me = this; + if (!me.readOnly && !me.disabled) { + if (me.isExpanded) { + me.collapse(); + } else { + me.expand(); + } + me.inputEl.focus(); + } + }, + + mimicBlur: function(e) { + var me = this, + picker = me.picker; + // ignore mousedown events within the picker element + if (!picker || !e.within(picker.el, false, true)) { + me.callParent(arguments); + } + }, + + onDestroy : function(){ + var me = this; + Ext.destroy(me.picker, me.keyNav); + me.callParent(); + } + +}); +