/** * @class Ext.view.NavigationModel * @private * This class listens for key events fired from a {@link Ext.view.View DataView}, and moves the currently focused item * by adding the class {@link #focusCls}. */Ext.define('Ext.view.NavigationModel', { mixins: [ 'Ext.util.Observable', 'Ext.mixin.Factoryable', 'Ext.util.StoreHolder' ], alias: 'view.navigation.default', config: { store: null }, /** * @event navigate Fired when a key has been used to navigate around the view. * @param {Object} event * @param {Ext.event.Event} keyEvent The key event which caused the navigation. * @param {Number} event.previousRecordIndex The previously focused record index. * @param {Ext.data.Model} event.previousRecord The previously focused record. * @param {HTMLElement} event.previousItem The previously focused view item. * @param {Number} event.recordIndex The newly focused record index. * @param {Ext.data.Model} event.record the newly focused record. * @param {HTMLElement} event.item the newly focused view item. */ /** * @private */ focusCls: Ext.baseCSSPrefix + 'view-item-focused', constructor: function() { this.mixins.observable.constructor.call(this); }, bindComponent: function(view) { if (this.view !== view) { this.view = view; this.bindView(view); } }, bindView: function(view) { var me = this, dataSource = view.dataSource, listeners; me.initKeyNav(view); if (!dataSource.isEmptyStore) { me.setStore(dataSource); } listeners = me.getViewListeners(); listeners.destroyable = true; me.viewListeners = me.viewListeners || []; me.viewListeners.push(view.on(listeners)); }, updateStore: function(store) { this.mixins.storeholder.bindStore.apply(this, [store]); }, getStoreListeners: function() { var me = this; return { // We must process removes before the view has been updated so we can // check if we contain focus, and arrange for refocus in either Navigable or Actionable mode. remove: { fn: me.onStoreRemove, priority: 1000 }, scope: me }; }, getViewListeners: function() { var me = this; return { containermousedown: me.onContainerMouseDown, itemmousedown: me.onItemMouseDown, // We focus on click if the mousedown handler did not focus because it was a translated "touchstart" event. itemclick: me.onItemClick, itemcontextmenu: me.onItemMouseDown, scope: me }; }, initKeyNav: function(view) { var me = this; // Drive the KeyNav off the View's itemkeydown event so that beforeitemkeydown listeners may veto. // By default KeyNav uses defaultEventAction: 'stopEvent', and this is required for movement keys // which by default affect scrolling. me.keyNav = new Ext.util.KeyNav({ target: view, ignoreInputFields: true, eventName: 'itemkeydown', defaultEventAction: 'stopEvent', processEvent: me.processViewEvent, up: me.onKeyUp, down: me.onKeyDown, right: me.onKeyRight, left: me.onKeyLeft, pageDown: me.onKeyPageDown, pageUp: me.onKeyPageUp, home: me.onKeyHome, end: me.onKeyEnd, 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 }, scope: me }); }, processViewEvent: function(view, record, node, index, event) { return event; }, addKeyBindings: function(binding) { this.keyNav.addBindings(binding); }, enable: function() { this.keyNav.enable(); this.disabled = false; }, disable: function() { this.keyNav.disable(); this.disabled = true; }, onContainerMouseDown: function(view, mousedownEvent) { // If already focused, do not disturb the focus. if (this.view.containsFocus) { mousedownEvent.preventDefault(); } }, onItemMouseDown: function(view, record, item, index, mousedownEvent) { var parentEvent = mousedownEvent.parentEvent; // If the ExtJS mousedown event is a translated touchstart, leave it until the click to focus if (!parentEvent || parentEvent.type !== 'touchstart') { this.setPosition(index); } }, onItemClick: function(view, record, item, index, clickEvent) { // If the mousedown that initiated the click has navigated us to the correct spot, just fire the event if (this.record === record) { this.fireNavigateEvent(clickEvent); } else { this.setPosition(index, clickEvent); } }, /** * @template * @protected * Called by {@link Ext.view.AbstractView#method-refresh} before refresh to allow * the current focus position to be cached. * @return {undefined} */ beforeViewRefresh: function() { this.focusRestorePosition = this.view.dataSource.isBufferedStore ? this.recordIndex : this.record; }, /** * @template * @protected * Called by {@link Ext.view.AbstractView#method-refresh} after refresh to allow * cached focus position to be restored. * @return {undefined} */ onViewRefresh: function() { if (this.focusRestorePosition != null) { this.setPosition(this.focusRestorePosition); this.focusRestorePosition = null; } }, onStoreRemove: function(store) { // On record remove, if we contain focus, then arrange to re-establish focus after the remove. var me = this; if (me.record && me.view.el.contains(Ext.Element.getActiveElement())) { // focusExit is ignored during refresh me.view.refreshing = true; Ext.on({ idle: me.afterStoreRemove, scope: me, single: true, args: [me.record, me.recordIndex, store] }); } }, afterStoreRemove: function(lastFocusedRec, lastFocusedIndex, store) { var me = this, view = me.view; view.refreshing = false; // Store is empty - try to go back to what was focused before our View was focused if (!store.getCount()) { me.setPosition(); view.revertFocus(); } // If we lost focus during the delete, re-establish it if (!view.el.contains(Ext.Element.getActiveElement())) { me.setPosition(store.contains(lastFocusedRec) ? lastFocusedRec : lastFocusedIndex, null, null, true); } }, setPosition: function(recordIndex, keyEvent, suppressEvent, preventNavigation) { var me = this, view = me.view, selModel = view.getSelectionModel(), dataSource = view.dataSource, newRecord, newRecordIndex; if (recordIndex == null || !view.all.getCount()) { me.record = me.recordIndex = null; } else { if (typeof recordIndex === 'number') { newRecordIndex = Math.max(Math.min(recordIndex, dataSource.getCount() - 1), 0); newRecord = dataSource.getAt(recordIndex); } // row is a Record else if (recordIndex.isEntity) { newRecord = dataSource.getById(recordIndex.id); newRecordIndex = dataSource.indexOf(newRecord); // Previous record is no longer present; revert to first. if (newRecordIndex === -1) { newRecord = dataSource.getAt(0); newRecordIndex = 0; } } // row is a view item else if (recordIndex.tagName) { newRecord = view.getRecord(recordIndex); newRecordIndex = dataSource.indexOf(newRecord); } else { newRecord = newRecordIndex = null; } } // No change; just ensure the correct item is focused and return early. // Do not push current position into previous position, do not fire events. // We must check record instances, not indices because of store reloads (combobox remote filtering). // If there's a new record, focus it. Note that the index may be different even though // the record is the same (filtering, sorting) if (newRecord === me.record) { me.recordIndex = newRecordIndex; return me.focusPosition(newRecordIndex); } if (me.item) { me.item.removeCls(me.focusCls); } // Track the last position. // Used by SelectionModels as the navigation "from" position. me.previousRecordIndex = me.recordIndex; me.previousRecord = me.record; me.previousItem = me.item; // Update our position me.recordIndex = newRecordIndex; me.record = newRecord; // Prevent navigation if focus has not moved preventNavigation = preventNavigation || me.record === me.lastFocused; // Maintain lastFocused, so that on non-specific focus of the View, we can focus the correct descendant. if (newRecord) { me.focusPosition(me.recordIndex); } else { me.item = null; } if (!suppressEvent) { selModel.fireEvent('focuschange', selModel, me.previousRecord, me.record); } // If we have moved, fire an event if (!preventNavigation && keyEvent) { me.fireNavigateEvent(keyEvent); } }, /** * @private * Focuses the currently active position. * This is used on view refresh and on replace. */ focusPosition: function(recordIndex) { var me = this; if (recordIndex != null && recordIndex !== -1) { if (recordIndex.isEntity) { recordIndex = me.view.dataSource.indexOf(recordIndex); } me.item = me.view.all.item(recordIndex); if (me.item) { me.lastFocused = me.record; me.lastFocusedIndex = me.recordIndex; me.focusItem(me.item); } else { me.record = null; } } else { me.item = null; } }, /** * @template * @protected * Called to focus an item in the client {@link Ext.view.View DataView}. * The default implementation adds the {@link #focusCls} to the passed item focuses it. * Subclasses may choose to keep focus in another target. * * For example {@link Ext.view.BoundListKeyNav} maintains focus in the input field. * @param {Ext.dom.Element} item * @return {undefined} */ focusItem: function(item) { item.addCls(this.focusCls); item.focus(); }, getPosition: function() { return this.record ? this.recordIndex : null; }, getRecordIndex: function() { return this.recordIndex; }, getItem: function() { return this.item; }, getRecord: function() { return this.record; }, getLastFocused: function() { // No longer there. The caller must fall back to a default. if (this.view.dataSource.indexOf(this.lastFocused) === -1) { return null; } return this.lastFocused; }, onKeyUp: function(keyEvent) { var newPosition = this.recordIndex - 1; if (newPosition < 0) { newPosition = this.view.all.getCount() - 1; } this.setPosition(newPosition, keyEvent); }, onKeyDown: function(keyEvent) { var newPosition = this.recordIndex + 1; if (newPosition > this.view.all.getCount() - 1) { newPosition = 0; } this.setPosition(newPosition, keyEvent); }, onKeyRight: function(keyEvent) { var newPosition = this.recordIndex + 1; if (newPosition > this.view.all.getCount() - 1) { newPosition = 0; } this.setPosition(newPosition, keyEvent); }, onKeyLeft: function(keyEvent) { var newPosition = this.recordIndex - 1; if (newPosition < 0) { newPosition = this.view.all.getCount() - 1; } this.setPosition(newPosition, keyEvent); }, onKeyPageDown: Ext.emptyFn, onKeyPageUp: Ext.emptyFn, onKeyHome: function(keyEvent) { this.setPosition(0, keyEvent); }, onKeyEnd: function(keyEvent) { this.setPosition(this.view.all.getCount() - 1, keyEvent); }, onKeySpace: function(keyEvent) { this.fireNavigateEvent(keyEvent); }, // ENTER emulates an itemclick event at the View level onKeyEnter: function(keyEvent) { // Stop the keydown event so that an ENTER keyup does not get delivered to // any element which focus is transferred to in a click handler. keyEvent.stopEvent(); keyEvent.view.fireEvent('itemclick', keyEvent.view, keyEvent.record, keyEvent.item, keyEvent.recordIndex, keyEvent); }, onSelectAllKeyPress: function(keyEvent) { this.fireNavigateEvent(keyEvent); }, fireNavigateEvent: function(keyEvent) { var me = this; me.fireEvent('navigate', { navigationModel: me, keyEvent: keyEvent, previousRecordIndex: me.previousRecordIndex, previousRecord: me.previousRecord, previousItem: me.previousItem, recordIndex: me.recordIndex, record: me.record, item: me.item }); }, destroy: function() { var me = this; me.setStore(null); Ext.destroy(me.viewListeners, me.keyNav); me.keyNav = me.viewListeners = me.dataSource = me.lastFocused = null; me.callParent(); }});