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...