Hi,
Sorry for late answer... but ExtJs 5 gave me a bit of headache :)
So my solution is based on the idea of emulating Ctrl key pressed all the time. I took two methods from ExtJs4 source code of the selection model:
- afterKeyNavigate
- selectWithEvent
I have added bool member which I called "freeSelection" that allow to turn off/on that feature. It is not proper override of the selection but a bit of hack (I needed that feature only for one grid panel in my app so did not override whole selection model class)
Mainly I have changed e.ctrlKey to e.ctrlKey || me.freeSelection.
Ext.define("MyApp.MyGrid.MySelection", {
// Private
// Called after a new record has been navigated to by a keystroke.
// Event is passed so that shift and ctrl can be handled.
afterKeyNavigate: function (e, record) {
var me = this,
recIdx,
fromIdx,
isSelected = me.isSelected(record),
from = (me.selectionStart && me.isSelected(me.lastFocused)) ? me.selectionStart : (me.selectionStart = me.lastFocused),
key = e.getCharCode(),
isSpace = key === e.SPACE,
direction = key === e.UP || key === e.PAGE_UP ? 'up' : (key === e.DOWN || key === e.DOWN ? 'down' : null);
switch (me.selectionMode) {
case 'MULTI':
if (isSpace) {
// SHIFT+SPACE, select range
if (e.shiftKey) {
me.selectRange(from, record, e.ctrlKey || me.freeSelection);
} else {
// SPACE pessed on a selected item: deselect but leave it focused.
// e.ctrlKey means "keep existing"
if (isSelected) {
me.doDeselect(record, e.ctrlKey || me.freeSelection);
// This record is already focused. To get the focus effect put on it (as opposed to selected)
// we have to focus null first.
me.setLastFocused(null);
me.setLastFocused(record);
}
// SPACE on an unselected item: select it
else {
me.doSelect(record, e.ctrlKey || me.freeSelection);
}
}
}
// SHIFT-navigate selects intervening rows from the last selected (or last focused) item and target item
else if (e.shiftKey && from) {
// If we are going back *into* the selected range, we deselect.
fromIdx = me.store.indexOf(from);
recIdx = me.store.indexOf(record);
// If we are heading back TOWARDS the start rec - deselect skipped range...
if (direction === 'up' && fromIdx <= recIdx) {
me.deselectRange(me.lastFocused, recIdx + 1);
}
else if (direction === 'down' && fromIdx >= recIdx) {
me.deselectRange(me.lastFocused, recIdx - 1);
}
// If we are heading AWAY from start point, or no CTRL key, so just select the range and let the CTRL control "keepExisting"...
else if (from !== record) {
me.selectRange(from, record, e.ctrlKey || me.freeSelection);
}
me.lastSelected = record;
me.setLastFocused(record);
}
// CTRL-navigate onto a selected item just focuses it
else if ((e.ctrlKey || me.freeSelection) && isSelected) {
me.setLastFocused(record);
}
// CTRL-navigate, just move focus
else if (e.ctrlKey || me.freeSelection) {
me.setLastFocused(record);
}
// Just navigation - select the target
else {
me.doSelect(record, false);
}
break;
case 'SIMPLE':
if (isSelected) {
me.doDeselect(record);
} else {
me.doSelect(record, true);
}
break;
case 'SINGLE':
// Space hit
if (isSpace) {
if (isSelected) {
me.doDeselect(record);
me.setLastFocused(record);
} else {
me.doSelect(record);
}
}
// CTRL-navigation: just move focus
else if (e.ctrlKey || me.freeSelection) {
me.setLastFocused(record);
}
// if allowDeselect is on and this record isSelected, deselect it
else if (me.allowDeselect && isSelected) {
me.doDeselect(record);
}
// select the record and do NOT maintain existing selections
else {
me.doSelect(record, false);
}
break;
}
// selectionStart is a start point for shift/mousedown to create a range from.
// If the mousedowned record was not already selected, then it becomes the
// start of any range created from now on.
// If we drop to no records selected, then there is no range start any more.
if (!e.shiftKey) {
if (me.isSelected(record)) {
me.selectionStart = record;
}
}
},
// Provides differentiation of logic between MULTI, SIMPLE and SINGLE
// selection modes. Requires that an event be passed so that we can know
// if user held ctrl or shift.
selectWithEvent: function (record, e) {
var me = this,
isSelected = me.isSelected(record),
shift = e.shiftKey,
ctrl = e.ctrlKey,
start = me.selectionStart,
selected = me.getSelection(),
len = selected.length,
allowDeselect = me.allowDeselect,
toDeselect, i, item;
switch (me.selectionMode) {
case 'MULTI':
if (shift && start) {
me.selectRange(start, record, ctrl);
} else if (ctrl && isSelected) {
me.doDeselect(record, false);
} else if (ctrl) {
me.doSelect(record, true, false);
} else if (isSelected && !shift && !ctrl && len > 1) {
if (!me.freeSelection) {
toDeselect = [];
for (i = 0; i < len; ++i) {
item = selected[i];
if (item !== record) {
toDeselect.push(item);
}
}
me.doDeselect(toDeselect);
}
} else if (!isSelected) {
if (!me.freeSelection) {
me.doSelect(record, false);
}
else {
me.setLastFocused(record);
}
}
break;
case 'SIMPLE':
if (isSelected) {
me.doDeselect(record);
} else {
me.doSelect(record, true);
}
break;
case 'SINGLE':
if (allowDeselect && !ctrl) {
allowDeselect = me.toggleOnClick;
}
if (allowDeselect && isSelected) {
me.doDeselect(record);
} else {
me.doSelect(record, false);
}
break;
}
// selectionStart is a start point for shift/mousedown to create a range from.
// If the mousedowned record was not already selected, then it becomes the
// start of any range created from now on.
// If we drop to no records selected, then there is no range start any more.
if (!shift) {
if (me.isSelected(record)) {
me.selectionStart = record;
} else {
me.selectionStart = null;
}
}
}
});
Once you have the code in place I replace these methods in selection model:
var selection = myGrid.getSelectionModel();
selection.freeSelection = true;
selection.afterKeyNavigate = me.afterKeyNavigate;
selection.selectWithEvent = me.selectWithEvent;
"me" is instance of MyApp.MyGrid.MySelection
The solution is kind of workaround... and I have not tested it thoroughly yet as I have decided to move on to ExtJS5. Once I have got this done (maybe in a better way) in ExtJS 5 I will create thread in Ext.Net 3.0.
Hope that you give you some head start how to archive the desired effect.