├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── css └── style.css ├── index.html ├── index.js ├── package.json ├── rollup.config.js └── src └── metatable.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | node_modules 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [1.0.1](https://github.com/mapbox/d3-metatable/compare/v1.0.0...v1.0.1) (2017-12-03) 7 | 8 | 9 | 10 | 11 | # [1.0.0](https://github.com/mapbox/d3-metatable/compare/v0.3.0...v1.0.0) (2017-11-27) 12 | 13 | 14 | ### Features 15 | 16 | * Compatibility with d3 v4 & ES6 modules ([33f7864](https://github.com/mapbox/d3-metatable/commit/33f7864)) 17 | 18 | 19 | ### BREAKING CHANGES 20 | 21 | * will no longer play well with d3 v3. 22 | 23 | 24 | 25 | ### v0.4.0 26 | 27 | - Interfaces like `renameCol`, `deleteCol`, and `newCol` can now be disabled 28 | if the option is passed in metatable. 29 | - Allow a client to pass in a custom interface for renaming or deleting a column. 30 | - HTML Markup cleanup 31 | - Adds `renameprompt` and `deleteprompt` events to disable default behaviour with users own. 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c), Mapbox 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | - Neither the name "Mapbox" nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## d3-metatable 2 | 3 | A table view component for [d3js](http://d3js.org/) designed for JSON 4 | objects of varying schemas. 5 | 6 | ### Example 7 | 8 | ```js 9 | container.append('div') 10 | .data([props]) 11 | .call( 12 | metatable() 13 | .on('change', function(d, i) { 14 | // a row's data is changed 15 | }) 16 | .on('rowfocus', function(d, i) { 17 | // a row is focused 18 | }) 19 | ``` 20 | 21 | ### API 22 | 23 | ```js 24 | metatable(options); 25 | ``` 26 | 27 | The `options` param should be an object and is optional. 28 | 29 | | property | default | description | 30 | | ---- | ---- | ---- | 31 | | newCol | true | Adds a link to add a new column to a table. | 32 | | deleteCol | true | Adds a link to rename a column in a table. | 33 | | renameCol | true | Adds a link to delete a column in a table. | 34 | 35 | A behavior that expects to be called with a selection an array of objects 36 | of data. Emits events: 37 | 38 | * `change`: a row is changed. returns the object and the index 39 | * `rowfocus`: a row is focused. returns the object and the index 40 | * `renameprompt`: Column is about to be renamed. Useful for passing in a custom workflow for submitting a value that overrides the default. usage: 41 | 42 | ``` js 43 | .on('renameprompt', function(d, process) { 44 | // Prevent the default prompt. 45 | this.preventprompt('rename'); 46 | 47 | // Do it your own way 48 | var newname = prompt('Rename column to:'); 49 | 50 | // Continue internally by passing the new name and current one. 51 | if (newname) process(newname, d); 52 | }); 53 | ``` 54 | 55 | * `deleteprompt`: Column is about to be deleted. Useful for passing in a custom workflow for delete confirmation to override the default one. usage: 56 | 57 | ``` js 58 | .on('deleteprompt', function(d, process) { 59 | // Prevent the default prompt. 60 | this.preventprompt('delete'); 61 | 62 | // Do it your own way 63 | if (confirm('Are you sure you want to delete ' + d + '?')) process(d); 64 | }); 65 | ``` 66 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | /* Reset ♥ 2 | http://meyerweb.com/eric/tools/css/reset/ 3 | v2.0 | 20110126 4 | License: none (public domain) 5 | ------------------------------------------------------- */ 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin:0; 20 | padding:0; 21 | border:0; 22 | font-size:100%; 23 | font:inherit; 24 | vertical-align:baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display:block; 30 | } 31 | body { line-height:1; } 32 | ol, ul { list-style:none; } 33 | blockquote, q { quotes:none; } 34 | blockquote:before, blockquote:after, 35 | q:before, q:after { content:''; content:none; } 36 | /* tables still need 'cellspacing="0"' in the markup */ 37 | table { border-collapse: collapse; border-spacing:0; } 38 | /* remember to define focus styles. Hee Haw */ 39 | :focus { outline:0; } 40 | 41 | /* Inline Elements & Typography 42 | ------------------------------------------------------- */ 43 | body, 44 | input, 45 | textarea { 46 | color:#333; 47 | font:15px/1.67 'Helvetica Neue', Helvetica, Arial, sans-serif; 48 | -webkit-font-smoothing:antialiased; 49 | } 50 | *, *:after, *:before { 51 | -webkit-box-sizing: border-box; 52 | -moz-box-sizing: border-box; 53 | box-sizing: border-box; 54 | } 55 | 56 | h1, 57 | h2, 58 | h3, 59 | h4, 60 | h5, 61 | h6 { 62 | margin:0; 63 | font-weight:bold; 64 | } 65 | 66 | h1 { 67 | font-size:32px; 68 | margin-bottom:20px; 69 | line-height:1em; 70 | } 71 | 72 | h2 { 73 | font-size:28px; 74 | margin-bottom:20px; 75 | line-height:1.25em; 76 | } 77 | 78 | h3 { 79 | font-size:20px; 80 | margin-bottom:20px; 81 | line-height:1.5em; 82 | } 83 | 84 | h4, h5 { 85 | font-size:15px; 86 | margin-bottom:0; 87 | line-height:1.67em; 88 | } 89 | 90 | p { 91 | margin:0 0 20px; 92 | } 93 | p:last-child { margin-bottom:0;} 94 | 95 | abbr { 96 | border-bottom:1px dotted #000; 97 | cursor:help; 98 | } 99 | 100 | address { font-style:italic;} 101 | small { font-size:11px;} 102 | strong { font-weight:bold;} 103 | em { font-style:italic;} 104 | 105 | hr { 106 | margin:0 0 20px; 107 | border:0; 108 | height:1px; 109 | background:#f8f8f8; 110 | } 111 | 112 | /* Block Quotes */ 113 | blockquote, 114 | q { 115 | quotes:none; 116 | font-style:italic; 117 | padding-left:20px; 118 | margin:10px; 119 | } 120 | 121 | blockquote:before, 122 | blockquote:after, 123 | q:before, 124 | q:after { 125 | content:''; 126 | } 127 | 128 | /* Code Blocks & Pre */ 129 | code, 130 | pre { 131 | padding:5px; 132 | font-family:Menlo, Bitstream Vera Sans Mono, Monaco, Consolas, monospace; 133 | font-size:12px; 134 | border-radius:3px; 135 | } 136 | code { 137 | padding:5px; 138 | background:#f8f8f8; 139 | border:1px solid #ddd; 140 | } 141 | pre { 142 | display:block; 143 | padding:10px; 144 | margin-bottom:10px; 145 | font-size:12px; 146 | word-break:break-all; 147 | word-wrap:break-word; 148 | white-space:pre; 149 | white-space:pre-wrap; 150 | background:#f8f8f8; 151 | border:1px solid #ddd; 152 | border-radius:3px; 153 | } 154 | pre code { 155 | padding:0; 156 | color:inherit; 157 | background-color:transparent; 158 | border:0; 159 | } 160 | .pre-scrollable { 161 | max-height:300px; 162 | overflow-y:scroll; 163 | } 164 | 165 | /* sub/superscripts */ 166 | sup, 167 | sub { 168 | height:0; 169 | line-height:1; 170 | vertical-align:baseline; 171 | _vertical-align:bottom; 172 | position:relative; 173 | font-size:75%; 174 | } 175 | sup { 176 | top:.5em; 177 | bottom:1em; 178 | } 179 | 180 | label { 181 | display:block; 182 | } 183 | select, 184 | textarea, 185 | input[type=text] { 186 | display:inline-block; 187 | height:30px; 188 | width:95%; 189 | max-width:400px; 190 | margin-bottom:10px; 191 | font-size:13px; 192 | font-weight:500; 193 | line-height:20px; 194 | color:#a0a0a0; 195 | vertical-align:middle; 196 | padding:4px 6px; 197 | -webkit-border-radius:1px; 198 | border-radius:1px; 199 | } 200 | textarea, 201 | input[type=text] { 202 | background-color:#fff; 203 | border:1px solid #ccc; 204 | -webkit-box-shadow:1px 1px 2px rgba(0,0,0,0.1); 205 | -moz-box-shadow:1px 1px 2px rgba(0,0,0,0.1); 206 | box-shadow:1px 1px 2px rgba(0,0,0,0.1); 207 | -webkit-transition:border linear .2s, box-shadow linear .2s; 208 | -moz-transition:border linear .2s, box-shadow linear .2s; 209 | -o-transition:border linear .2s, box-shadow linear .2s; 210 | transition:border linear .2s, box-shadow linear .2s; 211 | } 212 | textarea:focus, 213 | input[type=text]:focus { 214 | outline:thin dotted\8; /* ie8 below */ 215 | color:#404040; 216 | border-color:#00395D; 217 | border-width:1px; 218 | } 219 | 220 | textarea { 221 | height:200px; 222 | max-width:none; 223 | } 224 | input[type=submit] { 225 | background-color:#00395D; 226 | cursor:pointer; 227 | color:#fff; 228 | font-weight:bold; 229 | text-transform:uppercase; 230 | border:none; 231 | padding:9px 20px; 232 | -webkit-box-shadow:2px 2px 4px rgba(0,0,0,0.1); 233 | -moz-box-shadow:2px 2px 4px rgba(0,0,0,0.1); 234 | box-shadow:2px 2px 4px rgba(0,0,0,0.1); 235 | } 236 | input[type=submit]:hover { 237 | background-color:#002f4c; 238 | } 239 | input[type=submit]:active { 240 | position:relative; 241 | top:1px; 242 | } 243 | 244 | table { 245 | width:100%; 246 | background-color:transparent; 247 | border-collapse:collapse; 248 | border-spacing:0; 249 | margin-bottom:20px; 250 | table-layout:fixed; 251 | } 252 | th, 253 | td { 254 | padding:4px 0; 255 | line-height:20px; 256 | text-align:left; 257 | vertical-align:top; 258 | border-bottom:1px solid #d5d5d5; 259 | } 260 | th { 261 | font-weight:bold; 262 | } 263 | thead th { 264 | vertical-align:bottom; 265 | color:#57594D; 266 | } 267 | 268 | /* Read content styling */ 269 | .prose ul { 270 | list-style:disc; 271 | margin-left:40px; 272 | } 273 | .prose ol { 274 | list-style:decimal; 275 | } 276 | .prose p { 277 | margin:0 0 10px; 278 | } 279 | 280 | .icon { 281 | background:transparent url(img/sprite.png) no-repeat 0 0; 282 | display:block; 283 | width:30px; 284 | height:30px; 285 | text-indent:-999em; 286 | } 287 | 288 | /* Layout 289 | ------------------------------------------------------- */ 290 | .container { 291 | max-width:1600px; 292 | margin:0 auto; 293 | overflow:hidden; 294 | } 295 | 296 | /* Columns 297 | ------------------------------------------------------- */ 298 | .col0 { float:left; width:04.1666%; } 299 | .col1 { float:left; width:08.3333%; } 300 | .col2 { float:left; width:16.6666%; } 301 | .col3 { float:left; width:25.0000%; } 302 | .col4 { float:left; width:33.3333%; } 303 | .col5 { float:left; width:41.6666%; } 304 | .col6 { float:left; width:50.0000%; } 305 | .col7 { float:left; width:58.3333%; } 306 | .col8 { float:left; width:66.6666%; } 307 | .col9 { float:left; width:75.0000%; } 308 | .col10 { float:left; width:83.3333%; } 309 | .col11 { float:left; width:91.6666%; } 310 | .col12 { width:100%; } 311 | .margin0 { margin-left:04.1666%; } 312 | .margin1 { margin-left:08.3333%; } 313 | .margin2 { margin-left:16.6666%; } 314 | .margin3 { margin-left:25.0000%; } 315 | .margin4 { margin-left:33.3333%; } 316 | .margin5 { margin-left:41.6666%; } 317 | .margin6 { margin-left:50.0000%; } 318 | .margin7 { margin-left:58.3333%; } 319 | .margin8 { margin-left:66.6666%; } 320 | .margin9 { margin-left:75.0000%; } 321 | .margin10 { margin-left:83.3333%; } 322 | .margin11 { margin-left:91.6666%; } 323 | .margin12 { margin-left:100.0000%; } 324 | 325 | /* Padding 326 | ------------------------------------------------------- */ 327 | .pad1 { padding:10px; } 328 | .pad2 { padding:20px; } 329 | .pad21h { padding:10px 20px; } 330 | .pad2h { padding:0 20px; } 331 | .pad4 { padding:40px; } 332 | .pad4h { padding-left:40px; padding-right:40px; } 333 | .pad8 { padding:80px 40px; } 334 | .pad4c { padding:40px; } 335 | 336 | /* Additional Utility Classes 337 | ------------------------------------------------------- */ 338 | .fr { float:right; } 339 | .fl { float:left; } 340 | .show { display:block; } 341 | .hide { display:none; } 342 | .deemphasize { color:#888; } 343 | .center { text-align:center; } 344 | 345 | .tip-top:after, 346 | .tip-right:after, 347 | .tip-bottom:after, 348 | .tip-left:after { 349 | content:''; 350 | border-width:0 5px 5px; 351 | border-style:solid; 352 | position:absolute; 353 | border-color:#333 transparent; 354 | } 355 | .tip-bottom:after { 356 | border-width:5px 5px 0; 357 | } 358 | .tip-left:after { 359 | border-width:5px 5px 5px 0; 360 | border-color:transparent #333; 361 | } 362 | .tip-right:after { 363 | border-width:5px 0 5px 5px; 364 | } 365 | 366 | /* Markup free clearing 367 | Details: http://www.positioniseverything.net/easyclearing.html 368 | ------------------------------------------------------- */ 369 | .clearfix:after { 370 | content:'.'; 371 | display:block; 372 | height:0; 373 | clear:both; 374 | visibility:hidden; 375 | } 376 | 377 | .clearfix { display:inline-block; } 378 | 379 | /* Tablet Layout 380 | ------------------------------------------------------- */ 381 | @media only screen and (max-width:770px) { 382 | .hide-tablet { display:none; } 383 | .show-tablet { display:block; } 384 | } 385 | 386 | /* Mobile Layout 387 | ------------------------------------------------------- */ 388 | @media only screen and (max-width:640px) { 389 | .col1, 390 | .col2, 391 | .col3, 392 | .col4, 393 | .col5, 394 | .col6, 395 | .col7, 396 | .col8, 397 | .col9, 398 | .col10, 399 | .col11, 400 | .col12 { width:100%; max-width:100%; } 401 | .margin0, 402 | .margin1, 403 | .margin2, 404 | .margin3, 405 | .margin4, 406 | .margin5, 407 | .margin6, 408 | .margin7, 409 | .margin8, 410 | .margin9, 411 | .margin10, 412 | .margin11, 413 | .margin12 { margin-left:0; } 414 | .pad4c { padding:0; } 415 | .pad4, 416 | .pad4h { padding-left:20px; padding-right:20px; } 417 | .pad8 { padding:40px 20px; } 418 | .hide-mobile { display:none; } 419 | .show-mobile { display:block; } 420 | .icon { 421 | background-image:url(img/sprite@2x.png); 422 | background-size:240px 240px; 423 | } 424 | } 425 | 426 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | d3-metatable | MapBox 13 | 14 | 15 |
16 |
17 |

d3-metatable

18 |

A table interface for heterogeneous JSON collections.

19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {default as metatable} from "./src/metatable"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-metatable", 3 | "version": "1.0.1", 4 | "description": "a table element for heterogenous objects", 5 | "main": "build/d3-metatable.js", 6 | "jsnext:main": "index", 7 | "scripts": { 8 | "pretest": "rm -rf build && mkdir build && rollup -c", 9 | "test": "tape 'test/**/*-test.js'", 10 | "release": "standard-version", 11 | "prepare": "npm run test && uglifyjs -b beautify=false build/d3-metatable.js -c -m -o build/d3-metatable.min.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/mapbox/d3-metatable.git" 16 | }, 17 | "keywords": [ 18 | "table", 19 | "d3", 20 | "d3-module", 21 | "component", 22 | "client" 23 | ], 24 | "author": "Tom MacWright", 25 | "license": "BSD", 26 | "bugs": { 27 | "url": "https://github.com/mapbox/d3-metatable/issues" 28 | }, 29 | "dependencies": { 30 | "d3-array": "^1.2.1", 31 | "d3-collection": "^1.0.4", 32 | "d3-dispatch": "^1.0.3", 33 | "d3-selection": "^1.2.0" 34 | }, 35 | "devDependencies": { 36 | "rollup": "^0.52.0", 37 | "standard-version": "^4.2.0", 38 | "tape": "^4.8.0", 39 | "uglify-js": "^3.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const definition = require("./package.json"); 2 | const dependencies = Object.keys(definition.dependencies); 3 | 4 | export default { 5 | input: "index", 6 | external: dependencies, 7 | output: { 8 | extend: true, 9 | file: `build/${definition.name}.js`, 10 | format: "umd", 11 | globals: dependencies.reduce((p, v) => (p[v] = "d3", p), {}), 12 | name: "d3" 13 | } 14 | }; -------------------------------------------------------------------------------- /src/metatable.js: -------------------------------------------------------------------------------- 1 | import {select, event} from 'd3-selection'; 2 | import {dispatch} from 'd3-dispatch'; 3 | import {range} from 'd3-array'; 4 | import {map, set} from 'd3-collection'; 5 | 6 | export default function (options) { 7 | var dispatcher = dispatch('change', 'rowfocus', 'renameprompt', 'deleteprompt'); 8 | var _renamePrompt = true; 9 | var _deletePrompt = true; 10 | 11 | options = options || {}; 12 | 13 | var config = { 14 | newCol: options.newCol !== false, 15 | renameCol: options.renameCol !== false, 16 | deleteCol: options.deleteCol !== false 17 | }; 18 | 19 | function table(selection) { 20 | selection.each(function(d) { 21 | var sel = select(this), 22 | table; 23 | 24 | var keyset = set(); 25 | d.map(Object.keys).forEach(function(k) { 26 | k.forEach(function(_) { 27 | keyset.add(_); 28 | }); 29 | }); 30 | 31 | var keys = keyset.values(); 32 | 33 | bootstrap(); 34 | paint(); 35 | 36 | dispatcher.preventprompt = function(which) { 37 | switch(which) { 38 | case 'rename': 39 | _renamePrompt = false; 40 | break; 41 | case 'delete': 42 | _deletePrompt = false; 43 | break; 44 | } 45 | }; 46 | 47 | function bootstrap() { 48 | 49 | var controls = sel 50 | .selectAll('.controls') 51 | .data([d]) 52 | .enter() 53 | .append('div') 54 | .attr('class', 'controls'); 55 | 56 | if (config.newCol) { 57 | controls.append('a') 58 | .text('New column') 59 | .attr('href', '#') 60 | .attr('class', 'button icon plus') 61 | .on('click', function() { 62 | event.preventDefault(); 63 | var name = prompt('column name'); 64 | if (name) { 65 | keys.push(name); 66 | paint(); 67 | } 68 | }); 69 | } 70 | 71 | var enter = sel.selectAll('table').data([d]).enter().append('table'); 72 | var thead = enter.append('thead'); 73 | var tbody = enter.append('tbody'); 74 | var tr = thead.append('tr'); 75 | 76 | table = sel.select('table'); 77 | } 78 | 79 | function paint() { 80 | 81 | var th = table 82 | .select('thead') 83 | .select('tr') 84 | .selectAll('th') 85 | .data(keys, function(d) { return d; }); 86 | 87 | th.exit().remove(); 88 | 89 | var thEnter = th.enter() 90 | .append('th') 91 | .text(String); 92 | 93 | var actionLinks = thEnter 94 | .append('div') 95 | .attr('class', 'small'); 96 | 97 | if (config.deleteCol) { 98 | actionLinks 99 | .append('a') 100 | .attr('href', '#') 101 | .attr('class', 'icon trash') 102 | .text('Delete') 103 | .on('click', deleteClick); 104 | } 105 | 106 | if (config.renameCol) { 107 | actionLinks 108 | .append('a') 109 | .attr('href', '#') 110 | .attr('class', 'icon pencil') 111 | .text('Rename') 112 | .on('click', renameClick); 113 | } 114 | 115 | var tr = table.select('tbody').selectAll('tr') 116 | .data(function(d) { return d; }); 117 | 118 | tr.exit().remove(); 119 | 120 | tr = tr.enter().append('tr').merge(tr); 121 | 122 | var td = tr.selectAll('td') 123 | .data(keys, function(d) { return d; }); 124 | 125 | td.exit().remove(); 126 | 127 | td.enter() 128 | .append('td') 129 | .append('textarea') 130 | .attr('field', String); 131 | 132 | function deleteClick(d) { 133 | event.preventDefault(); 134 | var name = d; 135 | dispatcher.call('deleteprompt', dispatcher, d, completeDelete); 136 | if (_deletePrompt && confirm('Delete column ' + name + '?')) { 137 | completeDelete(d); 138 | } 139 | _deletePrompt = true; 140 | } 141 | 142 | function completeDelete(name) { 143 | keys.splice(keys.indexOf(name), 1); 144 | tr.selectAll('textarea') 145 | .data(function(d, i) { 146 | var m = map(d); 147 | m.remove(name); 148 | var reduced = mapToObject(m); 149 | dispatcher.call('change', dispatcher, reduced, i); 150 | return { 151 | data: reduced, 152 | index: i 153 | }; 154 | }); 155 | paint(); 156 | } 157 | 158 | function renameClick(d) { 159 | event.preventDefault(); 160 | var name = d; 161 | dispatcher.call('renameprompt', dispatcher, d, completeRename); 162 | 163 | var newname = (_renamePrompt) ? 164 | prompt('New name for column ' + name + '?') : 165 | undefined; 166 | 167 | if (_renamePrompt && newname) { 168 | completeRename(newname, name); 169 | } 170 | 171 | _renamePrompt = true; 172 | } 173 | 174 | function completeRename(value, name) { 175 | keys.splice(keys.indexOf(name), 1, value); 176 | tr.selectAll('textarea') 177 | .data(function(d, i) { 178 | var m = map(d); 179 | m.set(value, m.get(name)); 180 | m.remove(name); 181 | var reduced = mapToObject(m); 182 | dispatcher.call('change', dispatcher, reduced, i); 183 | return { 184 | data: reduced, 185 | index: i 186 | }; 187 | }); 188 | paint(); 189 | } 190 | 191 | function coerceNum(x) { 192 | var fl = parseFloat(x); 193 | if (fl.toString() === x) return fl; 194 | else return x; 195 | } 196 | 197 | function write(d) { 198 | d.data[select(this).attr('field')] = coerceNum(this.value); 199 | dispatcher.call('change', dispatcher, d.data, d.index); 200 | } 201 | 202 | function mapToObject(m) { 203 | return m.entries() 204 | .reduce(function(memo, d) { 205 | memo[d.key] = d.value; 206 | return memo; 207 | }, {}); 208 | } 209 | 210 | tr.selectAll('textarea') 211 | .data(function(d, i) { 212 | return range(keys.length).map(function() { 213 | return { 214 | data: d, 215 | index: i 216 | }; 217 | }); 218 | }) 219 | .classed('disabled', function(d) { 220 | return d.data[select(this).attr('field')] === undefined; 221 | }) 222 | .property('value', function(d) { 223 | var value = d.data[select(this).attr('field')]; 224 | return !isNaN(value) ? value : value || ''; 225 | }) 226 | .on('keyup', write) 227 | .on('change', write) 228 | .on('click', function(d) { 229 | if (d.data[select(this).attr('field')] === undefined) { 230 | d.data[select(this).attr('field')] = ''; 231 | paint(); 232 | } 233 | }) 234 | .on('focus', function(d) { 235 | dispatcher.call('rowfocus', dispatcher, d.data, d.index); 236 | }); 237 | } 238 | }); 239 | } 240 | 241 | table.on = function () { 242 | dispatcher.on.apply(dispatcher, arguments); 243 | return table; 244 | }; 245 | 246 | return table; 247 | } 248 | --------------------------------------------------------------------------------