├── .gitignore ├── History.md ├── Makefile ├── Readme.md ├── component.json ├── dist ├── spreadsheet.browserify.js ├── spreadsheet.css ├── spreadsheet.js └── spreadsheet.min.js ├── examples ├── browserify.html ├── calc.html ├── card.html ├── div_growth_model.html ├── margin_buyout.html ├── merge.html ├── model_template_ebit.html ├── model_template_ebitda.html ├── prior │ ├── LR_Model.html │ ├── apple.html │ ├── index.html │ ├── model-v2.html │ ├── model_template.html │ └── test.js ├── short_dcf.html └── wacc.html ├── index.js ├── lib ├── cell.js ├── collapsible.js ├── expand.js ├── format.js ├── match.js ├── outline.js ├── regex.js ├── selection.js ├── spreadsheet.js ├── tokens.js ├── type.js ├── utils.js └── workbook.js ├── package.json ├── spreadsheet.css ├── template.jade └── template.js /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | build 3 | .DS_Store 4 | node_modules 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.1.5 / 2015-04-16 3 | ================== 4 | 5 | * provide a browserify build 6 | * add dist 7 | * update dist/ 8 | * added: spreadsheet#format(x) plus custom formatting framework for future 9 | * ignore DS_Store 10 | * update dist 11 | * use test keyboard for decimal point 12 | 13 | 0.1.4 / 2014-08-20 14 | ================== 15 | 16 | * fix calc 17 | 18 | 0.1.3 / 2014-08-19 19 | ================== 20 | 21 | * fix spreadsheet 22 | 23 | 0.1.2 / 2014-08-19 24 | ================== 25 | 26 | * change .val() to be a getter 27 | * fix mother effing input bug on mobile 28 | * update dist 29 | * fix short-dcf 30 | * update dist 31 | * cleanup 32 | * update dist/ 33 | * 'change' consistent with 'changing'. now emitting 'move' events. 34 | * fix spreadsheet.on('change ..', fn) 35 | * fix for merged hidden cells. isn't full-proof 36 | 37 | 0.1.1 / 2014-07-03 38 | ================== 39 | 40 | * add number pad for editables on mobile 41 | * update dist/ 42 | * Merge branch 'mobile' 43 | * improve mobile 44 | * fix fill 45 | * fix disabled fill for safari 46 | 47 | 0.1.0 / 2014-07-01 48 | ================== 49 | 50 | * Initial Release 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: components spreadsheet.css template.js 3 | @component build 4 | 5 | components: component.json 6 | @component install --dev 7 | 8 | browserify: dist/spreadsheet.js 9 | @cat dist/spreadsheet.js | ./node_modules/.bin/derequire > dist/spreadsheet.b.tmp.js 10 | @./node_modules/.bin/browserify -s Spreadsheet dist/spreadsheet.b.tmp.js > dist/spreadsheet.browserify.js 11 | @rm dist/spreadsheet.b.tmp.js 12 | 13 | dist: components dist-build browserify dist-minify 14 | 15 | dist-build: 16 | @component build -o dist -n spreadsheet -s Spreadsheet 17 | 18 | dist-minify: dist/spreadsheet.js 19 | @curl -s \ 20 | -d compilation_level=SIMPLE_OPTIMIZATIONS \ 21 | -d output_format=text \ 22 | -d output_info=compiled_code \ 23 | --data-urlencode "js_code@$<" \ 24 | http://closure-compiler.appspot.com/compile \ 25 | > $<.tmp 26 | @mv $<.tmp dist/spreadsheet.min.js 27 | 28 | clean: 29 | rm -fr build components 30 | 31 | .PHONY: build clean template.js 32 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # spreadsheet 3 | 4 | create spreadsheets with excel-like features 5 | 6 | ![spreadsheet](https://i.cloudup.com/3TK3f5Cg6V.png) 7 | 8 | ## Installation 9 | 10 | Install with [component(1)](http://component.io): 11 | 12 | $ component install matthewmueller/spreadsheet 13 | 14 | Or include the the files in the `/dist` folder 15 | 16 | ## Example 17 | 18 | ```js 19 | var Spreadsheet = require('spreadsheet'); 20 | var spreadsheet = Spreadsheet(); 21 | document.body.appendChild(spreadsheet.el); 22 | 23 | spreadsheet 24 | .select('B1, C1, D1').insert(['Revenues', 'Expenses', 'Profit']).addClass('bold') 25 | .select('A2:A9').insert(years([1, 2, 3, 4, 5, 6, 7, 8])).addClass('bold') 26 | .select('B2:B9').insert([10, 12, 32, 43, 53, 23, 32, 43]).format('$').editable() 27 | .select('C2:C9').insert([32, 11, 22, 33, 45, 42, 10, 32]).format('$').editable() 28 | .select('D2:D9').calc(':B - :C').format('$') 29 | 30 | function years(arr) { 31 | return arr.map(function(item) { 32 | return 'Year ' + item; 33 | }) 34 | } 35 | ``` 36 | 37 | ## API 38 | 39 | ### Spreadsheet() 40 | 41 | Create a new spreadsheet 42 | 43 | #### Spreadsheet#select(sel) 44 | 45 | Create a selection on the spreadsheet. Selections can be a single cell, block, entire row, or an entire column. Selections have their own set of methods 46 | 47 | ```js 48 | spreadsheet 49 | .select('A1') // Select the cell A1 50 | .select('A1:H9') // Select a block A1 to A9 51 | .select(':2') // Select the entire 2nd row 52 | .select(':A') // Select the entire A column 53 | .select('D4, A10:F3, :3, :C') // Select many 54 | ``` 55 | 56 | ### Selection 57 | 58 | #### Selection#insert(arr) 59 | 60 | Insert cells into the spreadsheet. 61 | 62 | ```js 63 | spreadsheet 64 | .select('A1:A9').insert([1, 2, 3, 4, 5, 6]); 65 | ``` 66 | 67 | #### Selection#calc(expr) 68 | 69 | Calculate an `expr`. Reacts to changes in other cells. Automatically resolves columns and rows. 70 | 71 | ```js 72 | spreadsheet 73 | .select('A1').calc(':B + :C * 100') // resolves to: A1 = B1 + C1 74 | ``` 75 | 76 | You can also do a one-off calculation: 77 | 78 | ```js 79 | spreadsheet 80 | .select('A1').calc('B4 * C9 + 10') 81 | ``` 82 | 83 | #### Selection#format(format) 84 | 85 | Set the format of the cell. This function current supports: 86 | 87 | ```js 88 | spreadsheet.select('A1:A9').format('$') // dollar 89 | spreadsheet.select('B1:B9').format('%') // percentages 90 | ``` 91 | 92 | #### Selection#editable() 93 | 94 | Set the cells in the selection to be editable 95 | 96 | ```js 97 | spreadsheet.select('A1:A9').editable() 98 | ``` 99 | 100 | #### Selection#addClass(class) 101 | 102 | Add a class to each cell in the selection. 103 | 104 | ```js 105 | spreadsheet.select('A1:A9').addClass('bold') 106 | ``` 107 | 108 | ## TODO 109 | 110 | - merging cells 111 | - 112 | 113 | ## License 114 | 115 | MIT 116 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spreadsheet", 3 | "repo": "matthewmueller/spreadsheet", 4 | "description": "create spreadsheets with excel-like features", 5 | "version": "0.1.5", 6 | "keywords": [], 7 | "remotes": [ 8 | "https://raw.githubusercontent.com" 9 | ], 10 | "dependencies": { 11 | "component/domify": "1.2.2", 12 | "component/emitter": "1.1.3", 13 | "yields/isArray": "1.0.0", 14 | "component/classes": "1.2.1", 15 | "component/events": "1.0.7", 16 | "component/event": "0.1.4", 17 | "component/props": "1.1.1", 18 | "adamwdraper/Numeral-js": "1.5.3", 19 | "yields/shortcuts": "0.2.0", 20 | "yields/k": "0.6.1", 21 | "matthewmueller/delegates": "0.0.4", 22 | "component/closest": "0.1.3", 23 | "bmcmahen/modifier": "0.0.1", 24 | "matthewmueller/per-frame": "2.0.0", 25 | "matthewmueller/mini-tokenizer": "0.1.2", 26 | "visionmedia/jade": "1.3.1", 27 | "matthewmueller/grid": "*", 28 | "component/ie": "0.0.1", 29 | "component/tap": "1.0.0" 30 | }, 31 | "development": {}, 32 | "license": "MIT", 33 | "main": "index.js", 34 | "scripts": [ 35 | "index.js", 36 | "template.js", 37 | "lib/workbook.js", 38 | "lib/spreadsheet.js", 39 | "lib/selection.js", 40 | "lib/cell.js", 41 | "lib/utils.js", 42 | "lib/type.js", 43 | "lib/tokens.js", 44 | "lib/regex.js", 45 | "lib/expand.js", 46 | "lib/match.js", 47 | "lib/outline.js", 48 | "lib/collapsible.js", 49 | "lib/format.js" 50 | ], 51 | "styles": [ 52 | "spreadsheet.css" 53 | ] 54 | } -------------------------------------------------------------------------------- /dist/spreadsheet.css: -------------------------------------------------------------------------------- 1 | /** 2 | * General 3 | */ 4 | 5 | .spreadsheet { 6 | position: relative; 7 | font-family: helvetica; 8 | font-weight: 300; 9 | color: #222; 10 | } 11 | 12 | .spreadsheet + .spreadsheet { 13 | margin-top: 10px; 14 | } 15 | 16 | .spreadsheet * { 17 | box-sizing: border-box; 18 | } 19 | 20 | .spreadsheet table { 21 | width: 100%; 22 | border-spacing: 0; 23 | border-left: 0; 24 | border-collapse: separate; 25 | table-layout: fixed; 26 | 27 | /* prevent text flicker in safari */ 28 | -webkit-backface-visibility: hidden; 29 | } 30 | 31 | .spreadsheet .editable { 32 | color: #4285F4; 33 | font-style: italic; 34 | } 35 | 36 | .spreadsheet th, 37 | .spreadsheet td { 38 | padding: 0; 39 | } 40 | 41 | .spreadsheet th div { 42 | height: 100%; 43 | width: 100%; 44 | } 45 | 46 | /** 47 | * Input styling 48 | */ 49 | 50 | .spreadsheet input { 51 | border: 0; 52 | margin: 0; 53 | height: inherit; 54 | width: 100%; 55 | 56 | font: inherit; 57 | color: inherit; 58 | text-align: inherit; 59 | line-height: inherit; 60 | -webkit-text-fill-color: inherit; 61 | 62 | white-space: nowrap; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | } 66 | 67 | .spreadsheet input[disabled] { 68 | pointer-events: none; 69 | background: none; 70 | opacity: 1; 71 | } 72 | 73 | .spreadsheet input:focus { 74 | outline: none; 75 | } 76 | 77 | /** 78 | * Dimensions 79 | */ 80 | 81 | .spreadsheet .rowhead { 82 | width: 32px; 83 | } 84 | 85 | .spreadsheet .colhead div, 86 | .spreadsheet .rowhead div, 87 | .spreadsheet td input { 88 | padding: 6px; 89 | } 90 | 91 | /** 92 | * Cell borders 93 | */ 94 | 95 | .spreadsheet td { 96 | border-right: 1px solid #f2f2f2; 97 | border-bottom: 1px solid #f2f2f2; 98 | } 99 | 100 | .spreadsheet tr:first-child td { 101 | border-top: 1px solid #cdcdcd; 102 | } 103 | 104 | .spreadsheet td:first-of-type { 105 | border-left: 1px solid #cdcdcd; 106 | } 107 | 108 | .spreadsheet td:last-of-type { 109 | border-right: 1px solid #cdcdcd; 110 | } 111 | 112 | .spreadsheet tr:last-child td { 113 | border-bottom: 1px solid #cdcdcd; 114 | } 115 | 116 | .spreadsheet .colhead th:nth-child(5) div { 117 | border-left: 1px solid #cdcdcd; 118 | } 119 | 120 | .spreadsheet .colhead div { 121 | border-right: 1px solid #cdcdcd; 122 | border-top: 1px solid #cdcdcd; 123 | } 124 | 125 | .spreadsheet tr:first-child .rowhead div { 126 | border-top: 1px solid #cdcdcd; 127 | } 128 | 129 | .spreadsheet .rowhead div { 130 | border-bottom: 1px solid #cdcdcd; 131 | border-left: 1px solid #cdcdcd; 132 | } 133 | 134 | .spreadsheet.row-layers .rowhead div { 135 | border-right: 1px solid #cdcdcd; 136 | } 137 | 138 | /** 139 | * Outline styling 140 | */ 141 | 142 | .spreadsheet .outline { 143 | border: 2px solid #333; 144 | pointer-events: none; 145 | z-index: 1; 146 | position: absolute; 147 | } 148 | 149 | .spreadsheet .outline.editable { 150 | border-color: #4285F4; 151 | } 152 | 153 | /** 154 | * Hidden cells 155 | */ 156 | 157 | .spreadsheet tr.hidden, 158 | .spreadsheet td.hidden, 159 | .spreadsheet th.hidden { 160 | display: none; 161 | } 162 | 163 | .spreadsheet thead .colhead th.no-width { 164 | width: 0; 165 | } 166 | 167 | .spreadsheet thead tr.layer, 168 | .spreadsheet thead th.layerhead, 169 | .spreadsheet tbody th.layer { 170 | width: 0; 171 | height: 0; 172 | } 173 | 174 | .spreadsheet thead tr.layer.shown { 175 | display: table-row; 176 | } 177 | 178 | .spreadsheet thead th.layerhead.shown { 179 | width: 20px; 180 | } 181 | 182 | .spreadsheet thead tr.layer.shown { 183 | height: 20px; 184 | } 185 | 186 | .spreadsheet thead th { 187 | position: relative; 188 | } 189 | 190 | .spreadsheet th.dot, 191 | .spreadsheet th.line, 192 | .spreadsheet th.dash { 193 | cursor: pointer; 194 | } 195 | 196 | .spreadsheet tbody th.dot, 197 | .spreadsheet tbody th.line, 198 | .spreadsheet tbody th.dash { 199 | padding-right: 2px; 200 | padding-left: 2px; 201 | } 202 | 203 | .spreadsheet tbody th.dot { 204 | padding-top: 15px; 205 | } 206 | 207 | .spreadsheet tbody th.line, 208 | .spreadsheet tbody th.dash { 209 | padding: 0 8px; 210 | } 211 | 212 | .spreadsheet tbody th.dot div { 213 | background: #4285F4; 214 | border-radius: 8px; 215 | } 216 | 217 | .spreadsheet thead th.dot div { 218 | width: 16px; 219 | margin: 0 auto; 220 | } 221 | 222 | .spreadsheet thead th.dot div:after, 223 | .spreadsheet thead th.line div:after, 224 | .spreadsheet thead th.dash div:after { 225 | content: '\0'; 226 | } 227 | 228 | .spreadsheet thead th.dot, 229 | .spreadsheet thead th.line, 230 | .spreadsheet thead th.dash { 231 | overflow: hidden; 232 | } 233 | 234 | .spreadsheet thead th.dot div { 235 | position: absolute; 236 | right: 0; 237 | height: 4px; 238 | margin-top: -2px; 239 | width: 50%; 240 | background: #4285F4; 241 | } 242 | 243 | .spreadsheet thead th.dot.hiding div { 244 | background: none; 245 | } 246 | 247 | .spreadsheet thead th.line div { 248 | position: absolute; 249 | height: 4px; 250 | margin-top: -2px; 251 | background: #4285F4; 252 | } 253 | 254 | .spreadsheet thead th.dash div { 255 | position: absolute; 256 | height: 4px; 257 | margin-top: -2px; 258 | width: 50%; 259 | background: #4285F4; 260 | } 261 | 262 | .spreadsheet thead th.dash div:after { 263 | background: #4285F4; 264 | height: 16px; 265 | width: 4px; 266 | position: absolute; 267 | top: -6px; 268 | right: -4px; 269 | } 270 | 271 | .spreadsheet thead th.dot div:after { 272 | background: #4285F4; 273 | border-radius: 8px; 274 | height: 16px; 275 | width: 16px; 276 | position: absolute; 277 | top: -6px; 278 | left: -8px; 279 | } 280 | 281 | .spreadsheet tbody th.line div, 282 | .spreadsheet tbody th.dash div { 283 | background: #4285F4; 284 | } 285 | 286 | .spreadsheet tbody th.dash { 287 | border-bottom: 3px solid #4285F4; 288 | } 289 | 290 | /** 291 | * Headings 292 | */ 293 | 294 | .spreadsheet .colhead th, 295 | .spreadsheet th.rowhead { 296 | overflow: hidden; 297 | } 298 | 299 | .spreadsheet .colhead div, 300 | .spreadsheet .rowhead div { 301 | height: 100%; 302 | width: 100%; 303 | background: #f2f2f2; 304 | -webkit-transition: -webkit-transform ease-out .2s; 305 | -moz-transition: -moz-transform ease-out .2s; 306 | -ms-transition: -ms-transform ease-out .2s; 307 | -o-transition: -o-transform ease-out .2s; 308 | transition: transform ease-out .2s; 309 | } 310 | 311 | .spreadsheet .rowhead div { 312 | -webkit-transform: translateX(100%); 313 | -moz-transform: translateX(100%); 314 | -ms-transform: translateX(100%); 315 | -o-transform: translateX(100%); 316 | transform: translateX(100%); 317 | } 318 | 319 | .spreadsheet .colhead div { 320 | -webkit-transform: translateY(100%); 321 | -moz-transform: translateY(100%); 322 | -ms-transform: translateY(100%); 323 | -o-transform: translateY(100%); 324 | transform: translateY(100%); 325 | } 326 | 327 | .spreadsheet.headings .colhead div, 328 | .spreadsheet.headings .rowhead div { 329 | -webkit-transform: translate(0); 330 | -moz-transform: translate(0); 331 | -ms-transform: translate(0); 332 | -o-transform: translate(0); 333 | transform: translate(0); 334 | } -------------------------------------------------------------------------------- /examples/browserify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dividend Growth Model 6 | 7 | 8 | 44 | 45 | 46 | 47 |

LR Model Template

48 | 49 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /examples/calc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | spreadsheet component 5 | 6 | 7 | 39 | 40 | 41 |

Calculations

42 | 43 | 44 | 45 | 46 | 47 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /examples/card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | spreadsheet component 5 | 6 | 7 | 107 | 108 | 109 |

LR Model Template

110 | 111 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /examples/div_growth_model.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dividend Growth Model 6 | 7 | 8 | 44 | 45 | 46 | 47 |

LR Model Template

48 | 49 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /examples/margin_buyout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Panera Margin Buyout 5 | 6 | 7 | 101 | 102 | 103 |

LR Model Template

104 | 105 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /examples/merge.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | LR Model - Apple 4 | 5 | 7 | 8 | 9 | 10 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/model_template_ebitda.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | spreadsheet component 5 | 6 | 7 | 82 | 83 | 84 |

LR Model Template

85 | 86 | 937 | 938 | 939 | -------------------------------------------------------------------------------- /examples/prior/LR_Model.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | spreadsheet component 4 | 5 | 30 | 31 | 32 |

Calculations

33 | 34 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /examples/prior/apple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | LR Model - Apple 4 | 5 | 8 | 9 | 10 | 11 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/prior/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | spreadsheet component 4 | 5 | 6 | 7 |

spreadsheet component

8 | 9 | 10 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/prior/model-v2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | spreadsheet component 4 | 5 | 30 | 31 | 32 |

Calculations

33 | 34 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /examples/prior/test.js: -------------------------------------------------------------------------------- 1 | var Spreadsheet = require('spreadsheet'); 2 | var spreadsheet = Spreadsheet(); 3 | 4 | console.log(spreadsheet); 5 | 6 | 7 | function dir(object) { 8 | var s; 9 | stuff = []; 10 | for (s in object) { 11 | stuff.push(s); 12 | } 13 | stuff.sort(); 14 | return stuff; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /examples/short_dcf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DCF Model 6 | 7 | 8 | 9 | 250 | 251 | 252 | 253 |
254 |
255 | 256 | 257 | 490 | 491 | 492 | 493 | 494 | -------------------------------------------------------------------------------- /examples/wacc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Weighted Average Cost of Capital 6 | 7 | 8 | 37 | 38 | 39 | 40 |

WACC

41 | 42 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var Workbook = require('./lib/workbook'); 6 | 7 | /** 8 | * Expose `spreadsheet` 9 | */ 10 | 11 | exports = module.exports = spreadsheet; 12 | 13 | /** 14 | * Expose the `workbook` 15 | */ 16 | 17 | exports.workbook = Workbook; 18 | 19 | /** 20 | * Default workbook 21 | */ 22 | 23 | var workbook = new Workbook; 24 | 25 | /** 26 | * Initialize `spreadsheet` 27 | * 28 | * @param {Number} cols 29 | * @param {Number} rows 30 | * @return {Spreadsheet} 31 | * @api public 32 | */ 33 | 34 | function spreadsheet(cols, rows) { 35 | return workbook.spreadsheet(cols, rows); 36 | } 37 | -------------------------------------------------------------------------------- /lib/cell.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var numeral = require('numeral'); 6 | var event = require('event'); 7 | var events = require('events'); 8 | var Emitter = require('emitter'); 9 | var domify = require('domify'); 10 | var classes = require('classes'); 11 | var modifier = require('modifier'); 12 | var props = require('props'); 13 | var type = require('./type'); 14 | var formatter = require('./format'); 15 | var shortcuts = require('shortcuts'); 16 | var tokenizer = require('mini-tokenizer'); 17 | var throttle = require('per-frame'); 18 | var tap = require('tap'); 19 | var ie = require('ie'); 20 | 21 | /** 22 | * Regexs 23 | */ 24 | 25 | var rexpr = /\s*=/; 26 | 27 | /** 28 | * Touch 29 | */ 30 | 31 | var touch = 'ontouchstart' in window; 32 | 33 | /** 34 | * iFrame 35 | */ 36 | 37 | var iframe = parent != window; 38 | 39 | /** 40 | * Token compiler 41 | */ 42 | 43 | var rtokens = /\{([A-Za-z0-9]+)\}/g 44 | var tokens = tokenizer(rtokens, '$1'); 45 | 46 | /** 47 | * Recomputing cache 48 | */ 49 | 50 | var recomputing = {}; 51 | 52 | /** 53 | * Export `Cell` 54 | */ 55 | 56 | module.exports = Cell; 57 | 58 | /** 59 | * Initialize `Cell` 60 | * 61 | * @param {Element} el 62 | * @param {Number} at 63 | * @param {Table} spreadsheet 64 | */ 65 | 66 | function Cell(el, at, spreadsheet) { 67 | if (!(this instanceof Cell)) return new Cell(el, at, spreadsheet); 68 | this.spreadsheet = spreadsheet; 69 | this.outline = spreadsheet.outline; 70 | this.el = el; 71 | this.at = at; 72 | 73 | // create the element 74 | this.classes = classes(this.el); 75 | this.attr('name', at); 76 | 77 | // get the input 78 | this.input = this.el.firstChild; 79 | this.value = this.input.value || ''; 80 | 81 | this.expr = false; 82 | this.formatting = false; 83 | this.observing = []; 84 | 85 | // bind the events 86 | // 87 | // insane iOS bug if we're in an iframe 88 | // in a webview and define a touch event 89 | // input stops accepting keyboard input. 90 | if (iframe) { 91 | this.tap = events(this.el, this); 92 | this.tap.bind('click', 'onclick'); 93 | } else { 94 | this.tap = tap(this.el, this.onclick.bind(this)); 95 | } 96 | 97 | } 98 | 99 | /** 100 | * Get or set the value of the cell 101 | */ 102 | 103 | Cell.prototype.val = function(val, opts) { 104 | if (!arguments.length) return this.compute(this.value); 105 | recomputing = {}; 106 | this.update(val); 107 | }; 108 | 109 | /** 110 | * update the cell 111 | */ 112 | 113 | Cell.prototype.update = function(val, opts) { 114 | if (undefined == val) return this.compute(this.value, opts); 115 | opts = opts || {}; 116 | opts.compute = undefined == opts.compute ? true : opts.compute; 117 | 118 | var prevComputed = this.input.value; 119 | var spreadsheet = this.spreadsheet; 120 | var input = this.input; 121 | var prev = this.value; 122 | var at = this.at; 123 | var computed; 124 | 125 | // update the internal value 126 | this.value = val; 127 | 128 | // update the value 129 | if (opts.compute) { 130 | computed = input.value = this.compute(val); 131 | if (!opts.silent) { 132 | spreadsheet.emit('change ' + at, computed, prevComputed, this); 133 | } 134 | } 135 | 136 | if (!opts.silent) { 137 | spreadsheet.emit('changing ' + at, val, prev ? prev : prev, this); 138 | } 139 | 140 | return this; 141 | }; 142 | 143 | /** 144 | * Compute the value and apply formatting 145 | */ 146 | 147 | Cell.prototype.compute = function(value, opts) { 148 | value = value || this.value; 149 | opts = opts || {}; 150 | 151 | var format = undefined == opts.format ? true : opts.format; 152 | var formatting = this.formatting; 153 | 154 | if (rexpr.test(value)) { 155 | this.expr = (this.expr && this.value == value) ? this.expr : this.compile(value); 156 | } else { 157 | this.expr = false; 158 | } 159 | 160 | value = this.expr ? this.expr() : value; 161 | 162 | // apply formatting 163 | if (format && formatting && isNumber(value)) { 164 | value = formatter[formatting] 165 | ? formatter[formatting](value) 166 | : numeral(value).format(formatting) 167 | } 168 | 169 | return value; 170 | }; 171 | 172 | /** 173 | * Compile the expression 174 | * 175 | * @param {String} expr 176 | * @param {Object} opts 177 | * @return {Function} 178 | * @api private 179 | */ 180 | 181 | Cell.prototype.compile = function(expr, opts) { 182 | var spreadsheet = this.spreadsheet; 183 | var toks = tokens(expr); 184 | 185 | if (!toks.length) return expr; 186 | 187 | expr = expr 188 | .replace(/^\s*=/, '') 189 | .replace(rtokens, '_.$1'); 190 | 191 | expr = new Function('_', 'return ' + expr); 192 | this.observe(toks); 193 | 194 | return function() { 195 | var _ = {}; 196 | var val; 197 | 198 | for (var i = 0, len = toks.length; i < len; i++) { 199 | val = spreadsheet.at(toks[i]).update(null, { format: false }); 200 | _[toks[i]] = +val; 201 | } 202 | 203 | val = expr(_); 204 | 205 | return 'number' != typeof val || !isNaN(val) 206 | ? val 207 | : 0 208 | }; 209 | }; 210 | 211 | /** 212 | * Set the formatting 213 | * 214 | * @param {String} format 215 | * @return {Cell} 216 | * @api public 217 | */ 218 | 219 | Cell.prototype.format = function(format) { 220 | if('%' == format) this.formatting = '(0.0)%'; 221 | else if ('$' == format) this.formatting = '($0,0)'; 222 | else if ('$$' == format) this.formatting = '($0,0.00)'; 223 | else if ('#' == format) this.formatting = '(0,0)'; 224 | else if ('##' == format) this.formatting = '(0,0.00)'; 225 | else this.formatting = format; 226 | this.update(this.value); 227 | return this; 228 | }; 229 | 230 | /** 231 | * Render the cell 232 | * 233 | * @api private 234 | */ 235 | 236 | 237 | Cell.prototype.render = function() { 238 | return this.el; 239 | } 240 | 241 | /** 242 | * Make the cell editable 243 | */ 244 | 245 | Cell.prototype.editable = function() { 246 | var self = this; 247 | var el = this.el; 248 | var input = this.input; 249 | var blur = ie ? 'focusout' : 'blur'; 250 | 251 | this.classes.add('editable'); 252 | 253 | event.bind(input, 'input', function(e) { 254 | if (rexpr.test(input.value)) return; 255 | recomputing = {}; 256 | var val = percentage(input.value); 257 | self.update(val, { compute: false }); 258 | }); 259 | 260 | event.bind(input, 'focus', function(e) { 261 | input.value = self.value; 262 | }); 263 | 264 | event.bind(input, blur, function(e) { 265 | // TODO: temporary fix for blur firing twice, i think... 266 | e.stopImmediatePropagation(); 267 | if ('' == input.value) return; 268 | var val = percentage(input.value); 269 | self.update(val); 270 | }); 271 | 272 | function percentage(val) { 273 | return '(0.0)%' == self.formatting && val >= 1 274 | ? val / 100 275 | : val; 276 | } 277 | 278 | return this; 279 | }; 280 | 281 | /** 282 | * F2 puts you into "editmode" 283 | * 284 | * @param {Event} e 285 | * @return {Spreadsheet} 286 | * @api private 287 | */ 288 | 289 | Cell.prototype.onf2 = function(e) { 290 | e.stopPropagation(); 291 | 292 | // focus editable 293 | if (this.classes.has('editable')) { 294 | this.focus(); 295 | } else { 296 | this.reveal(); 297 | } 298 | 299 | return this; 300 | }; 301 | 302 | /** 303 | * Reveal formula 304 | */ 305 | 306 | Cell.prototype.reveal = function() { 307 | if ('=' != this.value[0]) return this; 308 | var classes = this.spreadsheet.classes; 309 | var input = this.input; 310 | var value = this.value; 311 | 312 | if (classes.has('headings')) { 313 | this.reset(); 314 | classes.remove('headings'); 315 | } else { 316 | setTimeout(swap, 0) 317 | classes.add('headings'); 318 | } 319 | 320 | function swap() { 321 | input.value = value.replace(/[\{\}]/g, ''); 322 | } 323 | 324 | return this; 325 | } 326 | 327 | /** 328 | * Blur when you escape 329 | * 330 | * @param {Event} e 331 | * @return {Spreadsheet} 332 | * @api private 333 | */ 334 | 335 | Cell.prototype.onesc = function(e) { 336 | e.stopPropagation(); 337 | this.blur(); 338 | return this; 339 | }; 340 | 341 | /** 342 | * onkeydown 343 | */ 344 | 345 | Cell.prototype.onkeydown = function(e) { 346 | if (modifier(e)) return this; 347 | 348 | var classes = this.classes; 349 | var input = this.input; 350 | 351 | if (!classes.has('editing') && classes.has('editable') && !classes.has('focused')) { 352 | input.focus(); 353 | input.value = ''; 354 | } 355 | 356 | classes.add('editing'); 357 | }; 358 | 359 | /** 360 | * onclick 361 | */ 362 | 363 | Cell.prototype.onclick = function() { 364 | var spreadsheet = this.spreadsheet; 365 | var active = spreadsheet.active; 366 | 367 | // remove any old classes, if we're not 368 | // clicking on the currently active cell 369 | active && active != this && active.deactivate(); 370 | spreadsheet.active = this; 371 | 372 | this.activate(); 373 | 374 | return this; 375 | }; 376 | 377 | /** 378 | * 379 | */ 380 | 381 | Cell.prototype.focus = function() { 382 | // TODO: lame. refactor. 383 | this.activate().activate(); 384 | this.input.focus(); 385 | return this; 386 | }; 387 | 388 | /** 389 | * reset 390 | */ 391 | 392 | Cell.prototype.reset = function() { 393 | var editable = this.classes.has('editable'); 394 | this.update(this.value, { silent: true, compute: !editable }); 395 | return this; 396 | }; 397 | 398 | /** 399 | * highlight 400 | */ 401 | 402 | Cell.prototype.activate = function() { 403 | var spreadsheet = this.spreadsheet; 404 | var workbook = spreadsheet.workbook; 405 | 406 | // hack to ensure spreadsheet is active 407 | workbook.active = workbook.active || spreadsheet; 408 | spreadsheet.active = spreadsheet.active || this; 409 | 410 | var highlighted = this.classes.has('highlighted'); 411 | var editable = this.classes.has('editable'); 412 | var outline = this.outline; 413 | var input = this.input; 414 | 415 | // add the outline 416 | var lining = outline.show(this.el); 417 | 418 | if (editable) { 419 | classes(lining).add('editable'); 420 | !touch && input.removeAttribute('disabled'); 421 | } else { 422 | classes(lining).remove('editable'); 423 | this.classes.remove('headings'); 424 | } 425 | 426 | // if we're already highlighted, focus 427 | if (highlighted || touch) { 428 | if (editable) { 429 | this.classes.add('focused'); 430 | touch && input.removeAttribute('disabled'); 431 | input.focus(); 432 | } else { 433 | this.reveal(); 434 | } 435 | } 436 | 437 | // add highlighted 438 | this.classes.add('highlighted'); 439 | 440 | return this; 441 | }; 442 | 443 | /** 444 | * deactivate 445 | */ 446 | 447 | Cell.prototype.deactivate = function() { 448 | this.reset(); 449 | this.blur(); 450 | this.classes.remove('highlighted'); 451 | this.input.setAttribute('disabled', true); 452 | return this; 453 | }; 454 | 455 | /** 456 | * blur 457 | */ 458 | 459 | Cell.prototype.blur = function() { 460 | this.classes.remove('focused').remove('editing') 461 | this.spreadsheet.classes.remove('headings'); 462 | this.input.blur(); 463 | return this; 464 | } 465 | 466 | /** 467 | * Add a class to the 468 | */ 469 | 470 | Cell.prototype.addClass = function(cls) { 471 | classes(this.el).add(cls); 472 | }; 473 | 474 | 475 | /** 476 | * set an attribute of 477 | */ 478 | 479 | Cell.prototype.attr = function(attr, value) { 480 | var el = this.el; 481 | if (undefined == value) return el.getAttibute(attr); 482 | else el.setAttribute(attr, value); 483 | return this; 484 | }; 485 | 486 | /** 487 | * Observe `cells` for changes 488 | */ 489 | 490 | Cell.prototype.observe = function(cells) { 491 | var self = this; 492 | var spreadsheet = this.spreadsheet; 493 | 494 | if (this.observing.length) { 495 | // remove observed 496 | console.log('TODO: remove observed'); 497 | } 498 | 499 | for (var i = 0, cell; cell = cells[i]; i++) { 500 | spreadsheet.on('changing ' + cell, throttle(recompute)); 501 | this.observing.push([cell, recompute]); 502 | } 503 | 504 | function recompute(val, prev, cell) { 505 | if (!recomputing[self.at]) { 506 | recomputing[self.at] = self.value; 507 | self.update(self.value); 508 | } 509 | } 510 | } 511 | 512 | /** 513 | * Replace with another cell or value 514 | * 515 | * @param {Cell|Mixed} cell 516 | * @return {Cell} new cell or self 517 | * @api public 518 | */ 519 | 520 | Cell.prototype.replace = function(cell) { 521 | if (!cell instanceof Cell) return this.update(cell); 522 | var tr = this.el.parentNode; 523 | if (!tr) return this; 524 | tr.replaceChild(cell.el, this.el); 525 | return cell; 526 | } 527 | 528 | /** 529 | * Edit the cell 530 | * 531 | * @param {Event} e 532 | * @api private 533 | */ 534 | 535 | Cell.prototype.edit = function(e) { 536 | var active = this.spreadsheet.active; 537 | if (active != this) return this; 538 | this.input.removeAttribute('disabled'); 539 | return this; 540 | } 541 | 542 | /** 543 | * Is a number utility 544 | * 545 | * @param {Mixed} n 546 | * @return {Boolean} 547 | */ 548 | 549 | function isNumber(n) { 550 | return !isNaN(parseFloat(n)) && isFinite(n); 551 | } 552 | 553 | /** 554 | * Hide 555 | * 556 | * @return {Cell} 557 | * @api public 558 | */ 559 | 560 | Cell.prototype.hide = function() { 561 | this.classes.add('hidden'); 562 | return this; 563 | }; 564 | 565 | /** 566 | * Show 567 | * 568 | * @return {Cell} 569 | * @api public 570 | */ 571 | 572 | Cell.prototype.show = function() { 573 | this.classes.remove('hidden'); 574 | return this; 575 | }; 576 | -------------------------------------------------------------------------------- /lib/collapsible.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var slice = [].slice; 6 | var domify = require('domify'); 7 | var closest = require('closest'); 8 | var type = require('./type'); 9 | var utils = require('./utils'); 10 | var events = require('events'); 11 | var event = require('event'); 12 | var match = require('./match'); 13 | var classes = require('classes'); 14 | var tokens = require('./tokens'); 15 | var utils = require('./utils'); 16 | var Emitter = require('emitter'); 17 | var lton = utils.lton; 18 | var ntol = utils.ntol; 19 | var rowrange = utils.rowrange; 20 | var colrange = utils.colrange; 21 | var Grid = require('grid'); 22 | 23 | /** 24 | * Export `Collapsible` 25 | */ 26 | 27 | module.exports = Collapsible; 28 | 29 | /** 30 | * Initialize `Collapsible` 31 | */ 32 | 33 | function Collapsible(spreadsheet) { 34 | if (!(this instanceof Collapsible)) return new Collapsible(spreadsheet); 35 | 36 | this.spreadsheet = spreadsheet; 37 | this.thead = spreadsheet.thead; 38 | this.tbody = spreadsheet.tbody; 39 | 40 | this.rg = this.rowgrid(); 41 | this.cg = this.colgrid(); 42 | } 43 | 44 | /** 45 | * rowgrid 46 | */ 47 | 48 | Collapsible.prototype.rowgrid = function() { 49 | var width = this.thead.querySelectorAll('th.layerhead').length; 50 | var height = this.tbody.querySelectorAll('th.layer').length / width; 51 | var ths = slice.call(this.tbody.querySelectorAll('th.layer')); 52 | var grid = new Grid(width, height); 53 | grid.insert(ths); 54 | return grid; 55 | 56 | }; 57 | 58 | /** 59 | * colgrid 60 | */ 61 | 62 | Collapsible.prototype.colgrid = function() { 63 | var height = this.thead.querySelectorAll('tr.layer').length; 64 | var width = this.thead.querySelectorAll('th.layer').length / height; 65 | var ths = slice.call(this.thead.querySelectorAll('th.layer')); 66 | var grid = new Grid(width, height); 67 | grid.insert(ths); 68 | return grid; 69 | }; 70 | 71 | /** 72 | * Add a range 73 | */ 74 | 75 | Collapsible.prototype.range = function(expr, collapsed) { 76 | var t = type(expr); 77 | if ('rows' == t) return this.rowrange(expr, collapsed); 78 | if ('cols' == t) return this.colrange(expr, collapsed); 79 | return this; 80 | }; 81 | 82 | /** 83 | * rowrange 84 | * 85 | * @param {String} expr 86 | */ 87 | 88 | Collapsible.prototype.rowrange = function(expr, collapsed) { 89 | var grid = this.rg.select(expr); 90 | var cols = grid.cols(); 91 | 92 | for (var l in cols) { 93 | var col = grid.find(l); 94 | if (empty(col)) { 95 | var m = match(expr); 96 | var e = l + m[1] + ':' + l + m[2]; 97 | var connector = Connector(grid.find(e), 'row', this.spreadsheet); 98 | if (collapsed) connector.hide(); 99 | break; 100 | } 101 | } 102 | 103 | function empty(col) { 104 | var empty = true; 105 | col.forEach(function(th) { 106 | if (classes(th).has('collapsible')) empty = false; 107 | }) 108 | return empty; 109 | } 110 | }; 111 | 112 | /** 113 | * colrange 114 | * 115 | * @param {String} expr 116 | */ 117 | 118 | Collapsible.prototype.colrange = function(expr, collapsed) { 119 | var grid = this.cg.select(expr); 120 | var rows = grid.rows(); 121 | 122 | for (var n in rows) { 123 | var row = grid.find(n); 124 | if (empty(row)) { 125 | var m = match(expr); 126 | var e = m[1] + n + ':' + m[2] + n; 127 | var connector = Connector(grid.find(e), 'col', this.spreadsheet); 128 | if (collapsed) connector.hide(); 129 | break; 130 | } 131 | } 132 | 133 | function empty(row) { 134 | var empty = true; 135 | row.forEach(function(th) { 136 | if (classes(th).has('collapsible')) empty = false; 137 | }) 138 | return empty; 139 | } 140 | }; 141 | 142 | 143 | /** 144 | * Initialize `Connector` 145 | */ 146 | 147 | function Connector(grid, type, spreadsheet, hidden) { 148 | if (!(this instanceof Connector)) return new Connector(grid, type, spreadsheet, hidden); 149 | this.hidden = undefined == hidden ? false : true; 150 | this.spreadsheet = spreadsheet; 151 | this.thead = spreadsheet.thead; 152 | 153 | var first = this.first = grid.value(0); 154 | var last = this.last = grid.value(-1); 155 | var toggle = this.ontoggle = this.toggle.bind(this); 156 | 157 | this.grid = grid; 158 | this.len = grid.selection.length; 159 | 160 | grid.forEach(function(v) { 161 | event.bind(v, 'click', toggle); 162 | 163 | if (v == first) cls = 'dot'; 164 | else if (v == last) cls = 'dash'; 165 | else cls = 'line'; 166 | 167 | classes(v).add(cls).add('collapsible'); 168 | }); 169 | 170 | if (type == 'col') { 171 | this.layer = first.parentNode; 172 | var expr = grid.expr.replace(/\d+/g, ''); 173 | var m = match(expr); 174 | this.hideExpr = ntol(lton(m[1]) + 1) + ':' + m[2]; 175 | } else if ('row') { 176 | var layerheads = this.thead.querySelectorAll('.layerhead'); 177 | var n = lton(grid.expr.match(/[A-Z]/)[0]); 178 | this.layer = layerheads.item(n); 179 | var expr = grid.expr.replace(/[A-Z]/gi, ''); 180 | var m = match(expr); 181 | this.hideExpr = (+m[1] + 1) + ':' + m[2]; 182 | } 183 | 184 | if (!this.hidden) classes(this.layer).add('shown'); 185 | } 186 | 187 | /** 188 | * Mixin `Emitter` 189 | */ 190 | 191 | Emitter(Connector.prototype); 192 | 193 | /** 194 | * toggle 195 | */ 196 | 197 | Connector.prototype.toggle = function() { 198 | return this.hidden ? this.show() : this.hide(); 199 | }; 200 | 201 | 202 | /** 203 | * show 204 | */ 205 | 206 | Connector.prototype.show = function() { 207 | this.spreadsheet.select(this.hideExpr).show(); 208 | classes(this.first).remove('hiding'); 209 | this.hidden = false; 210 | }; 211 | 212 | /** 213 | * hide 214 | */ 215 | 216 | Connector.prototype.hide = function() { 217 | this.spreadsheet.select(this.hideExpr).hide(); 218 | classes(this.first).add('hiding'); 219 | this.hidden = true; 220 | }; 221 | 222 | -------------------------------------------------------------------------------- /lib/expand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var tokens = require('./tokens'); 6 | var type = require('./type'); 7 | var utils = require('./utils'); 8 | var lton = utils.lton; 9 | var ntol = utils.ntol; 10 | 11 | /** 12 | * Regexs 13 | */ 14 | 15 | var regex = require('./regex'); 16 | var rblock = regex.block; 17 | var rcell = regex.cell; 18 | var rrows = regex.rows; 19 | var rcols = regex.cols; 20 | var rrow = regex.row; 21 | var rcol = regex.col; 22 | 23 | /** 24 | * Export `expand` 25 | */ 26 | 27 | module.exports = expand; 28 | 29 | /** 30 | * Expand the selection 31 | * 32 | * @param {String|Array} selections 33 | * @return {Array} 34 | */ 35 | 36 | function expand(selection, largest) { 37 | var toks = tokens(selection); 38 | var m = rcell.exec(largest); 39 | var lc = m[1]; 40 | var lr = m[2]; 41 | var maxcol = lton(m[1]); 42 | var maxrow = +m[2] 43 | var out = []; 44 | 45 | for (var i = 0, tok; tok = toks[i]; i++) { 46 | switch (type(tok)) { 47 | case 'block': 48 | m = rblock.exec(tok); 49 | out = out.concat(range(m[1], m[2], maxcol, maxrow)); 50 | break; 51 | case 'row': 52 | m = rrow.exec(tok); 53 | var n = +m[1]; 54 | var start = 'A' + n; 55 | var end = lc + n; 56 | out = out.concat(range(start, end, maxcol, maxrow)); 57 | break; 58 | case 'rows': 59 | m = rrows.exec(tok); 60 | var start = 'A' + m[1]; 61 | var end = lc + m[2]; 62 | out = out.concat(range(start, end, maxcol, maxrow)); 63 | break; 64 | case 'col': 65 | m = rcol.exec(tok); 66 | var l = m[1]; 67 | var start = l + 1; 68 | var end = l + lr; 69 | out = out.concat(range(start, end, maxcol, maxrow)); 70 | break; 71 | case 'cols': 72 | m = rcols.exec(tok); 73 | var start = m[1] + '1'; 74 | var end = m[2] + lr; 75 | out = out.concat(range(start, end, maxcol, maxrow)); 76 | break; 77 | case 'cell': 78 | out = out.concat(range(tok, tok, maxcol, maxrow)); 79 | } 80 | } 81 | 82 | return out 83 | }; 84 | 85 | /** 86 | * Expand a selection into it's range 87 | * 88 | * @param {String} from 89 | * @param {String} to 90 | * @return {Array} 91 | */ 92 | 93 | function range(from, to, maxcol, maxrow) { 94 | var out = []; 95 | 96 | var start = rcell.exec(from); 97 | if (!start) return this.error('invalid expansion: ' + from); 98 | var sc = Math.min(lton(start[1]), maxcol); 99 | var sr = Math.min(+start[2], maxrow); 100 | 101 | var end = rcell.exec(to); 102 | if (!end) return this.error('invalid expansion: ' + to); 103 | var ec = Math.min(lton(end[1]), maxcol); 104 | var er = Math.min(+end[2], maxrow); 105 | 106 | for (var i = sr; i <= er; i++) { 107 | for (var j = sc; j <= ec; j++) { 108 | out[out.length] = ntol(j) + i; 109 | } 110 | } 111 | 112 | return out; 113 | } 114 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom formats 3 | */ 4 | 5 | exports['x'] = function(val) { 6 | return round(val, 1) + 'x'; 7 | } 8 | 9 | /** 10 | * Round 11 | * 12 | * @param {Number} n 13 | * @return {Number} decimals 14 | * @return {Number} 15 | */ 16 | 17 | function round(n, decimals) { 18 | decimals *= 10; 19 | return Math.round(n * decimals) / decimals; 20 | } 21 | -------------------------------------------------------------------------------- /lib/match.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var regex = require('./regex'); 6 | 7 | /** 8 | * Export `match` 9 | */ 10 | 11 | module.exports = match; 12 | 13 | /** 14 | * Find a match 15 | * 16 | * @param {String} str 17 | * @return {Array} match 18 | */ 19 | 20 | function match(str) { 21 | return str.match(regex.block) 22 | || str.match(regex.rows) 23 | || str.match(regex.cols) 24 | || str.match(regex.cell) 25 | || str.match(regex.row) 26 | || str.match(regex.col) 27 | } 28 | -------------------------------------------------------------------------------- /lib/outline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var round = Math.round; 6 | var body = document.body; 7 | var event = require('event'); 8 | var events = require('events'); 9 | var raf = require('per-frame'); 10 | var domify = require('domify'); 11 | var closest = require('closest'); 12 | var classes = require('classes'); 13 | var host; 14 | 15 | /** 16 | * Singleton 17 | */ 18 | 19 | var el = domify('
'); 20 | 21 | /** 22 | * Export `Outline` 23 | */ 24 | 25 | module.exports = Outline; 26 | 27 | /** 28 | * Initialize `Outline` 29 | */ 30 | 31 | function Outline(parent) { 32 | if (!(this instanceof Outline)) return new Outline(parent); 33 | this.parent = parent; 34 | this.el = el.cloneNode(true); 35 | this.host = null; 36 | 37 | this.document = events(document, this); 38 | this.document.bind('click', 'maybehide'); 39 | 40 | this.window = events(window, this); 41 | this.window.bind('resize', 'resize'); 42 | } 43 | 44 | /** 45 | * show 46 | */ 47 | 48 | Outline.prototype.show = function(host) { 49 | var el = this.el; 50 | var parent = this.parent; 51 | 52 | this.host = host; 53 | this.resize(); 54 | 55 | !el.parentNode && parent.appendChild(el); 56 | 57 | return el; 58 | }; 59 | 60 | /** 61 | * maybeHide 62 | */ 63 | 64 | Outline.prototype.maybehide = function(e) { 65 | var parent = this.parent; 66 | var target = e.target; 67 | 68 | // don't hide clicks within the spreadsheet 69 | if (parent == target || parent.contains(target)) return this; 70 | 71 | this.hide(); 72 | }; 73 | 74 | 75 | /** 76 | * hide 77 | */ 78 | 79 | Outline.prototype.hide = function() { 80 | var parent = this.parent; 81 | var el = this.el; 82 | 83 | // remove the outline 84 | el.parentNode && parent.removeChild(el); 85 | 86 | // unset the host 87 | this.host = null; 88 | }; 89 | 90 | /** 91 | * destroy 92 | */ 93 | 94 | Outline.prototype.destroy = function() { 95 | el.parentNode && parent.removeChild(el); 96 | this.document.unbind(); 97 | this.window.unbind(); 98 | return this; 99 | }; 100 | 101 | /** 102 | * resize 103 | */ 104 | 105 | Outline.prototype.resize = function() { 106 | if (!this.host) return this; 107 | 108 | var el = this.el; 109 | var parent = this.parent; 110 | var pos = position(this.host); 111 | var off = position(parent); 112 | 113 | el.style.top = round(pos.top - off.top - 1) + 'px'; 114 | el.style.left = round(pos.left - off.left - 1) + 'px'; 115 | el.style.width = round(pos.width + 1) + 'px'; 116 | el.style.height = round(pos.height + 1) + 'px'; 117 | 118 | return this; 119 | }; 120 | 121 | 122 | 123 | /** 124 | * Get the position 125 | */ 126 | 127 | function position(el) { 128 | var box = el.getBoundingClientRect(); 129 | var scrollTop = window.pageYOffset; 130 | var scrollLeft = window.pageXOffset; 131 | 132 | return { 133 | top: box.top + scrollTop, 134 | right: box.right + scrollLeft, 135 | left: box.left + scrollLeft, 136 | bottom: box.bottom + scrollTop, 137 | width: box.width, 138 | height: box.height 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /lib/regex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Regexs 3 | */ 4 | 5 | var block = exports.block = /([A-Za-z]+[0-9]+)\:([A-Za-z]+[0-9]+)/; 6 | var cell = exports.cell = /([A-Za-z]+)([0-9]+)/; 7 | var cols = exports.cols = /([A-Za-z]+)\:([A-Za-z]+)/; 8 | var rows = exports.rows = /([0-9]+)\:([0-9]+)/; 9 | var col = exports.col = /([A-Za-z]+)/; 10 | var row = exports.row = /([0-9]+)/; 11 | var any = exports.any = new RegExp([block, cell, cols, rows, col, row].map(source).join('|'), 'g'); 12 | 13 | /** 14 | * Get the source of a regex 15 | * 16 | * @param {Regex} regex 17 | * @return {String} source 18 | * @api private 19 | */ 20 | 21 | function source(regex) { 22 | return regex.source; 23 | } 24 | -------------------------------------------------------------------------------- /lib/selection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var slice = [].slice; 6 | var isArray = require('isArray'); 7 | var Cell = require('./cell'); 8 | var type = require('./type'); 9 | var expand = require('./expand'); 10 | var utils = require('./utils'); 11 | var match = require('./match'); 12 | var regex = require('./regex'); 13 | var shift = utils.shift; 14 | var lton = utils.lton; 15 | var ntol = utils.ntol; 16 | var largest = utils.largest; 17 | var rows = utils.rows; 18 | var cols = utils.cols; 19 | var rowrange = utils.rowrange; 20 | var colrange = utils.colrange; 21 | var classes = require('classes'); 22 | var tokenizer = require('mini-tokenizer'); 23 | 24 | /** 25 | * Token compiler 26 | */ 27 | 28 | var rtokens = /\{([A-Za-z0-9]+)\}/g 29 | var tokens = tokenizer(rtokens, '$1'); 30 | 31 | /** 32 | * Export `Selection` 33 | */ 34 | 35 | module.exports = Selection; 36 | 37 | /** 38 | * Initialize `Selection` 39 | * 40 | * @param {String} expr 41 | * @param {Table} spreadsheet 42 | * @return {Selection} 43 | * @api private 44 | */ 45 | 46 | function Selection(expr, spreadsheet) { 47 | if (!(this instanceof Selection)) return new Selection(selection, spreadsheet); 48 | this.selection = expand(expr, spreadsheet.largest); 49 | this.spreadsheet = spreadsheet; 50 | // type() is not reliable for different types (ex. "A, 1") 51 | this.type = type(expr); 52 | this.expr = expr; 53 | } 54 | 55 | /** 56 | * Get the cells of the selection 57 | * 58 | * @return {Array} 59 | * @api public 60 | */ 61 | 62 | Selection.prototype.cells = function() { 63 | var cells = []; 64 | this.each(function(cell) { cells.push(cell); }); 65 | return cells; 66 | }; 67 | 68 | /** 69 | * Add width to a column 70 | * 71 | * @param {Number|String} w 72 | * @return {Selection} 73 | * @api public 74 | */ 75 | 76 | Selection.prototype.width = function(w) { 77 | if (!/cols?/.test(this.type)) return this; 78 | w += 'number' == typeof w ? 'px' : ''; 79 | var thead = this.spreadsheet.thead; 80 | var colhead = thead.querySelector('.colhead'); 81 | var columns = cols(this.selection); 82 | var el; 83 | 84 | for (var col in columns) { 85 | el = colhead.querySelector('th[name=' + col + ']'); 86 | if (el) el.style.width = w; 87 | } 88 | 89 | return this; 90 | }; 91 | 92 | /** 93 | * Add height to a row 94 | * 95 | * @param {Number|String} h 96 | * @return {Selection} 97 | * @api public 98 | */ 99 | 100 | Selection.prototype.height = function(h) { 101 | if (!/rows?/.test(this.type)) return this; 102 | h += 'number' == typeof h ? 'px' : ''; 103 | var tbody = this.spreadsheet.tbody; 104 | var rs = rows(this.selection); 105 | var el; 106 | 107 | for (var row in rs) { 108 | var el = tbody.querySelector('tr[name="' + row + '"]'); 109 | if (el) el.style.height = h; 110 | } 111 | 112 | return this; 113 | }; 114 | 115 | /** 116 | * Insert some data into the spreadsheets 117 | * 118 | * @param {Mixed} val 119 | * @return {Selection} 120 | * @api public 121 | */ 122 | 123 | Selection.prototype.insert = function(val) { 124 | val = isArray(val) ? val : [val]; 125 | var spreadsheet = this.spreadsheet; 126 | var sel = this.selection; 127 | 128 | this.each(function(cell, i) { 129 | // end the loop early if we're done 130 | if (undefined == val[i]) return false; 131 | cell.update(val[i]); 132 | }) 133 | 134 | return this; 135 | }; 136 | 137 | /** 138 | * Make the selection editable 139 | * 140 | * @param {String} expr 141 | * @return {Selection} 142 | * @api public 143 | */ 144 | 145 | Selection.prototype.calc = function(expr) { 146 | var toks = tokens(expr); 147 | 148 | this.each(function(cell) { 149 | var e = expr; 150 | for (var j = 0, tok; tok = toks[j]; j++) { 151 | var shifted = shift(cell.at, tok); 152 | e = e.replace(tok, shifted); 153 | } 154 | 155 | cell.update('= ' + e); 156 | }); 157 | 158 | return this; 159 | } 160 | 161 | /** 162 | * Loop through the selection, calling 163 | * `action` on each present cell in the 164 | * selection 165 | * 166 | * @param {String|Function} action 167 | * @return {Selection} 168 | */ 169 | 170 | Selection.prototype.each = function(action) { 171 | var spreadsheet = this.spreadsheet; 172 | var args = slice.call(arguments, 1); 173 | var isfn = 'function' == typeof action; 174 | var sel = this.selection; 175 | var cell; 176 | var ret; 177 | 178 | for (var i = 0, j = 0, at; at = sel[i]; i++) { 179 | cell = spreadsheet.at(at); 180 | 181 | // ignore merged cells 182 | if (!cell || cell.at != at) continue; 183 | 184 | // use fn or delegate to cell 185 | ret = isfn ? action(cell, j++) : cell[action].apply(cell, args); 186 | 187 | // break if false 188 | if (false === ret) break; 189 | } 190 | 191 | return this; 192 | }; 193 | 194 | /** 195 | * merge 196 | */ 197 | 198 | Selection.prototype.merge = function() { 199 | var cells = this.cells(); 200 | this.spreadsheet.merge(cells); 201 | return this; 202 | }; 203 | 204 | /** 205 | * Merge the rows together 206 | * 207 | * @return {Selection} 208 | * @api public 209 | */ 210 | 211 | Selection.prototype.mergeRows = function() { 212 | var cells = this.cells(); 213 | var cs = cols(cells); 214 | 215 | for (var col in cs) { 216 | this.spreadsheet.merge(cs[col]); 217 | } 218 | 219 | return this; 220 | }; 221 | 222 | /** 223 | * Merge the cols together 224 | * 225 | * @return {Selection} 226 | * @api public 227 | */ 228 | 229 | Selection.prototype.mergeCols = function() { 230 | var cells = this.cells(); 231 | var rs = rows(cells); 232 | 233 | for (var row in rs) { 234 | this.spreadsheet.merge(rs[row]); 235 | } 236 | 237 | return this; 238 | }; 239 | 240 | /** 241 | * Chain 242 | * 243 | * @param {String} sel 244 | * @return {Selection} new 245 | */ 246 | 247 | Selection.prototype.select = function(sel) { 248 | return new Selection(sel, this.spreadsheet); 249 | }; 250 | 251 | /** 252 | * show 253 | */ 254 | 255 | Selection.prototype.show = function() { 256 | var thead = this.spreadsheet.thead; 257 | var tbody = this.spreadsheet.tbody; 258 | var type = this.type; 259 | var m = match(this.expr); 260 | var els = []; 261 | 262 | if (/rows?/.test(type)) { 263 | var range = rowrange(m[1], m[2]); 264 | var els = selectrows(range); 265 | } else if (/cols?/.test(type)) { 266 | var range = colrange(m[1], m[2]); 267 | var els = selectcols(range); 268 | } else { 269 | return this; 270 | } 271 | 272 | for (var i = 0, el; el = els.item(i); i++) { 273 | classes(el).remove('hidden'); 274 | } 275 | 276 | return this.each('show'); 277 | 278 | function selectrows(range) { 279 | var q = range.map(function(n) { 280 | return 'tr[name="' + n + '"]'; 281 | }).join(', '); 282 | 283 | return tbody.querySelectorAll(q); 284 | } 285 | 286 | function selectcols(range) { 287 | var q = range.map(function(n) { 288 | return 'th[name="' + n + '"]'; 289 | }).join(', '); 290 | return thead.querySelectorAll(q); 291 | } 292 | }; 293 | 294 | /** 295 | * Hide the selection 296 | */ 297 | 298 | Selection.prototype.hide = function(cls) { 299 | cls = cls || 'hidden'; 300 | 301 | var thead = this.spreadsheet.thead; 302 | var tbody = this.spreadsheet.tbody; 303 | var type = this.type; 304 | var m = match(this.expr); 305 | var els = []; 306 | 307 | 308 | if (/rows?/.test(type)) { 309 | var range = rowrange(m[1], m[2]); 310 | var els = selectrows(range); 311 | } else if (/cols?/.test(type)) { 312 | var range = colrange(m[1], m[2]); 313 | var els = selectcols(range); 314 | } else { 315 | return this; 316 | } 317 | 318 | for (var i = 0, el; el = els.item(i); i++) { 319 | classes(el).add(cls); 320 | } 321 | 322 | return this.each('hide'); 323 | 324 | function selectrows(range) { 325 | var q = range.map(function(n) { 326 | return 'tr[name="' + n + '"]'; 327 | }).join(', '); 328 | 329 | return tbody.querySelectorAll(q); 330 | } 331 | 332 | function selectcols(range) { 333 | var q = range.map(function(n) { 334 | return 'th[name="' + n + '"]'; 335 | }).join(', '); 336 | return thead.querySelectorAll(q); 337 | } 338 | }; 339 | 340 | 341 | 342 | /** 343 | * collapsible 344 | */ 345 | 346 | Selection.prototype.collapsible = function(collapsed) { 347 | collapsed = undefined == collapsed ? true : false; 348 | this.spreadsheet.collapsible.range(this.expr, collapsed); 349 | return this; 350 | }; 351 | 352 | 353 | /** 354 | * Delegate each cell in the selection to Cell 355 | */ 356 | 357 | [ 358 | 'editable', 359 | 'format', 360 | 'addClass', 361 | 'attr', 362 | 'focus', 363 | 'activate' 364 | ].forEach(function(m) { 365 | Selection.prototype[m] = function() { 366 | var args = slice.call(arguments); 367 | return this.each.apply(this, [m].concat(args)); 368 | }; 369 | }); 370 | -------------------------------------------------------------------------------- /lib/spreadsheet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var classes = require('classes'); 6 | var event = require('event'); 7 | var events = require('events'); 8 | var closest = require('closest'); 9 | var delegate = require('delegates'); 10 | var isArray = require('isArray'); 11 | var Emitter = require('emitter'); 12 | var domify = require('domify'); 13 | var shortcuts = require('shortcuts'); 14 | var Selection = require('./selection'); 15 | var Cell = require('./cell'); 16 | var utils = require('./utils'); 17 | var match = require('./match'); 18 | var lton = utils.lton; 19 | var ntol = utils.ntol; 20 | var smallest = utils.smallest; 21 | var largest = utils.largest; 22 | var subtract = utils.subtract; 23 | var k = require('k')(document); 24 | var collapsible = require('./collapsible'); 25 | var Outline = require('./outline'); 26 | var tap = require('tap'); 27 | 28 | /** 29 | * Spreadsheet element 30 | */ 31 | 32 | var tpl = require('../template'); 33 | 34 | /** 35 | * Export `Spreadsheet` 36 | */ 37 | 38 | module.exports = Spreadsheet; 39 | 40 | /** 41 | * Initialize `Spreadsheet` 42 | * 43 | * @param {Number} numcols 44 | * @param {Number} numrows (optional) 45 | */ 46 | 47 | function Spreadsheet(numcols, numrows, workbook) { 48 | if (!(this instanceof Spreadsheet)) return new Spreadsheet(numcols, numrows, workbook); 49 | 50 | // parse string 51 | if (!numrows && 'string' == typeof numcols) { 52 | var m = match(numcols); 53 | this.numcols = numcols = lton(m[1]) + 1; 54 | this.numrows = numrows = +m[2] 55 | } else { 56 | this.numcols = numcols = numcols || 10; 57 | this.numrows = numrows = numrows || 10; 58 | } 59 | 60 | this.el = domify(tpl({ cols: numcols, rows: numrows })); 61 | this.thead = this.el.getElementsByTagName('thead')[0]; 62 | this.tbody = this.el.getElementsByTagName('tbody')[0]; 63 | this.classes = classes(this.el); 64 | 65 | this.largest = ntol(numcols) + numrows; 66 | 67 | this.workbook = workbook; 68 | this.spreadsheet = {}; 69 | this.merged = {}; 70 | this.cells = []; 71 | 72 | // active cell 73 | this.active = false; 74 | 75 | // bind events 76 | this.events = events(this.el, this); 77 | 78 | // initialize the outline 79 | this.outline = Outline(this.el); 80 | 81 | // initialize collapsible 82 | this.collapsible = collapsible(this); 83 | 84 | this.draw(); 85 | } 86 | 87 | /** 88 | * Mixin the `Emitter` 89 | */ 90 | 91 | Emitter(Spreadsheet.prototype); 92 | 93 | /** 94 | * Delegate to the active cell 95 | */ 96 | 97 | delegate(Spreadsheet.prototype, 'active') 98 | .method('onkeydown') 99 | .method('onesc') 100 | .method('onf2'); 101 | 102 | /** 103 | * Create a selection 104 | * 105 | * @param {String} selection 106 | * @return {Spreadsheet} 107 | * @api public 108 | */ 109 | 110 | Spreadsheet.prototype.select = function(selection) { 111 | return new Selection(selection, this); 112 | }; 113 | 114 | /** 115 | * Render the spreadsheet 116 | */ 117 | 118 | Spreadsheet.prototype.draw = function() { 119 | var spreadsheet = this.spreadsheet; 120 | var tds = this.el.querySelectorAll('td'); 121 | var rows = this.numrows; 122 | var cols = this.numcols; 123 | var at = 'A1'; 124 | var x = 0; 125 | 126 | for (var i = 0; i < rows; i++) { 127 | for (var j = 0; j < cols; j++, x++) { 128 | at = ntol(j) + (i+1); 129 | spreadsheet[at] = spreadsheet[at] || new Cell(tds[x], at, this); 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * Reset 136 | */ 137 | 138 | Spreadsheet.prototype.deactivate = function(e) { 139 | this.active && this.active.deactivate(); 140 | } 141 | 142 | /** 143 | * Get the cell reference 144 | * 145 | * @param {String} at 146 | * @return {Cell|null} 147 | * @api private 148 | */ 149 | 150 | Spreadsheet.prototype.at = function(at) { 151 | at = at.toUpperCase(); 152 | return this.spreadsheet[at] || null; 153 | }; 154 | 155 | /** 156 | * Merge cells 157 | * 158 | * @param {Array} cells 159 | * @return {Spreadsheet} 160 | * @api public 161 | */ 162 | 163 | Spreadsheet.prototype.merge = function(cells) { 164 | var spreadsheet = this.spreadsheet; 165 | var merged = this.merged; 166 | var captain = smallest(cells); 167 | var at = captain.at; 168 | var hidden = 0; 169 | var el, tr; 170 | merged[at] = []; 171 | 172 | // remove the remaining cells 173 | for (var i = 0, cell; cell = cells[i]; i++) { 174 | if (captain == cell) continue; 175 | el = cell.el; 176 | if (classes(el).has('hidden')) hidden++; 177 | tr = el.parentNode; 178 | if (tr) tr.removeChild(el); 179 | spreadsheet[cell.at] = captain; 180 | merged[at].push(cell.at); 181 | } 182 | 183 | // add the col and rowspan 184 | var biggest = largest(cells); 185 | var diff = subtract(biggest.at, captain.at); 186 | captain.attr('rowspan', diff.row - hidden + 1); 187 | captain.attr('colspan', diff.col - hidden + 1); 188 | 189 | return this; 190 | } 191 | 192 | /** 193 | * Move to another cell 194 | * 195 | * @param {Event} e 196 | * @param {String} dir 197 | */ 198 | 199 | Spreadsheet.prototype.move = function(dir) { 200 | var self = this; 201 | var active = this.active; 202 | 203 | if (!active) return this; 204 | 205 | this.traverse(dir, function(cell) { 206 | if (!cell.classes.has('hidden') && !active.classes.has('focused')) { 207 | cell.activate(); 208 | 209 | // blur old active 210 | active.deactivate(); 211 | self.active = cell; 212 | 213 | // emitters 214 | self.emit('move', dir, cell); 215 | self.emit('move ' + dir, cell); 216 | 217 | return false; 218 | } 219 | }); 220 | }; 221 | 222 | /** 223 | * Arrow key event listeners 224 | * 225 | * @param {Event} e 226 | * @return {Spreadsheet} 227 | */ 228 | 229 | ['left', 'right', 'up', 'down'].forEach(function(m) { 230 | Spreadsheet.prototype['on' + m] = function(e) { 231 | var active = this.active; 232 | 233 | if (active && !active.classes.has('focused')) { 234 | e.preventDefault(); 235 | 236 | // reset headings 237 | active.reset(); 238 | this.classes.remove('headings'); 239 | } 240 | 241 | e.stopPropagation(); 242 | this.move(m); 243 | 244 | return this; 245 | } 246 | }); 247 | 248 | /** 249 | * onbacktick 250 | */ 251 | 252 | Spreadsheet.prototype.onbacktick = function(e) { 253 | e.preventDefault(); 254 | e.stopPropagation(); 255 | this.classes.toggle('headings'); 256 | return this; 257 | }; 258 | 259 | /** 260 | * addClass 261 | */ 262 | 263 | Spreadsheet.prototype.addClass = function(cls) { 264 | this.classes.add(cls); 265 | return this; 266 | }; 267 | 268 | 269 | /** 270 | * Traverse 271 | * 272 | * @param {String} dir 273 | * @param {Function} fn 274 | * @return {Spreadsheet} 275 | * @api public 276 | */ 277 | 278 | Spreadsheet.prototype.traverse = function(cell, dir, fn) { 279 | if (arguments.length < 3) { 280 | if (!this.active) return this; 281 | fn = dir; 282 | dir = cell; 283 | cell = this.active; 284 | } 285 | 286 | var m = match(cell.at); 287 | var col = m[1]; 288 | var row = m[2]; 289 | var at; 290 | 291 | while (cell) { 292 | switch (dir) { 293 | case 'left': 294 | case 'l': 295 | col = ntol(lton(col) - 1); 296 | break; 297 | case 'right': 298 | case 'r': 299 | col = ntol(lton(col) + 1); 300 | break; 301 | case 'up': 302 | case 'top': 303 | case 'u': 304 | case 't': 305 | row--; 306 | break; 307 | case 'down': 308 | case 'bottom': 309 | case 'd': 310 | case 'b': 311 | row++; 312 | break; 313 | } 314 | 315 | at = col + row; 316 | cell = this.at(at); 317 | 318 | // if we found the next cell, break 319 | if (cell && at == cell.at && false == fn(cell)) break; 320 | } 321 | 322 | return this; 323 | }; 324 | -------------------------------------------------------------------------------- /lib/tokens.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var regex = require('./regex'); 6 | 7 | /** 8 | * Export `tokens` 9 | */ 10 | 11 | module.exports = tokens; 12 | 13 | /** 14 | * Initialize `tokens` 15 | */ 16 | 17 | function tokens(expr) { 18 | var toks = []; 19 | expr.replace(regex.any, function(m) { 20 | toks.push(m); 21 | }); 22 | return toks; 23 | } 24 | -------------------------------------------------------------------------------- /lib/type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var regex = require('./regex'); 6 | 7 | /** 8 | * Export `type` 9 | */ 10 | 11 | module.exports = type; 12 | 13 | /** 14 | * Initialize `type` 15 | */ 16 | 17 | function type(str) { 18 | return parse(str); 19 | } 20 | 21 | /** 22 | * Parse the type 23 | */ 24 | 25 | function parse(str) { 26 | if (regex.block.test(str)) return 'block'; 27 | if (regex.cell.test(str)) return 'cell'; 28 | if (regex.rows.test(str)) return 'rows'; 29 | if (regex.cols.test(str)) return 'cols'; 30 | if (regex.row.test(str)) return 'row'; 31 | if (regex.col.test(str)) return 'col'; 32 | }; 33 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var type = require('./type'); 6 | var match = require('./match'); 7 | var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 8 | 9 | /** 10 | * Letter to number 11 | * 12 | * @param {String} l 13 | * @return {Number} 14 | */ 15 | 16 | exports.lton = lton = function(l) { 17 | return letters.indexOf(l); 18 | } 19 | 20 | /** 21 | * Number to letter 22 | * 23 | * TODO: support Y, Z, AA, AB, AC, ... 24 | * 25 | * @param {String} l 26 | * @return {Number} 27 | */ 28 | 29 | exports.ntol = ntol = function(n) { 30 | return letters[n]; 31 | } 32 | 33 | /** 34 | * Shift the cell 35 | * 36 | * @param {String} cell 37 | * @param {String} row/col/cell 38 | */ 39 | 40 | exports.shift = shift = function(cell, shifter) { 41 | var t = type(shifter); 42 | shifter = shifter.replace(/^\:/, ''); 43 | 44 | switch(t) { 45 | case 'col': 46 | return cell.replace(/[A-Za-z]+/g, shifter); 47 | case 'row': 48 | return cell.replace(/[0-9]+/g, shifter); 49 | case 'cell': 50 | return shifter; 51 | } 52 | } 53 | 54 | /** 55 | * Get the largest value of the selection 56 | * aka, the bottom-right value 57 | * 58 | * @param {Array} cells 59 | * @return {String} largest 60 | * @api public 61 | */ 62 | 63 | exports.largest = largest = function(cells) { 64 | var out = null; 65 | var sum = 1; 66 | 67 | for (var i = 0, cell; cell = cells[i]; i++) { 68 | var m = match(cell.at || cell); 69 | var l = lton(m[1]); 70 | var n = +m[2]; 71 | 72 | if (l + n >= sum) { 73 | out = cell; 74 | sum = l + n; 75 | } 76 | } 77 | 78 | return out; 79 | }; 80 | 81 | /** 82 | * Get the smallest value of an array of 83 | * cells. aka, the top-right value. 84 | * 85 | * @param {Array} cells 86 | * @param {String} 87 | */ 88 | 89 | exports.smallest = smallest = function(cells) { 90 | var out = null; 91 | var sum = Infinity; 92 | 93 | for (var i = 0, cell; cell = cells[i]; i++) { 94 | var m = match(cell.at); 95 | var l = lton(m[1]); 96 | var n = +m[2]; 97 | 98 | if (l + n <= sum) { 99 | out = cell; 100 | sum = l + n; 101 | } 102 | } 103 | 104 | return out; 105 | }; 106 | 107 | /** 108 | * Subtract two positions 109 | * 110 | * @param {String} a 111 | * @param {String} b 112 | * @return {String} 113 | */ 114 | 115 | exports.subtract = subtract = function(a, b) { 116 | // a 117 | var m = match(a); 118 | var al = lton(m[1]); 119 | var an = +m[2]; 120 | 121 | // b 122 | var m = match(b); 123 | var bl = lton(m[1]); 124 | var bn = +m[2]; 125 | 126 | // a - b 127 | return { 128 | col: al - bl, 129 | row: an - bn 130 | }; 131 | }; 132 | 133 | /** 134 | * Get an array of the rows in a selection 135 | * 136 | * @param {Array} cells 137 | * @return {Array} 138 | */ 139 | 140 | exports.rows = rows = function(cells) { 141 | var buckets = {}; 142 | var rows = []; 143 | var number; 144 | 145 | for (var i = 0, cell; cell = cells[i]; i++) { 146 | number = match(cell.at || cell)[2]; 147 | if (!buckets[number]) buckets[number] = []; 148 | buckets[number].push(cell); 149 | } 150 | 151 | return buckets; 152 | }; 153 | 154 | /** 155 | * Get an array of the cols in a selection 156 | * 157 | * @param {Array} cells 158 | * @return {Array} 159 | */ 160 | 161 | exports.cols = cols = function(cells) { 162 | var buckets = {}; 163 | var cols = []; 164 | var letter; 165 | 166 | for (var i = 0, cell; cell = cells[i]; i++) { 167 | letter = match(cell.at || cell)[1]; 168 | if (!buckets[letter]) buckets[letter] = []; 169 | buckets[letter].push(cell); 170 | } 171 | 172 | return buckets; 173 | }; 174 | 175 | /** 176 | * Column range 177 | */ 178 | 179 | exports.colrange = colrange = function(from, to) { 180 | if (!to) return [from]; 181 | 182 | var out = []; 183 | from = lton(from); 184 | to = lton(to); 185 | 186 | for (var i = from; i <= to; i++) { 187 | out.push(letters[i]); 188 | } 189 | 190 | return out; 191 | } 192 | 193 | /** 194 | * Row range 195 | */ 196 | 197 | exports.rowrange = rowrange = function(from, to) { 198 | var out = []; 199 | from = +from; 200 | to = +to; 201 | 202 | if (!to) return [from]; 203 | 204 | for (var i = from; i <= to; i++) { 205 | out.push(i); 206 | } 207 | 208 | return out; 209 | } 210 | 211 | /** 212 | * element range 213 | */ 214 | 215 | exports.range = range = function(expr) { 216 | var t = type(expr); 217 | if (/rows?/.test(t)) { 218 | 219 | } else { 220 | 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /lib/workbook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var Spreadsheet = require('./spreadsheet'); 6 | var shortcuts = require('shortcuts'); 7 | var delegate = require('delegates'); 8 | var events = require('events'); 9 | 10 | /** 11 | * Export `Workbook` 12 | */ 13 | 14 | module.exports = Workbook; 15 | 16 | /** 17 | * Initialize `Workbook` 18 | */ 19 | 20 | function Workbook() { 21 | if (!(this instanceof Workbook)) return new Workbook(); 22 | this.spreadsheets = []; 23 | this.active = null; 24 | 25 | this.events = events(window, this); 26 | this.events.bind('click', 'onclick'); 27 | this.events.bind('keydown', 'onkeydown'); 28 | 29 | 30 | // set up keyboard shortcuts 31 | this.shortcuts = shortcuts(document, this); 32 | this.shortcuts.k.ignore = false; 33 | this.shortcuts.bind('right', 'onright'); 34 | this.shortcuts.bind('enter', 'ondown'); 35 | this.shortcuts.bind('down', 'ondown'); 36 | this.shortcuts.bind('left', 'onleft'); 37 | this.shortcuts.bind('up', 'onup'); 38 | this.shortcuts.bind('esc', 'onesc'); 39 | this.shortcuts.bind('f2', 'onf2'); 40 | this.shortcuts.bind('`', 'onbacktick'); 41 | } 42 | 43 | /** 44 | * Delegate keyboard shortcuts 45 | */ 46 | 47 | delegate(Workbook.prototype, 'active') 48 | .method('onkeydown') 49 | .method('onright') 50 | .method('onleft') 51 | .method('ondown') 52 | .method('onup') 53 | .method('onesc') 54 | .method('onf2') 55 | .method('onbacktick'); 56 | 57 | /** 58 | * Activate a spreadsheet on click 59 | */ 60 | 61 | Workbook.prototype.onclick = function(e) { 62 | var target = e.target; 63 | var active = null; 64 | for (var i = 0, s; s = this.spreadsheets[i]; i++) { 65 | if (s.el.contains(target)) { 66 | active = s; 67 | break; 68 | } 69 | } 70 | 71 | this.active && this.active != active && this.active.deactivate(); 72 | this.active = active; 73 | return this; 74 | }; 75 | 76 | 77 | /** 78 | * add a spreadsheet to the workbook 79 | */ 80 | 81 | Workbook.prototype.spreadsheet = function(cols, rows) { 82 | var spreadsheet = new Spreadsheet(cols, rows, this); 83 | this.spreadsheets.push(spreadsheet); 84 | return spreadsheet; 85 | }; 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spreadsheet", 3 | "version": "0.1.5", 4 | "description": "create spreadsheets with excel-like features", 5 | "main": "dist/spreadsheet.browserify.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "test": "make test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/MatthewMueller/spreadsheet.git" 15 | }, 16 | "author": "Levered Returns", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/MatthewMueller/spreadsheet/issues" 20 | }, 21 | "homepage": "https://github.com/MatthewMueller/spreadsheet", 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "browserify": "^9.0.8", 25 | "derequire": "^2.0.0" 26 | } 27 | } -------------------------------------------------------------------------------- /spreadsheet.css: -------------------------------------------------------------------------------- 1 | /** 2 | * General 3 | */ 4 | 5 | .spreadsheet { 6 | position: relative; 7 | font-family: helvetica; 8 | font-weight: 300; 9 | color: #222; 10 | } 11 | 12 | .spreadsheet + .spreadsheet { 13 | margin-top: 10px; 14 | } 15 | 16 | .spreadsheet * { 17 | box-sizing: border-box; 18 | } 19 | 20 | .spreadsheet table { 21 | width: 100%; 22 | border-spacing: 0; 23 | border-left: 0; 24 | border-collapse: separate; 25 | table-layout: fixed; 26 | 27 | /* prevent text flicker in safari */ 28 | -webkit-backface-visibility: hidden; 29 | } 30 | 31 | .spreadsheet .editable { 32 | color: #4285F4; 33 | font-style: italic; 34 | } 35 | 36 | .spreadsheet th, 37 | .spreadsheet td { 38 | padding: 0; 39 | } 40 | 41 | .spreadsheet th div { 42 | height: 100%; 43 | width: 100%; 44 | } 45 | 46 | /** 47 | * Input styling 48 | */ 49 | 50 | .spreadsheet input { 51 | border: 0; 52 | margin: 0; 53 | height: inherit; 54 | width: 100%; 55 | 56 | font: inherit; 57 | color: inherit; 58 | text-align: inherit; 59 | line-height: inherit; 60 | -webkit-text-fill-color: inherit; 61 | 62 | white-space: nowrap; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | } 66 | 67 | .spreadsheet input[disabled] { 68 | pointer-events: none; 69 | background: none; 70 | opacity: 1; 71 | } 72 | 73 | .spreadsheet input:focus { 74 | outline: none; 75 | } 76 | 77 | /** 78 | * Dimensions 79 | */ 80 | 81 | .spreadsheet .rowhead { 82 | width: 32px; 83 | } 84 | 85 | .spreadsheet .colhead div, 86 | .spreadsheet .rowhead div, 87 | .spreadsheet td input { 88 | padding: 6px; 89 | } 90 | 91 | /** 92 | * Cell borders 93 | */ 94 | 95 | .spreadsheet td { 96 | border-right: 1px solid #f2f2f2; 97 | border-bottom: 1px solid #f2f2f2; 98 | } 99 | 100 | .spreadsheet tr:first-child td { 101 | border-top: 1px solid #cdcdcd; 102 | } 103 | 104 | .spreadsheet td:first-of-type { 105 | border-left: 1px solid #cdcdcd; 106 | } 107 | 108 | .spreadsheet td:last-of-type { 109 | border-right: 1px solid #cdcdcd; 110 | } 111 | 112 | .spreadsheet tr:last-child td { 113 | border-bottom: 1px solid #cdcdcd; 114 | } 115 | 116 | .spreadsheet .colhead th:nth-child(5) div { 117 | border-left: 1px solid #cdcdcd; 118 | } 119 | 120 | .spreadsheet .colhead div { 121 | border-right: 1px solid #cdcdcd; 122 | border-top: 1px solid #cdcdcd; 123 | } 124 | 125 | .spreadsheet tr:first-child .rowhead div { 126 | border-top: 1px solid #cdcdcd; 127 | } 128 | 129 | .spreadsheet .rowhead div { 130 | border-bottom: 1px solid #cdcdcd; 131 | border-left: 1px solid #cdcdcd; 132 | } 133 | 134 | .spreadsheet.row-layers .rowhead div { 135 | border-right: 1px solid #cdcdcd; 136 | } 137 | 138 | /** 139 | * Outline styling 140 | */ 141 | 142 | .spreadsheet .outline { 143 | border: 2px solid #333; 144 | pointer-events: none; 145 | z-index: 1; 146 | position: absolute; 147 | } 148 | 149 | .spreadsheet .outline.editable { 150 | border-color: #4285F4; 151 | } 152 | 153 | /** 154 | * Hidden cells 155 | */ 156 | 157 | .spreadsheet tr.hidden, 158 | .spreadsheet td.hidden, 159 | .spreadsheet th.hidden { 160 | display: none; 161 | } 162 | 163 | .spreadsheet thead .colhead th.no-width { 164 | width: 0; 165 | } 166 | 167 | .spreadsheet thead tr.layer, 168 | .spreadsheet thead th.layerhead, 169 | .spreadsheet tbody th.layer { 170 | width: 0; 171 | height: 0; 172 | } 173 | 174 | .spreadsheet thead tr.layer.shown { 175 | display: table-row; 176 | } 177 | 178 | .spreadsheet thead th.layerhead.shown { 179 | width: 20px; 180 | } 181 | 182 | .spreadsheet thead tr.layer.shown { 183 | height: 20px; 184 | } 185 | 186 | .spreadsheet thead th { 187 | position: relative; 188 | } 189 | 190 | .spreadsheet th.dot, 191 | .spreadsheet th.line, 192 | .spreadsheet th.dash { 193 | cursor: pointer; 194 | } 195 | 196 | .spreadsheet tbody th.dot, 197 | .spreadsheet tbody th.line, 198 | .spreadsheet tbody th.dash { 199 | padding-right: 2px; 200 | padding-left: 2px; 201 | } 202 | 203 | .spreadsheet tbody th.dot { 204 | padding-top: 15px; 205 | } 206 | 207 | .spreadsheet tbody th.line, 208 | .spreadsheet tbody th.dash { 209 | padding: 0 8px; 210 | } 211 | 212 | .spreadsheet tbody th.dot div { 213 | background: #4285F4; 214 | border-radius: 8px; 215 | } 216 | 217 | .spreadsheet thead th.dot div { 218 | width: 16px; 219 | margin: 0 auto; 220 | } 221 | 222 | .spreadsheet thead th.dot div:after, 223 | .spreadsheet thead th.line div:after, 224 | .spreadsheet thead th.dash div:after { 225 | content: '\0'; 226 | } 227 | 228 | .spreadsheet thead th.dot, 229 | .spreadsheet thead th.line, 230 | .spreadsheet thead th.dash { 231 | overflow: hidden; 232 | } 233 | 234 | .spreadsheet thead th.dot div { 235 | position: absolute; 236 | right: 0; 237 | height: 4px; 238 | margin-top: -2px; 239 | width: 50%; 240 | background: #4285F4; 241 | } 242 | 243 | .spreadsheet thead th.dot.hiding div { 244 | background: none; 245 | } 246 | 247 | .spreadsheet thead th.line div { 248 | position: absolute; 249 | height: 4px; 250 | margin-top: -2px; 251 | background: #4285F4; 252 | } 253 | 254 | .spreadsheet thead th.dash div { 255 | position: absolute; 256 | height: 4px; 257 | margin-top: -2px; 258 | width: 50%; 259 | background: #4285F4; 260 | } 261 | 262 | .spreadsheet thead th.dash div:after { 263 | background: #4285F4; 264 | height: 16px; 265 | width: 4px; 266 | position: absolute; 267 | top: -6px; 268 | right: -4px; 269 | } 270 | 271 | .spreadsheet thead th.dot div:after { 272 | background: #4285F4; 273 | border-radius: 8px; 274 | height: 16px; 275 | width: 16px; 276 | position: absolute; 277 | top: -6px; 278 | left: -8px; 279 | } 280 | 281 | .spreadsheet tbody th.line div, 282 | .spreadsheet tbody th.dash div { 283 | background: #4285F4; 284 | } 285 | 286 | .spreadsheet tbody th.dash { 287 | border-bottom: 3px solid #4285F4; 288 | } 289 | 290 | /** 291 | * Headings 292 | */ 293 | 294 | .spreadsheet .colhead th, 295 | .spreadsheet th.rowhead { 296 | overflow: hidden; 297 | } 298 | 299 | .spreadsheet .colhead div, 300 | .spreadsheet .rowhead div { 301 | height: 100%; 302 | width: 100%; 303 | background: #f2f2f2; 304 | -webkit-transition: -webkit-transform ease-out .2s; 305 | -moz-transition: -moz-transform ease-out .2s; 306 | -ms-transition: -ms-transform ease-out .2s; 307 | -o-transition: -o-transform ease-out .2s; 308 | transition: transform ease-out .2s; 309 | } 310 | 311 | .spreadsheet .rowhead div { 312 | -webkit-transform: translateX(100%); 313 | -moz-transform: translateX(100%); 314 | -ms-transform: translateX(100%); 315 | -o-transform: translateX(100%); 316 | transform: translateX(100%); 317 | } 318 | 319 | .spreadsheet .colhead div { 320 | -webkit-transform: translateY(100%); 321 | -moz-transform: translateY(100%); 322 | -ms-transform: translateY(100%); 323 | -o-transform: translateY(100%); 324 | transform: translateY(100%); 325 | } 326 | 327 | .spreadsheet.headings .colhead div, 328 | .spreadsheet.headings .rowhead div { 329 | -webkit-transform: translate(0); 330 | -moz-transform: translate(0); 331 | -ms-transform: translate(0); 332 | -o-transform: translate(0); 333 | transform: translate(0); 334 | } 335 | -------------------------------------------------------------------------------- /template.jade: -------------------------------------------------------------------------------- 1 | //- TODO: compile with jade CLI 2 | //- For now: http://jade-lang.com/demo/ 3 | 4 | - var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 5 | - var heads = headers == undefined ? true : headers 6 | - var l = layers || 3 7 | - var r = rows || 10 8 | - var c = cols || 10 9 | 10 | mixin filler(n) 11 | - for (var i = 0; i < n; i++) 12 | th.filler 13 | 14 | mixin rowhead(value) 15 | th=value 16 | 17 | mixin th(n) 18 | - for (var i = 0; i < n; i++) 19 | th(name=letters[i]): div=letters[i] 20 | 21 | mixin layerhead(n) 22 | - for (var i = 0; i < n; i++) 23 | th.layerhead 24 | 25 | mixin collayer(n) 26 | - for (var i = 0; i < n; i++) 27 | th.layer(name=letters[i]): div 28 | 29 | mixin rowlayer(n) 30 | - for (var i = 0; i < n; i++) 31 | th.layer: div 32 | 33 | mixin td(n) 34 | - for (var i = 0; i < n; i++) 35 | td 36 | input(type='text' disabled='disabled') 37 | 38 | .spreadsheet 39 | table 40 | thead 41 | if (heads) 42 | tr.colhead 43 | th.rowhead 44 | +layerhead(l) 45 | +th(c) 46 | - for (var i = 0; i < l; i++) 47 | tr.layer 48 | +filler(l + (heads ? 1 : 0)) 49 | +collayer(c) 50 | tbody 51 | - for (var i = 1; i <= r; i++) 52 | tr(name=i) 53 | if (heads) 54 | th.rowhead: div=i 55 | +rowlayer(l) 56 | +td(c) 57 | 58 | -------------------------------------------------------------------------------- /template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var jade = require('jade'); 6 | 7 | /** 8 | * Expose template 9 | */ 10 | 11 | module.exports = template; 12 | 13 | /** 14 | * Create the template 15 | * 16 | * @param {Object} locals 17 | * @return {Function} 18 | * @api public 19 | */ 20 | 21 | function template(locals) { 22 | var buf = []; 23 | var jade_mixins = {}; 24 | var locals_ = locals || {}, headers = locals_.headers, undefined = locals_.undefined, layers = locals_.layers, rows = locals_.rows, cols = locals_.cols; 25 | var letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 26 | var heads = headers == undefined ? true : headers; 27 | var l = layers || 3; 28 | var r = rows || 10; 29 | var c = cols || 10; 30 | jade_mixins["filler"] = function(n) { 31 | var block = this && this.block, attributes = this && this.attributes || {}; 32 | for (var i = 0; i < n; i++) { 33 | buf.push(''); 34 | } 35 | }; 36 | jade_mixins["rowhead"] = function(value) { 37 | var block = this && this.block, attributes = this && this.attributes || {}; 38 | buf.push("" + jade.escape(null == (jade.interp = value) ? "" : jade.interp) + ""); 39 | }; 40 | jade_mixins["th"] = function(n) { 41 | var block = this && this.block, attributes = this && this.attributes || {}; 42 | for (var i = 0; i < n; i++) { 43 | buf.push("
" + jade.escape(null == (jade.interp = letters[i]) ? "" : jade.interp) + "
"); 44 | } 45 | }; 46 | jade_mixins["layerhead"] = function(n) { 47 | var block = this && this.block, attributes = this && this.attributes || {}; 48 | for (var i = 0; i < n; i++) { 49 | buf.push(''); 50 | } 51 | }; 52 | jade_mixins["collayer"] = function(n) { 53 | var block = this && this.block, attributes = this && this.attributes || {}; 54 | for (var i = 0; i < n; i++) { 55 | buf.push("
'); 56 | } 57 | }; 58 | jade_mixins["rowlayer"] = function(n) { 59 | var block = this && this.block, attributes = this && this.attributes || {}; 60 | for (var i = 0; i < n; i++) { 61 | buf.push('
'); 62 | } 63 | }; 64 | jade_mixins["td"] = function(n) { 65 | var block = this && this.block, attributes = this && this.attributes || {}; 66 | for (var i = 0; i < n; i++) { 67 | buf.push(''); 68 | } 69 | }; 70 | buf.push('
'); 71 | if (heads) { 72 | buf.push(''); 73 | jade_mixins["layerhead"](l); 74 | jade_mixins["th"](c); 75 | buf.push(""); 76 | } 77 | for (var i = 0; i < l; i++) { 78 | buf.push(''); 79 | jade_mixins["filler"](l + (heads ? 1 : 0)); 80 | jade_mixins["collayer"](c); 81 | buf.push(""); 82 | } 83 | buf.push(""); 84 | for (var i = 1; i <= r; i++) { 85 | buf.push(""); 86 | if (heads) { 87 | buf.push('"); 88 | } 89 | jade_mixins["rowlayer"](l); 90 | jade_mixins["td"](c); 91 | buf.push(""); 92 | } 93 | buf.push("
' + jade.escape(null == (jade.interp = i) ? "" : jade.interp) + "
"); 94 | return buf.join(""); 95 | } 96 | --------------------------------------------------------------------------------