1. 程式人生 > >Web版Excel製作過程分享

Web版Excel製作過程分享

由於專案需要製作一個Web版本Excel用於表單、報表線上繪製,網上搜了一圈沒有發現免費開源現成可用的資源,根據搜到的一些零散資訊決定自己動手做一個,本文分享這個製作過程,主要包含表格佈局、表頭固定、動態調整行高列寬、單元格選中、合併與拆分單元格等功能,供大家交流分享。廢話少說先上個效果圖如下:

 

一、技術選型

1. 本例基於Jquery庫和Vue框架實現,其中Vue並不是必須,僅僅因為專案需要而已,讀者只需稍作改造去掉對Vue的依賴即可。

2. 出於對簡單直觀的追求,筆者選擇基於table元素而不是基於div組合,

二、先用table做個Excel表格的樣子

本來覺得很容易,用框架動態生成一個n行m列的table,並在第一行自動填充ABC...Z等作為列表頭,在第一列自動填充123...n等作為行表頭,使用Vue框架v-for迴圈生成tr和td元素即可,不熟悉vue的同學可以簡單瞭解一下vue中v-for指令,當然也可以用原生js或jquery生成這個table的所有行列單元格,總體佈局的思路如下:

1. 外層用一個div控制顯示區域,讓表格在這個區域內顯示,超出該區域則滾動:overflow:scroll

2. 內層用table繪製表格,其中第一行和第一列單獨繪製,填入表頭字母和數字,每個單元格寬預設100px,tr行高預設28px

 1 <!DOCTYPE html>
 2 <html lang="zh" xmlns:v-bind="http://www.w3.org/1999/xhtml">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</
title> 6 <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> 7 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 8 </head> 9 <body> 10 <div id="app"> 11 <div class="form-frame"> 12 <
table class="form-table"> 13 <tr class="form-row"> 14 <th class="form-header" width="40px"></th> 15 <th class="form-header" v-for="col in 26" width="100px">{{String.fromCharCode(col+64)}}</th> 16 </tr> 17 <tr class="form-row" v-for="row in 40"> 18 <td class="form-header" width="40px">{{row}}</td> 19 <td class="form-cell" v-for="col in 26"></td> 20 </tr> 21 <tr></tr> 22 </table> 23 </div> 24 </div> 25 <script> 26 var vue = new Vue({ 27 el:'#app' 28 }); 29 </script> 30 <style> 31 .form-frame{ 32 width: 700px; 33 height: 350px; 34 margin: 0 auto; 35 overflow:scroll; 36 border: 1px solid red; 37 } 38 .form-table{ 39 border-spacing:0; 40 } 41 .form-header{ 42 font-weight: normal; 43 text-align: center; 44 } 45 .form-row{ 46 height: 28px; 47 } 48 th, td{ 49 border-right: 1px solid; 50 border-bottom: 1px solid; 51 } 52 .form-cell{ 53 width: 100px; 54 } 55 </style> 56 </body> 57 </html>
View Code

結果如下圖所示(紅色是外層div的邊框,為了除錯方便),程式碼中我們給每個單元格設定了100px的寬度,一共生成了27列,按理說他應該把整個table撐開到至少2700px;然而如圖所示整個table的寬度並沒有被撐開,而是自適應了外層div的寬度。這就是我們今天要解決的第一個問題,table元素td標籤寬度設定無效的問題。

三、解決table中td元素寬度設定無效的問題

網上有說給table新增table-layout: fixed樣式,然而這種方法測試後並沒效果;其實解決這個問題很簡單,就是給table直接指定一個明確的寬度,不妨我們先設個2700px看看效果

        .form-table{
            border-spacing:0;
            width: 2700px;/*先指定一個明確的寬度*/
        }

 這時候我們發現整個table確實變寬了,div出現了橫向滾動條(圖下圖所示),說明剛剛給table設定的2700px確實生效了;這也就是要求我們給table指定的寬度應該剛好是每一列的寬度之和,如果table寬度指定小了,那麼他會從每一列中扣除多餘的寬度,如果table寬度指定多了,他會給每一列加上相應的寬度,畢竟他要保證所有列不能超出table也不能填不滿table。總之每個td的實際寬度會受到整個table的寬度影響,並不完全由td自身的width屬性決定。

 然而很多時候我們並不能預判我們到底有多少列,每一列到底有多寬,因此我們很難一開始就給table設定一個準確的寬度,解決這個問題的辦法也很簡單,就是額外新增一個不指定寬度的列,這個列我們稱之為自適應列,有了這一列後,table就不需要指定一個準確的寬度,而是設定一個比預估寬度稍大一些的值,多出的這部分寬度都會由該列自適應,因此我們修改原始碼,新增一個自適應列:

                <tr class="form-row">
                    <th class="form-header" width="40px"></th>
                    <th class="form-header" v-for="col in 26" width="100px">{{String.fromCharCode(col+64)}}</th>
                    <th></th><!--自適應列-->
                </tr>
                <tr class="form-row" v-for="row in 40">
                    <td class="form-header" width="40px">{{row}}</td>
                    <td class="form-cell" v-for="col in 26"></td>
                    <td></td><!--自適應列-->
                </tr>

 同時把table寬的設定為3000px:

        .form-table{
            border-spacing:0;
            width: 3000px;/*設定一個稍大的寬度*/
        }

效果如下圖所示,最右側這一列會自動適應多出的寬度,在不調整列寬的情況下這樣就OK了。如果要調整列寬請看第五節內容。

關於table中td寬度的更多說明可以參考連結:http://www.cnblogs.com/mqingqing123/p/6163140.html

四、解決table表頭固定的問題

本例中table第一行和第一列都屬於Excel表格的表頭,需要固定不動。網上有解決方案就是使用兩個table,一個做表頭,一個做表身,這種方案只能解決列表頭的問題,如果要同時解決固定行表頭和列表頭的問題,可能需要三個table,這樣的方案會讓頁面佈局變得十分複雜,難以維護,違揹我們簡單直觀的初衷。

為了讓程式碼儘量簡潔而優雅,有沒有基於當前這一個table的辦法呢?當然有,網上已經有人介紹過了,那就是將表頭採用relative佈局,並通過滾輪事件實時更新表頭位置:

1. 給第一行表頭新增col-header類,給第一列表頭新增row-header類,注意:左上角第一個單元格,既是行表頭又是列表頭

2. 給所有表頭單元格設定樣式position: relative,並加上底色(表頭得看上去像表頭的樣子)

3. 監聽外層div的滾動事件,實時更新列表頭的top值為div的scrollTop值,實時更新行表頭的left值為div的scrollLeft值,這一步是關鍵,主要是保持表頭的位置,讓表頭單元格不隨著滾動條的滾動而移動。

html程式碼:

                <tr class="form-row">
                    <th class="form-header col-header row-header" width="40px"></th>
                    <th class="form-header col-header" v-for="col in 26" width="100px">{{String.fromCharCode(col+64)}}</th>
                    <th class="form-header col-header"></th>
                </tr>
                <tr class="form-row" v-for="row in 40">
                    <td class="form-header row-header" width="40px">{{row}}</td>
                    <td class="form-cell" v-for="col in 26"></td>
                    <td></td>
                </tr>

css程式碼:

        .form-header{
            font-weight: normal;
            text-align: center;
            position: relative;/*設定相對定位*/
            background-color: #f7f7f7;/*表頭背景色*/
        }

js程式碼:

$(".form-frame").scroll(function () {
    $(".col-header").css('top',$(".form-frame").scrollTop()); //實時更新第一行表頭的位置,讓他不隨滾動條滾動而移動
    $(".row-header").css('left',$(".form-frame").scrollLeft()); //實時更新第一列表頭的位置,讓他不隨滾動條滾動而移動
})

效果如下圖所示,可以實現內容滾動,而橫豎表頭都不動。

 然而如圖所示還有一點問題,就是左上角壓蓋的問題,這個問題也很簡單,我們只需要把第一行第一列的單元的z-index值設定為1即可,讓它始終在其他單元格上面就不會被覆蓋了。

 這種方案在chrome上表現十分完美,但是在ie會出現表頭閃爍的問題,估計與ie觸發滾動事件的頻率或機制有關,如果對瀏覽器沒有要求,那麼十分推薦這種方式,如果無法容忍ie上的表頭閃爍問題,那就只能另想辦法了。

五、table動態調整列寬和行高

所謂動態調整列寬和行高,就是通過滑鼠拖動表頭單元格之間的分割線來實現行高和列寬的調整,可以參考這一片文章:https://blog.csdn.net/zanychou/article/details/46988529,基本思路如下:

1. 監聽表頭單元格的mousedown、mousemove和mouseup事件,

2. 通過滑鼠座標位置來判斷是否處於可拖動區域,可以定義表頭單元格分割線及其左右(上下)兩邊5px範圍內為可拖動區域,如下圖所示,

3. mousedown記錄要調整的td及其原始寬度和座標,mousemove實時計算新的寬度,mouseup結束拖動,

在上述參考文章的基礎上,筆者稍做了些調整,基本思路不變,調整點有如下幾項:

1. 讓行表頭和列表頭都能動態調整列寬,但是第一列和第一行固定不動(表頭本身的寬高要固定)

2. 兩個單元格分割線的的兩側都可以拖動(原文只能拖動分割線的左側區域)

3. 讓整個table的寬度隨著列寬的調整一起調整(保證其他列寬度不變,此處銜接上面第三節留下的疑問,原因參考上面的第三節)

4. 監聽了整個table的mousemove和mouseup事件,讓滑鼠拖動操作不至於必須保持在表頭單元格上,這樣互動體驗會更好。

關鍵程式碼如下黃色背景標記:

html:監聽相關事件,同時為了讓調整行高列寬的js能夠生效,必須把第一行的單元格的寬度和第一列單元格的高度定義在html中,而不是在css中,如下:

<table class="form-table" @mousemove="table_mousemove" @mouseup="table_mouseup">
    <tr class="form-row">
        <th class="form-header col-header row-header all-header"></th>
        <th class="form-header col-header" v-for="col in 26" width="100px"
            @mousedown="col_header_mousedown" @mousemove="col_header_mousemove">
            {{String.fromCharCode(col+64)}}
        </th>
        <th class="col-header"></th>
    </tr>
    <tr class="form-row" v-for="row in 40">
        <td class="form-header row-header" height="28px"
            @mousedown="row_header_mousedown" @mousemove="row_header_mousemove">
            {{row}}
        </td>
        <td class="form-cell" v-for="col in 26"></td>
        <td></td>
    </tr>
</table>

css:為了保證第一行第一列即表頭本身的行高列寬不變,因此把第一個單元格的高寬放到css中而不是放在html,這樣動態調整行高列寬的js就對第一行第一列不生效了。

/*第一行第一列單元格*/
.all-header{
    z-index: 1;
    height: 28px;
    width: 40px;
}

js程式碼:僅列了列寬的動態調整,行高的邏輯與之相似

var vue = new Vue({
    el:'#app',
    data:{
        //記錄當前正在調整行高和列寬表頭單元格
        resize_header:{
            row_header: null,
            col_header: null
        },
    },
    //初始化固定表頭
    mounted:function(){
        $(".form-frame").scroll(function () {
            $(".col-header").css('top',$(".form-frame").scrollTop());
            $(".row-header").css('left',$(".form-frame").scrollLeft());
        })
    },
    methods:{
        //滑鼠點選列表頭(第一行)
        col_header_mousedown:function (event) {
            //判斷有效區域,單元格分割線前後5個畫素
            if (event.offsetX >= event.target.offsetWidth - 5 && event.buttons == 1) {
                this.resize_header.col_header = event.target; //當前單元格
            } else if (event.offsetX < 5 && event.buttons == 1) {
                this.resize_header.col_header = $(event.target).prev()[0];//左側單元格
            }
            //記錄表頭原始屬性
            if(this.resize_header.col_header != null){
                this.resize_header.col_header.oldX = event.clientX;
                this.resize_header.col_header.oldWidth = this.resize_header.col_header.offsetWidth;
                $(".form-table")[0].oldWidth = $(".form-table").width();//記錄整表寬度
            }
        },
        //滑鼠在第一行移動,改變游標符號
        col_header_mousemove:function (event) {
            //改變游標樣式
            if (event.offsetX >= event.target.offsetWidth - 5 || event.offsetX < 5)
                event.target.style.cursor = 'col-resize';
            else
                event.target.style.cursor = 'default';
        },
        //滑鼠拖動中,實時計算新的寬高
        table_mousemove:function (event) {
            //調整列寬
            var c_header = this.resize_header.col_header;
            if(c_header != null){
                if(c_header.oldWidth + event.clientX - c_header.oldX > 10){
                    c_header.width = c_header.oldWidth + event.clientX - c_header.oldX;
                    c_header.style.width = c_header.width;
                    $(".form-table").width($(".form-table")[0].oldWidth + event.clientX - c_header.oldX);//同步調整表格寬度
                }
            }
        },
        //表格滑鼠擡起,清空記錄
        table_mouseup:function (event) {
            this.resize_header.col_header = null;
        }
    }
});

效果圖如下:

 

六、table中td單元格單選和多選(滑鼠拖動選中或叫拉框選中)

單元格的選中是一個非常重要的功能,很多Excel其他功能都是針對當前選中單元格的,這裡我們主要討論通過滑鼠互動的單元格單選和多選,因此我們需要給每個單元格監聽三個滑鼠事件:mousedown、mouseover和mouseup。

  1. 通過mousedown實現單選和確定當前啟用單元格,而不使用mouseclick,因為click需要等滑鼠按鍵擡起才會觸發,而我們要求滑鼠點下立即觸發(可以參考MS Excel的互動機制);
  2. 使用mouseover實現滑鼠拖動時觸發區域多選,而不使用mousemove,因為mouseover只會在一個單元格內觸發一次,而mousemove在滑鼠移動過程中會不停的觸發,影響效能而且沒有必要;
  3. mouseup中做狀態清除工作。

6.1 樣式分析

首先我們來分析一下選中區域的樣式,有一個焦點單元格背景為白色,其他選中單元格背景為淺綠色,最外圍單元格存在綠色加粗邊框線:

通過簡單的分析我們可以用以下6個class來拆分這些樣式,最後將這些class分別疊加到相應的單元格上即可:

  • .cell-select: 淺綠色背景,應用到所有選中單元格上,後面可以通過該class一次性獲取所有選中單元格
  • .cell-focus: 白色背景,應用到焦點單元格上,覆蓋第一個class
  • .cell-select-top: 帶有上邊框,應用在最上面的單元格上
  • .cell-select-right: 帶有右邊框,應用在最右邊的單元格
  • .cell-select-bottom: 帶有下邊框,應用在最下邊單元格
  • .cell-select-left: 帶有左邊框,應用在最左邊單元格

將以上6個class用到對應的單元格上即可呈現上圖所示的選中效果,例如上圖中第一個單元格同時擁有:.cell-select、.cell-focus、.cell-select-left、.cell-select-top四個樣式。

6.2 位置分析

所謂位置分析即,根據滑鼠點選和移動的位置提取出所有選中的單元格,然後才能給他們設定相應的樣式,為了方便處理位置資訊,我們給所有單元格新增一個row和col屬性,用於標記該單元格的行列座標位置:

<table class="form-table" @mousemove="table_mousemove" @mouseup="table_mouseup">
    <tr class="form-row">
        <th class="form-header col-header row-header all-header"></th>
        <th class="form-header col-header" v-for="col in 26" width="100px"
            @mousedown="col_header_mousedown" @mousemove="col_header_mousemove">
            {{String.fromCharCode(col+64)}}
        </th>
        <th class="col-header"></th>
    </tr>
    <tr class="form-row" v-for="row in 40">
        <td class="form-header row-header" height="28px"
            @mousedown="row_header_mousedown" @mousemove="row_header_mousemove">
            {{row}}
        </td>
        <td class="form-cell" v-for="col in 26" v-bind:row="row" v-bind:col="col"
            @mousedown="cell_mousedown" @mouseover="cell_mousemove" @mouseup="cell_mouseup"></td>
        <td></td>
    </tr>
</table>

1. 監聽所有單元格mousedown事件,觸發該事件的單元格即為起始單元格,也是焦點單元格,記錄到全域性變數focus_td中,程式碼略

2. 監聽所有單元格mouseover事件,觸發該事件的單元格即為當前單元格起始單元格當前單元格之間的位置關係根據滑鼠移動方向不同有以下四種:

不論是哪一種方向,我都轉換為第一種型別,即轉換為通過左上角座標和右下角座標定位的方式,設 fromTd 為起始單元格,toTd為當前單元格,那麼設定選中區域核心程式碼如下:

js程式碼(在mouseover事件中呼叫):

//選中指定兩個單元格之間的所有單元格
region_select:function (fromTd, toTd) {
    //清除之前的選區
    this.remove_select();

    //獲取兩個單元格的座標資料
    var f_row = Number(fromTd.attr("row"));
    var f_col = Number(fromTd.attr("col"));
    var t_row = Number(toTd.attr("row"));
    var t_col = Number(toTd.attr("col"));

    //提取左上角座標和右下角座標
    var ltRow = f_row <= t_row ? f_row : t_row; //左上角對應行
    var ltCol = f_col <= t_col ? f_col : t_col; //左上角對應列
    var rbRow = f_row >= t_row ? f_row : t_row; //右下角對應行
    var rbCol = f_col >= t_col ? f_col : t_col; //右上角對應列

    //根據座標範圍遍歷單元格,設定相應的樣式
    var table = fromTd[0].offsetParent;
    for(var r=ltRow; r<=rbRow; r++){
        for(var c=ltCol; c<=rbCol; c++){
            table.rows[r].cells[c].classList.add("cell-select");
            if(r==ltRow) table.rows[r].cells[c].classList.add("cell-select-top");
            if(r==rbRow) table.rows[r].cells[c].classList.add("cell-select-bottom");
            if(c==ltCol) table.rows[r].cells[c].classList.add("cell-select-left");
            if(c==rbCol) table.rows[r].cells[c].classList.add("cell-select-right");
        }
    }
},
//清除所有選中效果
remove_select:function () {
    $(".cell-select").removeClass("cell-select");
    $(".cell-select-top").removeClass("cell-select-top");
    $(".cell-select-right").removeClass("cell-select-right");
    $(".cell-select-bottom").removeClass("cell-select-bottom");
    $(".cell-select-left").removeClass("cell-select-left");
}

具體的事件監聽及其處理邏輯程式碼略 

七、table中合併單元格與拆分單元格

在上一步完成後,就可以開始做單元格合併與拆分了,即將當前選中區域的所有單元格合併,或將已經合併的單元格拆分。

7.1 合併單元格

table標籤本身就支援合併單元格,這也是一開始技術選型使用table而不是div的好處之一,具體方法看圖分析如下:

1. 選區中第一個單元稱之為擴充套件單元格,本例中只需要設定該單元格的colspan=3,rowspan=4,即可達到擴充套件的效果,即合併單元格效果

2. 選區中其他單元格稱之為被合併單元格,被合併單元格如果不做任何處理,會被擴充套件單元格擠開而向兩邊順延導致整個table不規則;如果直接把這些被合併的單元格remove掉,那麼後面做拆分單元格的時候又需要重新create出來;因此最好的處理辦法是將他們設定為display:none,拆分單元格的時候去掉display樣式即可。

3. 為了後面做拆分單元格更加方便,我們需要把這一次合併的單元做一個統一的標記,例如統一新增一個merged-by屬性,屬性值為擴充套件單元格的行列座標。

//合併當前選中的所有單元格
merge:function () {
    var first = $(".cell-select:first");
    var last = $(".cell-select:last");
    if(!first.is(last)){
        var ltRow = Number(first.attr('row'));
        var ltCol = Number(first.attr('col'));
        var rbRow = Number(last.attr('row'));
        var rbCol = Number(last.attr('col'));

        var rest_cells = $(".cell-select:gt(0)");
        rest_cells.addClass("cell-removed"); //即display:none
        rest_cells.attr("merged-by", ltRow + '_' + ltCol); //新增merged-by標記,方便後期拆分單元格
        first.attr("colspan", rbCol - ltCol + 1);
        first.attr("rowspan", rbRow - ltRow + 1);

        this.region_select(first, first); //選中合併後的單元格
    }
}

7.2 拆分單元格

拆分單元格實際上就是合併單元格的逆過程:

1. 把當前要拆分單元格的colspan和rowspan屬性去掉

2. 把之前被合併的單元格根據merged-by屬性一次性選出來(此處是關鍵),去掉display屬性,去掉merged-by標記

//取消合併單元
demerge:function () {
    var colspan = Number(this.focus_td.attr("colspan"));
    var rowspan = Number(this.focus_td.attr("rowspan"));
    if(colspan > 1 || rowspan > 1) {
        //去掉colspan、rowspan
        this.focus_td.removeAttr("colspan");
        this.focus_td.removeAttr("rowspan");
        //根據merged-by找到被合併的單元格
        var flagAttr = this.focus_td.attr("row") + '_' + this.focus_td.attr("col")
        var merged_cells = $(".cell-removed[merged-by="+flagAttr+"]");
        merged_cells.removeClass("cell-removed"); //去掉display:none
        merged_cells.removeAttr("merged-by"); //去掉merged-by標記
        this.region_select(this.focus_td, merged_cells.last()); //選中拆分後的區域
    }
}

效果如圖所示

 

八、針對第六節中單元格選中的重構

當有了合併單元格後,第六節中的單元格選中功能就存在bug了,可能存在如下情況:

因此一旦選區中包含了合併的單元格,那麼整個選區範圍的計算就不一樣了,此時我們需要把所有相關的合併單元格都要納入到選區範圍計算中來。如果整個表格中存在多個合併單元格,情況會變得更加複雜:每次納入一個合併單元格後,選區範圍可能會擴大,選區範圍擴大後可能會再與另一個合併單元格相交,這時就需要繼續擴大選區,直到再沒有與其他合併單元格相交為止,已經變成一個遞迴問題了,考慮效能問題,還是轉換為迴圈問題處理。

本文采用邊緣掃描法來實現迴圈擴充套件選區:

    1. 給定一個初始的左上角和右下角單元格座標;

    2. 掃描初始座標構成的矩形邊緣單元格,尋找是否存在合併單元格(有merged-by屬性或colspan屬性);

        2.1. 如果找到了合併單元格,獲取該合併單元格左上角和右下角座標,並與初始座標範圍對比;

            2.1.1 如果超出了初始座標,則擴大初始座標至可以包含該合併單元格,返回到第1步;

            2.1.2 沒有超出座標則繼續;

        2.2. 如果沒有找到則繼續;

    3. 最終得到的座標範圍即為當前完整的選區。

下圖說明了整個邊緣掃描演算法的選區擴大過程:

修改之前的region_select方法如下:

//選中指定兩個單元格之間的所有單元格
region_select:function (fromTd, toTd) {
    this.remove_select();

    var f_row = Number(fromTd.attr("row"));
    var f_col = Number(fromTd.attr("col"));
    var t_row = Number(toTd.attr("row"));
    var t_col = Number(toTd.attr("col"));

    var ltRow = f_row <= t_row ? f_row : t_row; //左上角對應行
    var ltCol = f_col <= t_col ? f_col : t_col; //左上角對應列
    var rbRow = f_row >= t_row ? f_row : t_row; //右下角對應行
    var rbCol = f_col >= t_col ? f_col : t_col; //右上角對應列

    var table = fromTd[0].offsetParent;
    
    //從這裡開始進行邊緣掃描擴充套件選區
    do {
        var extend = false; //標記是否擴充套件了選區
        outer:for (var r = ltRow; r <= rbRow; r++) {
            inner:for (var c = ltCol; c <= rbCol; c++) {                        
                if (r == ltRow || r == rbRow || c == ltCol || c == rbCol) {//只取邊緣單元格
                    var edgeTd = $(table.rows[r].cells[c]);
                    var mergeTd = null;
                    
                    if (edgeTd[0].hasAttribute("merged-by")) {  //判斷是否合併單元格
                        var cordinate = edgeTd.attr("merged-by").split("_");
                        var rowNum = Number(cordinate[0]);
                        var colNum = Number(cordinate[1]);
                        mergeTd = $(table.rows[rowNum].cells[colNum]);
                    } else if (edgeTd[0].hasAttribute("colspan")) {  //判斷是否合併單元格
                        mergeTd = edgeTd;
                    }
                    
                    if (mergeTd != null) { //如果是合併單元格                                
                        var m_ltRow = Number(mergeTd.attr("row"));
                        var m_ltCol = Number(mergeTd.attr("col"));
                        var m_rbRow = m_ltRow + Number(mergeTd.attr("rowspan")) - 1;
                        var m_rbCol = m_ltCol + Number(mergeTd.attr("colspan")) - 1;
                        
                        //將合併單元格座標範圍與初始範圍對比,一旦超出則擴充套件初始範圍,標記extend=true
                        if (m_ltRow < ltRow) {
                            ltRow = m_ltRow;
                            extend = true;
                        }
                        if (m_ltCol < ltCol) {
                            ltCol = m_ltCol;
                            extend = true;
                        }
                        if (m_rbRow > rbRow) {
                            rbRow = m_rbRow;
                            extend = true;
                        }
                        if (m_rbCol > rbCol) {
                            rbCol = m_rbCol;
                            extend = true;
                        }
                    }
                    if(extend)break outer; //如果範圍擴充套件了,則重新掃描新的邊緣
                }
            }
        }
    }while(extend); //直到不再擴充套件,邊緣掃描結束

    //給選區單元格新增樣式
    for(var r=ltRow; r<=rbRow; r++){
        for(var c=ltCol; c<=rbCol; c++){
            //先確定該單元格要新增的樣式
            var classArray = ["cell-select"];
            if(r==ltRow) classArray.push("cell-select-top");
            if(r==rbRow) classArray.push("cell-select-bottom");
            if(c==ltCol) classArray.push("cell-select-left");
            if(c==rbCol) classArray.push("cell-select-right");
            //如果該單元格是個被合併的單元格,則將其樣式應用到合併它的單元格上
            var tmpTd = $(table.rows[r].cells[c]);
            if (tmpTd[0].hasAttribute("merged-by")) {
                var cordinate = tmpTd.attr("merged-by").split("_");
                var rowNum = Number(cordinate[0]);
                var colNum = Number(cordinate[1]);
                var tmpTd = $(table.rows[rowNum].cells[colNum]);
            }
            for(var i=0; i<classArray.length; i++){
                tmpTd.addClass(classArray[i]);
            }
        }
    }
}
View Code

效果圖如下,圖中“起”表示滑鼠開始位置,“止”表示滑鼠結束位置,由於受圖中三個合併單元格的影響,整個選取擴大至剛好能包含三個合併單元格的範圍:

九 其他相關功能

前面主要探討了:表格佈局,固定表頭、動態調整行高列寬,滑鼠區域選中,合併與拆分單元格等功能的實現原理,這些僅僅是Excel中最基本的互動操作,還有其他一些基本功能可以在此基礎上延伸,例如整行整列選中、文字對齊、字型字號、邊框設定、背景設定等等,大多數情況下只需要通過.cell-select樣式獲取到當前選中的單元格,然後應用相應的樣式即可,因此單元格選中是基礎功能中的基礎功能。

最後,筆者非專業前端開發出身,歡迎大家批評指正。