/** * A specialized {@link Ext.util.KeyNav} implementation for navigating a {@link Ext.view.BoundList} * using the keyboard. The up, down, pageup, pagedown, home, and end keys move the active highlight * through the list. The enter key invokes the selection model's select action using the highlighted * item. */Ext.define('Ext.view.BoundListKeyNav', { extend: 'Ext.view.NavigationModel', alias: 'view.navigation.boundlist', /** * @cfg {Ext.view.BoundList} boundList (required) * The {@link Ext.view.BoundList} instance for which key navigation will be managed. */ navigateOnSpace: true, initKeyNav: function(view) { var me = this, field = view.pickerField; // Add the regular KeyNav to the view. // Unless it's already been done (we may have to defer a call until the field is rendered. if (!me.keyNav) { me.callParent([view]); // Add ESC handling to the View's KeyMap to collapse the field me.keyNav.map.addBinding({ key: Ext.event.Event.ESC, fn: me.onKeyEsc, scope: me }); } // BoundLists must be able to function standalone with no bound field if (!field) { return; } if (!field.rendered) { field.on( 'render', Ext.Function.bind(me.initKeyNav, me, [view], 0), me, { single: true } ); return; } // BoundListKeyNav also listens for key events from the field to which it is bound. me.fieldKeyNav = new Ext.util.KeyNav({ disabled: true, target: field.inputEl, forceKeyDown: true, up: me.onKeyUp, down: me.onKeyDown, right: me.onKeyRight, left: me.onKeyLeft, pageDown: me.onKeyPageDown, pageUp: me.onKeyPageUp, home: me.onKeyHome, end: me.onKeyEnd, tab: me.onKeyTab, space: me.onKeySpace, enter: me.onKeyEnter, A: { ctrl: true, // Need a separate function because we don't want the key // events passed on to selectAll (causes event suppression). handler: me.onSelectAllKeyPress }, // This object has to get its key processing in first. // Specifically, before any Editor's key hyandling. priority: 1001, scope: me }); }, processViewEvent: function(view, record, node, index, event) { // Event is valid if it is from within the list if (event.within(view.listWrap)) { return event; } // If not from within the list, we're only interested in ESC. // Defeat the NavigationModel's ignoreInputFields for that. if (event.getKey() === event.ESC) { if (Ext.fly(event.target).isInputField()) { event.target = event.target.parentNode; } return event; } // Falsy return stops the KeyMap processing the event }, enable: function() { this.fieldKeyNav.enable(); this.callParent(); }, disable: function() { this.fieldKeyNav.disable(); this.callParent(); }, onItemMouseDown: function(view, record, item, index, event) { this.callParent([view, record, item, index, event]); if (event.pointerType === 'mouse') { // Stop the mousedown from blurring the input field // We can't do this for touch events otherwise scrolling // won't work. event.preventDefault(); } }, onKeyUp: function(e) { var me = this, boundList = me.view, allItems = boundList.all, oldItem = boundList.highlightedItem, oldItemIdx = oldItem ? boundList.indexOf(oldItem) : -1, newItemIdx = oldItemIdx > 0 ? oldItemIdx - 1 : allItems.getCount() - 1; // wraps around me.setPosition(newItemIdx); // Stop this from moving the cursor in the field e.preventDefault(); }, onKeyDown: function(e) { var me = this, boundList = me.view, allItems = boundList.all, oldItem = boundList.highlightedItem, oldItemIdx = oldItem ? boundList.indexOf(oldItem) : -1, newItemIdx = oldItemIdx < allItems.getCount() - 1 ? oldItemIdx + 1 : 0; // wraps around me.setPosition(newItemIdx); // Stop this from moving the cursor in the field e.preventDefault(); }, onKeyLeft: Ext.returnTrue, onKeyRight: Ext.returnTrue, onKeyTab: function(e) { var view = this.view, field = view.pickerField; if (view.isVisible()) { if (field.selectOnTab) { this.selectHighlighted(e); } if (field.collapse) { field.collapse(); } } // Tab key event is allowed to propagate to field return true; }, onKeyEnter: function(e) { var view = this.view, selModel = view.getSelectionModel(), field = view.pickerField, count = selModel.getCount(); // Stop the keydown event so that an ENTER keyup does not get delivered to // any element which focus is transferred to in a select handler. e.stopEvent(); this.selectHighlighted(e); // Handle the case where the highlighted item is already selected // In this case, the change event won't fire, so just collapse if (!field.multiSelect && count === selModel.getCount() && field.collapse) { field.collapse(); } // Stop propagation of the ENTER keydown event so that any Editor which owns the field // does not completeEdit, but we also need to still fire the specialkey event for ENTER, // so lets add fromBoundList to eOpts, and this will be handled by CellEditor#onSpecialKey. field.fireEvent('specialkey', field, e, { fromBoundList: true }); return false; }, onKeySpace: function() { if (this.navigateOnSpace) { this.callParent(arguments); } // Allow to propagate to field return true; }, onKeyEsc: function() { if (this.view.pickerField) { this.view.pickerField.collapse(); } }, /** * Highlights the item at the given index. * @param {Number} item */ focusItem: function(item) { var me = this, boundList = me.view; if (typeof item === 'number') { item = boundList.all.item(item); } if (item) { item = item.dom; boundList.highlightItem(item); boundList.getScrollable().ensureVisible(item, { x: false }); } }, /** * Triggers selection of the currently highlighted item according to the behavior of * the configured SelectionModel. */ selectHighlighted: function(e) { var me = this, boundList = me.view, selModel = boundList.getSelectionModel(), highlightedRec, highlightedPosition = me.recordIndex; // If all options have been filtered out, then do NOT add most recently highlighted. if (boundList.all.getCount()) { highlightedRec = me.getRecord(); if (highlightedRec) { // Select if not already selected. // If already selected, selecting with no CTRL flag will deselect the record. if (e.getKey() === e.ENTER || !selModel.isSelected(highlightedRec)) { selModel.selectWithEvent(highlightedRec, e); // If the result of that selection is that the record is removed // or filtered out, jump to the next one. if (!boundList.store.data.contains(highlightedRec)) { me.setPosition( Math.min(highlightedPosition, boundList.store.getCount() - 1) ); } } } } }, destroy: function() { this.fieldKeyNav = Ext.destroy(this.fieldKeyNav); this.callParent(); }});