做一个浏览器上的Excel(一)

本文旨在讲述一个控件的开发流水账式的故事,下图是本文所要讲述的表格控件的运行截图:

今天(2015.3.16)是这个插件正式停止开发一周年纪念日,于是我决定写一篇文章悼念一下。

按照惯例,我把这回讲的程序放了在runjs上:http://runjs.cn/detail/gcdxdyct

这个表格具有比较完整的编辑功能,包括生成下拉框、日期控件、像excel那样子通过鼠标拖动批量选择、批量编辑、通过拖表自动生成数据、与excel互相复制粘贴、导入导出xlsx等,但是已经没有使用的需求了,所以就停止了开发,封存了起来。

在做这个之前,我对WebApp是处于比较陌生的状态的,时至今日,回头看当初的代码,尽管不少地方都让今天的我觉得不妥,却总是能发现在渐渐熟悉WebApp开发之后所忘记的一些方法,因此,此文也是我的一个复习。

0.起源

在2013年的9月前后,我在一家游戏公司里担任服务端程序开发的工作,同时也兼任游戏后台管理系统的开发,这个管理系统基于PHP,主要用于管理游戏的配置、日志的监控、游戏的热更新等,现在,这个公司貌似不存在了,于是我可以比较放心地讲述一下在那期间我所做的一个小东西。

起源是上述的游戏配置系统。一个游戏里有数十上百个子功能,每一个功能都有着许多需要由策划来填写的配置项,譬如物品,物品的配置是一个二维表,策划们需要在物品表中配置成千上万个物品,如下图:

由于这个后台是数年前开发的,因此也没用到响应式技术,对于每一件物品,策划们需要点击“添加”,进入另一个页面后填写物品信息以完成添加操作,修改也是需要点击“修改”跳转到另一个页面去完成,上图只显示了几列属性,而事实上,要支撑一个游戏的运行,物品表有数十列属性,于是当游戏运营到后期,这种编辑变得十分艰难。

不止物品表,其实游戏中几乎所有配置都是这样子的二维表,需要批量编辑的时候,策划们在Excel中完成后,通过PHP程序导入到数据库。

后来,在我接手这个后台的后续开发工作之后,应策划们的要求,我先是把大量的编辑操作改为了ajax,也添加了大量DOM操作使得编辑更加简单,当这种优化修改持续了大概两个月后,一位策划提出了一个神奇的需求:

“能不能像excel那样子去操作?”

1.雏形 

    在经典的HTML应用里,表格就是table。

在后来出现的许多表格插件中,div成为了表格的躯干。

截至目前,有许许多多的表格插件,有的是面向编辑的,有的是面向显示的,有的是为了呈现更好的排版,有的是为了让人操作更加顺手。

而这位策划提出需求的时候,我花费了一整天之后也没找到有这样的表格插件可供使用。

那就自己写?

一开始的思路很简单,不就是要做一个数据编辑的控件嘛?那么,首先来解决把已有数据显示出来的问题?

在我的设想中,这个控件是这样初始化起来的:

1、PHP从数据库中获取整个数据表的数据,json_encode之后传到页面上来,假设这个数据存在变量data里;

2、页面加载完之后,调用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.统一处理事件

按照需求,毫无疑问,这个表格控件将会有非常多的交互操作,包括:

1、点击某个单元格时选中单元格、在某个地方显示出单元格里的完整内容,同时选中行列信息;

2、按下并拖动鼠标的时候,开启多选模式,在鼠标释放的时候把选中区域勾选出来;

3、某些区域被选中之后,按着ctrl继续选择更多的单元格;

4、某些区域被选中之后,点击单元格右下方的小点可以扩大单元格并自动填充;

5、在单元格上按下方向键的时候可移动光标,按下某些按键进入编辑;

……

因此,我抽离出了一个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.单元格的定位

当鼠标按下或者拖动、释放的时候,如何得知当前操作的单元格是哪个?

这个问题的解决思路很清晰:

1、MouseEvent.clientX、MouseEvent.clientY记录了事件发生的坐标;

2、单元格的行高是固定的,因此要知道事件发生在哪一行,只要用(clientY-表格当前屏的偏移) / 行高;

3、单元格每一列下面所有格子的宽度都是固定的,因此,只要记录下每一列的宽度及起始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⊙)

那就先到这吧……

时间: 2024-11-04 14:14:17

做一个浏览器上的Excel(一)的相关文章

VUE2.0+VUE-Router做一个图片上传预览的组件

之前发了一篇关于自己看待前端组件化的文章,但是由于学习和实践的业务逻辑差异,所以自己练习的一些demo逻辑比较简单,打算用vue重构现在公司做的项目,所以在一些小的功能页面上使用vue来做的,现在写的这个是项目中用户反馈功能而来的,收获还是挺多的. 收获:dom操作=>数据操作       router的使用       组件的使用,具体总结放在尾部. 功能:1.上传图片 2.显示缩略图 3.可以删除 4.可以重新选择文件 先上成品图(主要抽取图片这块),自己在家主要做的功能,样式就不计较了.

iOS网络篇3-利用UIWebView做一个浏览器功能

在UIkit中,苹果封装了一个特别好用的控件UIWebView能够实现简单的网页加载和文件加载 一.加载网页资源 1.首先新建一个singleView项目,在storyboard拖如下控件 2.设置相关属性连接如下 3.在ViewController.m里的类扩展遵守UISearchBarDelegate和UIWebViewDelegate @interface ViewController () <UISearchBarDelegate,UIWebViewDelegate> /**网页展示*

如何做一个好的实习生

最近一直在招实习生,希望从中能够选出两三个优秀的给自己当做助手用.在观察他们做事的头一两个星期中,其实孰优孰劣已基本分出,结合自己曾经当实习生的经历,想给后来的同学们提一些建议. 我每次面试实习生的时候都会问他们,你想从这份实习经历中获得些什么呢?然后跟他介绍所提供这份实习的工作内容,并请他考虑清楚与他的预期是否相符.比如有一次面试的一位实习生,她本科学的是人力资源管理,所以快毕业时的这份实习是想在专业上有所精进的,而我所在的部门属于业务部门,完全不能帮助她锻炼与人力相关的任何能力,于是我当时明

像在桌面手动创建表格一样,做一个共用のExcel导出

[前言]昔日龌龊不足夸,今朝放荡思无涯. [思路]我们在桌面上创建Excel的时候: 1.首先创建一个Excel也就是 //声明工作薄 HSSFWorkbook wb = new HSSFWorkbook(); 2.接下来未页签sheet重命名,也就是 //sheet页签部分 HSSFSheet sheet = wb.createSheet("页签的名字"); 3.规划一下有多少列,再起一个标题,也就是 //合并标题 sheet.addMergedRegion(new Region(0

使用plupload做一个类似qq邮箱附件上传的效果

公司项目中使用的框架是springmvc+hibernate+spring,目前需要做一个类似qq邮箱附件上传的功能,暂时只是上传小类型的附件 处理过程和解决方案都需要添加附件,处理过程和解决方案都可以添加多个附件,也可一个都不添加 以其中一个为例:(文件保存到了数据库中),有关plupload的内容可参考:http://www.360doc.com/content/14/0714/03/552866_394228686.shtml 首先是po package cn.com.plupload.p

奇怪也哉!做一个WebApp居然遇到了FF浏览器进不去某页的问题。

一个小WebApp,原定login完后就进某页面.前天晚上之前一直在FF浏览器上调试,都正常. 前天晚上稍修改了,提交war上传,之后就休息了. 昨天晚上有些事情处理打开一看,乖乖,点登录后就在登陆页呆着,只有html的title变成了某页的title. 等了一段时间,又试了几次,都是这样,有点急了,因为虽然是小App,但每天都有人去登记点信息的.要是报告不好用,那么是有不利影响的. 以为是war的问题,又重新打了一个,删掉原来的再发布到服务器,还是一样. 尝试用Chrome和Opera打开,这

浏览器将URL变成一个屏幕上显示的网页的过程?

前言 一个浏览器是怎么工作的? 正文 URL变网页过程: 1.浏览器通过http或https协议,向服务端请求页面 2.将请求过来的HEML代码通过解析,构建DOM树 3.计算DOM树上的CSS属性 4.根据CSS属性,对元素逐个进行渲染,得到内存位图 5.一个可选的步骤是对位图进行合成,这会极大地增加后续绘制的速度 6.合成之后,绘制带界面上. 以上内容为个人的学习笔记,仅作为学习交流之用. 欢迎大家关注公众号,不定时干货,只做有价值的输出 作者:Dawnzhang 出处:https://ww

做导出EXCEL时,如何删除在服务器上的EXCEL文件

来源   http://bbs.csdn.net/topics/310142538 刚导出的文件,也许没有办法即刻删除.它会提示,如:"文件“D:\..\rts.xls”正由另一进程使用,因此该进程无法访问该文件." 所以您得先有站点创建一个临时目录,把导出的文件放在这个临时目录中.http://www.cnblogs.com/insus/articles/1400266.html这个临时文作夹内的文件,当天的文件删除不了,我们只能做一个动作每天删除昨天或以前的临时文件.参考:http

做一个手机端页面时,遇到了一个奇怪的问题:字体的显示大小,与在CSS中指定的大小不一致

最近在做一个手机端页面时,遇到了一个奇怪的问题:字体的显示大小,与在CSS中指定的大小不一致.大家可以查看这个Demo(记得打开Chrome DevTools). 就如上图所示,你可以发现,原本指定的字体大小是24px,但是最终计算出来的却是53px,看到这诡异的结果,我心中暗骂一句:这什么鬼! 随后开始对问题各种排查:某个标签引起的?某个CSS引起的?又或者是某句JS代码引起的.通过一坨坨的删代码,发现貌似都不是.我不禁又骂,到底什么鬼!不过中间还是发现了一些端倪:当页面中的标签数量或者文本数