1. 程式人生 > 其它 >用react 構建電子表格(7)---合併單元格背後的資料結構

用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個矩形是否相交

ps:JS. 如何判斷兩個矩形是否相交

判斷兩矩形是否相交

判斷兩個矩形是否重疊

判斷兩個矩形相交以及求出相交的區域

JS中arr.forEach()如何跳出迴圈

summary:

合併單元格,需要三個資料模型:

  1. data_model:作為“底稿”
  2. struct_model:表現層模型,記錄每個單元格跨行列資訊
  3. 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中相應合併區記錄的資料

ps:idea git提交程式碼步驟

p2:GitHub公共庫與私有庫相互轉換