PDA

View Full Version : MultiCellSelectionModel



thomashoffmann
4 Jun 2009, 1:32 PM
Hi,

today i have written a MultiCellSelectionModel. There are some bugs in keyboard selection.


/**
* @class Ext.ux.MultiCellSelectionModel
* @extends Ext.grid.AbstractSelectionModel
* This class provides an implementation for <i>multible</i> <b>cell</b> selection in a grid.
*
* Mouse selection is supported, keyboard selection is bugy
*
* <div class="mdetail-params"><ul>
* <li><b>cells</b> : see {@link #getSelections}
* <li><b>cell</b> : see {@link #getSelectedCell}
* </ul></div>
* @constructor
* @param {Object} config The object containing the configuration of this model.
*/
Ext.ux.MultiCellSelectionModel = function(config) {
Ext.apply(this, config);

 
this.selections = new Ext.util.MixedCollection(false,
function(o) {
return o.id;
});

this.selection = null;

this.addEvents(
/**
* @event beforecellselect
* Fires before a cell is selected.
* @param {SelectionModel} this
* @param {Number} rowIndex The selected row index
* @param {Number} colIndex The selected cell index
*/
"beforecellselect",
/**
* @event cellselect
* Fires when a cell is selected.
* @param {SelectionModel} this
* @param {Number} rowIndex The selected row index
* @param {Number} colIndex The selected cell index
*/
"cellselect",
/**
* @event selectionchange
* Fires when the active selection changes.
* @param {SelectionModel} this
* @param {Object} selection null for no selection or an object with two properties
* <div class="mdetail-params"><ul>
* <li><b>cell</b> : see {@link #getSelectedCell}
* <li><b>record</b> : Ext.data.record<p class="sub-desc">The {@link Ext.data.Record Record}
* which provides the data for the row containing the selection</p></li>
* </ul></div>
*/
"selectionchange");

Ext.ux.MultiCellSelectionModel.superclass.constructor.call(this);
};

Ext.extend(Ext.ux.MultiCellSelectionModel, Ext.grid.AbstractSelectionModel, {

/** @ignore */
initEvents: function() {
this.grid.on("cellmousedown", this.handleMouseDown, this);
this.grid.getGridEl().on(Ext.isIE || Ext.isSafari3 || Ext.isChrome ? "keydown": "keypress", this.handleKeyDown, this);
var view = this.grid.view;
view.on("refresh", this.onViewChange, this);
view.on("rowupdated", this.onRowUpdated, this);
view.on("beforerowremoved", this.clearSelections, this);
view.on("beforerowsinserted", this.clearSelections, this);
if (this.grid.isEditor) {
this.grid.on("beforeedit", this.beforeEdit, this);
}
},

//private
beforeEdit: function(e) {
this.select(e.row, e.column, false, true, e.record);
},

//private
onRowUpdated: function(v, index, r) {
if (this.selection && this.selection.record == r) {
v.onCellSelect(index, this.selection.cell[1]);
}
},

//private
onViewChange: function() {
this.clearSelections(true);
},

/**
* Returns an array containing the row and column indexes of the currently selected cell
* (e.g., [0, 0]), or null if none selected. The array has elements:
* <div class="mdetail-params"><ul>
* <li><b>rowIndex</b> : Number<p class="sub-desc">The index of the selected row</p></li>
* <li><b>cellIndex</b> : Number<p class="sub-desc">The index of the selected cell.
* Due to possible column reordering, the cellIndex should <b>not</b> be used as an
* index into the Record's data. Instead, use the cellIndex to determine the <i>name</i>
* of the selected cell and use the field name to retrieve the data value from the record:<pre><code>
* </code></pre></p></li>
* </ul></div>
* @return {Array} An array containing the row and column indexes of the selected cell, or null if none selected.
*/
getSelectedCell: function() {
return this.selection ? this.selection.cell: null;
},

/**
* Returns the selected records
* @return {MixedCollection} MixedCollection of selected records, each item contain a cell property (an array containing the row and column indexes)
*/
getSelections: function() {
return this.selections;
},

/**
* Clears the single selection.
* @param {Boolean} true to prevent the gridview from being notified about the change.
*/
clearSelection: function(preventNotify) {
var s = this.selection;
if (s) {
if (preventNotify !== true) {
this.grid.view.onCellDeselect(s.cell[0], s.cell[1]);
}
this.selection = null;
this.fireEvent("selectionchange", this, null);
}
},

/**
* Clears all selections.
* @param {Boolean} true to prevent the gridview from being notified about the change.
*/
clearSelections: function(preventNotify) {
var s = this.selections;
//alert('cs');
for (var i = 0, len = s.length; i < len; i++) {
if (preventNotify !== true) {
this.grid.view.onCellDeselect(s.get(i).cell[0], s.get(i).cell[1]);
}
//this.selection = null;
}
this.selections.clear();
},

/**
* Returns true if there is a selection.
* @return {Boolean}
*/
hasSelection: function() {
return this.selection ? true: false;
},

/** @ignore */
stop_events: false,
/** @ignore */
m_select: false,
/** @ignore */
handleMouseDown: function(g, row, cell, e) {
if (e.button !== 0 || this.isLocked()) {
return;
};
this._xselect(row, cell, e, false);
},

_xselect: function(row, cell, e, x) {
if ((!e.shiftKey && !e.ctrlKey)) {
this.clearSelections();
this.clearSelection();
this.select(row, cell);
this.m_select = false;
} else {
if (e.ctrlKey) {
this.select(row, cell);
}
else {

if (this.m_select && (x == false)) {
var so = this.selection;
this.clearSelections();
this.clearSelection();
this.selection = so;

}

var r1 = 0;
var c1 = 0;
if (this.selection != 0) {
r1 = this.selection.cell[0];
c1 = this.selection.cell[1];
}
var r2 = row;
var c2 = cell;

if (c1 > c2) {
c_start = c2;
c_end = c1;
}
else {
c_start = c1;
c_end = c2;
}

if (r1 > r2) {
r_start = r2;
r_end = r1;
}
else {
r_start = r1;
r_end = r2;
}
this.stop_events = true;
for (r = r_start; r <= r_end; r++) {
for (c = c_start; c <= c_end; c++) {
this.select(r, c, false, true);
}
}
this.stop_events = false;
this.m_select = true;
//this.select(row, cell,false,true);
this.fireEvent("cellselect", this, r_end, c_end);
this.fireEvent("selectionchange", this, this.selection);

}
}
},

/**
* Selects a cell.
* @param {Number} rowIndex
* @param {Number} collIndex
*/
select: function(rowIndex, colIndex, preventViewNotify, preventFocus,
/*internal*/
r) {
if (this.fireEvent("beforecellselect", this, rowIndex, colIndex) !== false) {
//this.clearSelection();
//r = r || this.grid.store.getAt(rowIndex);
this.selection = {
//record : r,
cell: [rowIndex, colIndex]
};
this.selections.add(this.selection);
if (!preventViewNotify) {
var v = this.grid.getView();
v.onCellSelect(rowIndex, colIndex);
if (preventFocus !== true) {
v.focusCell(rowIndex, colIndex);
}
}
if (!this.stop_events) {
this.fireEvent("cellselect", this, rowIndex, colIndex);
this.fireEvent("selectionchange", this, this.selection);
}
}
},

//private
isSelectable: function(rowIndex, colIndex, cm) {
return ! cm.isHidden(colIndex);
},

/** @ignore */
_getMinMax: function() {
var sel = this.getSelections();
var selc = sel.getCount();
var min_row = 9999999;
var max_row = 0;

var min_col = 9999999;
var max_col = 0;

for (var j = 0; j < selc; j++) {
if (sel.get(j).cell[0] < min_row) {
min_row = sel.get(j).cell[0];
}
if (sel.get(j).cell[0] > max_row) {
max_row = sel.get(j).cell[0];
}

if (sel.get(j).cell[1] < min_col) {
min_col = sel.get(j).cell[1];
}
if (sel.get(j).cell[1] > max_col) {
max_col = sel.get(j).cell[1];
}
}

return {
minRow: min_row,
maxRow: max_row,

minCol: min_col,
maxCol: min_col
};
},
/** @ignore */
handleKeyDown: function(e) {
if (!e.isNavKeyPress()) {
return;
}
var g = this.grid,
s = this.selection;
if (!s) {
e.stopEvent();
var cell = g.walkCells(0, 0, 1, this.isSelectable, this);
if (cell) {
this._xselect(cell[0], cell[1], e, true);
//this.clearSelections();
//this.select(cell[0], cell[1]);
}
return;
}
var sm = this;
var walk = function(row, col, step) {
return g.walkCells(row, col, step, sm.isSelectable, sm);
};
var k = e.getKey(),
r = s.cell[0],
c = s.cell[1];
var newCell;

switch (k) {
case e.TAB:
if (e.shiftKey) {
newCell = walk(r, c - 1, -1);
} else {
newCell = walk(r, c + 1, 1);
}
break;
case e.DOWN:
newCell = walk(r + 1, c, 1);
break;
case e.UP:
newCell = walk(r - 1, c, -1);
break;
case e.RIGHT:
newCell = walk(r, c + 1, 1);
break;
case e.LEFT:
newCell = walk(r, c - 1, -1);
break;
case e.ENTER:
if (g.isEditor && !g.editing) {
g.startEditing(r, c);
e.stopEvent();
return;
}
break;
};
if (newCell) {
this._xselect(newCell[0], newCell[1], e, true);
//this.clearSelections();
//this.select(newCell[0], newCell[1]);
e.stopEvent();
}
},

acceptsNav: function(row, col, cm) {
return ! cm.isHidden(col) && cm.isCellEditable(col, row);
},

onEditorKey: function(field, e) {
var k = e.getKey(),
newCell,
g = this.grid,
ed = g.activeEditor;
if (k == e.TAB) {
if (e.shiftKey) {
newCell = g.walkCells(ed.row, ed.col - 1, -1, this.acceptsNav, this);
} else {
newCell = g.walkCells(ed.row, ed.col + 1, 1, this.acceptsNav, this);
}
e.stopEvent();
} else if (k == e.ENTER) {
ed.completeEdit();
e.stopEvent();
} else if (k == e.ESC) {
e.stopEvent();
ed.cancelEdit();
}
if (newCell) {
g.startEditing(newCell[0], newCell[1]);
}
}
});


Sample use:



var sel = grid.getSelectionModel().getSelections();
var selc = sel.getCount();
for (var j = 0; j < selc; j++) {
var row = sel.get(j).cell[0];
var col = sel.get(j).cell[1];
// ...
}

MD
4 Jun 2009, 3:45 PM
Live demo?

MD

tobiu
6 Jun 2009, 4:19 AM
hi thomas,

i am a bit short in time right now, so i do not have the time to test your ux.
but the multicelection-ux of ext2.2 works perfect with me and the 3.0-rc2.

kind regards, tobiu

j0452
6 Apr 2010, 7:33 PM
Hey Thomas,

Just came across this and found it useful, thanks!

Any progress since your original post (10 months ago)?

I'm working on a MultiCellSelection model that mimics Google Spreadsheets' behavior, and I'm making good progress. Interested in picking back up the thread?

Josh

thomashoffmann
6 Apr 2010, 9:33 PM
Hi,

my SelectionModel fits me needs, but sure i'm interested.

j0452
7 Apr 2010, 10:47 AM
So far I've added just-start-typing-to-edit, and hitting tab or enter while editing keeps you in edit mode. I'm also working on getting it to select the entire column when you click a column header instead of re-sorting, and selecting the entire row when you click a row number.

Do you know how I can find out if there's a plan to support a multi-cell selection model in Ext core? With all the different grid demos in the example gallery, I was surprised it didn't support a standard spreadsheet behavior grid out of the box. Maybe this is Coming Soon? Or is there an official repo for user-contributed extensions with proper releases, like jQuery has? (Sorry I'm new to the Ext community.)

Note: I've been posting to the thread at http://www.extjs.com/forum/showthread.php?p=455563 (which is how i was referred to this thread) with my progress, in case you want to follow that one.

Have you had any luck with the keyboard navigation?

steffenk
7 Apr 2010, 12:06 PM
If you want to see a very good implementation, test the spreadsheet at http://www.feyasoft.com/ (demo/demo)