序言
随着浏览器运行性能及前端技术的日新月异,对于使用在线表格做报表已经成为时下主流趋势,而在线电子表格也层出不穷,如Google的SpreadSheet等,由于公司报表类产品中需要使用在线电子表格,并且要在基本的电子表格控件上增加许多额外与业务相关的扩展,因此在咨询及使用过一些通用工具后决定自己造轮子。造轮子之前先学习前人的经验,如何设计在线电子表格。为了入门我们先选择了本文将介绍的这个99行代码完成的在线电子表格。
99行代码的电子表格简介
- 作者:audreyt
- 源码地址: http://jsfiddle.net/audreyt/LtDyP/
- 使用技术: HTML5+CSS3+AngularJS1.x
-
主界面截图:
绘制表格的方法
1 定义行数组(Rows
)与列数组(Cols
)
$scope.Cols = [], $scope.Rows = [];
2 初始化数组
- 将列数组(
Cols
)初始化为['A','B','C','D','E','F','G','H']
- 将行数组(
Rows
)初始化为[1,2,3,4,...,18,19,20]
makeRange($scope.Cols, 'A', 'H');
makeRange($scope.Rows, 1, 20);
- 其中
makeRange(array, cur, end)
函数的作用就是根据cur
至end
之间的范围初始化给array
数组,源码如下:
function makeRange(array, cur, end) {
while (cur <= end) {
array.push(cur);
// If it’s a number, increase it by one; otherwise move to next letter
cur = (isNaN( cur ) ? String.fromCharCode(cur.charCodeAt()+1 ) : cur+1);
}
}
3 绑定页面元素
- 利用AngularJS的双向数据绑定特性,将Javascript变量与页面元素进行绑定
<table>
<tr>
<th>
<button type="button" ng-click="reset();calc()">↻</button>
</th>
<th ng-repeat="col in Cols">{{ col }}</th>
</tr>
<tr ng-repeat="row in Rows">
<th>{{ row }}</th>
<td ng-repeat="col in Cols" ng-class="{ formula: ( '=' === sheet[col+row][0] ) }">
<input id="{{ col+row }}" ng-model="sheet[col+row]" ng-change="calc()" ng-model-options="{ debounce: 200 }" ng-keydown="keydown( $event, col, row )">
</td>
</tr>
</table>
通过该HTML代码可以了解,其实表格是通过<table>
标签实现的,其中的单元格就是<input>
文本框,通过行列的循环(ng-repeat
)绘制出一张电子表格。
- 列头为表格第一行,列头第一个元素是一个刷新按钮,其余通过循环(
ng-repeat
)列数组(Cols
)绘制,并将数组元素值作为元素显示内容
<tr>
<th>
<button type="button" ng-click="reset();calc()">↻</button>
</th>
<th ng-repeat="col in Cols">{{ col }}</th>
</tr>
- 第二行开始绘制表格内单元格,首先按行数组(
Rows
)进行循环(ng-repeat
),然后在单个行循环体内,第一个列作为行头,显示行数组的值作为行号,其余通过循环(ng-repeat
)列数组(Cols
)并插入<input>
文本框,并将列号加行号的组合字符串赋值给该单元格ID属性
<tr ng-repeat="row in Rows">
<th>{{ row }}</th>
<td ng-repeat="col in Cols" ng-class="{ formula: ( '=' === sheet[col+row][0] ) }">
<input id="{{ col+row }}" ng-model="sheet[col+row]" ng-change="calc()" ng-model-options="{ debounce: 200 }" ng-keydown="keydown( $event, col, row )">
</td>
</tr>
如此,一张电子表格就绘制完成
单元格添加键盘事件
- HTML中
<input>
标签上声明键盘按下事件(ng-keydown
)
<input id="{{ col+row }}" ng-model="sheet[col+row]" ng-change="calc()" ng-model-options="{ debounce: 200 }" ng-keydown="keydown( $event, col, row )">
- JS中实现
keydown( $event, col, row )
方法
// UP(38) and DOWN(40)/ENTER(13) move focus to the row above (-1) and below (+1).
$scope.keydown = function(event, col, row) {
switch(event.which) {
case 38: case 40: case 13: $timeout( function() {
var direction = (event.which === 38) ? -1 : +1;
var cell = document.querySelector( '#' + col + (row + direction) );
if (cell) {
cell.focus();
}
} );
}
};
如果当键盘按下“上”键(键值:38),则根据单元格ID属性找到上方第一个一个单元格,并使其成为焦点(focus);如果当键盘按下“下”键(键值:40)或“回车”键(键值:13),则根据单元格ID属性找到下方一个单元格,并使其成为焦点(focus)。
表格的数值存储
- 在Javascript代码中声明
sheet
对象
// Restore the previous .sheet
$scope.sheet = angular.fromJson( localStorage.getItem( '' ) );
- 在HTML中
<input>
文本框标签内绑定(ng-model
)sheet
变量
<input id="{{ col+row }}" ng-model="sheet[col+row]" ng-change="calc()" ng-model-options="{ debounce: 200 }" ng-keydown="keydown( $event, col, row )">
sheet
对象中存储形式为键值对,如
$scope.sheet = { B1: 1874, A2: '+', B2: 2046, A3: '⇒', B3: '=B1+B2' };
其中键为单元格ID属性,即行+列组合的字符串,值为单元格值。
表格的运算过程
- 表格计算通过Web Workers开启计算线程完成
$scope.worker = new Worker("/echo/js/?js="+encodeURIComponent("(" + WorkerJS.toString() + ")()"));
- 具体计算方法写在
WorkerJS()
函数内
// Worker.js
function WorkerJS () {
var sheet, errs, vals;
self.onmessage = function(message) {
sheet = message.data, errs = {}, vals = {};
Object.getOwnPropertyNames(sheet || {}).forEach(function(coord) {
// Four variable names pointing to the same coordinate: A1, a1, $A1, $a1
[ '', '$' ].forEach(function(p) { [ coord, coord.toLowerCase() ].forEach(function(c){
var name = p+c;
// Worker is reused across computations, so only define each variable once
if ((Object.getOwnPropertyDescriptor( self, name ) || {}).get) { return; }
// Define self['A1'], which is the same thing as the global variable A1
Object.defineProperty( self, name, { get: function() {
if (coord in vals) { return vals[coord]; }
vals[coord] = NaN;
// Turn numeric strings into numbers, so =A1+C1 works when both are numbers
var x = +sheet[coord];
if (sheet[coord] !== x.toString()) { x = sheet[coord]; }
// Evaluate formula cells that begin with =
try { vals[coord] = (('=' === x[0]) ? eval.call( null, x.slice( 1 ) ) : x);
} catch (e) {
var match = /\$?[A-Za-z]+[1-9][0-9]*\b/.exec( e );
if (match && !( match[0] in self )) {
// The formula refers to a uninitialized cell; set it to 0 and retry
self[match[0]] = 0;
delete vals[coord];
return self[coord];
}
// Otherwise, stringify the caught exception in the errs object
errs[coord] = e.toString();
}
// Turn vals[coord] into a string if it's not a number or boolean
switch (typeof vals[coord]) { case 'function': case 'object': vals[coord]+=''; }
return vals[coord];
} } );
}); });
});
// For each coordinate in the sheet, call the property getter defined above
for (var coord in sheet) { self[coord]; }
postMessage([ errs, vals ]);
};
}
-
计算流程为
总结
经过分析,99行实现的该电子表格虽然功能简单,但是基本的表格绘制及运算实现了,为我们未来设计电子表格结构提供了重要的参考价值。