之前在项目中突然遇见一个很奇怪的需求,要求单元格能和Excel一样进行多选,比如框选其中一个矩形区域,然后复制到同页面另一个Grid中!!并添加统计报表功能。当时这个需求就把我们都怔住了!很变态很强悍的需求,因为这个模块是一个基于报表的统计功能,使用了一个第三方的报表工具,但是页面展示非常难看。。。。。客户就对我们说,为什么不用Ext来制作报表呢。呵呵最后当时是被我们回绝了,因为这确实是对我们团队来说难度太大,而且成本太高。具备统计功能的Ext Grid再加上可编辑等等,就几乎是一个网页版的Excel了,我们不认为我们团队能和Google Doc叫板.......

    但是事后我看了看 CellSelectionModel 和 RowSelectionModel的源代码,发现单元格多选并不难,于是先没和项目经理说我的想法,而是抽时间没事研究了一下,终于搞了几天之后,初版的模型就出来,嘿嘿现在尝试着说服项目经理应用在系统中。OK,先上代码和理论知识吧:

先说说Ext GridPanel UI组件的结构,它由五个部分组成,GridView、ColumnModel、SelectionModel、Store、GridPanel,不得不佩服Ext团队的设计理念,这是一个典型的MVC结构组件。GridView负责管理表格的内容展示,ColumnModel负责列的定义,SelectionModel是选择模组,Store是数据层,最后它们全部组合在一起由GridPanel进行渲染,展现在我们的面前。因为只关注单元格多选,对我们有用的组件只有Gridview 和 SelecitonModel。

    Grid选择模型的实现原理是这样的:Ext组件关系非常复杂,这里用图表进行说明吧

    

左侧是加载初始化时的操作,右侧是触发事件时的操作。

现在我们自定义的SelectionModel 主要关注于handleMouseDown方法就行。我们的多选规则和Excel一样,按住shift这是区域全选,按住ctrl则是添加被选中的单元格

Ext.hl.MultiCellSelectionModel = Ext.extend(Ext.grid.CellSelectionModel, {

last :
false, // 上一次选中的单元格
selections : [], // 选择区缓存

constructor :
function() {
Ext.hl.MultiCellSelectionModel.superclass.constructor.call(
this);
},
initEvents :
function() {
Ext.hl.MultiCellSelectionModel.superclass.initEvents.call(
this);
},
// 根据行列以及是否按住Ctrl键或者shift键,执行多选操作的逻辑
handleMouseDown :
function(grid, row, col, event) {
var isSelected;
if(event.button !== 0 || this.isLocked())
return;
if(event.shiftKey && this.last !== false) { //是否按下shift
this.selectMatrix(row, col);
grid.getView().focusCell(row, col);
return;
}
else if(event.ctrlKey){ //是否按下ctrl
isSelected
= this.isSelected(row,col);
if (col === 0 && this.last[1] === 0){
isSelected
? this.deselectRow(row) : this.selectRow(row,true);
}
if(isSelected){ // 是否已被选中,是则反选,否则选中
this.deselectCell(row, col);
}
else {
this.selectCell(row,col,true);
this.last = [row, col];
}
}
else if (col === 0){ // 第一列是NumberColumn 点击则选择列
this.selectRow(row);
this.last = [row, col];
}
else { // 选择单个单元格
this.selectCell(row,col);
this.last = [row, col];
}
if(this.matrix)
delete this.matrix;
},
// 清楚选择区内所以单元格被选中的样式
clearCellSelections :
function(){
var l = this.selections.length,
i
= 0;
for(; i < l; i++){
cell
= this.selections[i];
this.grid.view.onCellDeselect(cell[0], cell[1]);// GridView的内置方法,改变某单元格样式
}
this.selections.length = 0;
},
// 反选指定单元格,并清除相应选择区缓存
deselectCell :
function(row, col, isDelrow){
var l = this.selections.length,
i
= 0, n = 0;
if(this.selections){
this.grid.view.onCellDeselect(row, col);// GridView的内置方法,改变某单元格样式
for(; i < l; i++){
cell
= this.selections[i];
if( row !== cell[0] || col !== cell[1] ){
this.selections[n++] = this.selections[i];
}
else if (!isDelrow) { // 是否删除行
this.selections.splice(i,1);
return;
}
}
this.selections.length = n;
}
},
// 根据选择区缓存中的数据,判断是否被选中
isSelected :
function(row, col){
var l = this.selections.length,
i
= 0;
for(; i < l; i++){
cell
= this.selections[i];
if( row === cell[0] && col === cell[1] ){
return true;
}
}
return false;
},
// 选中某个单元格
selectCell :
function(rowIndex, colIndex, keepExisting, preventViewNotify, preventFocus){
if(this.fireEvent("beforecellselect", this, rowIndex, colIndex) !== false){
if(!keepExisting)
this.clearCellSelections();
this.selections.push([rowIndex, colIndex]); // 加入选择区缓存
if(!preventViewNotify){
var v = this.grid.getView();
v.onCellSelect(rowIndex, colIndex); // GridView的内置方法,改变某单元格样式
if(preventFocus !== true){
v.focusCell(rowIndex, colIndex);
}
}
this.fireEvent("cellselect", this, rowIndex, colIndex);
this.fireEvent("selectionchange", this, this.selection);
}
},
// 选中某一行
selectRow :
function(rowIndex, keepExisting){
var clen = this.grid.getColumnModel().getColumnCount(),
c
= 0;
if(!keepExisting)// 是否清空所有已选择的单元格
this.clearCellSelections();
for(; c < clen; c++){
this.selectCell(rowIndex, c, true);
}
},
// 某行反选
deselectRow :
function(row){
var clen = this.grid.getColumnModel().getColumnCount(),
c
= 0;
if(this.selections){
for(; c < clen; c++){
this.deselectCell(row, c, true);
}
}
},
// 按shift键调用的方法,选中一个矩形区域内所有的单元格
selectMatrix :
function(row, col, keepExisting){
// 以上一次被选择的单元格为起点,形成一个矩阵区域
var r = this.last[0],
c
= this.last[1];
if(!keepExisting)
this.clearCellSelections();
if(r > row){
var temp = row;
row
= r;
r
= temp;
}
if(col === 0 && c === 0){ // 若选择了第一列序号,则选择行
for(; r <= row; r++){
this.selectRow(r, true);
}
return;
}
if(c > col){
var temp = col;
col
= c;
c
= temp;
}
for(; r <= row; r++){
for(var i = c; i <= col; i++){
this.selectCell(r, i, true);
}
}
this.matrix = { // 矩形区域选择区数据
start : [r, c],
end : [row, col]
};
}
});

代码可能有点长。操作逻辑比较复杂,但是大致流程就是根据键盘事件和当前选择区的状态,来判断表格单元格应该如何改变样式。这里用到一些内置的GridView方法,传递行和列的index,即可通过内部的Dom方法,取得相对应TD或者TR的DOM对象,然后改变样式,使单元格看起来被选中.handleMouseDown函数内部的逻辑可能有点乱,大家可以参考Excel的选择规则来理解这个逻辑。下面是实现效果:

定义GridPanel的时候,把sm 设成 MultiCellSelectionModel,设置第一列为NumberRowModel则可以具备多选行的功能。因为还没上生产环境测试,大家可以帮我测试一下这个自定义组件,哈哈欢迎反馈各种BUG哦。我计划在它的基础上,给EditorGridPanel补充动态增加列和某单元格统计的功能,如果可行再加上个命令栈来实现操作撤销功能,哈哈整个一个在线版的Excel啊,OK有机会一点慢慢搞定它,今天先到这里咯