Web Spreadsheet in 99 lines
https://github.com/audreyt/500lines/blob/master/spreadsheet/chapter.md
by odrelon
HTML
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.15/angular.min.js"></script>
<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 )">
<div ng-class="{ error: errs[col+row], text: vals[col+row][0] }">
{{ errs[col+row] || vals[col+row] }}</div>
CSS
table { border-collapse: collapse; }
th, td { border: 1px solid #ccc; }
th { background: #ddd; }
td.formula { background: #eef; }
td div { text-align: right; width: 120px; min-height: 1.2em;
overflow: hidden; text-overflow: ellipsis; }
div.text { text-align: left; }
div.error { text-align: center; color: #800; font-size: 90%; border: solid 1px #800 }
input { position: absolute; border: 0; padding: 0; width: 120px; height: 1.3em;
font-size: 100%; color: transparent; background: transparent; }
input:focus { color: #111; background: #efe; }
input:focus + div { white-space: nowrap; }
JavaScript
angular.module('500lines', []).controller('Spreadsheet', function ($scope, $timeout) {
// Begin of $scope properties; start with the column/row labels
$scope.Cols = [], $scope.Rows = [];
makeRange($scope.Cols, 'A', 'H');
makeRange($scope.Rows, 1, 20);
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);
} }
// 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(); }
} );
} };
// Default sheet content, with some data cells and one formula cell.
$scope.reset = function() { $scope.sheet = { B1: 1874, A2: '+', B2: 2046, A3: '⇒', B3: '=B1+B2' }; };
// Define the initializer, and immediately call it
($scope.init = function() {
// Restore the previous .sheet; reset to default if it’s the first run
$scope.sheet = angular.fromJson( localStorage.getItem( '' ) );
if (!$scope.sheet) { $scope.reset(); }
$scope.worker = new Worker( "/echo/js/?js="+encodeURIComponent(
"(" + WorkerJS.toString() + ")()"));
}).call();
// Formula cells may produce errors in .errs; normal cell contents are in .vals
$scope.errs = {}, $scope.vals = {};
// Define the calculation handler; not calling it yet
$scope.calc = function() {
var json = angular.toJson( $scope.sheet );
var promise = $timeout( function() {
// If the worker has not returned in 499 milliseconds, terminate it
$scope.worker.terminate();
// Back up to the previous state and make a new worker
$scope.init();
// Redo the calculation using the last-known...