├── .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 | 
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('' + jade.escape(null == (jade.interp = i) ? "" : jade.interp) + " | ");
88 | }
89 | jade_mixins["rowlayer"](l);
90 | jade_mixins["td"](c);
91 | buf.push("
");
92 | }
93 | buf.push("
");
94 | return buf.join("");
95 | }
96 |
--------------------------------------------------------------------------------