messengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscord
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
442 lines
14 KiB
442 lines
14 KiB
9 years ago
|
/**
|
||
|
* @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'
|
||
|
],
|
||
|
|
||
|
alias: 'view.navigation.default',
|
||
|
|
||
|
/**
|
||
|
* @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 (me.dataSource !== dataSource) {
|
||
|
me.dataSource = dataSource;
|
||
|
listeners = me.getStoreListeners();
|
||
|
listeners.destroyable = true;
|
||
|
me.dataSourceListeners = view.dataSource.on(listeners);
|
||
|
}
|
||
|
listeners = me.getViewListeners();
|
||
|
listeners.destroyable = true;
|
||
|
me.viewListeners = me.viewListeners || [];
|
||
|
me.viewListeners.push(view.on(listeners));
|
||
|
},
|
||
|
|
||
|
getStoreListeners: function() {
|
||
|
var me = this;
|
||
|
|
||
|
return {
|
||
|
clear: me.onStoreClear,
|
||
|
remove: me.onStoreRemove,
|
||
|
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,
|
||
|
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
|
||
|
},
|
||
|
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.
|
||
|
*/
|
||
|
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.
|
||
|
*/
|
||
|
onViewRefresh: function() {
|
||
|
if (this.focusRestorePosition != null) {
|
||
|
this.setPosition(this.focusRestorePosition);
|
||
|
this.focusRestorePosition = null;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// Store clearing removes focus
|
||
|
onStoreClear: function() {
|
||
|
this.setPosition();
|
||
|
},
|
||
|
|
||
|
// On record remove, it might have bumped the selection upwards.
|
||
|
// Pass the "preventSelection" flag.
|
||
|
onStoreRemove: function() {
|
||
|
this.setPosition(this.getRecord(), 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);
|
||
|
},
|
||
|
|
||
|
// As per WAI-ARIA requirements, a grid should support two modes: Navigable (default),
|
||
|
// and Actionable. In Navigable mode, pressing Tab key inside the grid should move focus
|
||
|
// to the next tabbable element outside the grid. In Actionable mode, pressing Tab key
|
||
|
// should move focus to the next tabbable/actionable element within the grid, wrapping over
|
||
|
// row end to the next row, and over last row end to the first row.
|
||
|
// See http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#grid
|
||
|
// In this method we implement the first (Navigable) part, which is shared between
|
||
|
// Grids and Views.
|
||
|
onKeyTab: function(keyEvent) {
|
||
|
var view = this.view;
|
||
|
|
||
|
// To prevent Tab key from moving focus to the next element inside the grid
|
||
|
// in Navigable mode, we make all elements untabbable so the focus flows out
|
||
|
// following the natural tab order.
|
||
|
view.toggleChildrenTabbability(false);
|
||
|
|
||
|
// Enable further event propagation
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
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;
|
||
|
Ext.destroy(me.dataSourceListeners, me.viewListeners, me.keyNav);
|
||
|
me.keyNav = me.dataSourceListeners = me.viewListeners = me.dataSource = null;
|
||
|
me.callParent();
|
||
|
}
|
||
|
});
|