用react 構建電子表格(7)---合併單元格背後的資料結構
技術標籤:reactjs
以下寫法錯誤:newRowData 是老資料的引用!!! 原因在於lastRowData本身就是二維陣列。
//複製最後一行 lastRowData是個二維陣列!!!
let lastRowData=data_model.slice(data_model.length - 1);
let newRowData = [...lastRowData]; //newRowData 也是二維陣列!!!
table_model.data_model=data_model.concat(newRowData);
搞了2個函式用於複製:
// twoDimArray 是個二維陣列,本方法是在每行末追加一個元素 copyArrayLastCol(twoDimArray){ let r=twoDimArray.map(item => { // item為陣列的元素 item=[...item,item[item.length-1]] return item; // 返回一個處理過的新陣列 }) return r; //返回一個新的陣列 } copyArrayLastRow(twoDimArray){ let r=twoDimArray[twoDimArray.length-1]; let newRowData = [...r]; twoDimArray[twoDimArray.length]=newRowData; } copy3DArrayLastRow(treeDimArray){ let r=treeDimArray[treeDimArray.length-1]; //r是二維陣列 //let newRowData = [[...r[0]]]; let newRowData=r.map(item => { // item為陣列的元素 item=[...item] return item; // 返回一個處理過的新陣列 }) treeDimArray[treeDimArray.length]=newRowData; } //複製最後一行,然後插入到指定位置 insertRow(twoDimArray,p){ let r=twoDimArray[twoDimArray.length-1]; let newRowData = [...r]; twoDimArray.splice(p,0,newRowData); } //複製最後一行,然後插入到指定位置 insertRow_3D(threeDimArray,p){ let r=threeDimArray[threeDimArray.length-1]; let newRowData=r.map(item => { // item為陣列的元素 item=[...item] return item; // 返回一個處理過的新陣列 }) threeDimArray.splice(p,0,newRowData); }
合併單元格的處理:struct_model
搞個數據表示這個表的結構,初始化它的方法,其中,i代表跨行數,j代表跨列數
struct_model_init[i][j]=[1,1];
把形成前端的錨改為...???
一個是原始座標體系:以data_model 為錨,相當於“底稿”?
一個含合併單元格層 以struct_model為“合併層”struct_model 中記錄的當前位置的跨行、跨列資訊,
先考慮一行內跨列的問題,且合併的單元格都是"基本cell",:第一個單元格
資料本身?我們再增加一個數組加以描述
//direction 1 左對齊 2 中對齊 3 右對齊 mergeCells=()=>{ const history = this.state.history_record; const current =history.slice(history.length - 1) const table_model = current[0]; //找出所有選擇的單元格 , 放入一個集合 ,對集合進行遍歷 let range= this.selectRange(); if(range==-1){ return;} let row_start=range[0]; let row_end= range[1]; let col_start= range[2]; let col_end= range[3]; let _spanRow=row_end-row_start+1; let _spanCol=col_end-col_start+1; //this.clearCssFilter(current_table_model); for (let i=row_start;i<=row_end;i++) { for (let j=col_start;j<=col_end;j++){ table_model.data_model[i][j]=0; //範圍內全部家filter table_model.struct_model[i][j][0]=0; //跨行 垂直方向 table_model.struct_model[i][j][1]=0; //跨列 水平方向 //table_model.struct_model[i][j]=[0,0] 這個寫法更簡單? } } table_model.data_model[row_start][col_start]=1; table_model.struct_model[row_start][col_start][0]=_spanRow; table_model.struct_model[row_start][col_start][1]=_spanCol; //setState 即可引發渲染 this.setState({ history_record: this.state.history_record, }); }
cell.js中的控制:
render() {
......
let _rowSpan=this.props.cell_struct_model[0];
let _colSpan=this.props.cell_struct_model[1];
if(this.props.commData==0){
return null;
}else{
return (
<td className="ttd"
onClick={e => this.clicked(e)}
onDoubleClick={e => this.doubleClicked(e)}
className={["ttd", alignCss,cell_font_family_css,cell_font_size_css,
cell_font_weight_css, cell_font_style_css,cell_text_decoration_css,
cell_border_top_css,cell_border_right_css,cell_border_bottom_css,cell_border_left_css,
cell_cell_filter_brightness_css].join(' ')}
rowSpan={_rowSpan}
colSpan={_colSpan}
style={{backgroundColor: this.props.commcellBackgroundColor}}
>
{/*{this.getCommData(this.props.commData)}*/}
</td>
)
}
}
以上程式碼可以看到,通過this.props.commData控制是否加入td.
現在,進一步考慮表格中已存在合併單元格的情況
必須用適當的資料結構記錄這一點,state當中定義一個mergeCellArray:[] 這個陣列的元素又是個陣列,描述一個合併單元格資訊,這個元素數長度為4,描述左上、右下2個點。
先準備測試資料:data_init、struct_model_init
data_init[2][2]=0;
data_init[2][3]=0;
struct_model_init[2][1][0]=1;
struct_model_init[2][1][1]=3;
判斷2個區域是否相交的思路:其實就是判斷矩形是否相交
1、一個是根據邊的位置判斷,做排除法
2、一個是求2個矩形的位置中心位置,通過2箇中心位置的距離,判斷是否相交
3、錯誤的做法:是判斷頂點位置的方式,判斷2個矩形是否相交
summary:
合併單元格,需要三個資料模型:
- data_model:作為“底稿”
- struct_model:表現層模型,記錄每個單元格跨行列資訊
- mergeCellArray:記錄合併單元格資訊
選擇單元格,核心問題是判斷2個矩形是否相交(選擇區域是否和已有合併區相交),如果存在相交矩形,則擴大選擇區域,如此反覆,直到選擇區域不和任何已存在合併單元格相交為止!
選擇單元格核心程式碼:
//處理單元格選擇 主要是改變相關樣式資料
handleCellSelect = (e,i,j) => {
if(e.ctrlKey){ // 按下ctrl key 不用重新
if(this.state.userActionType==1){ //上次動作點選的是單元格
const current_table_model =this.state.history_record[this.state.history_record.length -1];
this.state.selectCell2=new Array(i, j); //當前選中的單元格座標
let row_start= this.state.selectCell1[0]<i ? this.state.selectCell1[0] :i;
let row_end= this.state.selectCell1[0]>i ? this.state.selectCell1[0] :i;
let col_start= this.state.selectCell1[1]<j ? this.state.selectCell1[1] :j;
let col_end= this.state.selectCell1[1]>j ? this.state.selectCell1[1] :j;
//執行下面方法 會根據情況修改 this.state.selectCell1、 this.state.selectCell2
this.adjuestSelectRange(this.state.mergeCellArray,[row_start,col_start,row_end,col_end])
row_start= this.state.selectCell1[0];
row_end= this.state.selectCell2[0];
col_start= this.state.selectCell1[1];
col_end= this.state.selectCell2[1];
this.clearCssFilter(current_table_model);
for (let i=row_start;i<=row_end;i++)
{
for (let j=col_start;j<=col_end;j++){
current_table_model.cell_addFilter_bright_model[i][j]=1; //範圍內全部家filter
}
}
//setState 即可引發渲染
this.setState({
history_record: this.state.history_record,
});
return;
}
}
//複製一個history 元素,改變這個元素相關屬性
const history = this.state.history_record;
const current =history.slice(history.length - 1)
const table_model = current[0];
//複製一個數組
//let cellSelectStates_s=this.state.cellSelectStates;
let cellSelectStates_s=table_model.cell_addFilter_bright_model ;
let cellSelectStates_temp=[];
for(let i=0;i<cellSelectStates_s.length;i++){
cellSelectStates_temp.push(new Array(cellSelectStates_s[i].length).fill(0));
}
cellSelectStates_temp[i][j]=1; //1表示選中
table_model.cell_addFilter_bright_model=cellSelectStates_temp;
this.state.selectCell1=new Array(i, j); //當前選中的單元格座標
this.state.selectCell2=[]; //清除上一次ctrl+click資料
this.state.userActionType=1; //這個動作記錄使用者上次的操作
this.setState({
history_record: history.concat(table_model),
});
}
//調整選擇範圍 mergeCellArray 已合併單元格陣列,selectrectangle 選擇範圍的座標 是個一維陣列
adjuestSelectRange=(mergeCellArray, selectrectangle)=>{
let isNotCross=mergeCellArray.every((rectangle1)=>this.changeRectangle(rectangle1,selectrectangle));
if (isNotCross==true) {
return;
}else{
let selectrectangle=[...this.state.selectCell1,...this.state.selectCell2];
//這個時候的selectrectangle 是已經調整過的了
this.adjuestSelectRange(mergeCellArray,selectrectangle);
}
};
//返回true 即不相交 ,返回false 則相交
//react1 是個一維陣列[rowPosition1 0,colPosition1 1,rowPosition2 2,colPosition2 3] 左上 右下
changeRectangle=(rectangle, selectRect)=>{
let b=selectRect[3] < rectangle[1] || //左邊
selectRect[1] > rectangle[3] || //右邊
selectRect[2] < rectangle[0] || //上邊
selectRect[0] >rectangle[2] //下邊;
if(!b){ //繼續判斷selectRect是否包含了rectangle
if(selectRect[0]<=rectangle[0] &&
selectRect[2]>=rectangle[2] &&
selectRect[1]<=rectangle[1] &&
selectRect[3]>=rectangle[3]
)b=true;
}
if(b){
return true; //不相交
}else{
//調整範圍selectRect2
let row_start= rectangle[0]<selectRect[0] ? rectangle[0] :selectRect[0];
let row_end=rectangle[2]<selectRect[2] ? selectRect[2]:rectangle[2];
let col_start= rectangle[1]<selectRect[1] ? rectangle[1] :selectRect[1];;
let col_end= rectangle[3]<selectRect[3] ? selectRect[3] :rectangle[3];
this.state.selectCell1=[row_start,col_start];
this.state.selectCell2=[row_end,col_end];
return false;
}
}
合併單元格程式碼:
//合併單元格
mergeCells=()=>{
const history = this.state.history_record;
const current =history.slice(history.length - 1)
const table_model = current[0];
//找出所有選擇的單元格 , 放入一個集合 ,對集合進行遍歷
let range= this.selectRange();
if(range==-1){ return;}
let row_start=range[0];
let row_end= range[1];
let col_start= range[2];
let col_end= range[3];
let _spanRow=row_end-row_start+1;
let _spanCol=col_end-col_start+1;
//this.clearCssFilter(current_table_model);
for (let i=row_start;i<=row_end;i++)
{
for (let j=col_start;j<=col_end;j++){
table_model.data_model[i][j]=0; //範圍內全部家filter
table_model.struct_model[i][j][0]=0; //跨行 垂直方向
table_model.struct_model[i][j][1]=0; //跨列 水平方向
//table_model.struct_model[i][j]=[0,0] 這個寫法更簡單?
}
}
table_model.data_model[row_start][col_start]=1;
table_model.struct_model[row_start][col_start][0]=_spanRow;
table_model.struct_model[row_start][col_start][1]=_spanCol;
//刪除在選定範圍內的全部合併單元資訊 建立新的合併單元資訊
es6遍歷的時候 如何刪除?
this.state.mergeCellArray.forEach((item,index)=>{
//包含在選中區當中的合併單元格記錄全部清除掉
if(item[0]>=row_start && row_end>=item[2] && item[1]>=col_start && col_end >= item[3]){
this.state.mergeCellArray.splice(index,1);
}
})
this.state.mergeCellArray.push([row_start,col_start,row_end,col_end])
//setState 即可引發渲染
this.setState({
history_record: this.state.history_record,
});
}
拆分單元格:
1、還原data_model相關資料
2、還原struct_model相關資料
3、刪除mergeCellArray中相關記錄
值得注意的是,選中區域右下角如果是合併單元格,要注意修正相關選擇範圍的的修正。
如果不進行修正,則如圖:
左邊是待拆分割槽域,右邊是拆分結果,發現右邊缺了2個單元格!!!這肯定不是我們期望的.所以必須修正。
如何修正?檢查結束區域的stuct-mode對應位置的值啊,如果不是[1,1] 則必須根據這個值修正。
修訂行列的程式碼:
adjustSelectCell2=(struct_model,selectCell2)=>{
let c=struct_model[selectCell2[0]][selectCell2[1]];
return [c[0],c[1]]; //跨行 跨列數
}
拆分單元格:
splitCells=()=>{
const history = this.state.history_record;
const current =history.slice(history.length - 1)
const table_model = current[0];
//找出所有選擇的單元格 , 放入一個集合 ,對集合進行遍歷
let range= this.selectRange();
if(range==-1){ return;}
let row_start=range[0];
let row_end= range[1];
let col_start= range[2];
let col_end= range[3];
let v=this.adjustSelectCell2(table_model.struct_model,[row_end,col_end]); //需要調整的值
if(v[0]>1){
row_end=row_end+v[0]-1;
}
if(v[1]>1){
col_end=col_end+v[1]-1;
}
for (let i=row_start;i<=row_end;i++)
{
for (let j=col_start;j<=col_end;j++){
table_model.data_model[i][j]=1;
table_model.struct_model[i][j]=[1,1]
}
}
//刪除在選定範圍內的全部合併單元資訊 建立新的合併單元資訊
es6遍歷的時候 如何刪除?
this.state.mergeCellArray.forEach((item,index)=>{
//包含在選中區當中的合併單元格記錄全部清除掉
if(item[0]>=row_start && row_end>=item[2] && item[1]>=col_start && col_end >= item[3]){
this.state.mergeCellArray.splice(index,1);
}
})
//setState 即可引發渲染
this.setState({
history_record: this.state.history_record,
});
}
另外,要考慮插入列 刪除列 追加列、插入行、刪除行、追加行對這三個資料模型的影響 。
到目前為止,我們僅僅允許一行一列插入、刪除,
先考慮合併行就2行的情況,即rowspan=2,刪除一行的時候,程式該怎麼處理,要分2段考量,一個是對本身的影響,一個的外部影響
情況1
1、首先找到合併區
2、修正合併區struct_mode 下一行的[rowSpan,colSpan]陣列,中的值,具體就是下一行rowspan=行1的rowspan-1,colspan=colspan;
3、修正下一行第一個單元對應的data-mode的值,讓整個單元的值為1 其他不變
4、調整設計的mergecells中相應合併區記錄的資料
情況2
1、首先找到合併區
2、修正合併區struct_mode 上一行的[rowSpan,colSpan]陣列,中的值,具體就是下一行rowspan=行1的rowspan-1
3、調整設計的mergecells中相應合併區記錄的資料