/** * An abstract class for fields that have a single trigger which opens a "picker" popup * above the field. It provides a base implementation for toggling the picker's * visibility when the trigger is tapped. * * You would not normally use this class directly, but instead use it as the parent * class for a specific picker field implementation. */Ext.define('Ext.field.Picker', { extend: 'Ext.field.Text', xtype: 'pickerfield', requires: [ 'Ext.field.trigger.Expand' ], config: { /** * @cfg {String/Object} [picker='auto'] * * A string representing the type of picker to use. Can be one of the following values. * * - `'edge'` to use the {@link #edgePicker}, generally used on small formfactor devices. * - `'floated'` to use the {@link #floatedPicker}, generally used on tablets or desktops. * - `'auto'` to allow the framework to select the appropriate picker for the device. * * Can also be a config object for the picker. * */ picker: { lazy: true, $value: 'auto' }, /** * @cfg {Object} floatedPicker * A configuration object, containing an {@link cfg#xtype} property which specifies * the widget to create if `{@link #cfg!picker}: 'floated'` (or if it's '`auto'` and * the app is *not* on a phone). * * Replaces `defaultTabletPicker` * @since 6.5.0 */ floatedPicker: { lazy: true, $value: null }, /** * @cfg {Object} edgePicker * A configuration object, containing an {@link cfg#xtype} property which specifies * the widget to create if `{@link #cfg!picker}: 'edge'` (or if it's '`auto'` and the * app is on a phone). * * Replaces `defaultPhonePicker` * @since 6.5.0 */ edgePicker: { lazy: true, $value: null }, clearable: false, /** * @cfg {Boolean} [matchFieldWidth=true] * *Only valid when the `{@link #cfg!picker}: 'floated'` is used. * Whether the {@link #cfg!floatedPicker}'s width should be explicitly set to match * the width of the input element. */ matchFieldWidth: true, /** * @cfg {String} [floatedPickerAlign=tl-bl?] * *Only valud when the {@link #cfg!floatedPicker} is used. * The {@link Ext.Component#method!showBy} alignment string to use when showing the * floated picker by the input field. */ floatedPickerAlign: 'tl-bl?', /** * @cfg {String} pickerSlotAlign * The alignment of text in the picker created by this Select * @private */ pickerSlotAlign: 'center', /** * @cfg {Boolean} hideTrigger * `true` to hide the expand {@link #triggers trigger}. */ hideTrigger: false, /** * @private */ focusTrap: { lazy: true, $value: { tabIndex: -1, cls: 'x-hidden-clip' } } }, triggers: { expand: { type: 'expand' } }, /** * @cfg {String} alignTarget * The element reference to which the {@link #cfg!picker floated picker} aligns * and sizes to. By default, it sizes to the `bodyElement` which encapsulates the * input field and triggers. * * An alternate value which may be useful if using `floated` pickers on phone platforms * could be `el`, to align the picker to the field's encapsulating element. */ alignTarget: 'bodyElement', keyMap: { scope: 'this', DOWN: 'onDownArrow', ESC: 'onEsc' }, keyMapTarget: 'inputElement', /** * @cfg {Boolean} [autoComplete=false] * Autocomplete is disabled on Picker fields by default. */ autoComplete: false, classCls: Ext.baseCSSPrefix + 'pickerfield', /** * @event expand * Fires when the field's picker is expanded. * @param {Ext.form.field.Picker} field This field instance */ /** * @event collapse * Fires when the field's picker is collapsed. * @param {Ext.form.field.Picker} field This field instance */ /** * @private */ initialize: function() { var me = this; me.callParent(); Ext.on('hide', 'onGlobalHide', me); me.inputElement.on('click', 'onInputElementClick', me); }, onFocusEnter: function(info) { this.callParent([info]); if (Ext.isTouchMode() && info.event.toElement === this.inputElement.dom) { this.getFocusTrap().focus(); this.expand(); } }, onFocusMove: function(info) { var me = this, focusTrap; me.callParent([info]); if (Ext.isTouchMode()) { // Avoid creating the focus trap if not in touch mode focusTrap = me.getFocusTrap(); if (info.fromElement === focusTrap.dom && info.toElement === me.getFocusEl().dom) { if (me.getEditable()) { // virtual keyboard is about to display. collapse the picker me.collapse(); } else { // No keyboard can be displayed, so ensure the picker // is always visible focusTrap.focus(); me.expand(); } } } }, onFocusLeave: function(e) { this.callParent([e]); // Callparent first; collapse listener needs to read correct containsFocus state this.collapse(); }, /** * @private */ onEsc: function(e) { if (Ext.isIE) { // Stop the esc key from "restoring" the previous value in IE // For example, type "foo". Highlight all the text, hit backspace. // Hit esc, "foo" will be restored. This behaviour doesn't occur // in any other browsers e.preventDefault(); } if (this.expanded) { this.collapse(); e.stopEvent(); } }, onDownArrow: function(e) { var me = this; if ((e.time - me.lastDownArrow) > 150) { delete me.lastDownArrow; } if (!me.expanded) { // Do not let the down arrow event propagate into the picker e.stopEvent(); // Don't call expand() directly as there may be additional processing involved before // expanding, e.g. in the case of a ComboBox query. me.onExpandTap(); // Tell setPickerLocation that it's invoked from the keyboard so // that it may set the location regardless of other settings. // For example, ComboBox has autoSelect and autoSelectLast which *may* // be set to false for some applications. This information // can override that. me.setPickerLocation(true); me.lastDownArrow = e.time; } else if (!e.stopped && (e.time - me.lastDownArrow) < 150) { delete me.lastDownArrow; } }, /** * @template * @method * @param {Boolean} [fromKeyboard=false] Passed as `true` if the keyboard was used * to open the picker. This usually means that picker location should be set. * * A function which may be implemented in subclasses which moves the focus * to the value in the {@link #cfg!picker} which matches this field's value. * * For example, if you were writing a time picker, this method would be where * you synced the picker's value with the field's value. */ setPickerLocation: Ext.emptyFn, updateHideTrigger: function(hideTrigger) { var triggers = this.getTriggers(), expand = triggers && triggers.expand; if (expand) { expand.setHidden(hideTrigger); } }, applyPicker: function(picker) { var me = this, pickerListeners = { show: 'onPickerShow', hide: 'onPickerHide', scope: me }, type = picker, config; if (!type) { type = 'auto'; } else if (Ext.isObject(picker)) { type = null; if (!picker.isWidget && !picker.xtype) { config = picker; type = 'auto'; } } if (type) { if (type === 'auto') { type = me.getAutoPickerType(); } if (type === 'edge') { picker = me.createEdgePicker(config); } //<debug> else if (type !== 'floated') { Ext.raise('Picker type must be "edge" or "floated" not "' + type + '"'); } //</debug> else { picker = me.createFloatedPicker(config); pickerListeners.resize = pickerListeners.hiddenchange = 'realignFloatedPicker'; } } if (picker.isWidget) { picker.ownerField = me; } else { picker = Ext.apply({ ownerField: me }, picker); // Allow mutation of the picker configuration me.fireEvent('beforepickercreate', me, picker); picker = Ext.create(picker); } // Detect whether we are using a floated or edge picker. me.pickerType = type || (picker.isViewportMenu ? 'edge' : 'floated'); // Allow configuration of the instantiated picker me.fireEvent('pickercreate', me, picker); picker.on(pickerListeners); return picker; }, getAutoPickerType: function() { return Ext.platformTags.phone ? 'edge' : 'floated'; }, getRefItems: function(deep) { var me = this, result = me.callParent([deep]), picker = me.getConfig('picker', false, true); // Return our picker. if (picker) { result.push(picker); // And, if deep, the picker's refItems if (deep) { Ext.Array.push(result, picker.getRefItems(deep)); } } return result; }, updatePicker: function(picker) { var value = this.getValue(); if (picker && picker.setValue && value != null) { if (this.pickerType === 'floated' || picker.isPicker) { picker.setValue(value); } } }, applyFocusTrap: function(focusTrap) { var result = this.el.appendChild(Ext.dom.Element.create(focusTrap)); // Flag to indicate that it should not be considered for programmatic focus. // For example Grid Location actionable navigation ignores elements // with this property set when searching for actionable elements. result.$isFocusTrap = true; return result; }, onResize: function() { // See if the picker has been created var me = this, picker = me.getConfig('picker', false, true), GlobalEvents = Ext.GlobalEvents, scrollableAncestor; if (picker && me.pickerType === 'floated' && picker.isVisible()) { // Ensure we are completely visible, so that the // realigned picker aligns on a visible edge. scrollableAncestor = me.up('[scrollable]'); if (scrollableAncestor) { scrollableAncestor = scrollableAncestor.getScrollable(); GlobalEvents.suspendEvent('scroll'); scrollableAncestor.ensureVisible(me.el); } me.realignFloatedPicker(); if (scrollableAncestor) { // Defer the resumption of scroll event, otherwse it will fire // asynchronously from the above scroll and cause field collpse. Ext.defer(GlobalEvents.resumeEvent, 100, GlobalEvents, ['scroll']); } } }, /** * @private */ realignFloatedPicker: function(picker) { var me = this; picker = picker || me.getConfig('picker', false, true); if (picker && picker.isVisible()) { if (me.getMatchFieldWidth()) { picker.setWidth(me[me.alignTarget].getWidth()); } picker.realign(me[me.alignTarget], me.getFloatedPickerAlign(), { minHeight: 100 }); // If some keyboard gesture caused this, then there is an active location // which we don't want to disturb. if (!Ext.keyboardMode) { me.setPickerLocation(); } } }, onInputElementClick: function(e) { var me = this; if (e.pointerType === 'mouse' && (!me.getEditable() && !me.getReadOnly())) { me[me.expanded ? 'collapse' : 'expand'](); } }, onExpandTap: function() { if (this.expanded) { // Check the expended time to check that we are not being called in the immediate // aftermath of an expand. The reason being that expandTrigger does focusOnTap // and Picker fields expand on focus if the focus happened via touch. // But then, when the expandTrigger calls its handler, we get here immediately // and do a collapse. if (Ext.now() - this.expanded > 100) { this.collapse(); } } else { this.expand(); } return false; }, expand: function() { if (!this.expanded && !this.getReadOnly() && !this.getDisabled()) { this.showPicker(); } }, collapse: function() { var picker, eXt = Ext; // hide from Cmd if (this.expanded) { picker = this.getPicker(); // If we are collapsing an edge picker, we must not leave it as the default // edge swipe menu for that side. It must only be shown by the trigger (or // touch-tapping the unfocused field) if (this.pickerType === 'edge') { eXt.Viewport.removeMenu(picker.getSide(), true); } else { picker.hide(); } } }, /** * @private * Runs on touchstart of doc to check to see if we should collapse the picker. */ collapseIf: function(e) { var me = this; // If what was mousedowned on is outside of this Field, then collapse. if (!me.destroyed && (!e.within(me.bodyElement, false, true) && !me.owns(e.target))) { // If they have clicked on a focusable, we will let the default browser behaviour // take its course. // If they clicked on non-focusable content, then do not blur the input field, but // allow automatic focus reversion to jump safely back into the field. // TODO: // if (!Ext.fly(e.target).isFocusable()) { // // Don't blur the input field // e.preventDefault(); // } me.collapse(); } }, showPicker: function() { var me = this, alignTarget = me[me.alignTarget], picker = me.getPicker(); // TODO: what if virtual keyboard is present if (me.pickerType === 'floated') { if (me.getMatchFieldWidth()) { picker.setWidth(alignTarget.getWidth()); } picker.showBy(alignTarget, me.getFloatedPickerAlign(), { minHeight: 100 }); // Collapse on touch outside this component tree. // Because touch platforms do not focus document.body on touch // so no focusleave would occur to trigger a collapse. me.touchListeners = Ext.getDoc().on({ // Do not translate on non-touch platforms. // mousedown will blur the field. translate: false, touchstart: me.collapseIf, scope: me, delegated: false, destroyable: true }); } else { me.setShowPickerValue(picker); picker.show(); } }, updatePickerValue: function(picker, value) { var slot = picker.getSlots()[0], name = slot.name || slot.getName(), pickerValue = {}; pickerValue[name] = value; picker.setValue(pickerValue); }, onPickerShow: function() { var me = this; me.expanded = Ext.now(); // If there's an edge picker encroaching, then ensure this field is still visible. if (me.pickerType === 'edge') { me.el.dom.scrollIntoView(); } // If there's an edge picker encroaching, then ensure this field is still visible. if (me.pickerType === 'edge') { me.el.dom.scrollIntoView(); } // We have to explicitly hide on any pointer event outside the field's component tree // relying on focus is not enough because you can mousedown on a window header and // drag it, and the default will be prevented. // Scrolling of anything which causes this field to move should collapse me.hideEventListeners = Ext.on({ mousedown: 'collapseIf', scroll: 'onGlobalScroll', scope: me, destroyable: true }); me.fireEvent('expand', me); }, onPickerHide: function() { var me = this; me.expanded = false; Ext.destroy(me.hideEventListeners, me.touchListeners); me.fireEvent('collapse', me); }, doDestroy: function() { this.destroyMembers('picker', 'hideEventListeners', 'touchListeners', 'focusTrap'); this.callParent(); }, privates: { isFocusing: function(info) { return info.event.toElement === this.getFocusTrap().dom || this.callParent([info]); }, isBlurring: function(info) { return info.event.fromElement === this.getFocusTrap().dom || this.callParent([info]); }, onGlobalHide: function(cmp) { // hide picker if ancestor is hidden if (this === cmp || cmp.isAncestor(this)) { this.collapse(); } }, onGlobalScroll: function(scroller, x, y) { var me = this, scrollingEl = scroller.getElement(); if (me.expanded) { // Edge pickers are modal and anchored. We do not care if other // parts of the app scroll. if (me.pickerType === 'edge') { return; } // Collapse if the scroll is anywhere but inside the picker // Also ignore body element scrolling, that won't affect the alignment. if (!me.getPicker().owns(scrollingEl) && scrollingEl.dom !== document.body) { me.collapse(); } } }, revertFocusTo: function(target) { if (Ext.isTouchMode()) { this.getFocusTrap().focus(); } else { target.focus(); } }, setShowPickerValue: function(picker) { var value = this.getValue(); if (value != null) { this.updatePickerValue(picker, value); } } }});