├── .babelrc ├── .codeclimate.yml ├── .csslintrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── USAGE_TYPESCRIPT.md ├── docs ├── .gitignore ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ └── index.html ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── app.js │ ├── examples │ │ ├── BasicSheet.js │ │ ├── ComponentSheet.js │ │ ├── CustomRendererSheet.js │ │ ├── MathSheet.js │ │ ├── MathSheetFC.js │ │ ├── OverrideEverythingSheet.js │ │ ├── drag-drop.js │ │ ├── index.js │ │ └── override-everything.css │ ├── index.css │ ├── index.js │ └── lib └── yarn.lock ├── lib ├── Cell.js ├── CellShape.js ├── DataCell.js ├── DataEditor.js ├── DataSheet.js ├── Row.js ├── Sheet.js ├── ValueViewer.js ├── index.js ├── keys.js ├── react-datasheet.css └── renderHelpers.js ├── package-lock.json ├── package.json ├── params.json ├── src ├── Cell.js ├── CellShape.js ├── DataCell.js ├── DataEditor.js ├── DataSheet.js ├── Row.js ├── Sheet.js ├── ValueViewer.js ├── index.js ├── keys.js ├── react-datasheet.css └── renderHelpers.js ├── test └── Datasheet.js └── types └── react-datasheet.d.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"] 3 | } -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | csslint: 4 | enabled: true 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - javascript 10 | eslint: 11 | enabled: true 12 | fixme: 13 | enabled: true 14 | ratings: 15 | paths: 16 | - "src/**.css" 17 | - "src/**.js" 18 | exclude_paths: 19 | - test/ 20 | - lib/ 21 | - docs/ 22 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | node_modules 9 | .nyc_output 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | .babelrc 8 | .eslintrc 9 | npm-debug.log 10 | lib 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid", 5 | "printWidth": 80, 6 | "proseWrap": "always" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | after_success: npm run coverage 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Nadim Islam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## react-datasheet is no longer under active development. New maintainers are wanted. Please contact me if you wish to maintain this repo. 2 | 3 | --- 4 | 5 | 6 | [![Build Status](https://travis-ci.org/nadbm/react-datasheet.svg?branch=master)](https://travis-ci.org/nadbm/react-datasheet) 7 | [![Coverage Status](https://coveralls.io/repos/github/nadbm/react-datasheet/badge.svg)](https://coveralls.io/github/nadbm/react-datasheet) 8 | [![Issue Count](https://codeclimate.com/github/nadbm/react-datasheet/badges/issue_count.svg)](https://codeclimate.com/github/nadbm/react-datasheet) 9 | [![npm version](https://badge.fury.io/js/react-datasheet.svg)](https://badge.fury.io/js/react-datasheet) 10 | 11 | # React-Datasheet 12 | 13 | A simple react component to create a spreadsheet. 14 | 15 | Current features: 16 | 17 | - Select cells, cut, copy and paste cells 18 | - Navigation using keyboard keys 19 | - Deletion using keyboard keys 20 | - Callbacks for onCellsChanged, valueRenderer(visible data) 21 | - dataRenderer(underlying data in the input, takes the value by default) 22 | - Supply your own editors and view controls with custom renderers 23 | - Extensive control over generated markup via custom renderers 24 | 25 | Using Typescript? 26 | [View Usage](https://github.com/nadbm/react-datasheet/tree/master/USAGE_TYPESCRIPT.md) 27 | 28 | ## Installation 29 | 30 | Install from npm: 31 | 32 | ```bash 33 | $ npm install react-datasheet --save 34 | ``` 35 | 36 | Import in your project: 37 | 38 | ```javascript 39 | import ReactDataSheet from 'react-datasheet'; 40 | // Be sure to include styles at some point, probably during your bootstrapping 41 | import 'react-datasheet/lib/react-datasheet.css'; 42 | ``` 43 | 44 | ## Usage 45 | 46 | React-Datasheet generates a table with the cells. Double-clicking or typing 47 | edits the value and if changed, initiates an `onCellsChanged` callback. Pasting 48 | tabular data or deleting a range of cells also calls `onCellsChanged`. 49 | 50 | The data provided should be an array of rows, and each row should include the 51 | cells. 52 | 53 | ### Basic Usage 54 | 55 | ```jsx 56 | class App extends React.Component { 57 | constructor(props) { 58 | super(props); 59 | this.state = { 60 | grid: [ 61 | [{ value: 1 }, { value: 3 }], 62 | [{ value: 2 }, { value: 4 }], 63 | ], 64 | }; 65 | } 66 | render() { 67 | return ( 68 | cell.value} 71 | onCellsChanged={changes => { 72 | const grid = this.state.grid.map(row => [...row]); 73 | changes.forEach(({ cell, row, col, value }) => { 74 | grid[row][col] = { ...grid[row][col], value }; 75 | }); 76 | this.setState({ grid }); 77 | }} 78 | /> 79 | ); 80 | } 81 | } 82 | ``` 83 | 84 | ### Cells with underlying data 85 | 86 | There are two values that each cell shows. The first is via `valueRenderer` and 87 | the second is via `dataRenderer`. When a cell is in _edit mode_, it will show 88 | the value returned from `dataRenderer`. It needs to return a string as this 89 | value is set in an input field. Each of these callbacks are passed the cell 90 | value as well as the cell's coordinates in the spreadsheet. This allows you to 91 | apply formatting logic at rendering time, such as _all cells in the third column 92 | should be formatted as dates_. 93 | 94 | ```jsx 95 | const grid = [ 96 | [{value: 5, expr: '1 + 4'}, {value: 6, expr: '6'}, {value: new Date('2008-04-10')}], 97 | [{value: 5, expr: '1 + 4'}, {value: 5, expr: '1 + 4'}, {value: new Date('2004-05-28')}] 98 | ] 99 | const onCellsChanged = (changes) => changes.forEach(({cell, row, col, value}) => console.log("New expression :" + value)) 100 | j == 2 ? cell.value.toDateString() : cell.value} 103 | dataRenderer={(cell, i, j) => j == 2 ? cell.value.toISOString() : cell.expr} 104 | onCellsChanged={onCellsChanged} 105 | /> 106 | ``` 107 | 108 | ### Cells with underlying component 109 | 110 | ```jsx 111 | const grid = [ 112 | [{ 113 | value: 5, 114 | component: ( 115 | 118 | ) 119 | }] 120 | ] 121 | cell.value} 124 | /> 125 | ``` 126 | 127 | This renders a single cell with the value 5. Once in edit mode, the button will 128 | appear. 129 | 130 | ### Cells with extra attributes 131 | 132 | ```jsx 133 | const grid = [ 134 | [{value: 1, hint: 'Valid'}, {value: 3, hint: 'Not valid'}], 135 | [{value: 2}, {value: 4}] 136 | ] 137 | cell.value} 140 | attributesRenderer={(cell) => (cell.hint ? { 'data-hint': cell.hint } : {})} 141 | ... 142 | /> 143 | ``` 144 | 145 | This render 2 rows, each one with two cells, the cells in the first row will 146 | have an attribute data-hint and the other 2 will not. 147 | 148 | ### Custom renderers 149 | 150 | React-Datasheet allows you replace the renderers both for the overall structure 151 | (rows, cells, the sheet itself) as well as editors and viewers for individual 152 | cells. This allows you to radically refashion the sheet to suit your 153 | requirements. 154 | 155 | For example, this shows how to add separate headers and a checkbox at the start 156 | of each row to control row "selected" state. It also specifies a custom view 157 | renderer and a custom editor for the first column of each row: 158 | 159 | ```jsx 160 | const columns = getColumnsFromSomewhere() 161 | const isSelected = yourSelectionFunction 162 | const selectHandler = yourCallbackFunction 163 | 164 | cell.value} 167 | sheetRenderer={props => ( 168 | 169 | 170 | 171 | ))} 173 | 174 | 175 | 176 | {props.children} 177 | 178 |
172 | {columns.map(col => ({col.name}
179 | )} 180 | rowRenderer={props => ( 181 | 182 | 183 | 188 | 189 | {props.children} 190 | 191 | )} 192 | valueViewer={MyViewComponent} 193 | dataEditor={props => ( 194 | props.col === 0 ? : 195 | )} 196 | ... 197 | /> 198 | ``` 199 | 200 | _Note:_ For brevity, in this example the custom renderers are all defined as 201 | arrow functions inside of render, but using a 202 | [bound function](https://reactjs.org/docs/faq-functions.html) in the parent 203 | component or a separate custom component will let you avoid a lot of needless 204 | re-renders. 205 | 206 | ## Options 207 | 208 | | Option | Type | Description | 209 | | :-------------- | :----------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 210 | | data | Array | Array of rows and each row should contain the cell objects to display | 211 | | valueRenderer | func | Method to render the value of the cell `function(cell, i, j)`. This is visible by default | 212 | | dataRenderer | func | Method to render the underlying value of the cell `function(cell, i, j)`. This data is visible once in edit mode. | 213 | | overflow | 'wrap'\|'nowrap'\|'clip' | Grid default for how to render overflow text in cells | 214 | | onCellsChanged | func | onCellsChanged handler: `function(arrayOfChanges[, arrayOfAdditions]) {}`, where changes is an **array** of objects of the shape `{cell, row, col, value}`. See below for more details. | 215 | | onContextMenu | func | Context menu handler : `function(event, cell, i, j)` | 216 | | parsePaste | func | `function (string) {}` If set, the function will be called with the raw clipboard data. It should return an array of arrays of strings. This is useful for when the clipboard may have data with irregular field or line delimiters. If not set, rows will be split with line breaks and cells with tabs. | 217 | | isCellNavigable | func | `function (cell, row, col) {return true}` If set, the function is used to determine whether navigation to the indicated cell should be allowed or not. If not then using cursor or tab navigation will skip over not allowed cells until it finds the next allowed cell. | 218 | | handleCopy | func | `function ({ event, dataRenderer, valueRenderer, data, start, end, range })` If set, this function is called whenever the user copies cells. The return string of this function is stored on the clipboard. | 219 | 220 | ### Advanced options 221 | 222 | The following are optional functions or React Component that can completely 223 | override the native renderers of react datasheet. To know which props are passed 224 | down, see 225 | [custom renderers](https://github.com/nadbm/react-datasheet#custom-renderers-1) 226 | 227 | | Option | Type | Description | 228 | | :------------ | :----: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 229 | | sheetRenderer | func | Optional function or React Component to render the main sheet element. The default renders a `table` element. | 230 | | rowRenderer | func | Optional function or React Component to render each row element. The default renders a `tr` element. | 231 | | cellRenderer | func | Optional function or React Component to render each cell element. The default renders a `td` element. | 232 | | valueViewer | func | Optional function or React Component to customize the way the value for each cell in the sheet is displayed. Affects every cell in the sheet. See [cell options](https://github.com/nadbm/react-datasheet#cell-options) to override individual cells. | 233 | | dataEditor | func | Optional function or React Component to render a custom editor. Affects every cell in the sheet. See [cell options](https://github.com/nadbm/react-datasheet#cell-options) to override individual cells. | 234 | | selected | object | Optional. Whether the selection is controlled or uncontrolled. Must be an object of this format: `{ start: { i: number, j; number }, end: { i: number, j: number } }`, or `null` for no selection. | 235 | | onSelect | func | Optional. `function ({ start, end }) {}` Triggered on every selection change. `start` and `end` have the same format as the `selected` prop. | 236 | 237 | ## `onCellsChanged(arrayOfChanges[, arrayOfAdditions])` handler 238 | 239 | React-DataSheet will call this callback whenever data in the grid changes: 240 | 241 | - When the user enters a new value in a cell 242 | - When the user hits the delete key with one or more selected cells 243 | - When the user pastes tabular data into the table 244 | 245 | The argument to the callback usually will be one **array** of objects with these 246 | properties: 247 | 248 | | Property | Type | Description | 249 | | :------- | :----: | :----------------------------------------------------------------------------------------------- | 250 | | cell | object | the original cell object you provided in the `data` property. This may be `null` (see below) | 251 | | row | number | row index of changed cell | 252 | | col | number | column index of changed cell | 253 | | value | any | The new cell value. This is usually a string, but a custom editor may provide any type of value. | 254 | 255 | If the change is the result of a user edit, the array will contain a single 256 | change object. If the user pastes data or deletes a range of cells, the array 257 | will contain an element for each affected cell. 258 | 259 | **Additions:** If the user pastes data that extends beyond the bounds of the 260 | grid (for example, pasting two-row-high data on the last line), there will be a 261 | second argument to the handler containing an array of objects that represent the 262 | out-of-bounds data. These object will have the same properties, except: 263 | 264 | - There is no `cell` property 265 | - either `row` or `col`, or both, will be outside the bounds of your original 266 | grid. They will correspond to the indices the new data would occupy if you 267 | expanded your grid to hold them. 268 | 269 | You can choose to ignore the additions, or you can expand your model to 270 | accommodate the new data. 271 | 272 | ### Deprecated handlers 273 | 274 | Previously React-DataSheet supported two change handlers. These are still 275 | supported for backwards compatibility, but will be removed at some point in the 276 | future. 277 | 278 | | Option | Type | Description | 279 | | :------- | :--: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 280 | | onChange | func | onChange handler: `function(cell, i, j, newValue) {}` | 281 | | onPaste | func | onPaste handler: `function(array) {}` If set, the function will be called with an array of rows. Each row has an array of objects containing the cell and raw pasted value. If the pasted value cannot be matched with a cell, the cell value will be undefined. | 282 | 283 | ## Cell Options 284 | 285 | The cell object is what gets passed back to the onChange callback. They can 286 | contain the following options as well 287 | 288 | | Option | Type | Default | Description | 289 | | :------------- | :------------------------ | :-------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 290 | | readOnly | Bool | false | Cell will never go in edit mode | 291 | | key | String | undefined | By default, each cell is given the key of col number and row number. This would override that key | 292 | | className | String | undefined | Additional class names for cells. | 293 | | component | ReactElement | undefined | Insert a react element or JSX to this field. This will render on edit mode | 294 | | forceComponent | bool | false | Renders what's in component at all times, even when not in edit mode | 295 | | disableEvents | bool | false | Makes cell unselectable and read only | 296 | | colSpan | number | 1 | The colSpan of the cell's td element | 297 | | rowSpan | number | 1 | The rowSpan of the cell's td element | 298 | | width | number or String | undefined | Sets the cell's td width using a style attribute. Number is interpreted as pixels, strings are used as-is. Note: This will only work if the table does not have a set width. | 299 | | overflow | 'wrap'\|'nowrap'\| 'clip' | undefined | How to render overflow text. Overrides grid-level `overflow` option. | 300 | | valueViewer | func | undefined | Optional function or React Component to customize the way the value for this cell is displayed. Overrides grid-level `valueViewer` option. | 301 | | dataEditor | func | undefined | Optional function or React Component to render a custom editor. Overrides grid-level `dataEditor` option. | 302 | 303 | ## Custom Renderers 304 | 305 | Each of the following custom renderers should be either a React Component or a 306 | function that takes a `props` argument and returns a react element (a.k.a 307 | stateless functional component). React-DataSheet will supply certain properties 308 | to each renderer. 309 | 310 | In some cases React-DataSheet will include event handlers as properties to your 311 | custom renderer. You must hook up these handlers to your component or aspects of 312 | React-DataSheet's built-in behavior will cease to work. 313 | 314 | Except for `valueViewer` and `dataEditor`, each custom renderer will receive 315 | react's regular `props.children`. Be sure to render `{props.children}` in your 316 | custom renderer. 317 | 318 | ### Sheet Renderer 319 | 320 | The `sheetRenderer` is responsible for laying out the sheet's main parent 321 | component. By default, React-DataSheet uses a `table` element. React-DataSheet 322 | will supply these properties: 323 | 324 | | Option | Type | Description | 325 | | :-------- | :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | 326 | | data | Array | The same `data` array as from main `ReactDataSheet` component | 327 | | className | String | Classes to apply to your top-level element. You can add to these, but your should not overwrite or omit them unless you want to implement your own CSS also. | 328 | | children | Array or component | The regular react `props.children`. You must render `{props.children}` within your custom renderer or you won't see your rows and cells. | 329 | 330 | ### Row Renderer 331 | 332 | The `rowRenderer` lays out each row in the sheet. By default, React-DataSheet 333 | uses a `tr` element. React-DataSheet will supply these properties: 334 | 335 | | Option | Type | Description | 336 | | :------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------ | 337 | | row | number | The current row index | 338 | | selected | Bool | `true` in case the current row is selected | 339 | | cells | Array | The cells in the current row | 340 | | children | Array or component | The regular react `props.children`. You must render `{props.children}` within your custom renderer or you won't see your cells. | 341 | 342 | ### Cell Renderer 343 | 344 | The `cellRenderer` creates the container for each cell in the sheet. The default 345 | renders a `td` element. React-DataSheet will supply these properties: 346 | 347 | | Option | Type | Description | 348 | | :----------------- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | 349 | | row | number | The current row index | 350 | | col | number | The current column index | 351 | | cell | Object | The cell's raw data structure | 352 | | className | String | Classes to apply to your cell element. You can add to these, but your should not overwrite or omit them unless you want to implement your own CSS also. | 353 | | style | Object | Generated styles that you should apply to your cell element. This may be null or undefined. | 354 | | selected | Bool | Is the cell currently selected | 355 | | editing | Bool | Is the cell currently being edited | 356 | | updated | Bool | Was the cell recently updated | 357 | | attributesRenderer | func | As for the main `ReactDataSheet` component | 358 | | onMouseDown | func | Event handler important for cell selection behavior | 359 | | onMouseOver | func | Event handler important for cell selection behavior | 360 | | onDoubleClick | func | Event handler important for editing | 361 | | onContextMenu | func | Event handler to launch default content-menu handling. You can safely ignore this handler if you want to provide your own content menu handling. | 362 | | children | Array or component | The regular react `props.children`. You must render `{props.children}` within your custom renderer or you won't your cell's data. | 363 | 364 | ### Value Viewer 365 | 366 | The `valueViewer` displays your cell's data with a custom component when in view 367 | mode. For example, you might show a "three star rating" component instead the 368 | number 3. You can specify a `valueViewer` for the entire sheet and/or for an 369 | individual cell. 370 | 371 | React-DataSheet will supply these properties: 372 | 373 | | Option | Type | Description | 374 | | :----- | :----- | :----------------------------------------- | 375 | | value | node | The result of the `valueRenderer` function | 376 | | row | number | The current row index | 377 | | col | number | The current column index | 378 | | cell | Object | The cell's raw data structure | 379 | 380 | ### Data Editor 381 | 382 | The `dataEditor` displays your cell's data when in edit mode. You can can use 383 | any component you want, as long as you hook up the event handlers that 384 | constitute the contract between React-DataSheet and your editor. You can specify 385 | a `dataEditor` for the entire sheet and/or for an individual cell. 386 | 387 | | Option | Type | Description | 388 | | :-------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 389 | | value | String or node | The result of the `dataRenderer` (or `valueRenderer` if none) | 390 | | row | number | The current row index | 391 | | col | number | The current column index | 392 | | cell | Object | The cell's raw data structure | 393 | | onChange | func | `function (string) {}` callback for when the user changes the value during editing (for example, each time they type a character into an `input`). `onChange` does not indicate the _final_ edited value. It works just like a [controlled component](https://reactjs.org/docs/forms.html#controlled-components) in a form. | 394 | | onKeyDown | func | `function (event) {}` An event handler that you can call to use default React-DataSheet keyboard handling to signal reverting an ongoing edit (Escape key) or completing an edit (Enter or Tab). For most editors based on an `input` element this will probably work. However, if this keyboard handling is unsuitable for your editor you can trigger these changes explicitly using the `onCommit` and `onRevert` callbacks. | 395 | | onCommit | func | `function (newValue, [event]) {}` A callback to indicate that editing is over, here is the final value. If you pass a `KeyboardEvent` as the second argument, React-DataSheet will perform default navigation for you (for example, going down to the next row if you hit the enter key). You actually don't need to use `onCommit` if the default keyboard handling is good enough for you. | 396 | | onRevert | func | `function () {}` A no-args callback that you can use to indicate that you want to cancel ongoing edits. As with `onCommit`, you don't need to worry about this if the default keyboard handling works for your editor. | 397 | -------------------------------------------------------------------------------- /USAGE_TYPESCRIPT.md: -------------------------------------------------------------------------------- 1 | ### Usage with TypeScript 2 | 3 | The library comes with built-in type definitions, so there is no need to download anything separately from `@types`. Most of the defined types accept two generic parameters. The first (which is required) allows you to define the shape of the data in your `cell` objects. The second one allows you to define the type of the `value` property that is used by custom `dataEditor` components and `onCellsChanged` callbacks (this is not required, and it defaults to `string`) Basic usage looks like this: 4 | 5 | 6 | ```tsx 7 | import * as React from 'react'; 8 | import ReactDataSheet from 'react-datasheet'; 9 | import "react-datasheet/lib/react-datasheet.css"; 10 | 11 | export interface GridElement extends ReactDataSheet.Cell { 12 | value: number | null; 13 | } 14 | 15 | class MyReactDataSheet extends ReactDataSheet { } 16 | 17 | interface AppState { 18 | grid: GridElement[][]; 19 | } 20 | 21 | //You can also strongly type all the Components or SFCs that you pass into ReactDataSheet. 22 | let cellRenderer: ReactDataSheet.CellRenderer = (props) => { 23 | const backgroundStyle = props.cell.value && props.cell.value < 0 ? {color: 'red'} : undefined; 24 | return ( 25 | 26 | {props.children} 27 | 28 | ) 29 | } 30 | 31 | export class App extends React.Component<{}, AppState> { 32 | constructor (props: {}) { 33 | super(props) 34 | this.state = { 35 | grid: [ 36 | [{value: 1}, {value: -3}], 37 | [{value: -2}, {value: 4}] 38 | ] 39 | } 40 | } 41 | render () { 42 | return ( 43 | cell.value} 46 | onCellsChanged={changes => { 47 | const grid = this.state.grid.map(row => [...row]) 48 | changes.forEach(({cell, row, col, value}) => { 49 | grid[row][col] = {...grid[row][col], value} 50 | }) 51 | this.setState({grid}) 52 | }} 53 | cellRenderer={cellRenderer} 54 | /> 55 | ) 56 | } 57 | } 58 | ``` -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /docs/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/react-datasheet/static/css/main.afdff407.chunk.css", 4 | "main.js": "/react-datasheet/static/js/main.10f2c6ee.chunk.js", 5 | "main.js.map": "/react-datasheet/static/js/main.10f2c6ee.chunk.js.map", 6 | "runtime~main.js": "/react-datasheet/static/js/runtime~main.e087b50f.js", 7 | "runtime~main.js.map": "/react-datasheet/static/js/runtime~main.e087b50f.js.map", 8 | "static/css/2.f916d21b.chunk.css": "/react-datasheet/static/css/2.f916d21b.chunk.css", 9 | "static/js/2.ed658b4d.chunk.js": "/react-datasheet/static/js/2.ed658b4d.chunk.js", 10 | "static/js/2.ed658b4d.chunk.js.map": "/react-datasheet/static/js/2.ed658b4d.chunk.js.map", 11 | "index.html": "/react-datasheet/index.html", 12 | "precache-manifest.3af6e8757c15cf56685ad91c52be8c77.js": "/react-datasheet/precache-manifest.3af6e8757c15cf56685ad91c52be8c77.js", 13 | "service-worker.js": "/react-datasheet/service-worker.js", 14 | "static/css/2.f916d21b.chunk.css.map": "/react-datasheet/static/css/2.f916d21b.chunk.css.map", 15 | "static/css/main.afdff407.chunk.css.map": "/react-datasheet/static/css/main.afdff407.chunk.css.map" 16 | } 17 | } -------------------------------------------------------------------------------- /docs/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveNui/DataSheet-React/d8acd6fa53534203a9c7f88deb518f50c74d5f7a/docs/build/favicon.ico -------------------------------------------------------------------------------- /docs/build/index.html: -------------------------------------------------------------------------------- 1 | React Datasheet Component
-------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-datasheet-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "http://nadbm.github.io/react-datasheet", 6 | "devDependencies": { 7 | "react-scripts": "^3.1.1" 8 | }, 9 | "dependencies": { 10 | "gh-pages": "^2.1.1", 11 | "mathjs": "^6.2.1", 12 | "react": "^15.4.2", 13 | "react-dnd": "^2.5.4", 14 | "react-dnd-html5-backend": "^2.5.4", 15 | "react-dom": "^15.4.2", 16 | "react-select": "^1.0.0-rc.3" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject", 23 | "predeploy": "npm run build", 24 | "deploy": "gh-pages -d build" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveNui/DataSheet-React/d8acd6fa53534203a9c7f88deb518f50c74d5f7a/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | React Datasheet Component 20 | 21 | 22 | 23 |
24 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import 'react-select/dist/react-select.css' 3 | import './lib/react-datasheet.css' 4 | import {BasicSheet, MathSheet, ComponentSheet, CustomRendererSheet, OverrideEverythingSheet} from './examples/index'; 5 | 6 | export default class App extends React.Component { 7 | render () { 8 | return ( 9 |
10 |
11 |

React datasheet

12 |

Simple and highly customizable excel-like spreadsheet

13 |
npm install react-datasheet --save
14 | View on GitHub 15 |
16 |
17 |
18 | 25 | Star 26 | 27 | 28 |
29 |

Basic datasheet

30 | 31 | This small component allows you to integrate an excel-like datasheet. By default, 32 | the spreadsheet handles keyboard navigation and copy pasting of cells. 33 | 34 |
35 | 36 |
37 |
38 |

Formula datasheet

39 | 40 | This example computes expression underneath using mathjs. 41 | On a invalid expression the cell changes color to show the error. 42 | Note that react-datasheet does not handle the validation nor the formula computation 43 | 44 |
45 | 46 |
47 |
48 |

Sheet with components

49 |
50 | 51 |
52 |
53 |

Sheet with custom renderers

54 | 55 | Custom renderers allow you to add significant new capabilities 56 | to your sheets without requiring changes to react-datagrid itself. 57 | This example allows you to reorder both the columns and the rows 58 | using drag and drop. This is implemented by using custom components 59 | to render the main table (including a custom header) and each row. 60 | The drag handler for the rows is the gray cell at the beginning of each row. 61 | 62 | 63 | The "Rating" column also shows how to specify custom cell editing and viewing components. 64 | 65 |
66 | 67 |
68 |
69 |

Sheet with custom structure

70 | 71 | Ever wish you could customize how data is displayed, 72 | or easily add custom attributes to your cells, 73 | or add new behaviors that React-DataSheet 74 | doesn't currently support? 75 | This example demonstrates the great flexibility that custom renderers provide. 76 | You can completely change the sheet's structure: 77 |
    78 |
  • Table - similar to the default rendering
  • 79 |
  • List - renders the data grid using an html unordered list
  • 80 |
  • Div - renders using divs
  • 81 |
82 | Although a bit contrived, it shows that you can deeply customize your sheet's markup while 83 | still retaining data sheet behavior. This example also adds controls for selecting rows. Note that the 84 | model and controls for row selection are separate from the grid itself. 85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 |
93 | Check out the GitHub project at react-datasheet 94 |
95 |
96 |
97 | ) 98 | } 99 | } -------------------------------------------------------------------------------- /docs/src/examples/BasicSheet.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import Datasheet from '../lib/DataSheet'; 4 | 5 | export default class BasicSheet extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | grid: [ 10 | [ 11 | { readOnly: true, value: '' }, 12 | { value: 'A', readOnly: true }, 13 | { value: 'B', readOnly: true }, 14 | { value: 'C', readOnly: true }, 15 | { value: 'D', readOnly: true }, 16 | ], 17 | [ 18 | { readOnly: true, value: 1 }, 19 | { value: 1 }, 20 | { value: 3 }, 21 | { value: 3 }, 22 | { value: 3 }, 23 | ], 24 | [ 25 | { readOnly: true, value: 2 }, 26 | { value: 2 }, 27 | { value: 4 }, 28 | { value: 4 }, 29 | { value: 4 }, 30 | ], 31 | [ 32 | { readOnly: true, value: 3 }, 33 | { value: 1 }, 34 | { value: 3 }, 35 | { value: 3 }, 36 | { value: 3 }, 37 | ], 38 | [ 39 | { readOnly: true, value: 4 }, 40 | { value: 2 }, 41 | { value: 4 }, 42 | { value: 4 }, 43 | { value: 4 }, 44 | ], 45 | ], 46 | }; 47 | } 48 | valueRenderer = cell => cell.value; 49 | onCellsChanged = changes => { 50 | const grid = this.state.grid; 51 | changes.forEach(({ cell, row, col, value }) => { 52 | grid[row][col] = { ...grid[row][col], value }; 53 | }); 54 | this.setState({ grid }); 55 | }; 56 | onContextMenu = (e, cell, i, j) => 57 | cell.readOnly ? e.preventDefault() : null; 58 | 59 | render() { 60 | return ( 61 | 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/src/examples/ComponentSheet.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Select from 'react-select' 3 | import _ from 'lodash' 4 | import Datasheet from '../lib/DataSheet' 5 | 6 | export default class ComponentSheet extends React.Component { 7 | constructor (props) { 8 | super(props) 9 | this.options = [ 10 | { label: 'Bread', value: 2.35 }, 11 | { label: 'Berries', value: 3.05 }, 12 | { label: 'Milk', value: 3.99 }, 13 | { label: 'Apples', value: 4.35 }, 14 | { label: 'Chicken', value: 9.95 }, 15 | { label: 'Yoghurt', value: 4.65 }, 16 | { label: 'Onions', value: 3.45 }, 17 | { label: 'Salad', value: 1.55 } 18 | ] 19 | this.state = { 20 | grocery: {}, 21 | items: 3 22 | } 23 | } 24 | 25 | generateGrid () { 26 | const groceryValue = (id) => { 27 | if (this.state.grocery[id]) { 28 | const {label, value} = this.state.grocery[id] 29 | return `${label} (${value})` 30 | } else { 31 | return '' 32 | } 33 | } 34 | const component = (id) => { 35 | return ( 36 | 102 | ) 103 | } 104 | } 105 | 106 | class RangeEditor extends PureComponent { 107 | constructor (props) { 108 | super(props) 109 | this.handleChange = this.handleChange.bind(this) 110 | } 111 | 112 | componentDidMount () { 113 | this._input.focus() 114 | } 115 | 116 | handleChange (e) { 117 | this.props.onChange(e.target.value) 118 | } 119 | 120 | render () { 121 | const {value, onKeyDown} = this.props 122 | return ( 123 | { this._input = input }} 125 | type='range' 126 | className='data-editor' 127 | value={value} 128 | min='1' 129 | max='5' 130 | onChange={this.handleChange} 131 | onKeyDown={onKeyDown} 132 | /> 133 | ) 134 | } 135 | } 136 | 137 | const FillViewer = props => { 138 | const { value } = props 139 | return ( 140 |
141 | {[1, 2, 3, 4, 5].map(v => { 142 | const backgroundColor = v > value ? 'transparent' : '#007eff' 143 | return ( 144 |
145 | ) 146 | })} 147 |
148 | ) 149 | } 150 | 151 | class CustomRendererSheet extends PureComponent { 152 | constructor (props) { 153 | super(props) 154 | this.state = { 155 | columns: [ 156 | { label: 'Style', width: '40%' }, 157 | { label: 'IBUs', width: '20%' }, 158 | { label: 'Color (SRM)', width: '20%' }, 159 | { label: 'Rating', width: '20%' } 160 | ], 161 | grid: [ 162 | [{ value: 'Ordinary Bitter'}, { value: '20 - 35'}, { value: '5 - 12'}, { value: 4, dataEditor: RangeEditor }], 163 | [{ value: 'Special Bitter'}, { value: '28 - 40'}, { value: '6 - 14'}, { value: 4, dataEditor: RangeEditor }], 164 | [{ value: 'ESB'}, { value: '30 - 45'}, { value: '6 - 14'}, { value: 5, dataEditor: RangeEditor, valueViewer: FillViewer }], 165 | [{ value: 'Scottish Light'}, { value: '9 - 20'}, { value: '6 - 15'}, { value: 3, dataEditor: SelectEditor, valueViewer: FillViewer }], 166 | [{ value: 'Scottish Heavy'}, { value: '12 - 20'}, { value: '8 - 30'}, { value: 4, dataEditor: SelectEditor }], 167 | [{ value: 'Scottish Export'}, { value: '15 - 25'}, { value: '9 - 19'}, { value: 4, dataEditor: SelectEditor }], 168 | [{ value: 'English Summer Ale'}, { value: '20 - 30'}, { value: '3 - 7'}, { value: 3, dataEditor: SelectEditor }], 169 | [{ value: 'English Pale Ale'}, { value: '20 - 40'}, { value: '5 - 12'}, { value: 4, dataEditor: SelectEditor }], 170 | [{ value: 'English IPA'}, { value: '35 - 63'}, { value: '6 - 14'}, { value: 4, dataEditor: SelectEditor }], 171 | [{ value: 'Strong Ale'}, { value: '30 - 65'}, { value: '8 - 21'}, { value: 4, dataEditor: SelectEditor }], 172 | [{ value: 'Old Ale'}, { value: '30 -65'}, { value: '12 - 30'}, { value: 4, dataEditor: SelectEditor }], 173 | [{ value: 'Pale Mild Ale'}, { value: '10 - 20'}, { value: '6 - 9'}, { value: 3, dataEditor: SelectEditor }], 174 | [{ value: 'Dark Mild Ale'}, { value: '10 - 24'}, { value: '17 - 34'}, { value: 3, dataEditor: SelectEditor }], 175 | [{ value: 'Brown Ale'}, { value: '12 - 25'}, { value: '12 - 17'}, { value: 3, dataEditor: SelectEditor }] 176 | ].map((a, i) => a.map((cell, j) => Object.assign(cell, {key: `${i}-${j}`}))) 177 | } 178 | 179 | this.handleColumnDrop = this.handleColumnDrop.bind(this) 180 | this.handleRowDrop = this.handleRowDrop.bind(this) 181 | this.handleChanges = this.handleChanges.bind(this) 182 | this.renderSheet = this.renderSheet.bind(this) 183 | this.renderRow = this.renderRow.bind(this) 184 | } 185 | 186 | handleColumnDrop (from, to) { 187 | const columns = [...this.state.columns] 188 | columns.splice(to, 0, ...columns.splice(from, 1)) 189 | const grid = this.state.grid.map(r => { 190 | const row = [...r] 191 | row.splice(to, 0, ...row.splice(from, 1)) 192 | return row 193 | }) 194 | this.setState({ columns, grid }) 195 | } 196 | 197 | handleRowDrop (from, to) { 198 | const grid = [ ...this.state.grid ] 199 | grid.splice(to, 0, ...grid.splice(from, 1)) 200 | this.setState({ grid }) 201 | } 202 | 203 | handleChanges (changes) { 204 | const grid = this.state.grid.map(row => [...row]) 205 | changes.forEach(({cell, row, col, value}) => { 206 | if (grid[row] && grid[row][col]) { 207 | grid[row][col] = {...grid[row][col], value} 208 | } 209 | }) 210 | this.setState({grid}) 211 | } 212 | 213 | renderSheet (props) { 214 | return 215 | } 216 | 217 | renderRow (props) { 218 | const {row, cells, ...rest} = props 219 | return 220 | } 221 | 222 | render () { 223 | return ( 224 | 225 | cell.value} 228 | sheetRenderer={this.renderSheet} 229 | rowRenderer={this.renderRow} 230 | onCellsChanged={this.handleChanges} 231 | /> 232 | 233 | ) 234 | } 235 | } 236 | 237 | export default CustomRendererSheet 238 | -------------------------------------------------------------------------------- /docs/src/examples/MathSheet.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import * as mathjs from 'mathjs'; 4 | import Datasheet from '../lib/DataSheet' 5 | 6 | export default class MathSheet extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | this.onCellsChanged = this.onCellsChanged.bind(this); 10 | this.state = { 11 | 'A1': {key: 'A1', value: '200', expr: '200'}, 12 | 'A2': {key: 'A2', value: '200', expr: '=A1', className:'equation'}, 13 | 'A3': {key: 'A3', value: '', expr: ''}, 14 | 'A4': {key: 'A4', value: '', expr: ''}, 15 | 'B1': {key: 'B1', value: '', expr: ''}, 16 | 'B2': {key: 'B2', value: '', expr: ''}, 17 | 'B3': {key: 'B3', value: '', expr: ''}, 18 | 'B4': {key: 'B4', value: '', expr: ''}, 19 | 'C1': {key: 'C1', value: '', expr: ''}, 20 | 'C2': {key: 'C2', value: '', expr: ''}, 21 | 'C3': {key: 'C3', value: '', expr: ''}, 22 | 'C4': {key: 'C4', value: '', expr: ''}, 23 | 'D1': {key: 'D1', value: '', expr: ''}, 24 | 'D2': {key: 'D2', value: '', expr: ''}, 25 | 'D3': {key: 'D3', value: '', expr: ''}, 26 | 'D4': {key: 'D4', value: '', expr: ''} 27 | } 28 | } 29 | 30 | 31 | generateGrid() { 32 | return [0, 1,2,3,4].map((row, i) => 33 | ['', 'A', 'B', 'C', 'D'].map((col, j) => { 34 | if(i == 0 && j == 0) { 35 | return {readOnly: true, value: ''} 36 | } 37 | if(row === 0) { 38 | return {readOnly: true, value: col} 39 | } 40 | if(j === 0) { 41 | return {readOnly: true, value: row} 42 | } 43 | return this.state[col + row] 44 | }) 45 | ) 46 | } 47 | 48 | validateExp(trailKeys, expr) { 49 | let valid = true; 50 | const matches = expr.match(/[A-Z][1-9]+/g) || []; 51 | matches.map(match => { 52 | if(trailKeys.indexOf(match) > -1) { 53 | valid = false 54 | } else { 55 | valid = this.validateExp([...trailKeys, match], this.state[match].expr) 56 | } 57 | }) 58 | return valid 59 | } 60 | 61 | computeExpr(key, expr, scope) { 62 | let value = null; 63 | if(expr.charAt(0) !== '=') { 64 | return {className: '', value: expr, expr: expr}; 65 | } else { 66 | try { 67 | value = mathjs.evaluate(expr.substring(1), scope) 68 | } catch(e) { 69 | value = null 70 | } 71 | 72 | if(value !== null && this.validateExp([key], expr)) { 73 | return {className: 'equation', value, expr} 74 | } else { 75 | return {className: 'error', value: 'error', expr: ''} 76 | } 77 | } 78 | } 79 | 80 | cellUpdate(state, changeCell, expr) { 81 | const scope = _.mapValues(state, (val) => isNaN(val.value) ? 0 : parseFloat(val.value)) 82 | const updatedCell = _.assign({}, changeCell, this.computeExpr(changeCell.key, expr, scope)) 83 | state[changeCell.key] = updatedCell 84 | 85 | _.each(state, (cell, key) => { 86 | if(cell.expr.charAt(0) === '=' && cell.expr.indexOf(changeCell.key) > -1 && key !== changeCell.key) { 87 | state = this.cellUpdate(state, cell, cell.expr) 88 | } 89 | }) 90 | return state 91 | } 92 | 93 | onCellsChanged(changes) { 94 | const state = _.assign({}, this.state) 95 | changes.forEach(({cell, value}) => { 96 | this.cellUpdate(state, cell, value) 97 | }) 98 | this.setState(state) 99 | } 100 | 101 | render() { 102 | 103 | return ( 104 | cell.value} 107 | dataRenderer={(cell) => cell.expr} 108 | onCellsChanged={this.onCellsChanged} 109 | /> 110 | ) 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /docs/src/examples/MathSheetFC.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import * as mathjs from "mathjs"; 3 | import Datasheet from "react-datasheet"; 4 | 5 | const fetchCells = { 6 | "00": { key: "0", value: "name", readOnly: true, expr: "" }, 7 | "01": { key: "1", value: "one", readOnly: true, expr: "" }, 8 | "02": { key: "2", value: "two", readOnly: true, expr: "" }, 9 | "03": { key: "3", value: "three", readOnly: true, expr: "" }, 10 | "04": { key: "4", value: "four", readOnly: true, expr: "" }, 11 | A0: { key: "A0", value: "January", readOnly: true, expr: "" }, 12 | A1: { key: "A1", value: "200", expr: "" }, 13 | A2: { 14 | key: "A2", 15 | value: "200", 16 | expr: "=A1", 17 | readOnly: true 18 | }, 19 | A3: { key: "A3", value: "", expr: "" }, 20 | A4: { key: "A4", value: "", expr: "" }, 21 | B0: { key: "B0", value: "February", readOnly: true, expr: "" }, 22 | B1: { key: "B1", value: "", expr: "" }, 23 | B2: { key: "B2", value: "", expr: "" }, 24 | B3: { key: "B3", value: "", expr: "" }, 25 | B4: { key: "B4", value: "", expr: "" }, 26 | C0: { key: "C0", value: "March", readOnly: true, expr: "" }, 27 | C1: { key: "C1", value: "", expr: "" }, 28 | C2: { key: "C2", value: "", expr: "" }, 29 | C3: { key: "C3", value: "", expr: "" }, 30 | C4: { key: "C4", value: "", expr: "" }, 31 | D0: { key: "D0", value: "April", readOnly: true, expr: "" }, 32 | D1: { key: "D1", value: "", expr: "" }, 33 | D2: { key: "D2", value: "", expr: "" }, 34 | D3: { key: "D3", value: "", expr: "" }, 35 | D4: { key: "D4", value: "", expr: "" } 36 | }; 37 | 38 | export default () => { 39 | const [cells, setCells] = useState(fetchCells); 40 | 41 | const getCols = cells => [ 42 | ...new Set(Object.keys(cells).map(cell => cell.charAt(0))) 43 | ]; 44 | 45 | const getRows = cells => 46 | Object.entries(cells) 47 | .filter(([key], idx) => +key.match(/.(\d+)/)[1] === idx) 48 | .map(([_, filtredCell]) => filtredCell); 49 | 50 | const generateGrid = () => 51 | getRows(cells).map((row, i) => 52 | getCols(cells).map((col, j) => { 53 | if (i === 0 && j === 0) { 54 | return { readOnly: true, value: row.value }; 55 | } 56 | if (j === 0) { 57 | return { readOnly: true, value: row.value }; 58 | } 59 | 60 | return cells[col + row.key]; 61 | }) 62 | ); 63 | 64 | const validateExp = (trailKeys, expr) => { 65 | let valid = true; 66 | const matches = expr.match(/[A-Z][1-9]+/g) || []; 67 | matches.map(match => { 68 | if (trailKeys.indexOf(match) > -1) { 69 | valid = false; 70 | } else { 71 | valid = validateExp([...trailKeys, match], cells[match].expr); 72 | } 73 | return undefined; 74 | }); 75 | return valid; 76 | }; 77 | 78 | const computeExpr = (key, expr, scope) => { 79 | let value = null; 80 | if (expr.charAt(0) !== "=") { 81 | return { className: "", value: expr, expr: expr }; 82 | } else { 83 | try { 84 | value = mathjs.evaluate(expr.substring(1), scope); 85 | } catch (e) { 86 | value = null; 87 | } 88 | 89 | if (value !== null && validateExp([key], expr)) { 90 | return { className: "equation", value, expr }; 91 | } else { 92 | return { className: "error", value: "error", expr: "" }; 93 | } 94 | } 95 | }; 96 | 97 | const cellUpdate = (copyCells, changeCell, expr) => { 98 | const scope = Object.fromEntries( 99 | Object.entries(copyCells).map(([key, { value }]) => [ 100 | key, 101 | isNaN(value) ? 0 : parseFloat(value) 102 | ]) 103 | ); 104 | 105 | const updatedCell = Object.assign( 106 | {}, 107 | changeCell, 108 | computeExpr(changeCell.key, expr, scope) 109 | ); 110 | 111 | copyCells[changeCell.key] = updatedCell; 112 | 113 | Object.values(copyCells).forEach(cell => { 114 | if ( 115 | cell.expr.charAt(0) === "=" && 116 | cell.expr.indexOf(changeCell.key) > -1 && 117 | cell.key !== changeCell.key 118 | ) { 119 | copyCells = cellUpdate(copyCells, cell, cell.expr); 120 | } 121 | }); 122 | 123 | return copyCells; 124 | }; 125 | 126 | const onCellsChanged = changes => { 127 | const copyCells = { ...cells }; 128 | 129 | changes.forEach(({ cell, value }) => { 130 | cellUpdate(copyCells, cell, value); 131 | }); 132 | 133 | setCells(copyCells); 134 | }; 135 | 136 | return ( 137 | cell.value} 140 | dataRenderer={cell => cell.expr} 141 | onCellsChanged={onCellsChanged} 142 | /> 143 | ); 144 | }; 145 | -------------------------------------------------------------------------------- /docs/src/examples/OverrideEverythingSheet.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import DataSheet from '../lib' 3 | 4 | import './override-everything.css' 5 | 6 | const SheetRenderer = props => { 7 | const {as: Tag, headerAs: Header, bodyAs: Body, rowAs: Row, cellAs: Cell, 8 | className, columns, selections, onSelectAllChanged} = props 9 | return ( 10 | 11 |
12 | 13 | 14 | s)} 17 | onChange={e => onSelectAllChanged(e.target.checked)} 18 | /> 19 | 20 | {columns.map(column => {column.label})} 21 | 22 |
23 | 24 | {props.children} 25 | 26 |
27 | ) 28 | } 29 | 30 | const RowRenderer = props => { 31 | const {as: Tag, cellAs: Cell, className, row, selected, onSelectChanged} = props 32 | return ( 33 | 34 | 35 | onSelectChanged(row, e.target.checked)} 39 | /> 40 | 41 | {props.children} 42 | 43 | ) 44 | } 45 | 46 | const CellRenderer = props => { 47 | const { 48 | as: Tag, cell, row, col, columns, attributesRenderer, 49 | selected, editing, updated, style, 50 | ...rest 51 | } = props 52 | 53 | // hey, how about some custom attributes on our cell? 54 | const attributes = cell.attributes || {} 55 | // ignore default style handed to us by the component and roll our own 56 | attributes.style = { width: columns[col].width } 57 | if (col === 0) { 58 | attributes.title = cell.label 59 | } 60 | 61 | return ( 62 | 63 | {props.children} 64 | 65 | ) 66 | } 67 | 68 | export default class OverrideEverythingSheet extends PureComponent { 69 | constructor (props) { 70 | super(props) 71 | this.handleSelect = this.handleSelect.bind(this) 72 | this.handleSelectAllChanged = this.handleSelectAllChanged.bind(this) 73 | this.handleSelectChanged = this.handleSelectChanged.bind(this) 74 | this.handleCellsChanged = this.handleCellsChanged.bind(this) 75 | 76 | this.sheetRenderer = this.sheetRenderer.bind(this) 77 | this.rowRenderer = this.rowRenderer.bind(this) 78 | this.cellRenderer = this.cellRenderer.bind(this) 79 | 80 | this.state = { 81 | as: 'table', 82 | columns: [ 83 | { label: 'Style', width: '30%' }, 84 | { label: 'IBUs', width: '20%' }, 85 | { label: 'Color (SRM)', width: '20%' }, 86 | { label: 'Rating', width: '20%' } 87 | ], 88 | grid: [ 89 | [{ value: 'Ordinary Bitter' }, { value: '20 - 35' }, { value: '5 - 12' }, { value: 4, attributes: {'data-foo': 'bar' } }], 90 | [{ value: 'Special Bitter' }, { value: '28 - 40' }, { value: '6 - 14' }, { value: 4 }], 91 | [{ value: 'ESB' }, { value: '30 - 45' }, { value: '6 - 14' }, { value: 5 }], 92 | [{ value: 'Scottish Light' }, { value: '9 - 20' }, { value: '6 - 15' }, { value: 3 }], 93 | [{ value: 'Scottish Heavy' }, { value: '12 - 20' }, { value: '8 - 30' }, { value: 4 }], 94 | [{ value: 'Scottish Export' }, { value: '15 - 25' }, { value: '9 - 19' }, { value: 4 }], 95 | [{ value: 'English Summer Ale' }, { value: '20 - 30' }, { value: '3 - 7' }, { value: 3 }], 96 | [{ value: 'English Pale Ale' }, { value: '20 - 40' }, { value: '5 - 12' }, { value: 4 }], 97 | [{ value: 'English IPA' }, { value: '35 - 63' }, { value: '6 - 14' }, { value: 4 }], 98 | [{ value: 'Strong Ale' }, { value: '30 - 65' }, { value: '8 - 21' }, { value: 4 }], 99 | [{ value: 'Old Ale' }, { value: '30 -65' }, { value: '12 - 30' }, { value: 4 }], 100 | [{ value: 'Pale Mild Ale' }, { value: '10 - 20' }, { value: '6 - 9' }, { value: 3 }], 101 | [{ value: 'Dark Mild Ale' }, { value: '10 - 24' }, { value: '17 - 34' }, { value: 3 }], 102 | [{ value: 'Brown Ale' }, { value: '12 - 25' }, { value: '12 - 17' }, { value: 3 }] 103 | ], 104 | selections: [false, false, false, false, false, false, false, false, false, false, false, false, false, false] 105 | } 106 | } 107 | 108 | handleSelect (e) { 109 | this.setState({as: e.target.value}) 110 | } 111 | 112 | handleSelectAllChanged (selected) { 113 | const selections = this.state.selections.map(s => selected) 114 | this.setState({selections}) 115 | } 116 | 117 | handleSelectChanged (index, selected) { 118 | const selections = [...this.state.selections] 119 | selections[index] = selected 120 | this.setState({selections}) 121 | } 122 | 123 | handleCellsChanged (changes, additions) { 124 | const grid = this.state.grid.map(row => [...row]) 125 | changes.forEach(({cell, row, col, value}) => { 126 | grid[row][col] = {...grid[row][col], value} 127 | }) 128 | // paste extended beyond end, so add a new row 129 | additions && additions.forEach(({cell, row, col, value}) => { 130 | if (!grid[row]) { 131 | grid[row] = [{value: ''}, {value: ''}, {value: ''}, {value: 0}] 132 | } 133 | if (grid[row][col]) { 134 | grid[row][col] = {...grid[row][col], value} 135 | } 136 | }) 137 | this.setState({grid}) 138 | } 139 | 140 | sheetRenderer (props) { 141 | const {columns, selections} = this.state 142 | switch (this.state.as) { 143 | case 'list': 144 | return 145 | case 'div': 146 | return 147 | default: 148 | return 149 | } 150 | } 151 | 152 | rowRenderer (props) { 153 | const {selections} = this.state 154 | switch (this.state.as) { 155 | case 'list': 156 | return 157 | case 'div': 158 | return 159 | default: 160 | return 161 | } 162 | } 163 | 164 | cellRenderer (props) { 165 | switch (this.state.as) { 166 | case 'list': 167 | return 168 | case 'div': 169 | return 170 | default: 171 | return 172 | } 173 | } 174 | 175 | render () { 176 | return ( 177 |
178 |
179 | 187 |
188 | 189 | cell.value} 199 | /> 200 |
201 | ) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /docs/src/examples/drag-drop.js: -------------------------------------------------------------------------------- 1 | import { DragSource, DropTarget } from 'react-dnd' 2 | 3 | /** 4 | * Specifies which props to inject into your component. 5 | */ 6 | function rowSourceCollect (connect, _monitor) { 7 | return { 8 | // Call this function inside render() 9 | // to let React DnD handle the drag events: 10 | connectDragSource: connect.dragSource(), 11 | connectDragPreview: connect.dragPreview() 12 | } 13 | } 14 | 15 | /** 16 | * Specifies the drag source contract. 17 | * Only `beginDrag` function is required. 18 | */ 19 | const rowSourceSpec = { 20 | beginDrag (props) { 21 | console.log('beginDrag', props.rowIndex, props) 22 | return { 23 | rowIndex: props.rowIndex 24 | } 25 | } 26 | } 27 | 28 | function rowTargetCollect (connect, monitor) { 29 | return { 30 | connectDropTarget: connect.dropTarget(), 31 | isOver: monitor.isOver() && monitor.canDrop() 32 | } 33 | } 34 | 35 | const rowTargetSpec = { 36 | canDrop (props, monitor) { 37 | const item = monitor.getItem() 38 | return props.rowIndex !== item.rowIndex 39 | }, 40 | 41 | drop (props, monitor, component) { 42 | if (monitor.didDrop()) { 43 | return 44 | } 45 | // Obtain the dragged item 46 | const { rowIndex: fromIndex } = monitor.getItem() 47 | const { rowIndex: toIndex, onRowDrop } = props 48 | onRowDrop(fromIndex, toIndex) 49 | } 50 | } 51 | 52 | function colSourceCollect (connect, _monitor) { 53 | return { 54 | // Call this function inside render() 55 | // to let React DnD handle the drag events: 56 | connectDragSource: connect.dragSource() 57 | } 58 | } 59 | 60 | const colSourceSpec = { 61 | beginDrag (props) { 62 | return { 63 | columnIndex: props.columnIndex 64 | } 65 | } 66 | } 67 | 68 | function colTargetCollect (connect, monitor) { 69 | return { 70 | connectDropTarget: connect.dropTarget(), 71 | isOver: monitor.isOver() && monitor.canDrop() 72 | } 73 | } 74 | 75 | const colTargetSpec = { 76 | canDrop (props, monitor) { 77 | const item = monitor.getItem() 78 | // return item.row !== props.row 79 | return props.columnIndex !== item.columnIndex 80 | }, 81 | 82 | drop (props, monitor, component) { 83 | if (monitor.didDrop()) { 84 | return 85 | } 86 | 87 | // Obtain the dragged item 88 | const { columnIndex: fromIndex } = monitor.getItem() 89 | const { columnIndex: toIndex, onColumnDrop } = props 90 | onColumnDrop(fromIndex, toIndex) 91 | } 92 | } 93 | 94 | export const colDragSource = DragSource('col', colSourceSpec, colSourceCollect) 95 | export const colDropTarget = DropTarget('col', colTargetSpec, colTargetCollect) 96 | export const rowDragSource = DragSource('row', rowSourceSpec, rowSourceCollect) 97 | export const rowDropTarget = DropTarget('row', rowTargetSpec, rowTargetCollect) 98 | -------------------------------------------------------------------------------- /docs/src/examples/index.js: -------------------------------------------------------------------------------- 1 | import BasicSheet from './BasicSheet' 2 | import ComponentSheet from './ComponentSheet' 3 | import MathSheet from './MathSheet' 4 | import CustomRendererSheet from './CustomRendererSheet' 5 | import OverrideEverythingSheet from './OverrideEverythingSheet' 6 | 7 | export {BasicSheet, MathSheet, ComponentSheet, CustomRendererSheet, OverrideEverythingSheet} 8 | -------------------------------------------------------------------------------- /docs/src/examples/override-everything.css: -------------------------------------------------------------------------------- 1 | .custom-sheet { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, *:before, *:after { 6 | box-sizing: inherit; 7 | } 8 | 9 | .data-grid .data-header > div { 10 | width: 100%; 11 | } 12 | 13 | .data-grid .data-header > div:after, .data-grid .data-body .data-row:after { 14 | content: ""; 15 | display: table; 16 | clear: both; 17 | } 18 | 19 | .data-grid .data-header > div > div { 20 | float: left; 21 | font-weight: bold; 22 | font-size: 12px; 23 | padding: 2px; 24 | } 25 | 26 | .data-grid .data-header > div > div.action-cell, 27 | .data-grid .data-header > tr > .action-cell, 28 | .data-grid .data-body .data-row > div.action-cell { 29 | width: 10%; 30 | } 31 | 32 | .data-grid .data-body .data-row > div { 33 | float: left; 34 | font-size: 12px; 35 | } 36 | 37 | .data-grid .data-body .data-row > .cell > input.data-editor { 38 | width: 100%; 39 | height: 100%; 40 | } 41 | 42 | .data-grid ul.data-body { 43 | list-style: none; 44 | margin-left: 0; 45 | padding-left: 0; 46 | margin-top: 0; 47 | margin-bottom: 0; 48 | } 49 | -------------------------------------------------------------------------------- /docs/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: 'Open Sans', sans-serif; 5 | } 6 | 7 | .equation.cell { 8 | position: relative; 9 | } 10 | .error.cell { 11 | background: rgba(255,0,0,0.14); 12 | font-size: 0.8em; 13 | color: red; 14 | } 15 | .error.cell > div.text { 16 | text-align: center; 17 | } 18 | .equation.cell:before { 19 | content: ''; 20 | width: 0; 21 | height: 0; 22 | position: absolute; 23 | left: 0; 24 | top: 0; 25 | border-style: solid; 26 | border-width: 6px 6px 0 0; 27 | border-color: #2185d0 transparent transparent transparent; 28 | z-index: 2; 29 | } 30 | 31 | .row-handle.cell { 32 | width: 1rem; 33 | } 34 | 35 | tbody .row-handle.cell, thead .cell:not(.row-handle) { 36 | cursor: move; 37 | } 38 | 39 | .data-grid-container table.data-grid tr { 40 | background: white; 41 | } 42 | .data-grid-container table.data-grid .drop-target, .data-grid-container table.data-grid thead .cell.read-only.drop-target { 43 | background: #6F86FC; 44 | transition: none; 45 | color: white; 46 | } 47 | .data-grid-container table.data-grid thead .cell.read-only { 48 | transition: none; 49 | } 50 | 51 | 52 | .App { 53 | text-align: center; 54 | } 55 | 56 | .App-logo { 57 | animation: App-logo-spin infinite 20s linear; 58 | height: 80px; 59 | } 60 | 61 | .App-header { 62 | background-color: #222; 63 | height: 150px; 64 | padding: 20px; 65 | color: white; 66 | } 67 | 68 | @keyframes App-logo-spin { 69 | from { transform: rotate(0deg); } 70 | to { transform: rotate(360deg); } 71 | } 72 | 73 | .container { 74 | max-width: 600px; 75 | margin: auto; 76 | } 77 | table.data-grid { 78 | margin: auto; 79 | width: 100%; 80 | } 81 | .Select-control, .Select-input, .Select-placeholder,.Select-clear, .Select-placeholder, .Select--single > .Select-control .Select-value { 82 | height: 15px; 83 | line-height: 12px; 84 | font-size: 12px; 85 | text-align: left; 86 | border-radius: 0; 87 | border: 0; 88 | } 89 | 90 | .Select-control input{ 91 | height: 16px; 92 | font-size: 12px; 93 | padding: 0; 94 | } 95 | .sheet-container { 96 | display: block; 97 | padding: 5px; 98 | 99 | box-shadow: 0px 0px 6px #CCC; 100 | margin: auto; 101 | width: 500px; 102 | margin-top: 20px; 103 | transition: box-shadow 0.5s ease-in; 104 | } 105 | .sheet-container:hover { 106 | transition: box-shadow 0.5s ease-in; 107 | 108 | box-shadow: 0px 0px 1px #CCC; 109 | } 110 | .sheet-container table.data-grid tr td.cell:not(.selected){ 111 | border: 1px solid #ececec; 112 | } 113 | .sheet-container table.data-grid tr td.cell, .sheet-container table.data-grid tr th.cell { 114 | font-size: 12px; 115 | } 116 | div.divider { 117 | margin: 40px 0px; 118 | height: 1px; 119 | width: 100%; 120 | background-color: #EEE; 121 | } 122 | pre { 123 | display: inline-block; 124 | background: #333; 125 | padding: 10px 30px; 126 | border-left: 2px solid white; 127 | } 128 | .header { 129 | text-align: center; 130 | padding: 50px 0px; 131 | background: #e63946; 132 | color: #EEE; 133 | margin-bottom: 50px; 134 | } 135 | .footer-container a { 136 | color: white; 137 | } 138 | .footer-container { 139 | margin-top: 50px; 140 | text-align: center; 141 | padding: 50px 0px; 142 | background: #e63946; 143 | color: #EEE; 144 | } 145 | .add-grocery { 146 | text-align: left; 147 | padding: 5px 10px; 148 | color: #888; 149 | } 150 | .add-button { 151 | float: right; 152 | border-radius: 0; 153 | background: #CCC; 154 | border-radius: 2px; 155 | padding: 2px 20px; 156 | background: #e63946; 157 | color: white; 158 | cursor: pointer; 159 | font-size: 9px; 160 | } 161 | .add-button:hover { 162 | background: #f17d86; 163 | } 164 | .github-link { 165 | display: block; 166 | width: 200px; 167 | font-size: 12px; 168 | text-decoration: none; 169 | margin: auto; 170 | color: white; 171 | } 172 | .github-link:hover { 173 | color: #DDD; 174 | } 175 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | import './index.css'; 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /docs/src/lib: -------------------------------------------------------------------------------- 1 | ../../lib/ -------------------------------------------------------------------------------- /lib/Cell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _propTypes = require('prop-types'); 16 | 17 | var _propTypes2 = _interopRequireDefault(_propTypes); 18 | 19 | var _CellShape = require('./CellShape'); 20 | 21 | var _CellShape2 = _interopRequireDefault(_CellShape); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 26 | 27 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 28 | 29 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 30 | 31 | var Cell = function (_PureComponent) { 32 | _inherits(Cell, _PureComponent); 33 | 34 | function Cell() { 35 | _classCallCheck(this, Cell); 36 | 37 | return _possibleConstructorReturn(this, (Cell.__proto__ || Object.getPrototypeOf(Cell)).apply(this, arguments)); 38 | } 39 | 40 | _createClass(Cell, [{ 41 | key: 'render', 42 | value: function render() { 43 | var _props = this.props, 44 | cell = _props.cell, 45 | row = _props.row, 46 | col = _props.col, 47 | attributesRenderer = _props.attributesRenderer, 48 | className = _props.className, 49 | style = _props.style, 50 | onMouseDown = _props.onMouseDown, 51 | onMouseOver = _props.onMouseOver, 52 | onDoubleClick = _props.onDoubleClick, 53 | onContextMenu = _props.onContextMenu; 54 | var colSpan = cell.colSpan, 55 | rowSpan = cell.rowSpan; 56 | 57 | var attributes = attributesRenderer ? attributesRenderer(cell, row, col) : {}; 58 | 59 | return _react2.default.createElement( 60 | 'td', 61 | _extends({ 62 | className: className, 63 | onMouseDown: onMouseDown, 64 | onMouseOver: onMouseOver, 65 | onDoubleClick: onDoubleClick, 66 | onTouchEnd: onDoubleClick, 67 | onContextMenu: onContextMenu, 68 | colSpan: colSpan, 69 | rowSpan: rowSpan, 70 | style: style 71 | }, attributes), 72 | this.props.children 73 | ); 74 | } 75 | }]); 76 | 77 | return Cell; 78 | }(_react.PureComponent); 79 | 80 | exports.default = Cell; 81 | 82 | 83 | Cell.propTypes = { 84 | row: _propTypes2.default.number.isRequired, 85 | col: _propTypes2.default.number.isRequired, 86 | cell: _propTypes2.default.shape(_CellShape2.default).isRequired, 87 | selected: _propTypes2.default.bool, 88 | editing: _propTypes2.default.bool, 89 | updated: _propTypes2.default.bool, 90 | attributesRenderer: _propTypes2.default.func, 91 | onMouseDown: _propTypes2.default.func.isRequired, 92 | onMouseOver: _propTypes2.default.func.isRequired, 93 | onDoubleClick: _propTypes2.default.func.isRequired, 94 | onContextMenu: _propTypes2.default.func.isRequired, 95 | className: _propTypes2.default.string, 96 | style: _propTypes2.default.object 97 | }; 98 | 99 | Cell.defaultProps = { 100 | selected: false, 101 | editing: false, 102 | updated: false, 103 | attributesRenderer: function attributesRenderer() {} 104 | }; -------------------------------------------------------------------------------- /lib/CellShape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _propTypes = require('prop-types'); 8 | 9 | var _propTypes2 = _interopRequireDefault(_propTypes); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | /* 14 | readOnly Bool false Cell will never go in edit mode 15 | key String undefined By default, each cell is given the key of col number and row number. This would override that key 16 | className String undefined Additional class names for cells. 17 | component ReactElement undefined Insert a react element or JSX to this field. This will render on edit mode 18 | forceComponent bool false Renders what's in component at all times, even when not in edit mode 19 | disableEvents bool false Makes cell unselectable and read only 20 | colSpan number 1 The colSpan of the cell's td element 21 | rowSpan number 1 The rowSpan of the cell's td element 22 | width number or String undefined Sets the cell's td width using a style attribute. Number is interpreted as pixels, strings are used as-is. Note: This will only work if the table does not have a set width. 23 | overflow 'wrap'|'nowrap'| 'clip' undefined How to render overflow text. Overrides grid-level overflow option. 24 | editor func undefined A component used to render the cell's value when being edited 25 | viewer func undefined A component used to render the cell's value when not being edited 26 | */ 27 | var CellShape = { 28 | readOnly: _propTypes2.default.bool, 29 | key: _propTypes2.default.string, 30 | className: _propTypes2.default.string, 31 | component: _propTypes2.default.oneOfType([_propTypes2.default.element, _propTypes2.default.func]), 32 | forceComponent: _propTypes2.default.bool, 33 | disableEvents: _propTypes2.default.bool, 34 | disableUpdatedFlag: _propTypes2.default.bool, 35 | colSpan: _propTypes2.default.number, 36 | rowSpan: _propTypes2.default.number, 37 | width: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.string]), 38 | overflow: _propTypes2.default.oneOf(['wrap', 'nowrap', 'clip']), 39 | dataEditor: _propTypes2.default.func, 40 | valueViewer: _propTypes2.default.func 41 | }; 42 | 43 | exports.default = CellShape; -------------------------------------------------------------------------------- /lib/DataCell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true, 5 | }); 6 | 7 | var _createClass = (function () { 8 | function defineProperties(target, props) { 9 | for (var i = 0; i < props.length; i++) { 10 | var descriptor = props[i]; 11 | descriptor.enumerable = descriptor.enumerable || false; 12 | descriptor.configurable = true; 13 | if ('value' in descriptor) descriptor.writable = true; 14 | Object.defineProperty(target, descriptor.key, descriptor); 15 | } 16 | } 17 | return function (Constructor, protoProps, staticProps) { 18 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 19 | if (staticProps) defineProperties(Constructor, staticProps); 20 | return Constructor; 21 | }; 22 | })(); 23 | 24 | var _react = require('react'); 25 | 26 | var _react2 = _interopRequireDefault(_react); 27 | 28 | var _propTypes = require('prop-types'); 29 | 30 | var _propTypes2 = _interopRequireDefault(_propTypes); 31 | 32 | var _keys = require('./keys'); 33 | 34 | var _Cell = require('./Cell'); 35 | 36 | var _Cell2 = _interopRequireDefault(_Cell); 37 | 38 | var _CellShape = require('./CellShape'); 39 | 40 | var _CellShape2 = _interopRequireDefault(_CellShape); 41 | 42 | var _DataEditor = require('./DataEditor'); 43 | 44 | var _DataEditor2 = _interopRequireDefault(_DataEditor); 45 | 46 | var _ValueViewer = require('./ValueViewer'); 47 | 48 | var _ValueViewer2 = _interopRequireDefault(_ValueViewer); 49 | 50 | var _renderHelpers = require('./renderHelpers'); 51 | 52 | function _interopRequireDefault(obj) { 53 | return obj && obj.__esModule ? obj : { default: obj }; 54 | } 55 | 56 | function _classCallCheck(instance, Constructor) { 57 | if (!(instance instanceof Constructor)) { 58 | throw new TypeError('Cannot call a class as a function'); 59 | } 60 | } 61 | 62 | function _possibleConstructorReturn(self, call) { 63 | if (!self) { 64 | throw new ReferenceError( 65 | "this hasn't been initialised - super() hasn't been called", 66 | ); 67 | } 68 | return call && (typeof call === 'object' || typeof call === 'function') 69 | ? call 70 | : self; 71 | } 72 | 73 | function _inherits(subClass, superClass) { 74 | if (typeof superClass !== 'function' && superClass !== null) { 75 | throw new TypeError( 76 | 'Super expression must either be null or a function, not ' + 77 | typeof superClass, 78 | ); 79 | } 80 | subClass.prototype = Object.create(superClass && superClass.prototype, { 81 | constructor: { 82 | value: subClass, 83 | enumerable: false, 84 | writable: true, 85 | configurable: true, 86 | }, 87 | }); 88 | if (superClass) 89 | Object.setPrototypeOf 90 | ? Object.setPrototypeOf(subClass, superClass) 91 | : (subClass.__proto__ = superClass); 92 | } 93 | 94 | function initialData(_ref) { 95 | var cell = _ref.cell, 96 | row = _ref.row, 97 | col = _ref.col, 98 | valueRenderer = _ref.valueRenderer, 99 | dataRenderer = _ref.dataRenderer; 100 | 101 | return (0, _renderHelpers.renderData)( 102 | cell, 103 | row, 104 | col, 105 | valueRenderer, 106 | dataRenderer, 107 | ); 108 | } 109 | 110 | function initialValue(_ref2) { 111 | var cell = _ref2.cell, 112 | row = _ref2.row, 113 | col = _ref2.col, 114 | valueRenderer = _ref2.valueRenderer; 115 | 116 | return (0, _renderHelpers.renderValue)(cell, row, col, valueRenderer); 117 | } 118 | 119 | function widthStyle(cell) { 120 | var width = typeof cell.width === 'number' ? cell.width + 'px' : cell.width; 121 | return width ? { width: width } : null; 122 | } 123 | 124 | var DataCell = (function (_PureComponent) { 125 | _inherits(DataCell, _PureComponent); 126 | 127 | function DataCell(props) { 128 | _classCallCheck(this, DataCell); 129 | 130 | var _this = _possibleConstructorReturn( 131 | this, 132 | (DataCell.__proto__ || Object.getPrototypeOf(DataCell)).call(this, props), 133 | ); 134 | 135 | _this.handleChange = _this.handleChange.bind(_this); 136 | _this.handleCommit = _this.handleCommit.bind(_this); 137 | _this.handleRevert = _this.handleRevert.bind(_this); 138 | 139 | _this.handleKey = _this.handleKey.bind(_this); 140 | _this.handleMouseDown = _this.handleMouseDown.bind(_this); 141 | _this.handleMouseOver = _this.handleMouseOver.bind(_this); 142 | _this.handleContextMenu = _this.handleContextMenu.bind(_this); 143 | _this.handleDoubleClick = _this.handleDoubleClick.bind(_this); 144 | 145 | _this.state = { 146 | updated: false, 147 | reverting: false, 148 | committing: false, 149 | value: '', 150 | }; 151 | return _this; 152 | } 153 | 154 | _createClass(DataCell, [ 155 | { 156 | key: 'componentDidUpdate', 157 | value: function componentDidUpdate(prevProps) { 158 | var _this2 = this; 159 | 160 | if ( 161 | !this.props.cell.disableUpdatedFlag && 162 | initialValue(prevProps) !== initialValue(this.props) 163 | ) { 164 | this.setState({ updated: true }); 165 | this.timeout = setTimeout(function () { 166 | return _this2.setState({ updated: false }); 167 | }, 700); 168 | } 169 | if (this.props.editing === true && prevProps.editing === false) { 170 | var value = this.props.clearing ? '' : initialData(this.props); 171 | this.setState({ value: value, reverting: false }); 172 | } 173 | 174 | if ( 175 | prevProps.editing === true && 176 | this.props.editing === false && 177 | !this.state.reverting && 178 | !this.state.committing && 179 | this.state.value !== initialData(this.props) 180 | ) { 181 | this.props.onChange(this.props.row, this.props.col, this.state.value); 182 | } 183 | }, 184 | }, 185 | { 186 | key: 'componentWillUnmount', 187 | value: function componentWillUnmount() { 188 | clearTimeout(this.timeout); 189 | }, 190 | }, 191 | { 192 | key: 'handleChange', 193 | value: function handleChange(value) { 194 | this.setState({ value: value, committing: false }); 195 | }, 196 | }, 197 | { 198 | key: 'handleCommit', 199 | value: function handleCommit(value, e) { 200 | var _props = this.props, 201 | onChange = _props.onChange, 202 | onNavigate = _props.onNavigate; 203 | 204 | if (value !== initialData(this.props)) { 205 | this.setState({ value: value, committing: true }); 206 | onChange(this.props.row, this.props.col, value); 207 | } else { 208 | this.handleRevert(); 209 | } 210 | if (e) { 211 | e.preventDefault(); 212 | onNavigate(e, true); 213 | } 214 | }, 215 | }, 216 | { 217 | key: 'handleRevert', 218 | value: function handleRevert() { 219 | this.setState({ reverting: true }); 220 | this.props.onRevert(); 221 | }, 222 | }, 223 | { 224 | key: 'handleMouseDown', 225 | value: function handleMouseDown(e) { 226 | var _props2 = this.props, 227 | row = _props2.row, 228 | col = _props2.col, 229 | onMouseDown = _props2.onMouseDown, 230 | cell = _props2.cell; 231 | 232 | if (!cell.disableEvents) { 233 | onMouseDown(row, col, e); 234 | } 235 | }, 236 | }, 237 | { 238 | key: 'handleMouseOver', 239 | value: function handleMouseOver(e) { 240 | var _props3 = this.props, 241 | row = _props3.row, 242 | col = _props3.col, 243 | onMouseOver = _props3.onMouseOver, 244 | cell = _props3.cell; 245 | 246 | if (!cell.disableEvents) { 247 | onMouseOver(row, col); 248 | } 249 | }, 250 | }, 251 | { 252 | key: 'handleDoubleClick', 253 | value: function handleDoubleClick(e) { 254 | var _props4 = this.props, 255 | row = _props4.row, 256 | col = _props4.col, 257 | onDoubleClick = _props4.onDoubleClick, 258 | cell = _props4.cell; 259 | 260 | if (!cell.disableEvents) { 261 | onDoubleClick(row, col); 262 | } 263 | }, 264 | }, 265 | { 266 | key: 'handleContextMenu', 267 | value: function handleContextMenu(e) { 268 | var _props5 = this.props, 269 | row = _props5.row, 270 | col = _props5.col, 271 | onContextMenu = _props5.onContextMenu, 272 | cell = _props5.cell; 273 | 274 | if (!cell.disableEvents) { 275 | onContextMenu(e, row, col); 276 | } 277 | }, 278 | }, 279 | { 280 | key: 'handleKey', 281 | value: function handleKey(e) { 282 | var keyCode = e.which || e.keyCode; 283 | if (keyCode === _keys.ESCAPE_KEY) { 284 | return this.handleRevert(); 285 | } 286 | var _props6 = this.props, 287 | component = _props6.cell.component, 288 | forceEdit = _props6.forceEdit; 289 | 290 | var eatKeys = forceEdit || !!component; 291 | var commit = 292 | keyCode === _keys.ENTER_KEY || 293 | keyCode === _keys.TAB_KEY || 294 | (!eatKeys && 295 | [ 296 | _keys.LEFT_KEY, 297 | _keys.RIGHT_KEY, 298 | _keys.UP_KEY, 299 | _keys.DOWN_KEY, 300 | ].includes(keyCode)); 301 | 302 | if (commit) { 303 | this.handleCommit(this.state.value, e); 304 | } 305 | }, 306 | }, 307 | { 308 | key: 'renderComponent', 309 | value: function renderComponent(editing, cell) { 310 | var component = cell.component, 311 | readOnly = cell.readOnly, 312 | forceComponent = cell.forceComponent; 313 | 314 | if ((editing && !readOnly) || forceComponent) { 315 | return component; 316 | } 317 | }, 318 | }, 319 | { 320 | key: 'renderEditor', 321 | value: function renderEditor(editing, cell, row, col, dataEditor) { 322 | if (editing) { 323 | var Editor = cell.dataEditor || dataEditor || _DataEditor2.default; 324 | return _react2.default.createElement(Editor, { 325 | cell: cell, 326 | row: row, 327 | col: col, 328 | value: this.state.value, 329 | onChange: this.handleChange, 330 | onCommit: this.handleCommit, 331 | onRevert: this.handleRevert, 332 | onKeyDown: this.handleKey, 333 | }); 334 | } 335 | }, 336 | }, 337 | { 338 | key: 'renderViewer', 339 | value: function renderViewer(cell, row, col, valueRenderer, valueViewer) { 340 | var Viewer = cell.valueViewer || valueViewer || _ValueViewer2.default; 341 | var value = (0, _renderHelpers.renderValue)( 342 | cell, 343 | row, 344 | col, 345 | valueRenderer, 346 | ); 347 | return _react2.default.createElement(Viewer, { 348 | cell: cell, 349 | row: row, 350 | col: col, 351 | value: value, 352 | }); 353 | }, 354 | }, 355 | { 356 | key: 'render', 357 | value: function render() { 358 | var _props7 = this.props, 359 | row = _props7.row, 360 | col = _props7.col, 361 | cell = _props7.cell, 362 | CellRenderer = _props7.cellRenderer, 363 | valueRenderer = _props7.valueRenderer, 364 | dataEditor = _props7.dataEditor, 365 | valueViewer = _props7.valueViewer, 366 | attributesRenderer = _props7.attributesRenderer, 367 | selected = _props7.selected, 368 | editing = _props7.editing, 369 | onKeyUp = _props7.onKeyUp; 370 | var updated = this.state.updated; 371 | 372 | var content = 373 | this.renderComponent(editing, cell) || 374 | this.renderEditor(editing, cell, row, col, dataEditor) || 375 | this.renderViewer(cell, row, col, valueRenderer, valueViewer); 376 | 377 | var className = [ 378 | cell.className, 379 | 'cell', 380 | cell.overflow, 381 | selected && 'selected', 382 | editing && 'editing', 383 | cell.readOnly && 'read-only', 384 | updated && 'updated', 385 | ] 386 | .filter(function (a) { 387 | return a; 388 | }) 389 | .join(' '); 390 | 391 | return _react2.default.createElement( 392 | CellRenderer, 393 | { 394 | row: row, 395 | col: col, 396 | cell: cell, 397 | selected: selected, 398 | editing: editing, 399 | updated: updated, 400 | attributesRenderer: attributesRenderer, 401 | className: className, 402 | style: widthStyle(cell), 403 | onMouseDown: this.handleMouseDown, 404 | onMouseOver: this.handleMouseOver, 405 | onDoubleClick: this.handleDoubleClick, 406 | onContextMenu: this.handleContextMenu, 407 | onKeyUp: onKeyUp, 408 | }, 409 | content, 410 | ); 411 | }, 412 | }, 413 | ]); 414 | 415 | return DataCell; 416 | })(_react.PureComponent); 417 | 418 | exports.default = DataCell; 419 | 420 | DataCell.propTypes = { 421 | row: _propTypes2.default.number.isRequired, 422 | col: _propTypes2.default.number.isRequired, 423 | cell: _propTypes2.default.shape(_CellShape2.default).isRequired, 424 | forceEdit: _propTypes2.default.bool, 425 | selected: _propTypes2.default.bool, 426 | editing: _propTypes2.default.bool, 427 | editValue: _propTypes2.default.any, 428 | clearing: _propTypes2.default.bool, 429 | cellRenderer: _propTypes2.default.func, 430 | valueRenderer: _propTypes2.default.func.isRequired, 431 | dataRenderer: _propTypes2.default.func, 432 | valueViewer: _propTypes2.default.func, 433 | dataEditor: _propTypes2.default.func, 434 | attributesRenderer: _propTypes2.default.func, 435 | onNavigate: _propTypes2.default.func.isRequired, 436 | onMouseDown: _propTypes2.default.func.isRequired, 437 | onMouseOver: _propTypes2.default.func.isRequired, 438 | onDoubleClick: _propTypes2.default.func.isRequired, 439 | onContextMenu: _propTypes2.default.func.isRequired, 440 | onChange: _propTypes2.default.func.isRequired, 441 | onRevert: _propTypes2.default.func.isRequired, 442 | onEdit: _propTypes2.default.func, 443 | }; 444 | 445 | DataCell.defaultProps = { 446 | forceEdit: false, 447 | selected: false, 448 | editing: false, 449 | clearing: false, 450 | cellRenderer: _Cell2.default, 451 | }; 452 | -------------------------------------------------------------------------------- /lib/DataEditor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | var _CellShape = require('./CellShape'); 18 | 19 | var _CellShape2 = _interopRequireDefault(_CellShape); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var DataEditor = function (_PureComponent) { 30 | _inherits(DataEditor, _PureComponent); 31 | 32 | function DataEditor(props) { 33 | _classCallCheck(this, DataEditor); 34 | 35 | var _this = _possibleConstructorReturn(this, (DataEditor.__proto__ || Object.getPrototypeOf(DataEditor)).call(this, props)); 36 | 37 | _this.handleChange = _this.handleChange.bind(_this); 38 | return _this; 39 | } 40 | 41 | _createClass(DataEditor, [{ 42 | key: 'componentDidMount', 43 | value: function componentDidMount() { 44 | this._input.focus(); 45 | } 46 | }, { 47 | key: 'handleChange', 48 | value: function handleChange(e) { 49 | this.props.onChange(e.target.value); 50 | } 51 | }, { 52 | key: 'render', 53 | value: function render() { 54 | var _this2 = this; 55 | 56 | var _props = this.props, 57 | value = _props.value, 58 | onKeyDown = _props.onKeyDown; 59 | 60 | return _react2.default.createElement('input', { 61 | ref: function ref(input) { 62 | _this2._input = input; 63 | }, 64 | className: 'data-editor', 65 | value: value, 66 | onChange: this.handleChange, 67 | onKeyDown: onKeyDown 68 | }); 69 | } 70 | }]); 71 | 72 | return DataEditor; 73 | }(_react.PureComponent); 74 | 75 | exports.default = DataEditor; 76 | 77 | 78 | DataEditor.propTypes = { 79 | value: _propTypes2.default.node.isRequired, 80 | row: _propTypes2.default.number.isRequired, 81 | col: _propTypes2.default.number.isRequired, 82 | cell: _propTypes2.default.shape(_CellShape2.default), 83 | onChange: _propTypes2.default.func.isRequired, 84 | onCommit: _propTypes2.default.func.isRequired, 85 | onRevert: _propTypes2.default.func.isRequired, 86 | onKeyDown: _propTypes2.default.func.isRequired 87 | }; -------------------------------------------------------------------------------- /lib/Row.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | var _CellShape = require('./CellShape'); 18 | 19 | var _CellShape2 = _interopRequireDefault(_CellShape); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var Row = function (_PureComponent) { 30 | _inherits(Row, _PureComponent); 31 | 32 | function Row() { 33 | _classCallCheck(this, Row); 34 | 35 | return _possibleConstructorReturn(this, (Row.__proto__ || Object.getPrototypeOf(Row)).apply(this, arguments)); 36 | } 37 | 38 | _createClass(Row, [{ 39 | key: 'render', 40 | value: function render() { 41 | return _react2.default.createElement( 42 | 'tr', 43 | null, 44 | this.props.children 45 | ); 46 | } 47 | }]); 48 | 49 | return Row; 50 | }(_react.PureComponent); 51 | 52 | Row.propTypes = { 53 | row: _propTypes2.default.number.isRequired, 54 | cells: _propTypes2.default.arrayOf(_propTypes2.default.shape(_CellShape2.default)).isRequired 55 | }; 56 | 57 | exports.default = Row; -------------------------------------------------------------------------------- /lib/Sheet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | var Sheet = function (_PureComponent) { 26 | _inherits(Sheet, _PureComponent); 27 | 28 | function Sheet() { 29 | _classCallCheck(this, Sheet); 30 | 31 | return _possibleConstructorReturn(this, (Sheet.__proto__ || Object.getPrototypeOf(Sheet)).apply(this, arguments)); 32 | } 33 | 34 | _createClass(Sheet, [{ 35 | key: 'render', 36 | value: function render() { 37 | return _react2.default.createElement( 38 | 'table', 39 | { className: this.props.className }, 40 | _react2.default.createElement( 41 | 'tbody', 42 | null, 43 | this.props.children 44 | ) 45 | ); 46 | } 47 | }]); 48 | 49 | return Sheet; 50 | }(_react.PureComponent); 51 | 52 | Sheet.propTypes = { 53 | className: _propTypes2.default.string, 54 | data: _propTypes2.default.array.isRequired 55 | }; 56 | 57 | exports.default = Sheet; -------------------------------------------------------------------------------- /lib/ValueViewer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | var _CellShape = require('./CellShape'); 18 | 19 | var _CellShape2 = _interopRequireDefault(_CellShape); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var ValueViewer = function (_PureComponent) { 30 | _inherits(ValueViewer, _PureComponent); 31 | 32 | function ValueViewer() { 33 | _classCallCheck(this, ValueViewer); 34 | 35 | return _possibleConstructorReturn(this, (ValueViewer.__proto__ || Object.getPrototypeOf(ValueViewer)).apply(this, arguments)); 36 | } 37 | 38 | _createClass(ValueViewer, [{ 39 | key: 'render', 40 | value: function render() { 41 | var value = this.props.value; 42 | 43 | return _react2.default.createElement( 44 | 'span', 45 | { className: 'value-viewer' }, 46 | value 47 | ); 48 | } 49 | }]); 50 | 51 | return ValueViewer; 52 | }(_react.PureComponent); 53 | 54 | exports.default = ValueViewer; 55 | 56 | 57 | ValueViewer.propTypes = { 58 | row: _propTypes2.default.number.isRequired, 59 | col: _propTypes2.default.number.isRequired, 60 | cell: _propTypes2.default.shape(_CellShape2.default), 61 | value: _propTypes2.default.node.isRequired 62 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.renderData = exports.renderValue = exports.ValueViewer = exports.DataEditor = exports.Cell = exports.Row = exports.Sheet = undefined; 7 | 8 | var _DataSheet = require('./DataSheet'); 9 | 10 | var _DataSheet2 = _interopRequireDefault(_DataSheet); 11 | 12 | var _Sheet = require('./Sheet'); 13 | 14 | var _Sheet2 = _interopRequireDefault(_Sheet); 15 | 16 | var _Row = require('./Row'); 17 | 18 | var _Row2 = _interopRequireDefault(_Row); 19 | 20 | var _Cell = require('./Cell'); 21 | 22 | var _Cell2 = _interopRequireDefault(_Cell); 23 | 24 | var _DataEditor = require('./DataEditor'); 25 | 26 | var _DataEditor2 = _interopRequireDefault(_DataEditor); 27 | 28 | var _ValueViewer = require('./ValueViewer'); 29 | 30 | var _ValueViewer2 = _interopRequireDefault(_ValueViewer); 31 | 32 | var _renderHelpers = require('./renderHelpers'); 33 | 34 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 35 | 36 | exports.default = _DataSheet2.default; 37 | exports.Sheet = _Sheet2.default; 38 | exports.Row = _Row2.default; 39 | exports.Cell = _Cell2.default; 40 | exports.DataEditor = _DataEditor2.default; 41 | exports.ValueViewer = _ValueViewer2.default; 42 | exports.renderValue = _renderHelpers.renderValue; 43 | exports.renderData = _renderHelpers.renderData; -------------------------------------------------------------------------------- /lib/keys.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var TAB_KEY = exports.TAB_KEY = 9; 7 | var ENTER_KEY = exports.ENTER_KEY = 13; 8 | var ESCAPE_KEY = exports.ESCAPE_KEY = 27; 9 | var LEFT_KEY = exports.LEFT_KEY = 37; 10 | var UP_KEY = exports.UP_KEY = 38; 11 | var RIGHT_KEY = exports.RIGHT_KEY = 39; 12 | var DOWN_KEY = exports.DOWN_KEY = 40; 13 | var DELETE_KEY = exports.DELETE_KEY = 46; 14 | var BACKSPACE_KEY = exports.BACKSPACE_KEY = 8; -------------------------------------------------------------------------------- /lib/react-datasheet.css: -------------------------------------------------------------------------------- 1 | 2 | span.data-grid-container, span.data-grid-container:focus { 3 | outline: none; 4 | } 5 | 6 | .data-grid-container .data-grid { 7 | table-layout: fixed; 8 | border-collapse: collapse; 9 | } 10 | 11 | .data-grid-container .data-grid .cell.updated { 12 | background-color: rgba(0, 145, 253, 0.16); 13 | transition : background-color 0ms ease ; 14 | } 15 | .data-grid-container .data-grid .cell { 16 | height: 17px; 17 | user-select: none; 18 | -moz-user-select: none; 19 | -webkit-user-select: none; 20 | -ms-user-select: none; 21 | cursor: cell; 22 | background-color: unset; 23 | transition : background-color 500ms ease; 24 | vertical-align: middle; 25 | text-align: right; 26 | border: 1px solid #DDD; 27 | padding: 0; 28 | } 29 | .data-grid-container .data-grid .cell.selected { 30 | border: 1px double rgb(33, 133, 208); 31 | transition: none; 32 | box-shadow: inset 0 -100px 0 rgba(33, 133, 208, 0.15); 33 | } 34 | 35 | .data-grid-container .data-grid .cell.read-only { 36 | background: whitesmoke; 37 | color: #999; 38 | text-align: center; 39 | } 40 | 41 | .data-grid-container .data-grid .cell > .text { 42 | padding: 2px 5px; 43 | text-overflow: ellipsis; 44 | overflow: hidden; 45 | } 46 | 47 | 48 | .data-grid-container .data-grid .cell > input { 49 | outline: none !important; 50 | border: 2px solid rgb(33, 133, 208); 51 | text-align:right; 52 | width: calc(100% - 6px); 53 | height: 11px; 54 | background: none; 55 | display: block; 56 | } 57 | 58 | 59 | .data-grid-container .data-grid .cell { 60 | vertical-align: bottom; 61 | } 62 | 63 | .data-grid-container .data-grid .cell, 64 | .data-grid-container .data-grid.wrap .cell, 65 | .data-grid-container .data-grid.wrap .cell.wrap, 66 | .data-grid-container .data-grid .cell.wrap, 67 | .data-grid-container .data-grid.nowrap .cell.wrap, 68 | .data-grid-container .data-grid.clip .cell.wrap { 69 | white-space: normal; 70 | } 71 | 72 | .data-grid-container .data-grid.nowrap .cell, 73 | .data-grid-container .data-grid.nowrap .cell.nowrap, 74 | .data-grid-container .data-grid .cell.nowrap, 75 | .data-grid-container .data-grid.wrap .cell.nowrap, 76 | .data-grid-container .data-grid.clip .cell.nowrap { 77 | white-space: nowrap; 78 | overflow-x: visible; 79 | } 80 | 81 | .data-grid-container .data-grid.clip .cell, 82 | .data-grid-container .data-grid.clip .cell.clip, 83 | .data-grid-container .data-grid .cell.clip, 84 | .data-grid-container .data-grid.wrap .cell.clip, 85 | .data-grid-container .data-grid.nowrap .cell.clip { 86 | white-space: nowrap; 87 | overflow-x: hidden; 88 | } 89 | 90 | .data-grid-container .data-grid .cell .value-viewer, .data-grid-container .data-grid .cell .data-editor { 91 | display: block; 92 | } 93 | -------------------------------------------------------------------------------- /lib/renderHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.renderValue = renderValue; 7 | exports.renderData = renderData; 8 | function renderValue(cell, row, col, valueRenderer) { 9 | var value = valueRenderer(cell, row, col); 10 | return value === null || typeof value === 'undefined' ? '' : value; 11 | } 12 | 13 | function renderData(cell, row, col, valueRenderer, dataRenderer) { 14 | var value = dataRenderer ? dataRenderer(cell, row, col) : null; 15 | return value === null || typeof value === 'undefined' ? renderValue(cell, row, col, valueRenderer) : value; 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-datasheet", 3 | "version": "1.4.9", 4 | "description": "Excel-like data grid for React", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/nadbm/react-datasheet.git" 8 | }, 9 | "author": "Nadim Islam", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/nadbm/react-datasheet/issues" 13 | }, 14 | "keywords": [ 15 | "react-component", 16 | "react" 17 | ], 18 | "scripts": { 19 | "lint": "eslint ./src", 20 | "lintfix": "eslint ./src --fix", 21 | "clean": "rimraf dist", 22 | "build": "babel ./src --out-dir ./lib && shx cp src/react-datasheet.css lib", 23 | "build:watch": "watch 'npm run build' ./src", 24 | "prepublish": "npm run build", 25 | "coverage": "nyc report --reporter=text-lcov | coveralls", 26 | "test": "NODE_ENV=test nyc -- mocha ./test/**/*.js --require babel-core/register", 27 | "test:watch": "watch 'npm run test' ./test ./src", 28 | "format": "prettier --write '{src,test}/**/*.js' --ignore-path docs,.gitignore", 29 | "format-test": "prettier-check '**/*.js'" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "^16.0.38", 33 | "babel-cli": "^6.6.4", 34 | "babel-core": "^6.26.3", 35 | "babel-eslint": "^6.0.2", 36 | "babel-plugin-transform-es2015-modules-umd": "^6.24.0", 37 | "babel-polyfill": "^6.7.4", 38 | "babel-preset-es2015": "^6.6.0", 39 | "babel-preset-react": "^6.23.0", 40 | "babel-preset-stage-2": "^6.5.0", 41 | "chai": "^3.5.0", 42 | "coveralls": "^3.0.1", 43 | "cross-env": "^4.0.0", 44 | "enzyme": "^2.2.0", 45 | "eslint": "^2.7.0", 46 | "eslint-plugin-babel": "^3.1.0", 47 | "eslint-plugin-react": "^4.2.3", 48 | "expect": "^1.20.2", 49 | "husky": "^4.2.5", 50 | "jsdom": "^8.1.0", 51 | "mocha": "^5.2.0", 52 | "mocha-jsdom": "~1.1.0", 53 | "nodemon": "^1.17.5", 54 | "nyc": "^14.1.1", 55 | "prettier": "^2.0.5", 56 | "prettier-check": "^2.0.0", 57 | "prettier-quick": "0.0.5", 58 | "pretty-quick": "^2.0.1", 59 | "prop-types": "^15.7.2", 60 | "react": "^15.0.0", 61 | "react-addons-test-utils": "^15.6.2", 62 | "react-dom": "^15.0.0", 63 | "rimraf": "^2.6.1", 64 | "shx": "^0.3.3", 65 | "sinon": "^1.17.3", 66 | "watch": "^1.0.2" 67 | }, 68 | "peerDependencies": { 69 | "prop-types": ">=15", 70 | "react": ">=16", 71 | "react-dom": ">=16" 72 | }, 73 | "files": [ 74 | "lib", 75 | "types" 76 | ], 77 | "husky": { 78 | "hooks": { 79 | "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'" 80 | } 81 | }, 82 | "main": "lib/index.js", 83 | "types": "types/react-datasheet.d.ts" 84 | } 85 | -------------------------------------------------------------------------------- /params.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React-datasheet", 3 | "tagline": "Excel-like data grid component for react", 4 | "body": "### Welcome to GitHub Pages.\r\nThis automatic page generator is the easiest way to create beautiful pages for all of your projects. Author your page content here [using GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/), select a template crafted by a designer, and publish. After your page is generated, you can check out the new `gh-pages` branch locally. If you’re using GitHub Desktop, simply sync your repository and you’ll see the new branch.\r\n\r\n### Designer Templates\r\nWe’ve crafted some handsome templates for you to use. Go ahead and click 'Continue to layouts' to browse through them. You can easily go back to edit your page before publishing. After publishing your page, you can revisit the page generator and switch to another theme. Your Page content will be preserved.\r\n\r\n### Creating pages manually\r\nIf you prefer to not use the automatic generator, push a branch named `gh-pages` to your repository to create a page manually. In addition to supporting regular HTML content, GitHub Pages support Jekyll, a simple, blog aware static site generator. Jekyll makes it easy to create site-wide headers and footers without having to copy them across every page. It also offers intelligent blog support and other advanced templating features.\r\n\r\n### Authors and Contributors\r\nYou can @mention a GitHub username to generate a link to their profile. The resulting `` element will link to the contributor’s GitHub Profile. For example: In 2007, Chris Wanstrath (@defunkt), PJ Hyett (@pjhyett), and Tom Preston-Werner (@mojombo) founded GitHub.\r\n\r\n### Support or Contact\r\nHaving trouble with Pages? Check out our [documentation](https://help.github.com/pages) or [contact support](https://github.com/contact) and we’ll help you sort it out.\r\n", 5 | "note": "Don't delete this file! It's used internally to help with page regeneration." 6 | } -------------------------------------------------------------------------------- /src/Cell.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import CellShape from './CellShape'; 4 | 5 | export default class Cell extends PureComponent { 6 | render() { 7 | const { 8 | cell, 9 | row, 10 | col, 11 | attributesRenderer, 12 | className, 13 | style, 14 | onMouseDown, 15 | onMouseOver, 16 | onDoubleClick, 17 | onContextMenu, 18 | } = this.props; 19 | 20 | const { colSpan, rowSpan } = cell; 21 | const attributes = attributesRenderer 22 | ? attributesRenderer(cell, row, col) 23 | : {}; 24 | 25 | return ( 26 | 38 | {this.props.children} 39 | 40 | ); 41 | } 42 | } 43 | 44 | Cell.propTypes = { 45 | row: PropTypes.number.isRequired, 46 | col: PropTypes.number.isRequired, 47 | cell: PropTypes.shape(CellShape).isRequired, 48 | selected: PropTypes.bool, 49 | editing: PropTypes.bool, 50 | updated: PropTypes.bool, 51 | attributesRenderer: PropTypes.func, 52 | onMouseDown: PropTypes.func.isRequired, 53 | onMouseOver: PropTypes.func.isRequired, 54 | onDoubleClick: PropTypes.func.isRequired, 55 | onContextMenu: PropTypes.func.isRequired, 56 | className: PropTypes.string, 57 | style: PropTypes.object, 58 | }; 59 | 60 | Cell.defaultProps = { 61 | selected: false, 62 | editing: false, 63 | updated: false, 64 | attributesRenderer: () => {}, 65 | }; 66 | -------------------------------------------------------------------------------- /src/CellShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | /* 3 | readOnly Bool false Cell will never go in edit mode 4 | key String undefined By default, each cell is given the key of col number and row number. This would override that key 5 | className String undefined Additional class names for cells. 6 | component ReactElement undefined Insert a react element or JSX to this field. This will render on edit mode 7 | forceComponent bool false Renders what's in component at all times, even when not in edit mode 8 | disableEvents bool false Makes cell unselectable and read only 9 | colSpan number 1 The colSpan of the cell's td element 10 | rowSpan number 1 The rowSpan of the cell's td element 11 | width number or String undefined Sets the cell's td width using a style attribute. Number is interpreted as pixels, strings are used as-is. Note: This will only work if the table does not have a set width. 12 | overflow 'wrap'|'nowrap'| 'clip' undefined How to render overflow text. Overrides grid-level overflow option. 13 | editor func undefined A component used to render the cell's value when being edited 14 | viewer func undefined A component used to render the cell's value when not being edited 15 | */ 16 | const CellShape = { 17 | readOnly: PropTypes.bool, 18 | key: PropTypes.string, 19 | className: PropTypes.string, 20 | component: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), 21 | forceComponent: PropTypes.bool, 22 | disableEvents: PropTypes.bool, 23 | disableUpdatedFlag: PropTypes.bool, 24 | colSpan: PropTypes.number, 25 | rowSpan: PropTypes.number, 26 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 27 | overflow: PropTypes.oneOf(['wrap', 'nowrap', 'clip']), 28 | dataEditor: PropTypes.func, 29 | valueViewer: PropTypes.func, 30 | }; 31 | 32 | export default CellShape; 33 | -------------------------------------------------------------------------------- /src/DataCell.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | ENTER_KEY, 6 | ESCAPE_KEY, 7 | TAB_KEY, 8 | RIGHT_KEY, 9 | LEFT_KEY, 10 | UP_KEY, 11 | DOWN_KEY, 12 | } from './keys'; 13 | 14 | import Cell from './Cell'; 15 | import CellShape from './CellShape'; 16 | import DataEditor from './DataEditor'; 17 | import ValueViewer from './ValueViewer'; 18 | import { renderValue, renderData } from './renderHelpers'; 19 | 20 | function initialData({ cell, row, col, valueRenderer, dataRenderer }) { 21 | return renderData(cell, row, col, valueRenderer, dataRenderer); 22 | } 23 | 24 | function initialValue({ cell, row, col, valueRenderer }) { 25 | return renderValue(cell, row, col, valueRenderer); 26 | } 27 | 28 | function widthStyle(cell) { 29 | const width = typeof cell.width === 'number' ? cell.width + 'px' : cell.width; 30 | return width ? { width } : null; 31 | } 32 | 33 | export default class DataCell extends PureComponent { 34 | constructor(props) { 35 | super(props); 36 | this.handleChange = this.handleChange.bind(this); 37 | this.handleCommit = this.handleCommit.bind(this); 38 | this.handleRevert = this.handleRevert.bind(this); 39 | 40 | this.handleKey = this.handleKey.bind(this); 41 | this.handleMouseDown = this.handleMouseDown.bind(this); 42 | this.handleMouseOver = this.handleMouseOver.bind(this); 43 | this.handleContextMenu = this.handleContextMenu.bind(this); 44 | this.handleDoubleClick = this.handleDoubleClick.bind(this); 45 | 46 | this.state = { 47 | updated: false, 48 | reverting: false, 49 | committing: false, 50 | value: '', 51 | }; 52 | } 53 | 54 | componentDidUpdate(prevProps) { 55 | if ( 56 | !this.props.cell.disableUpdatedFlag && 57 | initialValue(prevProps) !== initialValue(this.props) 58 | ) { 59 | this.setState({ updated: true }); 60 | this.timeout = setTimeout(() => this.setState({ updated: false }), 700); 61 | } 62 | if (this.props.editing === true && prevProps.editing === false) { 63 | const value = this.props.clearing ? '' : initialData(this.props); 64 | this.setState({ value, reverting: false }); 65 | } 66 | 67 | if ( 68 | prevProps.editing === true && 69 | this.props.editing === false && 70 | !this.state.reverting && 71 | !this.state.committing && 72 | this.state.value !== initialData(this.props) 73 | ) { 74 | this.props.onChange(this.props.row, this.props.col, this.state.value); 75 | } 76 | } 77 | 78 | componentWillUnmount() { 79 | clearTimeout(this.timeout); 80 | } 81 | 82 | handleChange(value) { 83 | this.setState({ value, committing: false }); 84 | } 85 | 86 | handleCommit(value, e) { 87 | const { onChange, onNavigate } = this.props; 88 | if (value !== initialData(this.props)) { 89 | this.setState({ value, committing: true }); 90 | onChange(this.props.row, this.props.col, value); 91 | } else { 92 | this.handleRevert(); 93 | } 94 | if (e) { 95 | e.preventDefault(); 96 | onNavigate(e, true); 97 | } 98 | } 99 | 100 | handleRevert() { 101 | this.setState({ reverting: true }); 102 | this.props.onRevert(); 103 | } 104 | 105 | handleMouseDown(e) { 106 | const { row, col, onMouseDown, cell } = this.props; 107 | if (!cell.disableEvents) { 108 | onMouseDown(row, col, e); 109 | } 110 | } 111 | 112 | handleMouseOver(e) { 113 | const { row, col, onMouseOver, cell } = this.props; 114 | if (!cell.disableEvents) { 115 | onMouseOver(row, col); 116 | } 117 | } 118 | 119 | handleDoubleClick(e) { 120 | const { row, col, onDoubleClick, cell } = this.props; 121 | if (!cell.disableEvents) { 122 | onDoubleClick(row, col); 123 | } 124 | } 125 | 126 | handleContextMenu(e) { 127 | const { row, col, onContextMenu, cell } = this.props; 128 | if (!cell.disableEvents) { 129 | onContextMenu(e, row, col); 130 | } 131 | } 132 | 133 | handleKey(e) { 134 | const keyCode = e.which || e.keyCode; 135 | if (keyCode === ESCAPE_KEY) { 136 | return this.handleRevert(); 137 | } 138 | const { 139 | cell: { component }, 140 | forceEdit, 141 | } = this.props; 142 | const eatKeys = forceEdit || !!component; 143 | const commit = 144 | keyCode === ENTER_KEY || 145 | keyCode === TAB_KEY || 146 | (!eatKeys && [LEFT_KEY, RIGHT_KEY, UP_KEY, DOWN_KEY].includes(keyCode)); 147 | 148 | if (commit) { 149 | this.handleCommit(this.state.value, e); 150 | } 151 | } 152 | 153 | renderComponent(editing, cell) { 154 | const { component, readOnly, forceComponent } = cell; 155 | if ((editing && !readOnly) || forceComponent) { 156 | return component; 157 | } 158 | } 159 | 160 | renderEditor(editing, cell, row, col, dataEditor) { 161 | if (editing) { 162 | const Editor = cell.dataEditor || dataEditor || DataEditor; 163 | return ( 164 | 174 | ); 175 | } 176 | } 177 | 178 | renderViewer(cell, row, col, valueRenderer, valueViewer) { 179 | const Viewer = cell.valueViewer || valueViewer || ValueViewer; 180 | const value = renderValue(cell, row, col, valueRenderer); 181 | return ; 182 | } 183 | 184 | render() { 185 | const { 186 | row, 187 | col, 188 | cell, 189 | cellRenderer: CellRenderer, 190 | valueRenderer, 191 | dataEditor, 192 | valueViewer, 193 | attributesRenderer, 194 | selected, 195 | editing, 196 | onKeyUp, 197 | } = this.props; 198 | const { updated } = this.state; 199 | 200 | const content = 201 | this.renderComponent(editing, cell) || 202 | this.renderEditor(editing, cell, row, col, dataEditor) || 203 | this.renderViewer(cell, row, col, valueRenderer, valueViewer); 204 | 205 | const className = [ 206 | cell.className, 207 | 'cell', 208 | cell.overflow, 209 | selected && 'selected', 210 | editing && 'editing', 211 | cell.readOnly && 'read-only', 212 | updated && 'updated', 213 | ] 214 | .filter(a => a) 215 | .join(' '); 216 | 217 | return ( 218 | 234 | {content} 235 | 236 | ); 237 | } 238 | } 239 | 240 | DataCell.propTypes = { 241 | row: PropTypes.number.isRequired, 242 | col: PropTypes.number.isRequired, 243 | cell: PropTypes.shape(CellShape).isRequired, 244 | forceEdit: PropTypes.bool, 245 | selected: PropTypes.bool, 246 | editing: PropTypes.bool, 247 | editValue: PropTypes.any, 248 | clearing: PropTypes.bool, 249 | cellRenderer: PropTypes.func, 250 | valueRenderer: PropTypes.func.isRequired, 251 | dataRenderer: PropTypes.func, 252 | valueViewer: PropTypes.func, 253 | dataEditor: PropTypes.func, 254 | attributesRenderer: PropTypes.func, 255 | onNavigate: PropTypes.func.isRequired, 256 | onMouseDown: PropTypes.func.isRequired, 257 | onMouseOver: PropTypes.func.isRequired, 258 | onDoubleClick: PropTypes.func.isRequired, 259 | onContextMenu: PropTypes.func.isRequired, 260 | onChange: PropTypes.func.isRequired, 261 | onRevert: PropTypes.func.isRequired, 262 | onEdit: PropTypes.func, 263 | }; 264 | 265 | DataCell.defaultProps = { 266 | forceEdit: false, 267 | selected: false, 268 | editing: false, 269 | clearing: false, 270 | cellRenderer: Cell, 271 | }; 272 | -------------------------------------------------------------------------------- /src/DataEditor.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import CellShape from './CellShape'; 5 | 6 | export default class DataEditor extends PureComponent { 7 | constructor(props) { 8 | super(props); 9 | this.handleChange = this.handleChange.bind(this); 10 | } 11 | 12 | componentDidMount() { 13 | this._input.focus(); 14 | } 15 | 16 | handleChange(e) { 17 | this.props.onChange(e.target.value); 18 | } 19 | 20 | render() { 21 | const { value, onKeyDown } = this.props; 22 | return ( 23 | { 25 | this._input = input; 26 | }} 27 | className="data-editor" 28 | value={value} 29 | onChange={this.handleChange} 30 | onKeyDown={onKeyDown} 31 | /> 32 | ); 33 | } 34 | } 35 | 36 | DataEditor.propTypes = { 37 | value: PropTypes.node.isRequired, 38 | row: PropTypes.number.isRequired, 39 | col: PropTypes.number.isRequired, 40 | cell: PropTypes.shape(CellShape), 41 | onChange: PropTypes.func.isRequired, 42 | onCommit: PropTypes.func.isRequired, 43 | onRevert: PropTypes.func.isRequired, 44 | onKeyDown: PropTypes.func.isRequired, 45 | }; 46 | -------------------------------------------------------------------------------- /src/DataSheet.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Sheet from './Sheet'; 4 | import Row from './Row'; 5 | import Cell from './Cell'; 6 | import DataCell from './DataCell'; 7 | import DataEditor from './DataEditor'; 8 | import ValueViewer from './ValueViewer'; 9 | import { 10 | TAB_KEY, 11 | ENTER_KEY, 12 | DELETE_KEY, 13 | ESCAPE_KEY, 14 | BACKSPACE_KEY, 15 | LEFT_KEY, 16 | UP_KEY, 17 | DOWN_KEY, 18 | RIGHT_KEY, 19 | } from './keys'; 20 | 21 | const isEmpty = obj => Object.keys(obj).length === 0; 22 | 23 | const range = (start, end) => { 24 | const array = []; 25 | const inc = end - start > 0; 26 | for (let i = start; inc ? i <= end : i >= end; inc ? i++ : i--) { 27 | inc ? array.push(i) : array.unshift(i); 28 | } 29 | return array; 30 | }; 31 | 32 | const defaultParsePaste = str => { 33 | return str.split(/\r\n|\n|\r/).map(row => row.split('\t')); 34 | }; 35 | 36 | export default class DataSheet extends PureComponent { 37 | constructor(props) { 38 | super(props); 39 | this.onMouseDown = this.onMouseDown.bind(this); 40 | this.onMouseUp = this.onMouseUp.bind(this); 41 | this.onMouseOver = this.onMouseOver.bind(this); 42 | this.onDoubleClick = this.onDoubleClick.bind(this); 43 | this.onContextMenu = this.onContextMenu.bind(this); 44 | this.handleNavigate = this.handleNavigate.bind(this); 45 | this.handleKey = this.handleKey.bind(this).bind(this); 46 | this.handleCut = this.handleCut.bind(this); 47 | this.handleCopy = this.handleCopy.bind(this); 48 | this.handlePaste = this.handlePaste.bind(this); 49 | this.pageClick = this.pageClick.bind(this); 50 | this.onChange = this.onChange.bind(this); 51 | this.onRevert = this.onRevert.bind(this); 52 | this.isSelected = this.isSelected.bind(this); 53 | this.isEditing = this.isEditing.bind(this); 54 | this.isClearing = this.isClearing.bind(this); 55 | this.handleComponentKey = this.handleComponentKey.bind(this); 56 | 57 | this.handleKeyboardCellMovement = 58 | this.handleKeyboardCellMovement.bind(this); 59 | 60 | this.defaultState = { 61 | start: {}, 62 | end: {}, 63 | selecting: false, 64 | forceEdit: false, 65 | editing: {}, 66 | clear: {}, 67 | }; 68 | this.state = this.defaultState; 69 | 70 | this.removeAllListeners = this.removeAllListeners.bind(this); 71 | this.handleIEClipboardEvents = this.handleIEClipboardEvents.bind(this); 72 | } 73 | 74 | removeAllListeners() { 75 | document.removeEventListener('mousedown', this.pageClick); 76 | document.removeEventListener('mouseup', this.onMouseUp); 77 | document.removeEventListener('cut', this.handleCut); 78 | document.removeEventListener('copy', this.handleCopy); 79 | document.removeEventListener('paste', this.handlePaste); 80 | document.removeEventListener('keydown', this.handleIEClipboardEvents); 81 | } 82 | 83 | componentDidMount() { 84 | // Add listener scoped to the DataSheet that catches otherwise unhandled 85 | // keyboard events when displaying components 86 | this.dgDom && 87 | this.dgDom.addEventListener('keydown', this.handleComponentKey); 88 | } 89 | 90 | componentWillUnmount() { 91 | this.dgDom && 92 | this.dgDom.removeEventListener('keydown', this.handleComponentKey); 93 | this.removeAllListeners(); 94 | } 95 | 96 | isSelectionControlled() { 97 | return 'selected' in this.props; 98 | } 99 | 100 | getState() { 101 | let state = this.state; 102 | if (this.isSelectionControlled()) { 103 | let { start, end } = this.props.selected || {}; 104 | start = start || this.defaultState.start; 105 | end = end || this.defaultState.end; 106 | state = { ...state, start, end }; 107 | } 108 | return state; 109 | } 110 | 111 | _setState(state) { 112 | const { editModeChanged } = this.props; 113 | if (editModeChanged && state.editing) { 114 | const wasEditing = !isEmpty(this.state.editing); 115 | const wilBeEditing = !isEmpty(state.editing); 116 | if (wasEditing != wilBeEditing) { 117 | editModeChanged(wilBeEditing); 118 | } 119 | } 120 | if (this.isSelectionControlled() && ('start' in state || 'end' in state)) { 121 | let { start, end, ...rest } = state; 122 | let { selected, onSelect } = this.props; 123 | selected = selected || {}; 124 | if (!start) { 125 | start = 'start' in selected ? selected.start : this.defaultState.start; 126 | } 127 | if (!end) { 128 | end = 'end' in selected ? selected.end : this.defaultState.end; 129 | } 130 | onSelect && onSelect({ start, end }); 131 | this.setState(rest); 132 | } else { 133 | this.setState(state); 134 | } 135 | } 136 | 137 | pageClick(e) { 138 | if (this.props.disablePageClick) return; 139 | const element = this.dgDom; 140 | if (!element.contains(e.target)) { 141 | this.setState(this.defaultState); 142 | this.removeAllListeners(); 143 | } 144 | } 145 | 146 | handleCut(e) { 147 | if (isEmpty(this.state.editing)) { 148 | e.preventDefault(); 149 | this.handleCopy(e); 150 | const { start, end } = this.getState(); 151 | this.clearSelectedCells(start, end); 152 | } 153 | } 154 | 155 | handleIEClipboardEvents(e) { 156 | if (e.ctrlKey) { 157 | if (e.keyCode === 67) { 158 | // C - copy 159 | this.handleCopy(e); 160 | } else if (e.keyCode === 88) { 161 | // X - cut 162 | this.handleCut(e); 163 | } else if (e.keyCode === 86 || e.which === 86) { 164 | // P - patse 165 | this.handlePaste(e); 166 | } 167 | } 168 | } 169 | 170 | handleCopy(e) { 171 | if (isEmpty(this.state.editing)) { 172 | e.preventDefault(); 173 | const { dataRenderer, valueRenderer, data } = this.props; 174 | const { start, end } = this.getState(); 175 | 176 | if (this.props.handleCopy) { 177 | this.props.handleCopy({ 178 | event: e, 179 | dataRenderer, 180 | valueRenderer, 181 | data, 182 | start, 183 | end, 184 | range, 185 | }); 186 | } else { 187 | const text = range(start.i, end.i) 188 | .map(i => 189 | range(start.j, end.j) 190 | .map(j => { 191 | const cell = data[i][j]; 192 | const value = dataRenderer ? dataRenderer(cell, i, j) : null; 193 | if ( 194 | value === '' || 195 | value === null || 196 | typeof value === 'undefined' 197 | ) { 198 | return valueRenderer(cell, i, j); 199 | } 200 | return value; 201 | }) 202 | .join('\t'), 203 | ) 204 | .join('\n'); 205 | if (window.clipboardData && window.clipboardData.setData) { 206 | window.clipboardData.setData('Text', text); 207 | } else { 208 | e.clipboardData.setData('text/plain', text); 209 | } 210 | } 211 | } 212 | } 213 | 214 | handlePaste(e) { 215 | if (isEmpty(this.state.editing)) { 216 | let { start, end } = this.getState(); 217 | 218 | start = { i: Math.min(start.i, end.i), j: Math.min(start.j, end.j) }; 219 | end = { i: Math.max(start.i, end.i), j: Math.max(start.j, end.j) }; 220 | 221 | const parse = this.props.parsePaste || defaultParsePaste; 222 | const changes = []; 223 | let pasteData = []; 224 | if (window.clipboardData && window.clipboardData.getData) { 225 | // IE 226 | pasteData = parse(window.clipboardData.getData('Text')); 227 | } else if (e.clipboardData && e.clipboardData.getData) { 228 | pasteData = parse(e.clipboardData.getData('text/plain')); 229 | } 230 | 231 | // in order of preference 232 | const { data, onCellsChanged, onPaste, onChange } = this.props; 233 | if (onCellsChanged) { 234 | const additions = []; 235 | pasteData.forEach((row, i) => { 236 | row.forEach((value, j) => { 237 | end = { i: start.i + i, j: start.j + j }; 238 | const cell = data[end.i] && data[end.i][end.j]; 239 | if (!cell) { 240 | additions.push({ row: end.i, col: end.j, value }); 241 | } else if (!cell.readOnly) { 242 | changes.push({ cell, row: end.i, col: end.j, value }); 243 | } 244 | }); 245 | }); 246 | if (additions.length) { 247 | onCellsChanged(changes, additions); 248 | } else { 249 | onCellsChanged(changes); 250 | } 251 | } else if (onPaste) { 252 | pasteData.forEach((row, i) => { 253 | const rowData = []; 254 | row.forEach((pastedData, j) => { 255 | end = { i: start.i + i, j: start.j + j }; 256 | const cell = data[end.i] && data[end.i][end.j]; 257 | rowData.push({ cell: cell, data: pastedData }); 258 | }); 259 | changes.push(rowData); 260 | }); 261 | onPaste(changes); 262 | } else if (onChange) { 263 | pasteData.forEach((row, i) => { 264 | row.forEach((value, j) => { 265 | end = { i: start.i + i, j: start.j + j }; 266 | const cell = data[end.i] && data[end.i][end.j]; 267 | if (cell && !cell.readOnly) { 268 | onChange(cell, end.i, end.j, value); 269 | } 270 | }); 271 | }); 272 | } 273 | this._setState({ end }); 274 | } 275 | } 276 | 277 | handleKeyboardCellMovement(e, commit = false) { 278 | const { start, editing } = this.getState(); 279 | const { data } = this.props; 280 | const isEditing = editing && !isEmpty(editing); 281 | const currentCell = data[start.i] && data[start.i][start.j]; 282 | 283 | if (isEditing && !commit) { 284 | return false; 285 | } 286 | const hasComponent = currentCell && currentCell.component; 287 | 288 | const keyCode = e.which || e.keyCode; 289 | 290 | if (hasComponent && isEditing) { 291 | e.preventDefault(); 292 | return; 293 | } 294 | 295 | if (keyCode === TAB_KEY) { 296 | this.handleNavigate(e, { i: 0, j: e.shiftKey ? -1 : 1 }, true); 297 | } else if (keyCode === RIGHT_KEY) { 298 | this.handleNavigate(e, { i: 0, j: 1 }); 299 | } else if (keyCode === LEFT_KEY) { 300 | this.handleNavigate(e, { i: 0, j: -1 }); 301 | } else if (keyCode === UP_KEY) { 302 | this.handleNavigate(e, { i: -1, j: 0 }); 303 | } else if (keyCode === DOWN_KEY) { 304 | this.handleNavigate(e, { i: 1, j: 0 }); 305 | } else if (commit && keyCode === ENTER_KEY) { 306 | this.handleNavigate(e, { i: e.shiftKey ? -1 : 1, j: 0 }); 307 | } 308 | } 309 | 310 | handleKey(e) { 311 | if (e.isPropagationStopped && e.isPropagationStopped()) { 312 | return; 313 | } 314 | const keyCode = e.which || e.keyCode; 315 | const { start, end, editing } = this.getState(); 316 | const isEditing = editing && !isEmpty(editing); 317 | const noCellsSelected = !start || isEmpty(start); 318 | const ctrlKeyPressed = e.ctrlKey || e.metaKey; 319 | const deleteKeysPressed = 320 | keyCode === DELETE_KEY || keyCode === BACKSPACE_KEY; 321 | const enterKeyPressed = keyCode === ENTER_KEY; 322 | const numbersPressed = keyCode >= 48 && keyCode <= 57; 323 | const lettersPressed = keyCode >= 65 && keyCode <= 90; 324 | const latin1Supplement = keyCode >= 160 && keyCode <= 255; 325 | const numPadKeysPressed = keyCode >= 96 && keyCode <= 105; 326 | const currentCell = !noCellsSelected && this.props.data[start.i][start.j]; 327 | const equationKeysPressed = 328 | [ 329 | 187 /* equal */, 189 /* substract */, 190 /* period */, 107 /* add */, 330 | 109 /* decimal point */, 110, 331 | ].indexOf(keyCode) > -1; 332 | 333 | if (noCellsSelected || ctrlKeyPressed) { 334 | return true; 335 | } 336 | 337 | if (!isEditing) { 338 | this.handleKeyboardCellMovement(e); 339 | if (deleteKeysPressed) { 340 | e.preventDefault(); 341 | this.clearSelectedCells(start, end); 342 | } else if (currentCell && !currentCell.readOnly) { 343 | if (enterKeyPressed) { 344 | this._setState({ editing: start, clear: {}, forceEdit: true }); 345 | e.preventDefault(); 346 | } else if ( 347 | numbersPressed || 348 | numPadKeysPressed || 349 | lettersPressed || 350 | latin1Supplement || 351 | equationKeysPressed 352 | ) { 353 | // empty out cell if user starts typing without pressing enter 354 | this._setState({ editing: start, clear: start, forceEdit: false }); 355 | } 356 | } 357 | } 358 | } 359 | 360 | getSelectedCells(data, start, end) { 361 | let selected = []; 362 | range(start.i, end.i).map(row => { 363 | range(start.j, end.j).map(col => { 364 | if (data[row] && data[row][col]) { 365 | selected.push({ cell: data[row][col], row, col }); 366 | } 367 | }); 368 | }); 369 | return selected; 370 | } 371 | 372 | clearSelectedCells(start, end) { 373 | const { data, onCellsChanged, onChange } = this.props; 374 | const cells = this.getSelectedCells(data, start, end) 375 | .filter(cell => !cell.cell.readOnly) 376 | .map(cell => ({ ...cell, value: '' })); 377 | if (onCellsChanged) { 378 | onCellsChanged(cells); 379 | this.onRevert(); 380 | } else if (onChange) { 381 | // ugly solution brought to you by https://reactjs.org/docs/react-component.html#setstate 382 | // setState in a loop is unreliable 383 | setTimeout(() => { 384 | cells.forEach(({ cell, row, col, value }) => { 385 | onChange(cell, row, col, value); 386 | }); 387 | this.onRevert(); 388 | }, 0); 389 | } 390 | } 391 | 392 | updateLocationSingleCell(location) { 393 | this._setState({ 394 | start: location, 395 | end: location, 396 | editing: {}, 397 | }); 398 | } 399 | 400 | updateLocationMultipleCells(offsets) { 401 | const { start, end } = this.getState(); 402 | const { data } = this.props; 403 | const oldStartLocation = { i: start.i, j: start.j }; 404 | const newEndLocation = { 405 | i: end.i + offsets.i, 406 | j: Math.min(data[0].length - 1, Math.max(0, end.j + offsets.j)), 407 | }; 408 | this._setState({ 409 | start: oldStartLocation, 410 | end: newEndLocation, 411 | editing: {}, 412 | }); 413 | } 414 | 415 | searchForNextSelectablePos(isCellNavigable, data, start, offsets, jumpRow) { 416 | const previousRow = location => ({ 417 | i: location.i - 1, 418 | j: data[0].length - 1, 419 | }); 420 | const nextRow = location => ({ i: location.i + 1, j: 0 }); 421 | const advanceOffset = location => ({ 422 | i: location.i + offsets.i, 423 | j: location.j + offsets.j, 424 | }); 425 | const isCellDefined = ({ i, j }) => 426 | data[i] && typeof data[i][j] !== 'undefined'; 427 | 428 | let newLocation = advanceOffset(start); 429 | 430 | while ( 431 | isCellDefined(newLocation) && 432 | !isCellNavigable( 433 | data[newLocation.i][newLocation.j], 434 | newLocation.i, 435 | newLocation.j, 436 | ) 437 | ) { 438 | newLocation = advanceOffset(newLocation); 439 | } 440 | 441 | if (!isCellDefined(newLocation)) { 442 | if (!jumpRow) { 443 | return null; 444 | } 445 | if (offsets.j < 0) { 446 | newLocation = previousRow(newLocation); 447 | } else { 448 | newLocation = nextRow(newLocation); 449 | } 450 | } 451 | 452 | if ( 453 | isCellDefined(newLocation) && 454 | !isCellNavigable( 455 | data[newLocation.i][newLocation.j], 456 | newLocation.i, 457 | newLocation.j, 458 | ) 459 | ) { 460 | return this.searchForNextSelectablePos( 461 | isCellNavigable, 462 | data, 463 | newLocation, 464 | offsets, 465 | jumpRow, 466 | ); 467 | } else if (isCellDefined(newLocation)) { 468 | return newLocation; 469 | } else { 470 | return null; 471 | } 472 | } 473 | 474 | handleNavigate(e, offsets, jumpRow) { 475 | if (offsets && (offsets.i || offsets.j)) { 476 | const { data } = this.props; 477 | const { start } = this.getState(); 478 | 479 | const multiSelect = e.shiftKey && !jumpRow; 480 | const isCellNavigable = this.props.isCellNavigable 481 | ? this.props.isCellNavigable 482 | : () => true; 483 | 484 | if (multiSelect) { 485 | this.updateLocationMultipleCells(offsets); 486 | } else { 487 | const newLocation = this.searchForNextSelectablePos( 488 | isCellNavigable, 489 | data, 490 | start, 491 | offsets, 492 | jumpRow, 493 | ); 494 | if (newLocation) { 495 | this.updateLocationSingleCell(newLocation); 496 | } 497 | } 498 | e.preventDefault(); 499 | } 500 | } 501 | 502 | handleComponentKey(e) { 503 | // handles keyboard events when editing components 504 | const keyCode = e.which || e.keyCode; 505 | if (![ENTER_KEY, ESCAPE_KEY, TAB_KEY].includes(keyCode)) { 506 | return; 507 | } 508 | const { editing } = this.state; 509 | const { data } = this.props; 510 | const isEditing = !isEmpty(editing); 511 | if (isEditing) { 512 | const currentCell = data[editing.i][editing.j]; 513 | const offset = e.shiftKey ? -1 : 1; 514 | if (currentCell && currentCell.component && !currentCell.forceComponent) { 515 | e.preventDefault(); 516 | let func = this.onRevert; // ESCAPE_KEY 517 | if (keyCode === ENTER_KEY) { 518 | func = () => this.handleNavigate(e, { i: offset, j: 0 }); 519 | } else if (keyCode === TAB_KEY) { 520 | func = () => this.handleNavigate(e, { i: 0, j: offset }, true); 521 | } 522 | // setTimeout makes sure that component is done handling the event before we take over 523 | setTimeout(() => { 524 | func(); 525 | this.dgDom && this.dgDom.focus({ preventScroll: true }); 526 | }, 1); 527 | } 528 | } 529 | } 530 | 531 | onContextMenu(evt, i, j) { 532 | let cell = this.props.data[i][j]; 533 | if (this.props.onContextMenu) { 534 | this.props.onContextMenu(evt, cell, i, j); 535 | } 536 | } 537 | 538 | onDoubleClick(i, j) { 539 | let cell = this.props.data[i][j]; 540 | if (!cell.readOnly) { 541 | this._setState({ editing: { i: i, j: j }, forceEdit: true, clear: {} }); 542 | } 543 | } 544 | 545 | onMouseDown(i, j, e) { 546 | const isNowEditingSameCell = 547 | !isEmpty(this.state.editing) && 548 | this.state.editing.i === i && 549 | this.state.editing.j === j; 550 | let editing = 551 | isEmpty(this.state.editing) || 552 | this.state.editing.i !== i || 553 | this.state.editing.j !== j 554 | ? {} 555 | : this.state.editing; 556 | 557 | this._setState({ 558 | selecting: !isNowEditingSameCell, 559 | start: e.shiftKey ? this.getState().start : { i, j }, 560 | end: { i, j }, 561 | editing: editing, 562 | forceEdit: !!isNowEditingSameCell, 563 | }); 564 | 565 | var ua = window.navigator.userAgent; 566 | var isIE = /MSIE|Trident/.test(ua); 567 | // Listen for Ctrl + V in case of IE 568 | if (isIE) { 569 | document.addEventListener('keydown', this.handleIEClipboardEvents); 570 | } 571 | 572 | // Keep listening to mouse if user releases the mouse (dragging outside) 573 | document.addEventListener('mouseup', this.onMouseUp); 574 | // Listen for any outside mouse clicks 575 | document.addEventListener('mousedown', this.pageClick); 576 | 577 | // Cut, copy and paste event handlers 578 | document.addEventListener('cut', this.handleCut); 579 | document.addEventListener('copy', this.handleCopy); 580 | document.addEventListener('paste', this.handlePaste); 581 | } 582 | 583 | onMouseOver(i, j) { 584 | if (this.state.selecting && isEmpty(this.state.editing)) { 585 | this._setState({ end: { i, j } }); 586 | } 587 | } 588 | 589 | onMouseUp() { 590 | this._setState({ selecting: false }); 591 | document.removeEventListener('mouseup', this.onMouseUp); 592 | } 593 | 594 | onChange(row, col, value) { 595 | const { onChange, onCellsChanged, data } = this.props; 596 | if (onCellsChanged) { 597 | onCellsChanged([{ cell: data[row][col], row, col, value }]); 598 | } else if (onChange) { 599 | onChange(data[row][col], row, col, value); 600 | } 601 | this.onRevert(); 602 | } 603 | 604 | onRevert() { 605 | this._setState({ editing: {} }); 606 | // setTimeout makes sure that component is done handling the new state before we take over 607 | setTimeout(() => { 608 | this.dgDom && this.dgDom.focus({ preventScroll: true }); 609 | }, 1); 610 | } 611 | 612 | componentDidUpdate(prevProps, prevState) { 613 | let { start, end } = this.state; 614 | let prevEnd = prevState.end; 615 | if ( 616 | !isEmpty(end) && 617 | !(end.i === prevEnd.i && end.j === prevEnd.j) && 618 | !this.isSelectionControlled() 619 | ) { 620 | this.props.onSelect && this.props.onSelect({ start, end }); 621 | } 622 | } 623 | 624 | isSelectedRow(rowIndex) { 625 | const { start, end } = this.getState(); 626 | const startY = start.i; 627 | const endY = end.i; 628 | if (startY <= endY) { 629 | return rowIndex >= startY && rowIndex <= endY; 630 | } else { 631 | return rowIndex <= startY && rowIndex >= endY; 632 | } 633 | } 634 | 635 | isSelected(i, j) { 636 | const { start, end } = this.getState(); 637 | const posX = j >= start.j && j <= end.j; 638 | const negX = j <= start.j && j >= end.j; 639 | const posY = i >= start.i && i <= end.i; 640 | const negY = i <= start.i && i >= end.i; 641 | 642 | return (posX && posY) || (negX && posY) || (negX && negY) || (posX && negY); 643 | } 644 | 645 | isEditing(i, j) { 646 | return this.state.editing.i === i && this.state.editing.j === j; 647 | } 648 | 649 | isClearing(i, j) { 650 | return this.state.clear.i === i && this.state.clear.j === j; 651 | } 652 | 653 | render() { 654 | const { 655 | sheetRenderer: SheetRenderer, 656 | rowRenderer: RowRenderer, 657 | cellRenderer, 658 | dataRenderer, 659 | valueRenderer, 660 | dataEditor, 661 | valueViewer, 662 | attributesRenderer, 663 | className, 664 | overflow, 665 | data, 666 | keyFn, 667 | } = this.props; 668 | const { forceEdit } = this.state; 669 | return ( 670 | { 672 | this.dgDom = r; 673 | }} 674 | tabIndex="0" 675 | className="data-grid-container" 676 | onKeyDown={this.handleKey} 677 | > 678 | a) 682 | .join(' ')} 683 | > 684 | {data.map((row, i) => ( 685 | 691 | {row.map((cell, j) => { 692 | const isEditing = this.isEditing(i, j); 693 | return ( 694 | 723 | ); 724 | })} 725 | 726 | ))} 727 | 728 | 729 | ); 730 | } 731 | } 732 | 733 | DataSheet.propTypes = { 734 | data: PropTypes.array.isRequired, 735 | className: PropTypes.string, 736 | disablePageClick: PropTypes.bool, 737 | overflow: PropTypes.oneOf(['wrap', 'nowrap', 'clip']), 738 | onChange: PropTypes.func, 739 | onCellsChanged: PropTypes.func, 740 | onContextMenu: PropTypes.func, 741 | onSelect: PropTypes.func, 742 | isCellNavigable: PropTypes.func, 743 | selected: PropTypes.shape({ 744 | start: PropTypes.shape({ 745 | i: PropTypes.number, 746 | j: PropTypes.number, 747 | }), 748 | end: PropTypes.shape({ 749 | i: PropTypes.number, 750 | j: PropTypes.number, 751 | }), 752 | }), 753 | valueRenderer: PropTypes.func.isRequired, 754 | dataRenderer: PropTypes.func, 755 | sheetRenderer: PropTypes.func.isRequired, 756 | rowRenderer: PropTypes.func.isRequired, 757 | cellRenderer: PropTypes.func.isRequired, 758 | valueViewer: PropTypes.func, 759 | dataEditor: PropTypes.func, 760 | parsePaste: PropTypes.func, 761 | attributesRenderer: PropTypes.func, 762 | keyFn: PropTypes.func, 763 | handleCopy: PropTypes.func, 764 | editModeChanged: PropTypes.func, 765 | }; 766 | 767 | DataSheet.defaultProps = { 768 | sheetRenderer: Sheet, 769 | rowRenderer: Row, 770 | cellRenderer: Cell, 771 | valueViewer: ValueViewer, 772 | dataEditor: DataEditor, 773 | }; 774 | -------------------------------------------------------------------------------- /src/Row.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import CellShape from './CellShape'; 5 | 6 | class Row extends PureComponent { 7 | render() { 8 | return {this.props.children}; 9 | } 10 | } 11 | 12 | Row.propTypes = { 13 | row: PropTypes.number.isRequired, 14 | cells: PropTypes.arrayOf(PropTypes.shape(CellShape)).isRequired, 15 | }; 16 | 17 | export default Row; 18 | -------------------------------------------------------------------------------- /src/Sheet.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Sheet extends PureComponent { 5 | render() { 6 | return ( 7 | 8 | {this.props.children} 9 |
10 | ); 11 | } 12 | } 13 | 14 | Sheet.propTypes = { 15 | className: PropTypes.string, 16 | data: PropTypes.array.isRequired, 17 | }; 18 | 19 | export default Sheet; 20 | -------------------------------------------------------------------------------- /src/ValueViewer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import CellShape from './CellShape'; 5 | 6 | export default class ValueViewer extends PureComponent { 7 | render() { 8 | const { value } = this.props; 9 | return {value}; 10 | } 11 | } 12 | 13 | ValueViewer.propTypes = { 14 | row: PropTypes.number.isRequired, 15 | col: PropTypes.number.isRequired, 16 | cell: PropTypes.shape(CellShape), 17 | value: PropTypes.node.isRequired, 18 | }; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DataSheet from './DataSheet'; 2 | import Sheet from './Sheet'; 3 | import Row from './Row'; 4 | import Cell from './Cell'; 5 | import DataEditor from './DataEditor'; 6 | import ValueViewer from './ValueViewer'; 7 | import { renderValue, renderData } from './renderHelpers'; 8 | 9 | export default DataSheet; 10 | 11 | export { Sheet, Row, Cell, DataEditor, ValueViewer, renderValue, renderData }; 12 | -------------------------------------------------------------------------------- /src/keys.js: -------------------------------------------------------------------------------- 1 | export const TAB_KEY = 9; 2 | export const ENTER_KEY = 13; 3 | export const ESCAPE_KEY = 27; 4 | export const LEFT_KEY = 37; 5 | export const UP_KEY = 38; 6 | export const RIGHT_KEY = 39; 7 | export const DOWN_KEY = 40; 8 | export const DELETE_KEY = 46; 9 | export const BACKSPACE_KEY = 8; 10 | -------------------------------------------------------------------------------- /src/react-datasheet.css: -------------------------------------------------------------------------------- 1 | 2 | span.data-grid-container, span.data-grid-container:focus { 3 | outline: none; 4 | } 5 | 6 | .data-grid-container .data-grid { 7 | table-layout: fixed; 8 | border-collapse: collapse; 9 | } 10 | 11 | .data-grid-container .data-grid .cell.updated { 12 | background-color: rgba(0, 145, 253, 0.16); 13 | transition : background-color 0ms ease ; 14 | } 15 | .data-grid-container .data-grid .cell { 16 | height: 17px; 17 | user-select: none; 18 | -moz-user-select: none; 19 | -webkit-user-select: none; 20 | -ms-user-select: none; 21 | cursor: cell; 22 | background-color: unset; 23 | transition : background-color 500ms ease; 24 | vertical-align: middle; 25 | text-align: right; 26 | border: 1px solid #DDD; 27 | padding: 0; 28 | } 29 | .data-grid-container .data-grid .cell.selected { 30 | border: 1px double rgb(33, 133, 208); 31 | transition: none; 32 | box-shadow: inset 0 -100px 0 rgba(33, 133, 208, 0.15); 33 | } 34 | 35 | .data-grid-container .data-grid .cell.read-only { 36 | background: whitesmoke; 37 | color: #999; 38 | text-align: center; 39 | } 40 | 41 | .data-grid-container .data-grid .cell > .text { 42 | padding: 2px 5px; 43 | text-overflow: ellipsis; 44 | overflow: hidden; 45 | } 46 | 47 | 48 | .data-grid-container .data-grid .cell > input { 49 | outline: none !important; 50 | border: 2px solid rgb(33, 133, 208); 51 | text-align:right; 52 | width: calc(100% - 6px); 53 | height: 11px; 54 | background: none; 55 | display: block; 56 | } 57 | 58 | 59 | .data-grid-container .data-grid .cell { 60 | vertical-align: bottom; 61 | } 62 | 63 | .data-grid-container .data-grid .cell, 64 | .data-grid-container .data-grid.wrap .cell, 65 | .data-grid-container .data-grid.wrap .cell.wrap, 66 | .data-grid-container .data-grid .cell.wrap, 67 | .data-grid-container .data-grid.nowrap .cell.wrap, 68 | .data-grid-container .data-grid.clip .cell.wrap { 69 | white-space: normal; 70 | } 71 | 72 | .data-grid-container .data-grid.nowrap .cell, 73 | .data-grid-container .data-grid.nowrap .cell.nowrap, 74 | .data-grid-container .data-grid .cell.nowrap, 75 | .data-grid-container .data-grid.wrap .cell.nowrap, 76 | .data-grid-container .data-grid.clip .cell.nowrap { 77 | white-space: nowrap; 78 | overflow-x: visible; 79 | } 80 | 81 | .data-grid-container .data-grid.clip .cell, 82 | .data-grid-container .data-grid.clip .cell.clip, 83 | .data-grid-container .data-grid .cell.clip, 84 | .data-grid-container .data-grid.wrap .cell.clip, 85 | .data-grid-container .data-grid.nowrap .cell.clip { 86 | white-space: nowrap; 87 | overflow-x: hidden; 88 | } 89 | 90 | .data-grid-container .data-grid .cell .value-viewer, .data-grid-container .data-grid .cell .data-editor { 91 | display: block; 92 | } 93 | -------------------------------------------------------------------------------- /src/renderHelpers.js: -------------------------------------------------------------------------------- 1 | export function renderValue(cell, row, col, valueRenderer) { 2 | const value = valueRenderer(cell, row, col); 3 | return value === null || typeof value === 'undefined' ? '' : value; 4 | } 5 | 6 | export function renderData(cell, row, col, valueRenderer, dataRenderer) { 7 | const value = dataRenderer ? dataRenderer(cell, row, col) : null; 8 | return value === null || typeof value === 'undefined' 9 | ? renderValue(cell, row, col, valueRenderer) 10 | : value; 11 | } 12 | -------------------------------------------------------------------------------- /types/react-datasheet.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, ReactNode, KeyboardEventHandler, MouseEventHandler } from "react"; 2 | 3 | declare namespace ReactDataSheet { 4 | /** The cell object is what gets passed to the callbacks and events, and contains the basic information about what to show in each cell. You should extend this interface to build a place to store your data. 5 | * @example 6 | * interface GridElement extends ReactDataSheet.Cell { 7 | * value: number | string | null; 8 | * } 9 | */ 10 | export interface Cell, V = string> { 11 | /** Optional function or React Component to render a custom editor. Overrides grid-level dataEditor option. Default: undefined. */ 12 | dataEditor?: DataEditor 13 | /** Additional class names for cells. Default: undefined. */ 14 | className?: string | undefined; 15 | /** Insert a react element or JSX to this field. This will render on edit mode. Default: undefined. */ 16 | component?: JSX.Element; 17 | /** The colSpan of the cell's td element. Default: 1 */ 18 | colSpan?: number; 19 | /** If true, renders what's in component at all times, even when not in edit mode. Default: false. */ 20 | forceComponent?: boolean; 21 | /** By default, each cell is given the key of col number and row number. This would override that key. Default: undefined. */ 22 | key?: string | undefined; 23 | /** Makes cell unselectable and read only. Default: false. */ 24 | disableEvents?: boolean; 25 | /** How to render overflow text. Overrides grid-level overflow option. Default: undefined. */ 26 | overflow?: 'wrap' | 'nowrap' | 'clip'; 27 | /** If true, the cell will never go in edit mode. Default: false. */ 28 | readOnly?: boolean; 29 | /** The rowSpan of the cell's td element. Default: 1. */ 30 | rowSpan?: number; 31 | /** Optional function or React Component to customize the way the value for this cell is displayed. Overrides grid-level valueViewer option. Default: undefined. */ 32 | valueViewer?: ValueViewer; 33 | /** Sets the cell's td width using a style attribute. Number is interpreted as pixels, strings are used as-is. Note: This will only work if the table does not have a set width. Default: undefined. */ 34 | width?: number | string; 35 | } 36 | 37 | export interface Location { 38 | i: number, 39 | j: number 40 | } 41 | export interface Selection { 42 | start: Location, 43 | end: Location 44 | } 45 | /** Properties of the ReactDataSheet component. */ 46 | export interface DataSheetProps, V = string> { 47 | /** Optional function to add attributes to the rendered cell element. It should return an object with properties corresponding to the name and vales of the attributes you wish to add. */ 48 | attributesRenderer?: AttributesRenderer; 49 | /** Optional function or React Component to render each cell element. The default renders a td element. */ 50 | cellRenderer?: CellRenderer; 51 | className?: string; 52 | /** Array of rows and each row should contain the cell objects to display. */ 53 | data: T[][]; 54 | /** Optional: Avoid Datasheet to listen for clicks on the page */ 55 | disablePageClick?: boolean; 56 | /** Optional function or React Component to render a custom editor. Affects every cell in the sheet. Affects every cell in the sheet. See cell options to override individual cells. */ 57 | dataEditor?: DataEditor; 58 | /** Method to render the underlying value of the cell function(cell, i, j). This data is visible once in edit mode. */ 59 | dataRenderer?: DataRenderer; 60 | /** Method to handle copying from cell */ 61 | handleCopy?: HandleCopyFunction; 62 | /** onCellsChanged handler: function(arrayOfChanges[, arrayOfAdditions]) {}, where changes is an array of objects of the shape {cell, row, col, value}. */ 63 | onCellsChanged?: CellsChangedHandler; 64 | /** Context menu handler : function(event, cell, i, j). */ 65 | onContextMenu?: ContextMenuHandler; 66 | /** Grid default for how to render overflow text in cells. */ 67 | overflow?: 'wrap' | 'nowrap' | 'clip'; 68 | /** Optional function or React Component to render each row element. The default renders a element. */ 69 | rowRenderer?: RowRenderer; 70 | /** If set, the function will be called with the raw clipboard data. It should return an array of arrays of strings. This is useful for when the clipboard may have data with irregular field or line delimiters. If not set, rows will be split with line breaks and cells with tabs. */ 71 | parsePaste?: PasteParser; 72 | /** Optional function or React Component to render the main sheet element. The default renders a table element. */ 73 | sheetRenderer?: SheetRenderer; 74 | /** Method to render the value of the cell function(cell, i, j). This is visible by default. */ 75 | valueRenderer: ValueRenderer; 76 | /** Optional function or React Component to customize the way the value for each cell in the sheet is displayed. Affects every cell in the sheet. See cell options to override individual cells. */ 77 | valueViewer?: ValueViewer; 78 | /** Optional. Passing a selection format will make the selection controlled, pass a null for usual behaviour**/ 79 | selected?: Selection | null; 80 | /** Optional. Calls the function whenever the user changes selection**/ 81 | onSelect?: (selection: Selection) => void; 82 | /** Optional. Function to set row key. **/ 83 | keyFn?: (row: number) => string | number; 84 | /** Optional: Function that can decide whether navigating to the indicated cell is possible. */ 85 | isCellNavigable?: (cell: T, row: number, col: number, jumpNext: boolean) => boolean; 86 | /** Optional: Is called when datasheet changes edit mode. */ 87 | editModeChanged?: (inEditMode: boolean) => void; 88 | } 89 | 90 | /** A function to process the raw clipboard data. It should return an array of arrays of strings. This is useful for when the clipboard may have data with irregular field or line delimiters. If not set, rows will be split with line breaks and cells with tabs. To wire it up pass your function to the parsePaste property of the ReactDataSheet component. */ 91 | export type PasteParser = (pastedString: string) => string[][]; 92 | 93 | /** A function to render the value of the cell function(cell, i, j). This is visible by default. To wire it up, pass your function to the valueRenderer property of the ReactDataSheet component. */ 94 | export type ValueRenderer, V = string> = (cell: T, row: number, col: number) => string | number | null | void; 95 | 96 | /** A function to render the underlying value of the cell. This data is visible once in edit mode. To wire it up, pass your function to the dataRenderer property of the ReactDataSheet component. */ 97 | export type DataRenderer, V = string> = (cell: T, row: number, col: number) => string | number | null | void; 98 | 99 | /** A function to add attributes to the rendered cell element. It should return an object with properties corresponding to the name and vales of the attributes you wish to add. To wire it up, pass your function to the attributesRenderer property of the ReactDataSheet component. */ 100 | export type AttributesRenderer, V = string> = (cell: T, row: number, col: number) => {}; 101 | 102 | /** The properties that will be passed to the SheetRenderer component or function. */ 103 | export interface SheetRendererProps, V = string> { 104 | /** The same data array as from main ReactDataSheet component */ 105 | data: T[][]; 106 | /** Classes to apply to your top-level element. You can add to these, but your should not overwrite or omit them unless you want to implement your own CSS also. */ 107 | className: string; 108 | /** The regular react props.children. You must render {props.children} within your custom renderer or you won't see your rows and cells. */ 109 | children: ReactNode; 110 | } 111 | 112 | /** Optional function or React Component to render the main sheet element. The default renders a table element. To wire it up, pass your function to the sheetRenderer property of the ReactDataSheet component. */ 113 | export type SheetRenderer, V = string> = React.ComponentClass> | React.SFC>; 114 | 115 | /** The properties that will be passed to the RowRenderer component or function. */ 116 | export interface RowRendererProps, V = string> { 117 | /** The current row index */ 118 | row: number; 119 | /** The cells in the current row */ 120 | cells: T[]; 121 | /** The regular react props.children. You must render {props.children} within your custom renderer or you won't see your cells. */ 122 | children: ReactNode; 123 | } 124 | 125 | /** Optional function or React Component to render each row element. The default renders a tr element. To wire it up, pass your function to the rowRenderer property of the ReactDataSheet component. */ 126 | export type RowRenderer, V = string> = React.ComponentClass> | React.SFC>; 127 | 128 | /** The arguments that will be passed to the first parameter of the onCellsChanged handler function. These represent all the changes _inside_ the bounds of the existing grid. The first generic parameter (required) indicates the type of the cell property, and the second generic parameter (default: string) indicates the type of the value property. */ 129 | export type CellsChangedArgs, V = string> = { 130 | /** the original cell object you provided in the data property. This may be null */ 131 | cell: T | null; 132 | /** row index of changed cell */ 133 | row: number; 134 | /** column index of changed cell */ 135 | col: number; 136 | /** The new cell value. This is usually a string, but a custom editor may provide any type of value. */ 137 | value: V | null; 138 | }[]; 139 | 140 | /** The arguments that will be passed to the second parameter of the onCellsChanged handler function. These represent all the changes _outside_ the bounds of the existing grid. The generic parameter (default: string) indicates the type of the value property. */ 141 | export type CellAdditionsArgs = { 142 | row: number; 143 | col: number; 144 | value: V | null; 145 | }[]; 146 | 147 | /** onCellsChanged handler: function(arrayOfChanges[, arrayOfAdditions]) {}, where changes is an array of objects of the shape {cell, row, col, value}. To wire it up, pass your function to the onCellsChanged property of the ReactDataSheet component. */ 148 | export type CellsChangedHandler, V = string> = (arrayOfChanges: CellsChangedArgs, arrayOfAdditions?: CellAdditionsArgs) => void; 149 | 150 | /** Context menu handler : function(event, cell, i, j). To wire it up, pass your function to the onContextMenu property of the ReactDataSheet component. */ 151 | export type ContextMenuHandler, V = string> = (event: MouseEvent, cell: T, row : number, col: number) => void; 152 | 153 | /* Props available for handleCopy */ 154 | export interface HandleCopyProps, V = string> { 155 | event: Event; 156 | dataRenderer: DataRenderer; 157 | valueRenderer: ValueRenderer; 158 | data: T[][]; 159 | start: Location; 160 | end: Location; 161 | range: (start: number, end: number) => []; 162 | } 163 | 164 | export type HandleCopyFunction, V = string> = (props: HandleCopyProps) => void; 165 | 166 | /** The properties that will be passed to the CellRenderer component or function. */ 167 | export interface CellRendererProps, V = string> { 168 | /** The current row index */ 169 | row: number; 170 | /** The current column index */ 171 | col: number; 172 | /** The cell's raw data structure */ 173 | cell: T; 174 | /** Classes to apply to your cell element. You can add to these, but your should not overwrite or omit them unless you want to implement your own CSS also. */ 175 | className: string; 176 | /** Generated styles that you should apply to your cell element. This may be null or undefined. */ 177 | style: object | null | undefined; 178 | /** Is the cell currently selected */ 179 | selected: boolean; 180 | /** Is the cell currently being edited */ 181 | editing: boolean; 182 | /** Was the cell recently updated */ 183 | updated: boolean; 184 | /** As for the main ReactDataSheet component */ 185 | attributesRenderer: AttributesRenderer; 186 | /** Event handler: important for cell selection behavior */ 187 | onMouseDown: MouseEventHandler; 188 | /** Event handler: important for cell selection behavior */ 189 | onMouseOver: MouseEventHandler; 190 | /** Event handler: important for editing */ 191 | onDoubleClick: MouseEventHandler; 192 | /** Event handler: to launch default content-menu handling. You can safely ignore this handler if you want to provide your own content menu handling. */ 193 | onContextMenu: MouseEventHandler; 194 | /** Event handler: important for cell selection behavior */ 195 | onKeyUp: KeyboardEventHandler; 196 | /** The regular react props.children. You must render {props.children} within your custom renderer or you won't your cell's data. */ 197 | children: ReactNode; 198 | } 199 | 200 | /** A function or React Component to render each cell element. The default renders a td element. To wire it up, pass it to the cellRenderer property of the ReactDataSheet component. */ 201 | export type CellRenderer, V = string> = React.ComponentClass> | React.SFC>; 202 | 203 | /** The properties that will be passed to the CellRenderer component or function. */ 204 | export interface ValueViewerProps, V = string> { 205 | /** The result of the valueRenderer function */ 206 | value: string | number | null; 207 | /** The current row index */ 208 | row: number; 209 | /** The current column index */ 210 | col: number;     211 | /** The cell's raw data structure */ 212 | cell: T; 213 | } 214 | 215 | /** Optional function or React Component to customize the way the value for each cell in the sheet is displayed. If it is passed to the valueViewer property of the ReactDataSheet component, it affects every cell in the sheet. Different editors can also be passed to the valueViewer property of each Cell to control each cell separately. */ 216 | export type ValueViewer, V = string> = React.ComponentClass> | React.SFC>; 217 | 218 | /** The properties that will be passed to the DataEditor component or function. */ 219 | export interface DataEditorProps { 220 | /** The result of the dataRenderer (or valueRenderer if none) */ 221 | value: string | number | null; 222 | /** The current row index */ 223 | row: number; 224 | /** The current column index */ 225 | col: number; 226 | /** The cell's raw data structure */ 227 | cell: T; 228 | /** A callback for when the user changes the value during editing (for example, each time they type a character into an input). onChange does not indicate the final edited value. It works just like a controlled component in a form. */ 229 | onChange: (newValue: V) => void; 230 | /** An event handler that you can call to use default React-DataSheet keyboard handling to signal reverting an ongoing edit (Escape key) or completing an edit (Enter or Tab). For most editors based on an input element this will probably work. However, if this keyboard handling is unsuitable for your editor you can trigger these changes explicitly using the onCommit and onRevert callbacks. */ 231 | onKeyDown: React.KeyboardEventHandler; 232 | /** function (newValue, [event]) {} A callback to indicate that editing is over, here is the final value. If you pass a KeyboardEvent as the second argument, React-DataSheet will perform default navigation for you (for example, going down to the next row if you hit the enter key). You actually don't need to use onCommit if the default keyboard handling is good enough for you. */ 233 | onCommit: (newValue: V, event?: React.KeyboardEvent) => void; 234 | /** function () {} A no-args callback that you can use to indicate that you want to cancel ongoing edits. As with onCommit, you don't need to worry about this if the default keyboard handling works for your editor. */ 235 | onRevert: () => void; 236 | } 237 | 238 | /** A function or React Component to render a custom editor. If it is passed to the dataEditor property of the ReactDataSheet component, it affects every cell in the sheet. Different editors can also be passed to the dataEditor property of each Cell to control each cell separately. */ 239 | export type DataEditor, V = string> = React.ComponentClass> | React.SFC>; 240 | 241 | export interface CellReference { 242 | row: number; 243 | col: number; 244 | } 245 | 246 | export interface DataSheetState { 247 | start?: CellReference; 248 | end?: CellReference; 249 | selecting?: boolean; 250 | forceEdit?: boolean; 251 | editing?: CellReference; 252 | clear?: CellReference; 253 | } 254 | } 255 | 256 | declare class ReactDataSheet, V = string> extends Component, ReactDataSheet.DataSheetState> { 257 | getSelectedCells: (data: T[][], start: ReactDataSheet.CellReference, end: ReactDataSheet.CellReference) => {cell: T, row: number, col: number}[]; 258 | } 259 | 260 | export default ReactDataSheet; 261 | --------------------------------------------------------------------------------