// TODO: Implement http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#grid standards
/**
* @class Ext.grid.NavigationModel
* @private
* This class listens for key events fired from a {@link Ext.grid.Panel GridPanel},
* and moves the currently focused item by adding the class {@link #focusCls}.
*/
Ext.define('Ext.grid.NavigationModel', {
extend: 'Ext.view.NavigationModel',
alias: 'view.navigation.grid',
/**
* @event navigate Fired when a key has been used to navigate around the view.
* @param {Object} event
* @param {Ext.event.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 grid cell.
* @param {Ext.grid.Column} event.previousColumn The previously focused grid column.
* @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 grid cell.
* @param {Ext.grid.Column} event.column The newly focused grid column.
*/
/**
* @event cellactivate Fired when a cell is activated while in actionable mode
* @param {Ext.grid.Panel} grid The grid panel that has the cell activated
* @param {Ext.grid.CellContext} position The position in the grid that was activated
* @param {Object} event
*/
focusCls: Ext.baseCSSPrefix + 'grid-item-focused',
constructor: function() {
var me = this;
me.onKeyPageDown = Ext.Function.createThrottled(me.onKeyPageDown, 120, me);
me.onKeyPageUp = Ext.Function.createThrottled(me.onKeyPageUp, 120, me);
me.callParent();
},
getViewListeners: function() {
var me = this;
return {
focusmove: {
element: 'el',
fn: me.onFocusMove
},
containermousedown: me.onContainerMouseDown,
cellmousedown: me.onCellMouseDown,
// We focus on click if the mousedown handler did not focus
// because it was a translated "touchstart" event.
cellclick: me.onCellClick,
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.onItemClick,
scope: me
};
},
initKeyNav: function(view) {
var me = this,
nav;
// We will have two keyNavs if we are the navigation model for a lockable assembly
if (!me.keyNav) {
me.keyNav = [];
me.position = new Ext.grid.CellContext(view);
}
// 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.
nav = new Ext.util.KeyNav({
target: view,
ignoreInputFields: true,
// Must use the same event that form fields use to detect keystrokes.
// keypress happens *after* keydown, but the framework must see key events
// in bubble sequence order
// So a field in actionable mode must see its key event before this nav model.
eventName: Ext.supports.SpecialKeyDownRepeat ? 'itemkeydown' : 'itemkeypress',
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,
esc: me.onKeyEsc,
113: {
// Actionable mode is triggered by F2 key with no modifiers
ctrl: false,
shift: false,
alt: false,
handler: me.onKeyF2
},
tab: me.onKeyTab,
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
});
me.keyNav.push(nav);
me.onKeyNavCreate(nav);
},
onKeyNavCreate: Ext.emptyFn,
addKeyBindings: function(binding) {
var len = this.keyNav.length,
i;
// We will have two keyNavs if we are the navigation model for a lockable assembly
for (i = 0; i < len; i++) {
this.keyNav[i].addBindings(binding);
}
},
enable: function() {
var len = this.keyNav.length,
i;
// We will have two keyNavs if we are the navigation model for a lockable assembly
for (i = 0; i < len; i++) {
this.keyNav[i].enable();
}
this.disabled = false;
},
disable: function() {
var len = this.keyNav.length,
i;
// We will have two keyNavs if we are the navigation model for a lockable assembly
for (i = 0; i < len; i++) {
this.keyNav[i].disable();
}
this.disabled = true;
},
/**
* @private
* Every key event is tagged with the source view, so the NavigationModel is independent.
* Called in the scope of the KeyNav. This function is injected into the NavigationModel's
* {@link Ext.util.KeyNav KeyNav) as its {@link Ext.util.KeyNav#processEvent processEvent}
* config.
*/
processViewEvent: function(view, record, row, recordIndex, event) {
var key = event.getKey();
// In actionable mode, we only listen for TAB, F2 and ESC to exit actionable mode
if (view.actionableMode) {
this.map.ignoreInputFields = false;
if (key === event.TAB || key === event.ESC || key === event.F2) {
return event;
}
}
// In navigation mode, we process all keys
else {
this.map.ignoreInputFields = true;
// Ignore TAB key in navigable mode
return key === event.TAB ? null : event;
}
},
onContainerMouseDown: function(view, mousedownEvent) {
var me = this,
context = new Ext.grid.CellContext(view),
lastFocused, position;
me.callParent([view, mousedownEvent]);
lastFocused = view.lastFocused;
position = (view.actionableMode && view.actionPosition) || lastFocused;
if (!position || lastFocused === 'scrollbar') {
return;
}
context.setPosition(position.record, position.column);
mousedownEvent.position = context;
me.attachClosestCell(mousedownEvent);
// If we are not already on that position, set position there.
if (!me.position.isEqual(context)) {
me.setPosition(context, null, mousedownEvent);
}
},
onCellMouseDown: function(view, cell, cellIndex, record, row, recordIndex, mousedownEvent) {
var me = this,
targetComponent = Ext.Component.from(mousedownEvent, cell),
actionableEl = mousedownEvent.getTarget(me.isFocusableEl, cell),
ac, position;
// If actionable mode, and
// (mousedown on a tabbable, or anywhere in the ownership tree of an inner active
// component), we should just keep the action position synchronized.
// The tabbable element will be part of actionability.
// If the mousedown was NOT on some focusable object, we need to exit actionable mode.
if (view.actionableMode) {
// If mousedown is on a focusable element, or in the component tree of the active
// component (which is NOT this)
if (!actionableEl) {
actionableEl = (ac = Ext.ComponentManager.getActiveComponent()) &&
ac !== view && ac.owns(mousedownEvent);
}
// Attempt to see if we are clicking on another actionable cell.
// findFocusPosition will focus and activate any action on the target cell
// and return the focusTarget item should be activated.
if (me.previousPosition && actionableEl && !targetComponent) {
position = view.focusPosition(cell, mousedownEvent.position);
}
// If the target cell contains an actionable item, update the actionPosition
// and focus on the item.
if (position) {
me.actionPosition = position.view.actionPosition = position;
mousedownEvent.preventDefault();
}
else {
if (actionableEl) {
// Keep actionPosition synched
view.setActionableMode(true, mousedownEvent.position);
}
// Not on anything actionable, then exit actionable mode
else {
view.setActionableMode(false, mousedownEvent.position);
}
}
return;
}
// If the event is a touchstart, leave it until the click to focus.
// Mousedowns may have a focusing effect.
if (mousedownEvent.pointerType !== 'touch') {
if (mousedownEvent.position.column.cellFocusable !== false) {
if (actionableEl) {
// So that the impending onFocusEnter does not
// process the event and delegate focus. We
// control that here. This means disabling tabbability.
if (!view.containsFocus) {
view.containsFocus = true;
view.toggleChildrenTabbability(false);
}
if (view.setActionableMode(true, mousedownEvent.position) !== false) {
actionableEl.focus();
}
}
else {
cell.focus();
}
if (mousedownEvent.button === 2) {
this.fireNavigateEvent(mousedownEvent);
}
// If mousedowning on a focusable Component.
// After having set the position according to the mousedown, we then
// enter actionable mode and focus the component just as if the user
// Had navigated here and pressed F2.
if (targetComponent && targetComponent.isFocusable &&
targetComponent.isFocusable()) {
view.setActionableMode(true, mousedownEvent.position);
// Focus the targeted Component
targetComponent.focus();
}
}
else {
mousedownEvent.preventDefault(true);
}
}
},
onCellClick: function(view, cell, cellIndex, record, row, recordIndex, clickEvent) {
var me = this,
targetComponent = Ext.Component.from(clickEvent, cell),
clickOnFocusable = targetComponent && targetComponent.isFocusable &&
targetComponent.isFocusable();
// If a prior click handler has moved focus out of the view
// we cannot navigate because navigation has moved outside of the view.
// Must check that we contains the focused element, not the containsFocus flag
// because asynchronous focus events might mean that flag is not yet set
// even though the active element is within the view.
if (!Ext.isIE10m && !view.el.contains(Ext.Element.getActiveElement()) &&
clickEvent.pointerType !== 'touch') {
return;
}
// In actionable mode, we fire a navigate event in case the column's
// stopSelection is false
if (view.actionableMode) {
// Only continue if action position is in a different place
// Test using the guaranteed present clickEvent.position.
// actionPosition might be null if action is right now in the other
// side of a lockable.
if (!clickEvent.position.isEqual(view.actionPosition)) {
// Must still set position so that the other actionable
// at the different action position blurs and finishes.
if (!clickOnFocusable) {
view.setActionableMode(false, clickEvent.position);
}
}
me.fireEvent('navigate', {
view: view,
navigationModel: me,
keyEvent: clickEvent,
previousPosition: me.previousPosition,
previousRecordIndex: me.previousRecordIndex,
previousRecord: me.previousRecord,
previousItem: me.previousItem,
previousCell: me.previousCell,
previousColumnIndex: me.previousColumnIndex,
previousColumn: me.previousColumn,
position: clickEvent.position,
recordIndex: clickEvent.position.rowIdx,
record: clickEvent.position.record,
selectionStart: me.selectionStart,
item: clickEvent.item,
cell: clickEvent.position.cellElement,
columnIndex: clickEvent.position.colIdx,
column: clickEvent.position.column
});
}
else {
// If the mousedown that initiated the click has navigated us to the correct spot,
// just fire the event
if (me.position.isEqual(clickEvent.position) || clickOnFocusable) {
// IE10m has asynchronous focus events and the only way to detect
// if something else was focused after onCellMouseDown was executed
// is to verify if navigationModel has a record
//<legacyBrowser>
if (Ext.isIE10m && !me.record) {
return;
}
//</legacyBrowser>
me.fireNavigateEvent(clickEvent);
}
// If the column is focusable, focus the cell.
// The onFocusMove listener will react to the focus change
else if (clickEvent.position.column.cellFocusable !== false) {
me.setPosition(clickEvent.position, null, clickEvent);
}
else {
clickEvent.preventDefault();
}
}
},
/**
* @private
* @param {Ext.event.Event} e The focusmove event
* This is where we are informed of intra-view cell navigation which may be caused
* by screen readers. We have to react to that and keep our internal state consistent.
*/
onFocusMove: function(e) {
var view = Ext.Component.from(e.delegatedTarget, null, 'tableview'),
cell = e.target,
isCell = Ext.fly(cell).is(view.cellSelector),
record, column, newPosition;
if (view) {
// Focus moved to the view - process as if an onfocusEnter
if (e.toElement === view.el.dom) {
view.actionableMode = false;
return view.onFocusEnter(e);
}
// If what was focused was a cell...
if (!view.actionableMode && isCell) {
record = view.getRecord(cell);
column = view.getHeaderByCell(cell);
if (record && column) {
newPosition = new Ext.grid.CellContext(view).setPosition(record, column);
// The focus might have been the *result* of setting the position
if (!newPosition.isEqual(this.position)) {
this.setPosition(newPosition);
}
}
}
else if ((view.actionableMode || view.activating) && !isCell &&
view.el.contains(e.target) && view.el.dom !== e.target) {
view.ownerGrid.fireEvent('cellactivate', view.ownerGrid, view.actionPosition);
}
}
},
onItemMouseDown: function(view, record, item, index, mousedownEvent) {
var me = this,
scroller;
// If the event is a touchstart, leave it until the click to focus
// A mousedown outside a cell. Must be in a Feature, or on a row border
if (!mousedownEvent.position.cellElement && (mousedownEvent.pointerType !== 'touch')) {
// We are going to redirect focus, so do not allow default focus to proceed
// but allow text selection if the view is configured with enableTextSelection
if (!view.enableTextSelection) {
mousedownEvent.preventDefault();
}
// Stamp the closest cell into the event as if it were a cellmousedown
me.attachClosestCell(mousedownEvent);
// If we are not already on that position, set position there.
if (!me.position.isEqual(mousedownEvent.position)) {
me.setPosition(mousedownEvent.position, null, mousedownEvent);
}
// If the browser autoscrolled to bring the cell into focus
// undo that.
scroller = view.getScrollable();
if (scroller) {
scroller.restoreState();
}
}
},
onItemClick: function(view, record, item, index, clickEvent) {
// A mousedown outside a cell. Must be in a Feature, or on a row border
if (!clickEvent.position.cellElement) {
this.attachClosestCell(clickEvent);
// touchstart does not focus the closest cell, leave it until touchend
// (translated as a click)
if (clickEvent.pointerType === 'touch') {
this.setPosition(clickEvent.position, null, clickEvent);
}
else {
this.fireNavigateEvent(clickEvent);
}
}
},
attachClosestCell: function(event) {
var position = event.position,
targetCell = position.cellElement,
x, columns, len, i, b;
if (!targetCell) {
x = event.getX();
columns = position.view.getVisibleColumnManager().getColumns();
len = columns.length;
for (i = 0; i < len; i++) {
b = columns[i].getBox();
if (x >= b.left && x < b.right) {
position.setColumn(columns[i]);
position.rowElement = position.getRow(true);
position.cellElement = position.getCell(true);
return;
}
}
}
},
// eslint-disable-next-line max-len
deferSetPosition: function(delay, recordIndex, columnIndex, keyEvent, suppressEvent, preventNavigation) {
var setPositionTask = this.view.getFocusTask();
// This is essentially a focus operation. Use the singleton focus task
// used by Focusable Components to schedule a setPosition call. This way
// it can be superseded programmatically by regular Component focus calls.
setPositionTask.delay(
delay, this.setPosition, this,
[recordIndex, columnIndex, keyEvent, suppressEvent, preventNavigation]
);
return setPositionTask;
},
setPosition: function(recordIndex, columnIndex, keyEvent, suppressEvent, preventNavigation) {
var me = this,
clearing = recordIndex == null && columnIndex == null,
isClear = me.record == null && me.recordIndex == null && me.item == null,
view, scroller, selModel, dataSource, columnManager,
newRecordIndex, newColumnIndex, newRecord,
newColumn, columns;
// Work out the view we are operating on.
// If they passed a CellContext, use the view from that.
// Otherwise, use the view injected into the event by Ext.view.View#processEvent.
// Otherwise, use the last focused view.
// Failing that, use the view we were bound to.
if (recordIndex && recordIndex.isCellContext) {
view = recordIndex.view;
}
else if (keyEvent && keyEvent.view) {
view = keyEvent.view;
}
else if (me.lastFocused) {
view = me.lastFocused.view;
}
else {
view = me.view;
}
// In case any async focus was requested before this call.
view.cancelFocusTask();
// Return if the view was destroyed between the deferSetPosition call and now,
// or if the call is a no-op or if there are no items which could be focused.
if (view.destroyed || !view.refreshCounter || !view.ownerCt || clearing && isClear ||
!view.all.getCount()) {
return;
}
selModel = view.getSelectionModel();
dataSource = view.dataSource;
columnManager = view.getVisibleColumnManager();
columns = columnManager.getColumns();
// If a CellContext is passed, use it.
// Passing null happens on blur to remove focus class.
if (recordIndex && recordIndex.isCellContext) {
newRecord = recordIndex.record;
newRecordIndex = recordIndex.rowIdx;
newColumnIndex = Math.min(recordIndex.colIdx, columns.length - 1);
newColumn = columns[newColumnIndex];
// If the record being focused is not available (eg, after a removal),
// then go to the same position
if (dataSource.indexOf(newRecord) === -1) {
scroller = view.getScrollable();
// Change recordIndex so that the "No movement" test is bypassed
// if the record is not found
me.recordIndex = -1;
// If the view will not jump upwards to bring the next row under the mouse
// as expected because it's at the end, focus the previous row
// eslint-disable-next-line max-len
if (scroller && (scroller.getPosition().y >= scroller.getMaxPosition().y - view.all.last(true).offsetHeight)) {
recordIndex.rowIdx--;
}
newRecordIndex = Math.min(recordIndex.rowIdx, dataSource.getCount() - 1);
newRecord = dataSource.getAt(newRecordIndex);
}
}
else {
// Both axes are null, we defocus
if (clearing) {
newRecord = newRecordIndex = null;
}
else {
// AbstractView's default behaviour on focus is to call setPosition(0);
// A call like this should default to the last column focused, or column 0;
if (columnIndex == null) {
columnIndex = me.lastFocused ? me.lastFocused.column : 0;
}
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 = recordIndex;
newRecordIndex = dataSource.indexOf(newRecord);
}
// row is a grid row
else if (recordIndex.tagName) {
newRecord = view.getRecord(recordIndex);
newRecordIndex = dataSource.indexOf(newRecord);
if (newRecordIndex === -1) {
newRecord = null;
}
}
else {
if (isClear) {
return;
}
clearing = true;
newRecord = newRecordIndex = null;
}
}
// Record position was successful
if (newRecord) {
// If the record being focused is not available (eg, after a sort), then go to 0,0
if (newRecordIndex === -1) {
// Change recordIndex so that the "No movement" test is bypassed
// if the record is not found
me.recordIndex = -1;
newRecord = dataSource.getAt(0);
newRecordIndex = 0;
columnIndex = null;
}
// No columnIndex passed, and no previous column position - default to column 0
if (columnIndex == null) {
if (!(newColumn = me.column)) {
newColumnIndex = 0;
newColumn = columns[0];
}
}
else if (typeof columnIndex === 'number') {
newColumn = columns[columnIndex];
newColumnIndex = columnIndex;
}
else {
newColumn = columnIndex;
newColumnIndex = columnManager.indexOf(columnIndex);
}
}
else {
clearing = true;
newColumn = newColumnIndex = null;
}
}
// The column requested may have been hidden or removed (eg reconfigure)
// Fall back to column index.
if (newColumn && columnManager.indexOf(newColumn) === -1) {
if (newColumnIndex === -1) {
newColumnIndex = 0;
}
else {
newColumnIndex = Math.min(newColumnIndex, columns.length - 1);
}
newColumn = columns[newColumnIndex];
}
// If we are in actionable mode and focusing a cell, exit actionable mode
// at the requested position
if (view.actionableMode && !clearing) {
return view.ownerGrid.setActionableMode(
false, new Ext.grid.CellContext(view).setPosition(newRecord, newColumn)
);
}
// No movement; just ensure the correct item is focused and return early.
// Do not push current position into previous position, do not fire events.
if (newRecordIndex === me.recordIndex && newColumnIndex === me.columnIndex &&
view === me.position.view) {
return me.focusPosition(me.position);
}
if (me.cell) {
me.cell.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;
me.previousCell = me.cell;
me.previousColumn = me.column;
me.previousColumnIndex = me.columnIndex;
me.previousPosition = me.position.clone();
// Track the last selectionStart position to correctly track ranges
// (i.e., SHIFT + selection).
me.selectionStart = selModel.selectionStart;
// Set our CellContext to the new position
me.position.setAll(
view,
me.recordIndex = newRecordIndex,
me.columnIndex = newColumnIndex,
me.record = newRecord,
me.column = newColumn
);
if (clearing) {
me.item = me.cell = null;
}
else {
me.focusPosition(me.position);
}
// Legacy API is that the SelectionModel fires focuschange events
// and the TableView fires rowfocus and cellfocus events.
if (!suppressEvent) {
selModel.fireEvent('focuschange', selModel, me.previousRecord, me.record);
view.fireEvent('rowfocus', me.record, me.item, me.recordIndex);
view.fireEvent('cellfocus', me.record, me.cell, me.position);
}
// If we have moved, fire an event
if (keyEvent && !preventNavigation && me.cell !== me.previousCell) {
me.fireNavigateEvent(keyEvent);
}
},
/**
* @private
* Focuses the currently active position.
* This is used on view refresh and on replace.
* @return {undefined}
*/
focusPosition: function(position) {
var me = this,
view,
row,
scroller;
me.item = me.cell = null;
if (position && position.record && position.column) {
view = position.view;
// If the position is passed from a grid event, the rowElement will be stamped into it.
// Otherwise, select it from the indicated item.
if (position.rowElement) {
row = me.item = position.rowElement;
}
else {
// Get the dataview item for the position's record
row = view.getRowByRecord(position.record);
// If there is no item at that index, it's probably because
// there's buffered rendering. This is handled below.
}
if (row) {
// If the position is passed from a grid event, the cellElement
// will be stamped into it. Otherwise, select it from the row.
me.cell = position.cellElement ||
Ext.fly(row).down(position.column.getCellSelector(), true);
// Maintain the cell as a Flyweight to avoid transient elements ending up
// in the cache as full Ext.Elements.
if (me.cell) {
me.cell = new Ext.dom.Fly(me.cell);
// Maintain lastFocused in the view so that on non-specific focus of the View,
// we can focus the view's correct descendant.
view.lastFocused = me.lastFocused = me.position.clone();
// Use explicit scrolling rather than relying on the browser's focus behaviour.
// Scroll on focus overscrolls. ensureVisible scrolls exatly correctly.
scroller = view.getScrollable();
if (scroller) {
scroller.ensureVisible(me.cell);
}
me.focusItem(me.cell);
view.focusEl = me.cell;
}
// Cell no longer in view. Clear current position.
else {
me.position.setAll();
me.record = me.column = me.recordIndex = me.columnIndex = null;
}
}
// View node no longer in view. Clear current position.
// Attempt to scroll to the record if it is in the store, but out of rendered range.
else {
row = view.dataSource.indexOf(position.record);
me.position.setAll();
me.record = me.column = me.recordIndex = me.columnIndex = null;
// The reason why the row could not be selected from the DOM could be because it's
// out of rendered range, so scroll to the row, and then try focusing it.
if (row !== -1 && view.bufferedRenderer) {
me.lastKeyEvent = null;
view.bufferedRenderer.scrollTo(row, false, me.afterBufferedScrollTo, me);
}
}
}
},
/**
* @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();
},
getCell: function() {
return this.cell;
},
getPosition: function(skipChecks) {
var me = this,
position = me.position,
curIndex,
view,
dataSource;
if (position.record && position.column) {
// If caller doesn't care whether the record and column is still there,
// just needs to know about focus
if (skipChecks) {
return position;
}
view = position.view;
dataSource = view.dataSource;
curIndex = dataSource.indexOf(position.record);
// If not with the same ID, at the same index if that is in range
if (curIndex === -1) {
curIndex = position.rowIdx;
// If no record now at that index (even if it's less than the totalCount,
// it may be a BufferedStore) then there is no focus position,
// and we must return null
if (!(position.record = dataSource.getAt(curIndex))) {
curIndex = -1;
}
}
// If the positioned record or column has gone away, we have no position
if (curIndex === -1 || view.getVisibleColumnManager().indexOf(position.column) === -1) {
position.setAll();
me.record = me.column = me.recordIndex = me.columnIndex = null;
}
else {
return position;
}
}
return null;
},
getLastFocused: function() {
var me = this,
view,
lastFocused = me.lastFocused;
if (lastFocused && lastFocused.record && lastFocused.column) {
view = lastFocused.view;
// If the last focused record or column has gone away, we have no lastFocused
if (view.dataSource.indexOf(lastFocused.record) !== -1 &&
view.getVisibleColumnManager().indexOf(lastFocused.column) !== -1) {
return lastFocused;
}
}
},
onKeyTab: function(keyEvent) {
var forward = !keyEvent.shiftKey,
view = keyEvent.position.view,
ret, focusTarget, position;
ret = view.findFocusPosition(keyEvent.target, keyEvent.position, forward, keyEvent);
focusTarget = ret.target;
position = ret.position;
// We found a focus target either in the cell or in a sibling cell
// in the direction of navigation.
if (focusTarget) {
// Keep actionPosition synched
this.actionPosition = position.view.actionPosition = position;
Ext.fly(focusTarget).focus();
}
// Focus target not found, we need to exit the row
else {
view.onRowExit(
keyEvent, keyEvent.item,
keyEvent.item[forward ? 'nextSibling' : 'previousSibling'],
forward
);
}
// We control navigation when in actionable mode.
// no TAB events must navigate.
keyEvent.preventDefault();
},
onKeyUp: function(keyEvent) {
var newRecord = keyEvent.view.walkRecs(keyEvent.record, -1),
pos = this.getPosition();
if (newRecord) {
pos.setRow(newRecord);
// If no cell at the current column, move towards row start
if (!pos.getCell(true)) {
pos.navigate(-1);
}
this.setPosition(pos, null, keyEvent);
}
},
onKeyDown: function(keyEvent) {
// If we are in the middle of an animated node expand, jump to next sibling.
// The first child record is in a temp animation DIV and will be removed, so will blur.
// eslint-disable-next-line max-len
var newRecord = keyEvent.record.isExpandingOrCollapsing ? null : keyEvent.view.walkRecs(keyEvent.record, 1),
pos = this.getPosition();
if (newRecord) {
pos.setRow(newRecord);
// If no cell at the current column, move towards row start
if (!pos.getCell(true)) {
pos.navigate(-1);
}
this.setPosition(pos, null, keyEvent);
}
},
onKeyRight: function(keyEvent) {
var newPosition = this.move('right', keyEvent);
if (newPosition) {
this.setPosition(newPosition, null, keyEvent);
}
},
onKeyLeft: function(keyEvent) {
var newPosition = this.move('left', keyEvent);
if (newPosition) {
this.setPosition(newPosition, null, keyEvent);
}
},
// ENTER emulates a dblclick event at the TableView level
onKeyEnter: function(keyEvent) {
var eventArgs = [
'cellclick', keyEvent.view, keyEvent.position.cellElement, keyEvent.position.colIdx,
keyEvent.record, keyEvent.position.rowElement, keyEvent.recordIndex, keyEvent
],
actionCell = keyEvent.position.getCell(true);
// May have been deleted by now by an ActionColumn handler
if (actionCell) {
// 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.
if (!actionCell.querySelector('[tabIndex="-1"]')) {
keyEvent.stopEvent();
keyEvent.view.fireEvent.apply(keyEvent.view, eventArgs);
eventArgs[0] = 'celldblclick';
keyEvent.view.fireEvent.apply(keyEvent.view, eventArgs);
}
// Enters actionable mode. Unless the emulated evenbts have done it
if (!this.view.actionableMode) {
this.view.ownerGrid.setActionableMode(true, this.getPosition());
}
}
},
onKeyF2: function(keyEvent) {
// Toggles actionable mode
var grid = this.view.ownerGrid,
actionableMode = grid.actionableMode;
grid.setActionableMode(!actionableMode, actionableMode ? null : this.getPosition());
},
onKeyEsc: function(keyEvent) {
var grid = this.view.ownerGrid;
// Exits actionable mode
if (grid.actionableMode) {
grid.setActionableMode(false);
}
// If we are NOT in actionable mode, we must return true so that the event is not stopped.
// ESC might be consumed at a higher level - for example an encapsulating Window.
else {
return true;
}
},
move: function(dir, keyEvent) {
var me = this,
position = me.getPosition(),
result = position,
rowVeto = keyEvent.shiftKey && (dir === 'right' || dir === 'left');
if (position && position.record) {
while (result) {
// Do not allow SHIFT+(left|right) to wrap.
// Important to use result.view, since a call to walkCells could change the
// resulting view if we're using locking.
result = result.view.walkCells(result, dir, rowVeto ? me.vetoRowChange : null, me);
// If the new position is fousable, we're done.
if (result && result.getCell(true) && result.column.cellFocusable !== false) {
return result;
}
}
}
//<debug>
// Enforce code correctness in unbuilt source.
return null;
//</debug>
},
vetoRowChange: function(newPosition) {
return this.getPosition().record === newPosition.record;
},
// Go one page down from the lastFocused record in the grid.
onKeyPageDown: function(keyEvent) {
var me = this,
view = Ext.Component.fromElement(keyEvent.target, undefined, 'tableview'),
rowsVisible = me.getRowsVisible(),
newIdx,
newRecord;
if (rowsVisible) {
// If rendering is buffered, we cannot just increment the row - the row may not be there
// We have to ask the BufferedRenderer to navigate to the target.
// And that may involve asynchronous I/O, so must post-process in a callback.
if (view.bufferedRenderer) {
newIdx = Math.min(keyEvent.recordIndex + rowsVisible,
view.dataSource.getCount() - 1);
me.lastKeyEvent = keyEvent;
view.bufferedRenderer.scrollTo(
view.dataSource.getAt(newIdx), false, me.afterBufferedScrollTo, me
);
}
else {
newRecord = view.walkRecs(keyEvent.record, rowsVisible);
me.setPosition(newRecord, null, keyEvent);
}
}
},
// Go one page up from the lastFocused record in the grid.
onKeyPageUp: function(keyEvent) {
var me = this,
view = Ext.Component.fromElement(keyEvent.target, undefined, 'tableview'),
rowsVisible = me.getRowsVisible(),
newIdx,
newRecord;
if (rowsVisible) {
// If rendering is buffered, we cannot just increment the row - the row may not be there
// We have to ask the BufferedRenderer to navigate to the target.
// And that may involve asynchronous I/O, so must post-process in a callback.
if (view.bufferedRenderer) {
newIdx = Math.max(keyEvent.recordIndex - rowsVisible, 0);
me.lastKeyEvent = keyEvent;
view.bufferedRenderer.scrollTo(
view.dataSource.getAt(newIdx), false, me.afterBufferedScrollTo, me
);
}
else {
newRecord = view.walkRecs(keyEvent.record, -rowsVisible);
me.setPosition(newRecord, null, keyEvent);
}
}
},
// Home moves the focus to the first cell of the current row.
onKeyHome: function(keyEvent) {
var me = this,
view = keyEvent.view;
// ALT+Home - go to first visible record in grid.
if (keyEvent.altKey) {
if (view.bufferedRenderer) {
// If rendering is buffered, we cannot just increment the row - the row
// may not be there
// We have to ask the BufferedRenderer to navigate to the target.
// And that may involve asynchronous I/O, so must post-process in a callback.
me.lastKeyEvent = keyEvent;
view.bufferedRenderer.scrollTo(
view.dataSource.first(), false, me.afterBufferedScrollTo, me);
}
else {
// Walk forwards to the first record
me.setPosition(
view.walkRecs(keyEvent.record, -view.dataSource.indexOf(keyEvent.record)),
null, keyEvent
);
}
}
// Home moves the focus to the First cell in the current row.
else {
me.setPosition(keyEvent.record, 0, keyEvent);
}
},
afterBufferedScrollTo: function(newIdx, newRecord) {
this.setPosition(newRecord, null, this.lastKeyEvent, null, !this.lastKeyEvent);
},
// End moves the focus to the last cell in the current row.
onKeyEnd: function(keyEvent) {
var me = this,
view = keyEvent.view;
// ALT/End - go to last visible record in grid.
if (keyEvent.altKey) {
if (view.bufferedRenderer) {
// If rendering is buffered, we cannot just increment the row - the row
// may not be there
// We have to ask the BufferedRenderer to navigate to the target.
// And that may involve asynchronous I/O, so must postprocess in a callback.
me.lastKeyEvent = keyEvent;
view.bufferedRenderer.scrollTo(
view.dataSource.last(), false, me.afterBufferedScrollTo, me
);
}
else {
// Walk forwards to the end record
me.setPosition(
view.walkRecs(
keyEvent.record,
view.dataSource.getCount() - 1 - view.dataSource.indexOf(keyEvent.record)
),
null, keyEvent
);
}
}
// End moves the focus to the last cell in the current row.
else {
me.setPosition(
keyEvent.record,
keyEvent.view.getVisibleColumnManager().getColumns().length - 1,
keyEvent
);
}
},
// Returns the number of rows currently visible on the screen or
// false if there were no rows. This assumes that all rows are
// of the same height and the first view is accurate.
getRowsVisible: function() {
var rowsVisible = false,
view = this.view.ownerGrid.getView(),
firstRow = view.all.first(),
rowHeight, gridViewHeight;
if (firstRow) {
rowHeight = firstRow.getHeight();
if (view.bufferedRenderer) {
gridViewHeight = view.bufferedRenderer.viewClientHeight;
}
else {
gridViewHeight = view.el.getHeight();
}
rowsVisible = Math.floor(gridViewHeight / rowHeight);
}
return rowsVisible;
},
fireNavigateEvent: function(keyEvent) {
var me = this;
me.fireEvent('navigate', {
view: me.position.view,
navigationModel: me,
keyEvent: keyEvent || new Ext.event.Event({}),
previousPosition: me.previousPosition,
previousRecordIndex: me.previousRecordIndex,
previousRecord: me.previousRecord,
previousItem: me.previousItem,
previousCell: me.previousCell,
previousColumnIndex: me.previousColumnIndex,
previousColumn: me.previousColumn,
position: me.position,
recordIndex: me.recordIndex,
record: me.record,
selectionStart: me.selectionStart,
item: me.item,
cell: me.cell,
columnIndex: me.columnIndex,
column: me.column
});
},
isFocusableEl: function(el) {
return Ext.fly(el).isFocusable();
}
});