作一個瀏覽器上的Excel(一)

   本文旨在講述一個控件的開發流水帳式的故事,下圖是本文所要講述的表格控件的運行截圖:html

    

   今天(2015.3.16)是這個插件正式中止開發一週年記念日,因而我決定寫一篇文章悼念一下。
ajax

   按照慣例,我把這回講的程序放了在runjs上:http://runjs.cn/detail/gcdxdyct數據庫

   這個表格具備比較完整的編輯功能,包括生成下拉框、日期控件、像excel那樣子經過鼠標拖動批量選擇、批量編輯、經過拖表自動生成數據、與excel互相複製粘貼、導入導出xlsx等,可是已經沒有使用的需求了,因此就中止了開發,封存了起來。
json

   在作這個以前,我對WebApp是處於比較陌生的狀態的,時至今日,回頭看當初的代碼,儘管很多地方都讓今天的我以爲不妥,卻老是能發如今漸漸熟悉WebApp開發以後所忘記的一些方法,所以,此文也是個人一個複習。
ide

0.起源函數

    在2013年的9月先後,我在一家遊戲公司裏擔任服務端程序開發的工做,同時也兼任遊戲後臺管理系統的開發,這個管理系統基於PHP,主要用於管理遊戲的配置、日誌的監控、遊戲的熱更新等,如今,這個公司貌似不存在了,因而我能夠比較放心地講述一下在那期間我所作的一個小東西。
優化

    起源是上述的遊戲配置系統。一個遊戲裏有數十上百個子功能,每個功能都有着許多須要由策劃來填寫的配置項,譬如物品,物品的配置是一個二維表,策劃們須要在物品表中配置成千上萬個物品,以下圖:
this

    因爲這個後臺是數年前開發的,所以也沒用到響應式技術,對於每一件物品,策劃們須要點擊「添加」,進入另外一個頁面後填寫物品信息以完成添加操做,修改也是須要點擊「修改」跳轉到另外一個頁面去完成,上圖只顯示了幾列屬性,而事實上,要支撐一個遊戲的運行,物品表有數十列屬性,因而當遊戲運營到後期,這種編輯變得十分艱難。
prototype

    不止物品表,其實遊戲中幾乎全部配置都是這樣子的二維表,須要批量編輯的時候,策劃們在Excel中完成後,經過PHP程序導入到數據庫。
插件

    後來,在我接手這個後臺的後續開發工做以後,應策劃們的要求,我先是把大量的編輯操做改成了ajax,也添加了大量DOM操做使得編輯更加簡單,當這種優化修改持續了大概兩個月後,一位策劃提出了一個神奇的需求:

    「能不能像excel那樣子去操做?」


1.雛形 

    在經典的HTML應用裏,表格就是table。

    在後來出現的許多表格插件中,div成爲了表格的軀幹。

    截至目前,有許許多多的表格插件,有的是面向編輯的,有的是面向顯示的,有的是爲了呈現更好的排版,有的是爲了讓人操做更加順手。

    而這位策劃提出需求的時候,我花費了一成天以後也沒找到有這樣的表格插件可供使用。

    那就本身寫?

    一開始的思路很簡單,不就是要作一個數據編輯的控件嘛?那麼,首先來解決把已有數據顯示出來的問題?

    在個人設想中,這個控件是這樣初始化起來的:

    一、PHP從數據庫中獲取整個數據表的數據,json_encode以後傳到頁面上來,假設這個數據存在變量data裏;

    二、頁面加載完以後,調用new Table('#table-div', data);生成表格。

    既然如此,那我再想象一下,這個data裏邊,須要有什麼樣的內容,以供生成一個表格?

    必須的兩部分是表頭與表的數據,我把格式定爲:

    

var data = {
    col : [
        {
        	name    : 'id',
        	display : '編號'
        },
        {
        	name    : 'name',
        	display : '名字'
        },
        {
        	name    : 'sex',
        	display : '性別'
        }
    ],
    row : [
    	{
    		id   : '1',
    		name : '張三',
    		sex  : '男'
    	},
    	{
    		id   : '2',
    		name : '李四',
    		sex  : '女'
    	},
    ]
};

    其中,col定義了表頭,row中包含了數據。

    下一步就是讓表格顯示出來,我設計Table函數在接收到上面的數據時生成以下結構的一段HTML:

<div class="hd">
	<div class="col">編號</div>
	<div class="col">名字</div>
	<div class="col">性別</div>
</div>
<div class="row">
	<div class="col">1</div>
	<div class="col">張三</div>
	<div class="col">男</div>
</div>
<div class="row">
	<div class="col">2</div>
	<div class="col">李四</div>
	<div class="col">女</div>
</div>

    在定義了幾個簡單的樣式以後,我開始思考如何安排一系列的鼠標事件處理函數。


1.統一處理事件   

    按照需求,毫無疑問,這個表格控件將會有很是多的交互操做,包括:

    一、點擊某個單元格時選中單元格、在某個地方顯示出單元格里的完整內容,同時選中行列信息;

    二、按下並拖動鼠標的時候,開啓多選模式,在鼠標釋放的時候把選中區域勾選出來;

    三、某些區域被選中以後,按着ctrl繼續選擇更多的單元格;

    四、某些區域被選中以後,點擊單元格右下方的小點能夠擴大單元格並自動填充;

    五、在單元格上按下方向鍵的時候可移動光標,按下某些按鍵進入編輯;

    ……

    所以,我抽離出了一個Mouse對象和Keyboard對象,用於存放一系列基本的EventListener。

Keyboard : {
    // 鍵盤碼
    KEY_A : 65, KEY_B : 66, KEY_C : 67, KEY_D : 68, KEY_E : 69, KEY_F : 70, KEY_G : 71, KEY_H : 72,
    KEY_I : 73, KEY_J : 74, KEY_K : 75, KEY_L : 76, KEY_M : 77, KEY_N : 78, KEY_O : 79, KEY_P : 80,
    KEY_Q : 81, KEY_R : 82, KEY_S : 83, KEY_T : 84, KEY_U : 85, KEY_V : 86, KEY_W : 87, KEY_X : 88,
    KEY_Y : 89, KEY_Z : 90, KEY_0 : 48, KEY_1 : 49, KEY_2 : 50, KEY_3 : 51, KEY_4 : 52, KEY_5 : 53,
    KEY_6 : 54, KEY_7 : 55, KEY_8 : 56, KEY_9 : 57,
    KEY_NUM_1 :  96, KEY_NUM_2 :  97, KEY_NUM_3 :  98, KEY_NUM_4 :  99, KEY_NUM_5 : 100, 
    KEY_NUM_6 : 101, KEY_NUM_7 : 102, KEY_NUM_8 : 103, KEY_NUM_9 : 104, KEY_NUM_0 : 105,
    KEY_ESC   :  27, KEY_ENTER :  13, KEY_SPACE :  32,
    KEY_LEFT  :  37, KEY_UP    :  38, KEY_RIGHT :  39, KEY_DOWN  :  40,
    KEY_SHIFT :  16, KEY_CTRL  :  17, KEY_ALT   :  18,
    // 右鍵菜單可見時對鍵盤的響應
    menu_down : function(keyCode){
        
    },
    // 一般狀態下對鍵盤的響應
    down : function(e){
        
    },
    up : function(e){
        
    }
},
// 鼠標事件
Mouse : {
    table_down : function(e){

    },
    table_move_auto : function(e){
        
    },
    table_move : function(e){
        
    },
    table_up   : function(e){
        
    },
    table_leave : function(e){

    },
    table_dblclick : function(e){
        
    },
    table_scroll : function(e, d, dx, dy){
        
    }
}

    而後,在Table的構造函數裏,定義兩個對象用於存儲鼠標鍵盤的狀態:

    

this.mouse    = {
    // override爲true時大部分做用在表格本體上的鼠標事件不響應
    override   : false, 
    // 拖放起始點及結束點
    dragStartX : null,
    dragStartY : null,
    dragOverX  : null,
    dragOverY  : null,
    // 點擊座標
    clickX     : null,
    clickY     : null,
    // 滾動條鼠標響應
    scrollV    : false,
    scrollH    : false,
    scroll_y   : 0,
    scroll_x   : 0
}
this.keyboard = {
    // override爲true時大部分做用在表格本體上的鍵盤事件不響應
    override : false,
    ctrlKey  : false,
    shiftKey : false
}

    表格當中有許多的元素,然而,我想要讓全部鼠標點擊事件都集中到一個元素中來處理,那麼,最簡單的實現方法就是用一個透明的遮罩去蓋住整個表格,阻止全部元素的鼠標事件產生,接下來,一切操做均可以基於這個遮罩所接受到得鼠標事件進行處理了。

2.單元格的定位

    當鼠標按下或者拖動、釋放的時候,如何得知當前操做的單元格是哪一個?

    這個問題的解決思路很清晰:

    一、MouseEvent.clientX、MouseEvent.clientY記錄了事件發生的座標;

    二、單元格的行高是固定的,所以要知道事件發生在哪一行,只要用(clientY-表格當前屏的偏移) / 行高;

    三、單元格每一列下面全部格子的寬度都是固定的,所以,只要記錄下每一列的寬度及起始x值,就能經過遍歷找到事件發生在哪一列,也所以,我給data.col的每一項添加了一個可選的配置參數「width」,用於控制每一列的初始寬度;

    由於這個表格控件自定義了滾動條,並且這個自定義滾動條是經過操做整片單元格的left、top實現的,因此每次滾動條的事件發生的時候都要驅改變一些參數,譬如「表格當前屏的偏移」。

    爲了更好地處理像素座標與行列座標的關係,我定義了三個額外的類:

    Pos類,用於描述某個像素點,在交互事件發生的時候,首先根據clientX與clientY產生一個Pos,後續操做基於Pos處理;

    Grid類,用於從方位上描述一個單元格,包含單元格的像素座標以及行列座標;

    Area類,寄存若干個Grid,用於多選、批量編輯。

    基於這些,我給Table添加了兩個原型方法:get_grid(x, y)和get_pos(row, col)來快速轉換位置關係。

    爲了快速定位某個指定name的列或者某個指定display值的列,我給表格添加了一個map_col:

/**
 * 重映射表格數據的列信息
 */
Grid.Table.prototype.remap_col = function(){
    var i;
    this.map_col = {
        name    : {},
        display : {}
    };
    for(i in this.data.col){
        this.map_col.name[this.data.col[i].name]       = i;
        this.map_col.display[this.data.col[i].display] = i;
    }
    return this;
}


3.雙擊編輯

    在excel中,雙擊某個單元格或者在單元格上按下字母鍵時,是會彈出編輯框的,而根據被編輯列的不一樣屬性,這個表格實現了幾種相似的編輯交互:

文本形式

下拉選擇形式

自定義編輯方式

    上面說過,我用一個透明的遮罩把整個表格遮了起來,所以,雙擊某個單元格以後,其實是產生了一個覆蓋在遮罩上、尺寸位置與被編輯的單元格相同的編輯框或者下拉框,爲了實現下拉選擇與自定義編輯功能,我給data.col引入了兩個可選的配置項:replace與select。

    replace是一個可選的函數,當某一列配置有這個屬性時,在將任一行該列的內容賦值給對應單元格的innerHTML以前,都將先將此內容的實際值傳給replace,而後將replace的返回值用於顯示,這個功能被普遍用在物品ID與物品名的替換顯示等之上。

    select能夠是一個函數或者對象,用於生成可供選擇的下拉框,一個典型的replace與select的應用以下:

    

{
    name:'type',
    display:'類型',
    replace : function(raw){
        raw = parseInt(raw);
        if(raw > 10){
            return raw;
        }
        var types = ['無', '攻擊', '氣血', '防護', '堅韌', '閃避', '命中', '暴擊', '冰抗', '毒抗', '火抗'];
        return types[raw];
    },
    select : [{value:1,display:'攻擊'}, {value:2,display:'氣血'}, {value:3,display:'防護'}, {value:4,display:'堅韌'}, {value:5,display:'閃避'}, {value:6,display:'命中'}, {value:7,display:'暴擊'}, {value:8,display:'冰抗'}, {value:9,display:'毒抗'}, {value:10,display:'火抗'}]
}

    寫到這裏,我才發現,OSC的博客正文原來有字數限制!(⊙o⊙)

    那就先到這吧……

相關文章
相關標籤/搜索