Web版Excel製做過程分享

因爲項目須要製做一個Web版本Excel用於表單、報表在線繪製,網上搜了一圈沒有找到合適的資源,根據搜到的一些零散信息決定本身動手作一個,本文分享這個製做過程,主要包含表格佈局、表頭固定、動態調整行高列寬、單元格選中、合併與拆分單元格等功能,供你們交流分享。廢話少說先上個效果圖以下:css

 

1、技術選型

1. 本例基於Jquery庫和Vue框架實現,其中Vue並非必須,僅僅由於項目須要而已,讀者只需稍做改造去掉對Vue的依賴便可。html

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

2、先用table作個Excel表格的樣子

原本以爲很容易,用框架動態生成一個n行m列的table,並在第一行自動填充ABC...Z等做爲列表頭,在第一列自動填充123...n等做爲行表頭,使用Vue框架v-for循環生成tr和td元素便可,不熟悉vue的同窗能夠簡單瞭解一下vue中v-for指令,固然也能夠用原生js或jquery生成這個table的全部行列單元格,整體佈局的思路以下:vue

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

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

 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標籤寬度設置無效的問題。算法

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

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

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

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

 然而不少時候咱們並不能預判咱們到底有多少列,每一列到底有多寬,所以咱們很難一開始就給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

4、解決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上的表頭閃爍問題,那就只能另想辦法了。

5、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; } } });

效果圖以下:

 

6、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"); }

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

7、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()); //選中拆分後的區域
 } }

效果如圖所示

 

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

當有了合併單元格後,第六節中的單元格選中功能就存在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樣式獲取到當前選中的單元格,而後應用相應的樣式便可,所以單元格選中是基礎功能中的基礎功能。

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

相關文章
相關標籤/搜索