text: /**
   * @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]);
      },
  
      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 the mousedown in the view element is NOT inside the client region,
          // that is, it was on a scrollbar, then prevent default.
          //
          // Mousedowning on a scrollbar will focus the View.
          // If they have scrolled to the bottom, then onFocusEnter will
          // try to focus the lastFocused or first item. This is undesirable.
          // So on mousedown outside of view client area, prevent the default focus behaviour.
          // See Ext.view.Table#onFocusEnter for this being acted upon.
          if (Ext.getScrollbarSize().width) {
              if (!view.el.getClientRegion().contains(mousedownEvent.getPoint())) {
                  mousedownEvent.preventDefault();
                  view.lastFocused = 'scrollbar';
              }
          }
      },
  
      onItemMouseDown: function(view, record, item, index, mousedownEvent) {
          // If the event is a touchstart, leave it until the click to focus.
          if (mousedownEvent.pointerType !== 'touch') {
              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);
          }
      },
  
      setPosition: function(recordIndex, keyEvent, suppressEvent, preventNavigation, preventFocus) {
          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 if (!preventFocus) {
              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() {
          this.setStore(null);
          Ext.destroy(this.viewListeners, this.keyNav);
          
          this.callParent();
      }
  });
  