├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── data.csv ├── package.json └── public ├── css └── spreadsheet.css ├── index.html └── js ├── components ├── application.js ├── cell.js ├── row.js ├── spreadsheet.js └── toolbar.js ├── dispatchers └── spreadsheet.js ├── entities ├── cell.js └── formula.js ├── libs └── filesaver.js ├── main.js ├── mixins └── cell.js └── stores └── spreadsheet.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | public/js/main.dist.js 30 | npm-debug.log 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ){ 2 | grunt.initConfig({ 3 | browserify: { 4 | options: { 5 | transform: [ require('grunt-react').browserify ] 6 | }, 7 | app: { 8 | src: 'public/js/main.js', 9 | dest: 'public/js/main.dist.js' 10 | } 11 | }, 12 | 13 | connect: { 14 | server: { 15 | options: { 16 | port: 3000, 17 | base: 'public', 18 | livereload: true 19 | } 20 | } 21 | }, 22 | 23 | watch: { 24 | js: { 25 | files: ['public/**/**/*.js', 'public/**/*.js', '!public/js/main.dist.js'], 26 | tasks: ['browserify'] 27 | }, 28 | livereload: { 29 | options: { 30 | livereload: true 31 | }, 32 | files: [ 33 | 'public/**/**/*.js', 34 | 'public/**/*.js', 35 | 'public/css/*.css', 36 | '!public/js/main.dist.js', 37 | 'public/index.html' 38 | ] 39 | } 40 | 41 | } 42 | }); 43 | 44 | grunt.loadNpmTasks('grunt-browserify'); 45 | grunt.loadNpmTasks('grunt-contrib-watch'); 46 | grunt.loadNpmTasks('grunt-contrib-connect'); 47 | 48 | grunt.registerTask( 'dev', ['browserify', 'connect', 'watch'] ) 49 | grunt.registerTask('default', ['browserify']); 50 | }; 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Carlos Villuendas Zambrana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-spreadsheet 2 | ================= 3 | 4 | Spreadsheet as a reactJS component. 5 | 6 | ### Install 7 | 8 | Dependencies: 9 | 10 | * nodeJS 0.10 11 | * npm 1.4 12 | * grunt 0.4 13 | 14 | #### How to install 15 | 16 | $ git clone https://github.com/carlosvillu/react-spreadsheet 17 | $ cd react-spreadsheet 18 | $ npm install 19 | $ grunt dev 20 | 21 | Open your browser in http://localhost:3000 22 | 23 | ### Description 24 | 25 | The goal of this project is to build an online spreadsheet. It is a 40x40 grid with editable cells. Cells adapt to the data entered by the user. 26 | 27 | When clicking a cell, the background of the cell turns blue, indicating it has been selected. Double click turns the background orange, indicating it can be edited. 28 | 29 | Cell editing allows the user to enter numeric values or strings, as well as formulas. Formulas must have the following format: =(row number, column number) {op} (row number, column number)... 30 | 31 | =A2+B8 this will add up the contents of the two respective cells 32 | 33 | Editing ends when clicking any different cell. For formulas, the grid shows "Formula result". 34 | 35 | #### Saving a file 36 | 37 | The app saves files in csv format (comma-separated spreadsheet). To save a file click on the download icon (leftmost icon on the toolbar) and an alert will ask you to specify the name of the file. The default name was set as `spreadsheet.csv`. 38 | 39 | Formulas, and not results, are saved in the csv file. 40 | 41 | #### Loading 42 | 43 | To load an existing csv file, simply drag a file onto the grid. The grid will show the exact number of rows and columns with the original data. 44 | 45 | ### Architecture 46 | 47 | The development is web-component oriented. Thus, the spreadhsheet is a single component that can be used in the following way, ``. On the basis of this, it is very simple to create a multi-tab app with several spreadsheets, each with its own store. 48 | 49 | I used ReactJS framework for this design, following Flux's development philosophy. 50 | 51 | The main actors are 52 | 53 | * *Store*: saves the state of the full spreadsheet at any given point. If changes are made in the store, it emits a chain event to update the view. 54 | * *View*: consists of n row views, which, in turn, consist in m cell views. 55 | * *Entity*: there is a single basic entity: the cell. It contains its data and state and knows what to show, depending on its state (editing or non-editing). 56 | * *Dispatcher*: directs the information flow from the view to the store, so that the view never communicates directly with the store. This allows one to keep separate view events from data state-changes. 57 | 58 | ### To do 59 | 60 | 61 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/carlosvillu/react-spreadsheet/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 62 | 63 | -------------------------------------------------------------------------------- /data.csv: -------------------------------------------------------------------------------- 1 | 0,1,2,3,4,5,6,7,8,9 2 | 10,11,12,13,14,15,16,17,18,19 3 | 20,21,22,23,24,25,26,27,28,29 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-spreadsheet", 3 | "version": "1.0.0", 4 | "description": "Spreadsheet like ReactJS component", 5 | "main": "Gruntfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/carlosvillu/react-spreadsheet" 12 | }, 13 | "keywords": [ 14 | "reactjs", 15 | "web", 16 | "component", 17 | "spreadsheet" 18 | ], 19 | "author": "Carlos Villuendas", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/carlosvillu/react-spreadsheet/issues" 23 | }, 24 | "homepage": "https://github.com/carlosvillu/react-spreadsheet", 25 | "devDependencies": { 26 | "grunt": "^0.4.5", 27 | "grunt-browserify": "^3.2.0", 28 | "grunt-contrib-connect": "^0.8.0", 29 | "grunt-contrib-watch": "^0.6.1", 30 | "grunt-react": "^0.10.0" 31 | }, 32 | "dependencies": { 33 | "backbone": "^1.1.2", 34 | "debug": "^2.1.0", 35 | "drag-drop": "^2.0.0", 36 | "flux": "^2.0.1", 37 | "jquery": "^2.1.1", 38 | "lodash": "^2.4.1", 39 | "mathjs": "^1.1.1", 40 | "react": "^0.12.0", 41 | "underscore": "^1.7.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/css/spreadsheet.css: -------------------------------------------------------------------------------- 1 | [contenteditable]:focus { 2 | outline: 0px solid transparent; 3 | } 4 | .cell { 5 | min-height: 20px; 6 | min-width: 40px; 7 | } 8 | .cell.selected { 9 | background: rgba(169, 208, 221, 0.490196); 10 | } 11 | .cell.selected.active { 12 | background: antiquewhite; 13 | } 14 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bootstrap 101 Template 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/js/components/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require( 'react' ), 4 | Spreadsheet = require( './spreadsheet' ), 5 | Toolbar = require( './toolbar' ), 6 | SpreadsheetStore = require( '../stores/spreadsheet' ), 7 | spreadsheetStore = new SpreadsheetStore( 20, 20 ); 8 | 9 | var Application = React.createClass( { 10 | render: function(){ 11 | return ( 12 |
13 |
14 | 15 | 16 |
17 |
18 | ) 19 | } 20 | } ); 21 | 22 | module.exports = Application; 23 | -------------------------------------------------------------------------------- /public/js/components/cell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require( 'react/addons' ), 4 | CellMixin = require( '../mixins/cell' ), 5 | SpreadsheetDispatcher = require( '../dispatchers/spreadsheet' ), 6 | debug = require( 'debug' )('Spreadsheet:cell'); 7 | 8 | var Cell = React.createClass( { 9 | mixins: [CellMixin], 10 | updateCell: function( evt ){ 11 | var cell = this.coords( evt.target ), 12 | value = evt.target.innerHTML; 13 | SpreadsheetDispatcher.dispatch( { 14 | actionType: 'cell-update', 15 | cell: cell, 16 | value: value 17 | } ); 18 | }, 19 | 20 | selectCell: function( evt ){ 21 | var cell = this.coords( evt.target ); 22 | SpreadsheetDispatcher.dispatch( { 23 | actionType: 'cell-selected', 24 | cell: cell 25 | } ); 26 | }, 27 | 28 | activeCell: function( evt ){ 29 | var cell = this.coords( evt.target ); 30 | SpreadsheetDispatcher.dispatch( { 31 | actionType: 'cell-active', 32 | cell: cell 33 | } ); 34 | }, 35 | 36 | render: function(){ 37 | var self = this, 38 | classes = React.addons.classSet({ 39 | cell: true, 40 | selected: this.props.cell.status( 'selected' ), 41 | active: this.props.cell.status( 'active' ) 42 | }); 43 | return ( 44 | 53 | ) 54 | } 55 | } ); 56 | 57 | module.exports = Cell; 58 | -------------------------------------------------------------------------------- /public/js/components/row.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require( 'react' ), 4 | Cell = require( './cell' ); 5 | 6 | var Row = React.createClass( { 7 | render: function(){ 8 | var self = this; 9 | return ( 10 | 11 | { 12 | this.props.row.map( function( cell, index ){ 13 | return 14 | } ) 15 | } 16 | 17 | ) 18 | } 19 | } ); 20 | 21 | module.exports = Row; 22 | -------------------------------------------------------------------------------- /public/js/components/spreadsheet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require( 'react' ), 4 | debug = require( 'debug' )('Spreadsheet:spreadsheet'), 5 | Row = require( './row' ), 6 | dragDrop = require('drag-drop/buffer'); 7 | 8 | var Spreadsheet = React.createClass( { 9 | componentDidMount: function(){ 10 | var self = this; 11 | dragDrop( 'body', function( files ){ 12 | files.forEach( function( file ){ 13 | self.props.spreadsheetStore.updateFromCSV( file.toString( 'utf8' ) ); 14 | self.forceUpdate(); 15 | debug( self.props.spreadsheetStore.toString() ); 16 | } ); 17 | }); 18 | this.props.spreadsheetStore.on( 'change', function(){ self.forceUpdate() } ); 19 | }, 20 | render: function(){ 21 | return ( 22 |
23 | 24 | 25 | { 26 | this.props.spreadsheetStore.grid().map( function( row, index ){ 27 | return 28 | } ) 29 | } 30 | 31 |
32 |
33 | ) 34 | } 35 | } ); 36 | 37 | module.exports = Spreadsheet; 38 | -------------------------------------------------------------------------------- /public/js/components/toolbar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require( 'react' ), 4 | SpreadsheetDispatcher = require( '../dispatchers/spreadsheet' ), 5 | saveAs = require( '../libs/filesaver' ); 6 | 7 | var Toolbar = React.createClass( { 8 | componentDidMount: function(){ 9 | }, 10 | dowloadSpreadsheet: function( evt ){ 11 | var datas = this.props.spreadsheetStore.toString( true ).replace( /^\n/, '' ), 12 | name = prompt("Please enter your name", "spreadsheet.csv"); 13 | saveAs( new Blob( [datas], {type: "text/plain;charset=utf-8"} ), name || 'spreadsheet.csv' ); 14 | }, 15 | addRowSpreadsheet: function(){ 16 | SpreadsheetDispatcher.dispatch( { 17 | actionType: 'spreadsheet-add-row', 18 | } ); 19 | }, 20 | addColumnSpreadsheet: function(){ 21 | SpreadsheetDispatcher.dispatch( { 22 | actionType: 'spreadsheet-add-column', 23 | } ); 24 | }, 25 | render: function(){ 26 | return ( 27 |
28 | 38 |
39 | ) 40 | } 41 | } ); 42 | 43 | module.exports = Toolbar; 44 | -------------------------------------------------------------------------------- /public/js/dispatchers/spreadsheet.js: -------------------------------------------------------------------------------- 1 | var Dispatcher = require( 'flux' ).Dispatcher; 2 | 3 | var SpreadsheetDispatcher = new Dispatcher(); 4 | 5 | module.exports = SpreadsheetDispatcher; 6 | -------------------------------------------------------------------------------- /public/js/entities/cell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Formula = require( './formula' ); 4 | 5 | var FORMULA = /^=/; 6 | 7 | var Cell = function( content, spreadsheet ){ 8 | var self = this; 9 | this._content = content || ''; 10 | this._status = {active: false, selected: false} 11 | 12 | this._spreadsheet = spreadsheet; 13 | this._spreadsheet.setMaxListeners( 0 ); // There is not memory leaks 14 | this._spreadsheet.on( 'reset:status', function(){ 15 | self.status( 'selected', false ); 16 | self.status( 'active' , false ); 17 | } ); 18 | }; 19 | 20 | Cell.prototype.content = function( content ){ 21 | return this._content = content ? content : this._content; 22 | }; 23 | 24 | Cell.prototype.status = function( status, value ){ 25 | if( typeof(value) === 'boolean' ){ 26 | this._status[status] = value; 27 | return true; 28 | } 29 | return this._status[status]; 30 | 31 | }; 32 | 33 | Cell.prototype.isFormula = function(){ 34 | return !!this._content.match( FORMULA); 35 | }; 36 | 37 | Cell.prototype.resolve = function( formula ){ 38 | return new Formula( formula, this._spreadsheet ).resolve(); 39 | }; 40 | 41 | Cell.prototype.toString = function( isExport ){ 42 | return isExport || this.status( 'active' ) ? this._content 43 | : this.isFormula( this._content ) ? this.resolve( this._content ) 44 | : this._content 45 | }; 46 | 47 | module.exports = Cell; 48 | -------------------------------------------------------------------------------- /public/js/entities/formula.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require( 'debug' )( 'Spreadsheet:entity:formula' ), 4 | _ = require( 'lodash' ), 5 | mathjs = require( 'mathjs' ); 6 | 7 | var OPERANDOS = /([A-Z]+[0-9]+)/g; 8 | 9 | var Formula = function( expression, spreadsheet ){ 10 | this._expression = expression; 11 | this._spreadsheet = spreadsheet; 12 | }; 13 | 14 | Formula.prototype.parse = function(){ 15 | var self = this, 16 | dicc; 17 | dicc = this._expression.split( OPERANDOS ) 18 | .filter( function( elem ){ 19 | return elem.match( OPERANDOS ); 20 | } ) 21 | .reduce( function( dicc, oper ){ 22 | dicc[oper] = self._spreadsheet.cell( [self.y(oper), self.x( oper )] ).content(); 23 | return dicc; 24 | }, {} ); 25 | 26 | return _.keys( dicc ).reduce( function( expression, entry ){ 27 | return expression.replace( new RegExp( entry, 'g' ), dicc[entry] ); 28 | }, this._expression ).replace( /^=/, '' ); 29 | }; 30 | 31 | Formula.prototype.x = function( oper ){ 32 | var X = /[A-Z]+/, 33 | XAXY = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' ], 34 | literals = oper.match( X )[0].split( '' ); 35 | 36 | return literals.reduce( function( x, literal, index ){ 37 | return x + ( _.lastIndexOf(XAXY, literal) + ( XAXY.length * index ) ); 38 | }, 0 ); 39 | 40 | }; 41 | 42 | Formula.prototype.y = function( oper ){ 43 | var Y = /[0-9]+/; 44 | 45 | return parseInt( oper.match( Y )[0], 10 ); 46 | }; 47 | 48 | Formula.prototype.resolve = function(){ 49 | var result; 50 | try{ 51 | result = mathjs.eval( this.parse() ); 52 | } catch( e ) { 53 | result = "#!ERROR"; 54 | } 55 | return result; 56 | }; 57 | 58 | module.exports = Formula; 59 | -------------------------------------------------------------------------------- /public/js/libs/filesaver.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 2 | var saveAs=saveAs||"undefined"!==typeof navigator&&navigator.msSaveOrOpenBlob&&navigator.msSaveOrOpenBlob.bind(navigator)||function(a){"use strict";if("undefined"===typeof navigator||!/MSIE [1-9]\./.test(navigator.userAgent)){var k=a.document,n=k.createElementNS("http://www.w3.org/1999/xhtml","a"),w="download"in n,x=function(c){var e=k.createEvent("MouseEvents");e.initMouseEvent("click",!0,!1,a,0,0,0,0,0,!1,!1,!1,!1,0,null);c.dispatchEvent(e)},q=a.webkitRequestFileSystem,u=a.requestFileSystem||q||a.mozRequestFileSystem, 3 | y=function(c){(a.setImmediate||a.setTimeout)(function(){throw c;},0)},r=0,s=function(c){var e=function(){"string"===typeof c?(a.URL||a.webkitURL||a).revokeObjectURL(c):c.remove()};a.chrome?e():setTimeout(e,10)},t=function(c,a,d){a=[].concat(a);for(var b=a.length;b--;){var l=c["on"+a[b]];if("function"===typeof l)try{l.call(c,d||c)}catch(f){y(f)}}},m=function(c,e){var d=this,b=c.type,l=!1,f,p,k=function(){t(d,["writestart","progress","write","writeend"])},g=function(){if(l||!f)f=(a.URL||a.webkitURL|| 4 | a).createObjectURL(c);p?p.location.href=f:void 0==a.open(f,"_blank")&&"undefined"!==typeof safari&&(a.location.href=f);d.readyState=d.DONE;k();s(f)},h=function(a){return function(){if(d.readyState!==d.DONE)return a.apply(this,arguments)}},m={create:!0,exclusive:!1},v;d.readyState=d.INIT;e||(e="download");if(w)f=(a.URL||a.webkitURL||a).createObjectURL(c),n.href=f,n.download=e,x(n),d.readyState=d.DONE,k(),s(f);else{a.chrome&&b&&"application/octet-stream"!==b&&(v=c.slice||c.webkitSlice,c=v.call(c,0, 5 | c.size,"application/octet-stream"),l=!0);q&&"download"!==e&&(e+=".download");if("application/octet-stream"===b||q)p=a;u?(r+=c.size,u(a.TEMPORARY,r,h(function(a){a.root.getDirectory("saved",m,h(function(a){var b=function(){a.getFile(e,m,h(function(a){a.createWriter(h(function(b){b.onwriteend=function(b){p.location.href=a.toURL();d.readyState=d.DONE;t(d,"writeend",b);s(a)};b.onerror=function(){var a=b.error;a.code!==a.ABORT_ERR&&g()};["writestart","progress","write","abort"].forEach(function(a){b["on"+ 6 | a]=d["on"+a]});b.write(c);d.abort=function(){b.abort();d.readyState=d.DONE};d.readyState=d.WRITING}),g)}),g)};a.getFile(e,{create:!1},h(function(a){a.remove();b()}),h(function(a){a.code===a.NOT_FOUND_ERR?b():g()}))}),g)}),g)):g()}},b=m.prototype;b.abort=function(){this.readyState=this.DONE;t(this,"abort")};b.readyState=b.INIT=0;b.WRITING=1;b.DONE=2;b.error=b.onwritestart=b.onprogress=b.onwrite=b.onabort=b.onerror=b.onwriteend=null;return function(a,b){return new m(a,b)}}}("undefined"!==typeof self&& 7 | self||"undefined"!==typeof window&&window||this.content);"undefined"!==typeof module&&null!==module?module.exports=saveAs:"undefined"!==typeof define&&null!==define&&null!=define.amd&&define([],function(){return saveAs}); 8 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require( 'react' ), 4 | debug = require('debug'), 5 | Application = require( './components/application' ); 6 | 7 | debug.enable( 'Spreadsheet:*' ); 8 | 9 | React.render( , document.getElementById( 'spreadsheet-container' ) ); 10 | -------------------------------------------------------------------------------- /public/js/mixins/cell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CellMixin = { 4 | coords: function( node ){ 5 | return node.id 6 | .split('-') 7 | .map( function( number ){ return parseInt( number, 10 ) } ); 8 | } 9 | }; 10 | 11 | module.exports = CellMixin; 12 | -------------------------------------------------------------------------------- /public/js/stores/spreadsheet.js: -------------------------------------------------------------------------------- 1 | var Cell = require( '../entities/cell' ), 2 | debug = require( 'debug' )('Spreadsheet:spreadsheetStore'), 3 | SpreadsheetDispatcher = require( '../dispatchers/spreadsheet' ), 4 | EventEmitter = require( 'events' ).EventEmitter, 5 | inherits = require( 'util' ).inherits, 6 | _ = require( 'lodash' ); 7 | 8 | var SpreadsheetStore = function( width, height, def ){ 9 | var self = this; 10 | this._def = def; 11 | this._dispatcherToken = SpreadsheetDispatcher.register( _.bind( this.dispatcherCallback, this ) ); 12 | this._grid = _.range( width ).map( function(row ){ 13 | return _.range( height ).map( function( cell ){ 14 | return new Cell( def, self ); 15 | } ); 16 | } ); 17 | }; 18 | inherits( SpreadsheetStore, EventEmitter ); 19 | 20 | SpreadsheetStore.prototype.dispatcherCallback = function( payload ){ 21 | var self = this; 22 | switch( payload.actionType ){ 23 | 24 | case 'spreadsheet-add-row': 25 | this._grid[this._grid.length++] = _.range( /* One of them */this._grid[0].length ).map( function( cell ){ 26 | return new Cell( self._def, self ); 27 | } ); 28 | this.emit( 'change' ); 29 | break; 30 | 31 | case 'spreadsheet-add-column': 32 | this._grid.forEach( function( row ){ 33 | row[row.length++] = new Cell( self._def, self ); 34 | } ); 35 | this.emit( 'change' ); 36 | break; 37 | 38 | case 'cell-update': 39 | this.cell( payload.cell ).content( payload.value ); 40 | break; 41 | 42 | case 'cell-selected': 43 | this.emit( 'reset:status' ); 44 | var cell = this.cell( payload.cell ), 45 | isSelect = cell.status( 'selected' ); 46 | if( !isSelect ){ 47 | cell.status( 'selected', true ); 48 | } 49 | this.emit( 'change' ); 50 | break; 51 | 52 | case 'cell-active': 53 | // FIXED: https://code.google.com/p/chromium/issues/detail?id=170148 54 | // When the content is editable the cursor is hidden until press a key 55 | this.emit( 'reset:status' ); 56 | var cell = this.cell( payload.cell ), 57 | isActive = cell.status( 'active' ); 58 | if( !isActive ){ 59 | cell.status( 'selected', true ); 60 | cell.status( 'active' , true ); 61 | }else{ 62 | cell.status( 'selected', false ); 63 | cell.status( 'active' , false ); 64 | } 65 | this.emit( 'change' ); 66 | break; 67 | 68 | default: 69 | debug( 'payload.actiontype %s unkown', payload.actionType ); 70 | } 71 | }; 72 | 73 | SpreadsheetStore.prototype.grid = function(){ 74 | return this._grid; 75 | }; 76 | 77 | SpreadsheetStore.prototype.cell = function( /* (y,x) */ coords ){ 78 | return this._grid[coords[0]][coords[1]]; 79 | }; 80 | 81 | SpreadsheetStore.prototype.updateFromCSV = function( csv ){ 82 | var self = this; 83 | this._grid = csv.split(/\n/) 84 | .filter(function(row){ return row !== "" }) 85 | .map( function( row ){ return row.split(',').map( function( content ){ return new Cell( content, self ); } ); } ); 86 | }; 87 | 88 | SpreadsheetStore.prototype.toString = function( isExport ){ 89 | return _.reduce( this._grid, function( memo, row ){ 90 | return memo + row.map( function( cell ){ return cell.toString( isExport ); } ).join(',') + '\n'; 91 | }, '\n' ); 92 | }; 93 | 94 | module.exports = SpreadsheetStore; 95 | --------------------------------------------------------------------------------