Compare commits

...
29 Commits
Author SHA1 Message Date
Tanner Linsley 3b39f523be 5.5.0 2017-04-05 18:51:31 -06:00
Aaron SchwartzandTanner Linsley 9d85981c5b Resize columns (#170)
* Add column resizing for non pivot columns.

* Fixing resizing UI issues and mobile functionality.

* Remove calling onChange during resize events so that server example doesn't refetch data every time a column resizes.
2017-04-05 17:47:55 -06:00
Eric AbbottandTanner Linsley b779a98d7a Change precedence in 'getResolvedState' (#166)
* Change precedence in 'getResolvedState'
  * Previously existing props would overwrite passed in state
  * Now passed in state gets precedence

* added a controlled table example to storybook
2017-04-05 01:10:17 -06:00
Jolyon RussandTanner Linsley 1f4c9afd2b Updated README - wrapped example in render function (#168) 2017-04-04 13:24:03 -06:00
Aaron SchwartzandTanner Linsley cef7fbd780 Add filterRender column option to customize the filter that is shown (#162)
* Add filterRender column option to completely customize the filter that is shown

* Refactor filterRender as a defaultProp.
2017-03-31 15:41:57 -06:00
Tanner Linsley e8ebf7c780 5.4.1 2017-03-30 17:03:47 -06:00
Aaron SchwartzandTanner Linsley ac20220d55 Set page to max page available if data changes and has less pages than before (#160)
* Add column filtering.

* Fix javascript warning from yarn test. Compile storybook and docs.

* Pass standard linting

* Add support for filtering pivot columns.

* Build distribution files.

* Set page to max page available if data changes and has less pages than before.

Closes #159
2017-03-30 17:02:39 -06:00
Tanner Linsley 9acf8f37eb 5.4.0 2017-03-30 10:21:59 -06:00
Mike DeVitaandTanner Linsley 64a427d91b fix broken TOC Link (#157)
Custom Cell Header Rendering link was broken in table of contents.
2017-03-30 09:53:52 -06:00
Mike DeVitaandTanner Linsley d21ce226aa create ISSUE_TEMPLATE.md (#158)
hopefully this will alleviate some clutter on issues.
2017-03-30 09:52:40 -06:00
Tanner LinsleyandGitHub 92c9ae2196 minRows now works for datasets larger than 1 page
Closes #154
2017-03-29 18:02:33 -06:00
Aaron SchwartzandTanner Linsley 23a031b48a Add column filtering (#147)
* Add column filtering.

* Fix javascript warning from yarn test. Compile storybook and docs.

* Pass standard linting

* Add support for filtering pivot columns.

* Build distribution files.
2017-03-29 16:46:04 -06:00
Jolyon RussandTanner Linsley bd8b273dea Added an id in the example so it doesn't error (#151) 2017-03-29 07:08:54 -06:00
Tanner Linsley 1a9f0e57b4 5.3.5 2017-03-27 11:57:44 -06:00
Ryan PalermoandTanner Linsley 4c11248c69 switch -sort-asc and -sort-desc in ternary (#138) 2017-03-23 13:49:03 -06:00
Tanner Linsley 784311bcd6 Remove .DS_Store
Closes #126
2017-03-14 12:18:46 -06:00
Deepak Kumar JainandTanner Linsley 7b232cd6b4 Updated the package.json (#124) 2017-03-12 09:29:32 -06:00
MishaandTanner Linsley 533bea77b6 use button type button in pagination to prevent form submitting if using table inside of the form (#122) 2017-03-09 09:14:49 -07:00
MishaandTanner Linsley 218cf42bb2 removed form tag from pagination component to avoid nested forms error (#119) 2017-03-09 07:34:44 -07:00
Wojtek BażantandTanner Linsley 62287d1641 Add extra empty line to get the accessors table to show up nicely on github's main page (#121) 2017-03-09 07:34:22 -07:00
Ryan PalermoandTanner Linsley 10dc68acc1 use Object.getPrototypeOf instead of .prototype (#117) 2017-03-08 11:43:29 -07:00
Michael BridgeandTanner Linsley bd82468852 Pass relevant attributes to footer JSX (#116) 2017-03-08 11:42:12 -07:00
Tanner Linsley 6d28bb2c4d 5.3.4 2017-03-01 15:50:43 -07:00
Tanner Linsley 9e95b331df Feature: Changing defaultSorting now updates the sorting model 2017-03-01 15:50:36 -07:00
TacticalCodingandTanner Linsley f296b075fe Rename CollapseOnPageChange to CollapseOnDataChange (#100)
* Rename CollapseOnPageChange to CollapseOnDataChange to more accurately reflect its purpose.

* Add support for collapseOnPageChange.

* Fix collapseOnPageChange efficiency.
2017-02-27 16:05:07 -07:00
Tanner Linsley a948a9a41e 5.3.3 2017-02-27 10:01:06 -07:00
atlantehandTanner Linsley 08dbe38ced fix number of callback parameters and alignment (#95) 2017-02-27 08:15:45 -07:00
Daniel WinterandTanner Linsley 884058b459 added missing named export for ReactTableDefaults (#93) 2017-02-19 07:09:29 -07:00
Tanner Linsley ee37ad7101 Update Docs 2017-02-16 15:15:47 -07:00
29 changed files with 1332 additions and 297 deletions
Vendored
BIN
View File
Binary file not shown.
+2
View File
@@ -3,3 +3,5 @@ lib/
react-table.js
react-table.css
*.log
.idea
.DS_Store
+4
View File
@@ -24,6 +24,8 @@ import FunctionalRendering from '../stories/FunctionalRendering.js'
import CustomExpanderPosition from '../stories/CustomExpanderPosition.js'
import NoDataText from '../stories/NoDataText.js'
import Footers from '../stories/Footers.js'
import Filtering from '../stories/Filtering.js'
import ControlledTable from '../stories/ControlledTable.js'
//
configure(() => {
storiesOf('1. Docs')
@@ -53,4 +55,6 @@ configure(() => {
.add('Custom Expander Position', CustomExpanderPosition)
.add('Custom "No Data" Text', NoDataText)
.add('Footers', Footers)
.add('Custom Filtering', Filtering)
.add('Controlled Component', ControlledTable)
}, module)
+12
View File
@@ -4,3 +4,15 @@ body {
font-family: 'Open Sans', sans-serif;
font-weight: 300;
}
h1 {
font-size: 24px;
font-weight: bold;
margin-bottom: 15px;
margin-top: 5px;
}
p {
margin-bottom: 15px;
line-height: 22px;
}
+15
View File
@@ -0,0 +1,15 @@
## Problem Description
include a detailed explanation of the problem here...
## Code Snippet(s)
## Steps to Reproduce
1. list the steps
2. to reproduce the issue
## System Information
**Browser & Browser Version:**
**Node Version:**
**OS Version:**
**NPM Version:**
**Yarn Version:**
+104 -47
View File
@@ -47,7 +47,7 @@
- [Props](#props)
- [Columns](#columns)
- [Column Header Groups](#column-header-groups)
- [Custom Cell and Header Rendering](#custom-cell-and-header-rendering)
- [Custom Cell and Header and Footer Rendering](#custom-cell-header-and-footer-rendering)
- [Styles](#styles)
- [Custom Props](#custom-props)
- [Pivoting and Aggregation](#pivoting-and-aggregation)
@@ -56,6 +56,7 @@
- [Fully Controlled Component](#fully-controlled-component)
- [Functional Rendering](#functional-rendering)
- [Multi-Sort](#multi-sort)
- [Filtering](#filtering)
- [Component Overrides](#component-overrides)
- [Contributing](#contributing)
- [Scripts](#scripts)
@@ -89,36 +90,39 @@ import 'react-table/react-table.css'
```javascript
import ReactTable from 'react-table'
const data = [{
name: 'Tanner Linsley',
age: 26,
friend: {
name: 'Jason Maurer',
age: 23,
}
},{
...
}]
render() {
const data = [{
name: 'Tanner Linsley',
age: 26,
friend: {
name: 'Jason Maurer',
age: 23,
}
},{
...
}]
const columns = [{
header: 'Name',
accessor: 'name' // String-based value accessors!
}, {
header: 'Age',
accessor: 'age',
render: props => <span className='number'>{props.value}</span> // Custom cell components!
}, {
header: 'Friend Name',
accessor: d => d.friend.name // Custom value accessors!
}, {
header: props => <span>Friend Age</span>, // Custom header components!
accessor: 'friend.age'
}]
const columns = [{
header: 'Name',
accessor: 'name' // String-based value accessors!
}, {
header: 'Age',
accessor: 'age',
render: props => <span className='number'>{props.value}</span> // Custom cell components!
}, {
id: 'friendName', // Required because our accessor is not a string
header: 'Friend Name',
accessor: d => d.friend.name // Custom value accessors!
}, {
header: props => <span>Friend Age</span>, // Custom header components!
accessor: 'friend.age'
}]
<ReactTable
data={data}
columns={columns}
/>
<ReactTable
data={data}
columns={columns}
/>
}
```
## Data
@@ -138,21 +142,32 @@ These are all of the available props (and their default values) for the main `<R
defaultPageSize: 20,
showPageJump: true,
expanderColumnWidth: 35,
collapseOnSortingChange: false,
collapseOnSortingChange: true,
collapseOnPageChange: true,
collapseOnDataChange: true,
freezeWhenExpanded: false,
defaultSorting: [],
showFilters: false,
defaultFiltering: [],
defaultFilterMethod: (filter, row, column) => {
const id = filter.pivotId || filter.id
return row[id] !== undefined ? String(row[id]).startsWith(filter.value) : true
},
resizable: true,
defaultResizing: [],
// Controlled State Overrides (see Fully Controlled Component section)
page: undefined,
pageSize: undefined,
sorting: undefined
sorting: undefined,
// Controlled State Callbacks
onExpandSubComponent: undefined,
onPageChange: undefined,
onPageSizeChange: undefined,
onSortingChange: undefined,
onFilteringChange: undefined,
onResize: undefined,
// Pivoting
pivotBy: undefined,
@@ -183,6 +198,9 @@ These are all of the available props (and their default values) for the main `<R
getTheadProps: () => ({}),
getTheadTrProps: () => ({}),
getTheadThProps: () => ({}),
getTheadFilterProps: () => ({}),
getTheadFilterTrProps: () => ({}),
getTheadFilterThProps: () => ({}),
getTbodyProps: () => ({}),
getTrGroupProps: () => ({}),
getTrProps: () => ({}),
@@ -194,6 +212,7 @@ These are all of the available props (and their default values) for the main `<R
getPaginationProps: () => ({}),
getLoadingProps: () => ({}),
getNoDataProps: () => ({}),
getResizerProps: () => ({}),
// Global Column Defaults
column: {
@@ -214,7 +233,18 @@ These are all of the available props (and their default values) for the main `<R
footer: undefined,
footerClassName: '',
footerStyle: {},
getFooterProps: () => ({})
getFooterProps: () => ({}),
filterMethod: undefined,
hideFilter: false,
filterRender: ({filter, onFilterChange}) => (
<input type='text'
style={{
width: '100%'
}}
value={filter ? filter.value : ''}
onChange={(event) => onFilterChange(event.target.value)}
/>
)
},
// Text
@@ -256,16 +286,16 @@ Or just define them as props
```javascript
[{
// General
accessor: 'propertyName' // or Accessor eg. (row) => row.propertyName (see "Accessors" section for more details)
accessor: 'propertyName', // or Accessor eg. (row) => row.propertyName (see "Accessors" section for more details)
id: 'myProperty', // Conditional - A unique ID is required if the accessor is not a string or if you would like to override the column name used in server-side calls
sortable: true,
show: true, // can be used to hide a column
width: undefined, // A hardcoded width for the column. This overrides both min and max width options
minWidth: 100 // A minimum width for this column. If there is extra room, column will flex to fill available space (up to the max-width, if set)
maxWidth: undefined // A maximum width for this column.
minWidth: 100, // A minimum width for this column. If there is extra room, column will flex to fill available space (up to the max-width, if set)
maxWidth: undefined, // A maximum width for this column.
// Special
expander: false // This option will override all data-related options and designates the column to be used
expander: false, // This option will override all data-related options and designates the column to be used
// for pivoting and sub-component expansion
// Cell Options
@@ -282,17 +312,24 @@ Or just define them as props
header: 'Header Name', a function that returns a primitive, or JSX / React Component eg. ({data, column}) => <div>Header Name</div>,
headerClassName: '', // Set the classname of the `th` element of the column
headerStyle: {}, // Set the style of the `th` element of the column
getHeaderProps: (state, rowInfo, column, instance) => ({}) // a function that returns props to decorate the `th` element of the column
getHeaderProps: (state, rowInfo, column, instance) => ({}), // a function that returns props to decorate the `th` element of the column
// Header Groups only
columns: [...] // See Header Groups section below
columns: [...], // See Header Groups section below
// Footer
footer: 'Header Name' or JSX eg. ({data, column}) => <div>Header Name</div>,
footer: 'Footer Name' or JSX eg. ({data, column}) => <div>Footer Name</div>,
footerClassName: '', // Set the classname of the `td` element of the column's footer
footerStyle: {}, // Set the style of the `td` element of the column's footer
getFooterProps: (state, rowInfo, column, instance) => ({}) // a function that returns props to decorate the `td` element of the column's footer
getFooterProps: (state, rowInfo, column, instance) => ({}), // A function that returns props to decorate the `td` element of the column's footer
// Filtering
filterMethod: (filter, row, column) => {return true}, // A function returning a boolean that specifies the filtering logic for the column
// filter == an object specifying which filter is being applied. Format: {id: [the filter column's id], value: [the value the user typed in the filter field], pivotId: [if filtering on a pivot column, the pivotId will be set to the pivot column's id and the `id` field will be set to the top level pivoting column]}
// row == the row of data supplied to the table
// column == the column that the filter is on
hideFilter: false, // If `showFilters` is set on the table, this option will let you selectively hide the filter on a particular row
filterRender: JSX // eg. ({filter, onFilterChange}) => <select onChange={event => onFilterChange(event.target.value)} value={filter ? filter.value : ''}></select> // The value passed to onFilterChange will be the value passed to filter.value of the filterMethod
}]
```
@@ -307,6 +344,7 @@ Any values that contain bracket (`[]`) will be split.
This array is then used as the path to the value to return.
("$" is the placeholder value that would be returned by the default accessor)
| value | path | data |
|--------------|-----------------|------------------------|
| "a" | ["a"] | {"a": $} |
@@ -415,14 +453,16 @@ Every single built-in component's props can be dynamically extended using any on
getTdProps={fn}
getPaginationProps={fn}
getLoadingProps={fn}
getNoDataProps: {fn},
getResizerProps: {fn}
/>
```
These callbacks are executed with each render of the element with three parameters:
1. Table State
1. RowInfo (undefined if not applicable)
1. Column (undefined if not applicable)
1. React Table Instance
These callbacks are executed with each render of the element with four parameters:
1. Table State
2. RowInfo (undefined if not applicable)
3. Column (undefined if not applicable)
4. React Table Instance
This makes it extremely easy to add, say... a row click callback!
```javascript
@@ -530,7 +570,7 @@ By adding a `SubComponent` props, you can easily add an expansion level to all r
## Server-side Data
If you want to handle pagination, and sorting on the server, `react-table` makes it easy on you.
If you want to handle pagination, sorting, and filtering on the server, `react-table` makes it easy on you.
1. Feed React Table `data` from somewhere dynamic. eg. `state`, a redux store, etc...
1. Add `manual` as a prop. This informs React Table that you'll be handling sorting and pagination server-side
@@ -553,7 +593,8 @@ If you want to handle pagination, and sorting on the server, `react-table` makes
Axios.post('mysite.com/data', {
page: state.page,
pageSize: state.pageSize,
sorting: state.sorting
sorting: state.sorting,
filtering: state.filtering
})
.then((res) => {
// Update react-table
@@ -599,6 +640,8 @@ Here are the props and their corresponding callbacks that control the state of t
onPageSizeChange={(pageSize, pageIndex) => {...}} // Called when the pageSize is changed by the user. The resolve page is also sent to maintain approximate position in the data
onSortingChange={(column, shiftKey) => {...}} // Called when a sortable column header is clicked with the column itself and if the shiftkey was held. If the column is a pivoted column, `column` will be an array of columns
onExpandRow={(index, event) => {...}} // Called when an expander is clicked. Use this to manage `expandedRows`
onFilteringChange={(column, value) => {...}} // Called when a user enters a value into a filter input field or the value passed to the onFilterChange handler by the filterRender option.
onResize={(column, event, isTouch) => {...}} // Called when a user clicks on a resizing component (the right edge of a column header)
/>
```
@@ -642,6 +685,19 @@ The possibilities are endless!
## Multi-Sort
When clicking on a column header, hold shift to multi-sort! You can toggle `ascending` `descending` and `none` for multi-sort columns. Clicking on a header without holding shift will clear the multi-sort and replace it with the single sort of that column. It's quite handy!
## Filtering
Filtering can be enabled by setting the `showFilters` option on the table.
If you don't want particular column to be filtered you can set the `hideFilter` option on the column.
By default the table tries to filter by checking if the row's value starts with the filter text. The default method for filtering the table can be set with the table's `defaultFilterMethod` option.
If you want to override a particular column's filtering method, you can set the `filterMethod` option on a column.
To completely override the filter that is shown, you can set the `filterRender` column option. Using this option you can specify the JSX that is shown. The option is passed an `onFilterChange` method which must be called with the the value that you wan't to pass to the `filterMethod` option whenever the filter has changed.
See <a href="http://react-table.js.org/?selectedKind=2.%20Demos&selectedStory=Custom%20Filtering&full=0&down=1&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel" target="\_parent">Custom Filtering</a> demo for examples.
## Component Overrides
Though we confidently stand by the markup and architecture behind it, `react-table` does offer the ability to change the core componentry it uses to render everything. You can extend or override these internal components by passing a react component to it's corresponding prop on either the global props or on a one-off basis like so:
```javascript
@@ -662,6 +718,7 @@ Object.assign(ReactTableDefaults, {
NextComponent: undefined,
LoadingComponent: component,
NoDataComponent: component,
ResizerComponent: component
})
// Or change per instance
+1 -1
View File
@@ -16,7 +16,7 @@
<body>
<div id="root"></div>
<div id="error-display"></div>
<script src="static/preview.437e4f72f80621de5f68.bundle.js"></script>
<script src="static/preview.0cd0bb2bf09f3220bea7.bundle.js"></script>
</body>
</html>
+1 -1
View File
@@ -38,7 +38,7 @@
</head>
<body style="margin: 0;">
<div id="root"></div>
<script src="static/manager.3f690da6fe3fd9729ee0.bundle.js"></script>
<script src="static/manager.064c8fe6b78907f0a142.bundle.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"version":3,"file":"static/manager.064c8fe6b78907f0a142.bundle.js","sources":["webpack:///static/manager.064c8fe6b78907f0a142.bundle.js"],"mappings":"AAAA;AAkuDA;AA84DA;AAy8DA;AA00DA;AAsyEA;AA89CA;AA+rDA;AAsiDA;AAg6DA;AA2nDA;AA++CA;AAkvDA;AAsnEA;AA2oDA;AAivCA;AA+nDA;AAkpDA;AA6lEA;AAs4DA;AAquDA;AA+pDA;AAqxDA;AAsrDA;AA4yDA;AA88GA;AAj6CA;AAopGA;AAsuFA;AA+zEA;AAtMA;AAizEA","sourceRoot":""}
@@ -1 +0,0 @@
{"version":3,"file":"static/manager.3f690da6fe3fd9729ee0.bundle.js","sources":["webpack:///static/manager.3f690da6fe3fd9729ee0.bundle.js"],"mappings":"AAAA;AAkuDA;AA84DA;AA28DA;AA00DA;AAsyEA;AA89CA;AA+rDA;AAsiDA;AAg6DA;AA2nDA;AA++CA;AAkvDA;AAsnEA;AA2oDA;AAivCA;AA+nDA;AAkpDA;AA6lEA;AAs4DA;AAquDA;AA+pDA;AAoxDA;AAsrDA;AA4yDA;AA88GA;AAj6CA;AAopGA;AAsuFA;AA+zEA;AAtMA;AAizEA","sourceRoot":""}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"version":3,"file":"static/preview.0cd0bb2bf09f3220bea7.bundle.js","sources":["webpack:///static/preview.0cd0bb2bf09f3220bea7.bundle.js"],"mappings":"AAAA;AAkuDA;AA62DA;AAwtFA;AAsiFA;AA0uOA;AAgmGA;AA6rDA;AA0iEA;AA0xDA;AA25DA;AAmqDA;AA0vDA;AA+9CA;AAg5DA;AAwoDA;AAk7CA;AA2pDA;AAmjEA;AA27DA;AAghCA;AA4vDA;AA8kDA;AAwpEA;AAk3DA;AAy0DA;AA8xCA;AAu3EA;AAi4GA;AAwoDA;AAk+CA;AA+oCA;AAutCA;AA4jDA;AAwcA;AA+jFA","sourceRoot":""}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
{"version":3,"file":"static/preview.437e4f72f80621de5f68.bundle.js","sources":["webpack:///static/preview.437e4f72f80621de5f68.bundle.js"],"mappings":"AAAA;AAkuDA;AAq5DA;AA8xFA;AA2+EA;AA++NA;AA06GA;AAgsDA;AA8/DA;AA4xDA;AAigEA;AAmjDA;AA4sDA;AAk8CA;AAu/DA;AA8lDA;AA09CA;AAqoDA;AAywEA;AAokDA;AAsvCA;AA2mDA;AA4tDA;AAqjEA;AAs2DA;AAmyDA;AAsnDA;AAszFA;AA65FA;AA4/CA;AAwzCA;AAmlCA;AAotDA;AAyrBA;AAkPA;AA84EA","sourceRoot":""}
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "react-table",
"version": "5.3.2",
"version": "5.5.0",
"description": "A fast, lightweight, opinionated table and datagrid built on React",
"license": "MIT",
"homepage": "https://github.com/tannerlinsley/react-table#readme",
@@ -24,13 +24,13 @@
],
"scripts": {
"build:node": "babel src --out-dir lib --source-maps inline",
"build:css": "rm -rf react-table.css && stylus src/index.styl --compress -o react-table.css && yarn run css:autoprefix",
"build:css": "rimraf react-table.css && stylus src/index.styl --compress -o react-table.css && yarn run css:autoprefix",
"css:autoprefix": "postcss --use autoprefixer react-table.css -r",
"watch": "npm-run-all --parallel watch:*",
"watch:node": "onchange 'src/**/*.js' -i -- npm run build:node",
"watch:css": "onchange 'src/**/*.styl' -i -- npm run build:css",
"test": "standard",
"umd": "rm -rf react-table.js && browserify lib/index.js -s reactTable -x react -t babelify -g uglifyify -o react-table.js",
"umd": "rimraf react-table.js && browserify lib/index.js -s reactTable -x react -t babelify -g uglifyify -o react-table.js",
"prepublish": "npm-run-all build:* && npm run umd",
"storybook": "start-storybook -p 8000 -c .storybook",
"docs": "build-storybook -o docs && cp .storybook/CNAME docs/CNAME"
@@ -61,6 +61,7 @@
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-json-tree": "^0.10.1",
"rimraf": "^2.6.1",
"standard": "^8.0.0",
"storybook": "^0.0.0",
"stylus": "^0.54.5",
+30 -3
View File
@@ -16,10 +16,19 @@ export default {
defaultPageSize: 20,
showPageJump: true,
expanderColumnWidth: 35,
collapseOnSortingChange: false,
collapseOnSortingChange: true,
collapseOnPageChange: true,
collapseOnDataChange: true,
freezeWhenExpanded: false,
defaultSorting: [],
showFilters: false,
defaultFiltering: [],
defaultFilterMethod: (filter, row, column) => {
const id = filter.pivotId || filter.id
return row[id] !== undefined ? String(row[id]).startsWith(filter.value) : true
},
resizable: true,
defaultResizing: [],
// Controlled State Overrides
// page: undefined,
@@ -31,6 +40,8 @@ export default {
onPageChange: undefined,
onPageSizeChange: undefined,
onSortingChange: undefined,
onFilteringChange: undefined,
onResize: undefined,
// Pivoting
pivotBy: undefined,
@@ -61,6 +72,9 @@ export default {
getTheadProps: emptyObj,
getTheadTrProps: emptyObj,
getTheadThProps: emptyObj,
getTheadFilterProps: emptyObj,
getTheadFilterTrProps: emptyObj,
getTheadFilterThProps: emptyObj,
getTbodyProps: emptyObj,
getTrGroupProps: emptyObj,
getTrProps: emptyObj,
@@ -71,6 +85,7 @@ export default {
getPaginationProps: emptyObj,
getLoadingProps: emptyObj,
getNoDataProps: emptyObj,
getResizerProps: emptyObj,
// Global Column Defaults
column: {
@@ -91,7 +106,18 @@ export default {
footer: undefined,
footerClassName: '',
footerStyle: {},
getFooterProps: emptyObj
getFooterProps: emptyObj,
filterMethod: undefined,
hideFilter: false,
filterRender: ({filter, onFilterChange}) => (
<input type='text'
style={{
width: '100%'
}}
value={filter ? filter.value : ''}
onChange={(event) => onFilterChange(event.target.value)}
/>
)
},
// Text
@@ -148,5 +174,6 @@ export default {
</div>
</div>
),
NoDataComponent: _.makeTemplateComponent('rt-noData')
NoDataComponent: _.makeTemplateComponent('rt-noData'),
ResizerComponent: _.makeTemplateComponent('rt-resizer')
}
+222 -50
View File
@@ -4,6 +4,9 @@ import classnames from 'classnames'
import _ from './utils'
import lifecycle from './lifecycle'
import methods from './methods'
import defaults from './defaultProps'
export const ReactTableDefaults = defaults
export default React.createClass({
...lifecycle,
@@ -23,6 +26,9 @@ export default React.createClass({
getTheadProps,
getTheadTrProps,
getTheadThProps,
getTheadFilterProps,
getTheadFilterTrProps,
getTheadFilterThProps,
getTbodyProps,
getTrGroupProps,
getTrProps,
@@ -33,16 +39,21 @@ export default React.createClass({
getPaginationProps,
getLoadingProps,
getNoDataProps,
getResizerProps,
showPagination,
expanderColumnWidth,
manual,
loadingText,
noDataText,
showFilters,
resizable,
// State
loading,
pageSize,
page,
sorting,
filtering,
resizing,
pages,
// Pivoting State
pivotValKey,
@@ -63,6 +74,7 @@ export default React.createClass({
LoadingComponent,
SubComponent,
NoDataComponent,
ResizerComponent,
// Data model
resolvedData,
allVisibleColumns,
@@ -77,9 +89,7 @@ export default React.createClass({
const endRow = startRow + pageSize
const pageRows = manual ? resolvedData : sortedData.slice(startRow, endRow)
const minRows = this.getMinRows()
const padRows = pages > 1 ? _.range(pageSize - pageRows.length)
: minRows ? _.range(Math.max(minRows - pageRows.length, 0))
: []
const padRows = _.range(Math.max(minRows - pageRows.length, 0))
const hasColumnFooter = allVisibleColumns.some(d => d.footer)
@@ -100,7 +110,10 @@ export default React.createClass({
const canPrevious = page > 0
const canNext = page + 1 < pages
const rowMinWidth = _.sum(allVisibleColumns.map(d => _.getFirstDefined(d.width, d.minWidth)))
const rowMinWidth = _.sum(allVisibleColumns.map(d => {
const resized = resizing.find(x => x.id === d.id) || {}
return _.getFirstDefined(resized.value, d.width, d.minWidth)
}))
let rowIndex = -1
@@ -143,9 +156,18 @@ export default React.createClass({
}
const makeHeaderGroup = (column, i) => {
const flex = _.sum(column.columns.map(d => d.width ? 0 : d.minWidth))
const width = _.sum(column.columns.map(d => _.getFirstDefined(d.width, d.minWidth)))
const maxWidth = _.sum(column.columns.map(d => _.getFirstDefined(d.width, d.maxWidth)))
const flex = _.sum(column.columns.map(d => {
const resized = resizing.find(x => x.id === d.id) || {}
return d.width || resized.value ? 0 : d.minWidth
}))
const width = _.sum(column.columns.map(d => {
const resized = resizing.find(x => x.id === d.id) || {}
return _.getFirstDefined(resized.value, d.width, d.minWidth)
}))
const maxWidth = _.sum(column.columns.map(d => {
const resized = resizing.find(x => x.id === d.id) || {}
return _.getFirstDefined(resized.value, d.width, d.maxWidth)
}))
const theadGroupThProps = _.splitProps(getTheadGroupThProps(finalState, undefined, column, this))
const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column, this))
@@ -249,10 +271,11 @@ export default React.createClass({
}
const makeHeader = (column, i) => {
const resized = resizing.find(x => x.id === column.id) || {}
const sort = sorting.find(d => d.id === column.id)
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(column.width, column.minWidth)
const maxWidth = _.getFirstDefined(column.width, column.maxWidth)
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const theadThProps = _.splitProps(getTheadThProps(finalState, undefined, column, this))
const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column, this))
@@ -273,6 +296,14 @@ export default React.createClass({
...columnHeaderProps.rest
}
const resizer = resizable ? (
<ResizerComponent
onMouseDown={e => this.resizeColumnStart(column, e, false)}
onTouchStart={e => this.resizeColumnStart(column, e, true)}
{...resizerProps}
/>
) : null
if (column.expander) {
if (column.pivotColumns) {
const pivotSort = sorting.find(d => d.id === column.id)
@@ -281,9 +312,10 @@ export default React.createClass({
key={i}
className={classnames(
'rt-pivot-header',
'rt-resizable-header',
column.sortable && '-cursor-pointer',
classes,
pivotSort ? (pivotSort.desc ? '-sort-asc' : '-sort-desc') : ''
pivotSort ? (pivotSort.desc ? '-sort-desc' : '-sort-asc') : ''
)}
style={{
...styles,
@@ -296,19 +328,22 @@ export default React.createClass({
}}
{...rest}
>
{column.pivotColumns.map((pivotColumn, i) => {
return (
<span key={pivotColumn.id}>
{_.normalizeComponent(pivotColumn.header, {
data: sortedData,
column: column
})}
{i < column.pivotColumns.length - 1 && (
<ExpanderComponent />
)}
</span>
)
})}
<div className='rt-resizable-header-content'>
{column.pivotColumns.map((pivotColumn, i) => {
return (
<span key={pivotColumn.id}>
{_.normalizeComponent(pivotColumn.header, {
data: sortedData,
column: column
})}
{i < column.pivotColumns.length - 1 && (
<ExpanderComponent />
)}
</span>
)
})}
</div>
{resizer}
</ThComponent>
)
}
@@ -334,7 +369,8 @@ export default React.createClass({
key={i}
className={classnames(
classes,
sort ? (sort.desc ? '-sort-asc' : '-sort-desc') : '',
'rt-resizable-header',
sort ? (sort.desc ? '-sort-desc' : '-sort-asc') : '',
column.sortable && '-cursor-pointer',
!show && '-hidden',
)}
@@ -349,10 +385,153 @@ export default React.createClass({
}}
{...rest}
>
{_.normalizeComponent(column.header, {
data: sortedData,
column: column
})}
<div className='rt-resizable-header-content'>
{_.normalizeComponent(column.header, {
data: sortedData,
column: column
})}
</div>
{resizer}
</ThComponent>
)
}
const makeFilters = () => {
const theadFilterProps = _.splitProps(getTheadFilterProps(finalState, undefined, undefined, this))
const theadFilterTrProps = _.splitProps(getTheadFilterTrProps(finalState, undefined, undefined, this))
return (
<TheadComponent
className={classnames('-filters', theadFilterProps.className)}
style={{
...theadFilterProps.style,
minWidth: `${rowMinWidth}px`
}}
{...theadFilterProps.rest}
>
<TrComponent
className={theadFilterTrProps.className}
style={theadFilterTrProps.style}
{...theadFilterTrProps.rest}
>
{allVisibleColumns.map(makeFilter)}
</TrComponent>
</TheadComponent>
)
}
const makeFilter = (column, i) => {
const resized = resizing.find(x => x.id === column.id) || {}
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const theadFilterThProps = _.splitProps(getTheadFilterThProps(finalState, undefined, column, this))
const columnHeaderProps = _.splitProps(column.getHeaderProps(finalState, undefined, column, this))
const classes = [
column.headerClassName,
theadFilterThProps.className,
columnHeaderProps.className
]
const styles = {
...column.headerStyle,
...theadFilterThProps.style,
...columnHeaderProps.style
}
const rest = {
...theadFilterThProps.rest,
...columnHeaderProps.rest
}
if (column.expander) {
if (column.pivotColumns) {
const pivotCols = []
for (let i = 0; i < column.pivotColumns.length; i++) {
const col = column.pivotColumns[i]
const filter = filtering.find(filter => filter.id === column.id && filter.pivotId === col.id)
pivotCols.push(
<span key={col.id}
style={{flex: 1}}>
{!col.hideFilter ? (
_.normalizeComponent(col.filterRender,
{
col,
filter,
onFilterChange: (value) => (this.filterColumn(column, value, col))
},
defaults.column.filterRender
)
) : null}
</span>
)
if (i < column.pivotColumns.length - 1) {
pivotCols.push(<ExpanderComponent key={col.id + '-' + i} />)
}
}
return (
<ThComponent
key={i}
className={classnames(
'rt-pivot-header',
column.sortable && '-cursor-pointer',
classes
)}
style={{
...styles,
flex: `${width} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`,
display: 'flex'
}}
{...rest}
>
{pivotCols}
</ThComponent>
)
}
return (
<ThComponent
key={i}
className={classnames(
'rt-expander-header',
classes
)}
style={{
...styles,
flex: `0 0 auto`,
width: `${expanderColumnWidth}px`
}}
{...rest}
/>
)
}
const filter = filtering.find(filter => filter.id === column.id)
return (
<ThComponent
key={i}
className={classnames(
classes
)}
style={{
...styles,
flex: `${width} 0 auto`,
width: `${width}px`,
maxWidth: `${maxWidth}px`
}}
{...rest}
>
{!column.hideFilter ? (
_.normalizeComponent(column.filterRender,
{
column,
filter,
onFilterChange: (value) => (this.filterColumn(column, value))
},
defaults.column.filterRender
)
) : null}
</ThComponent>
)
}
@@ -385,9 +564,10 @@ export default React.createClass({
{...trProps.rest}
>
{allVisibleColumns.map((column, i2) => {
const resized = resizing.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(column.width, column.minWidth)
const maxWidth = _.getFirstDefined(column.width, column.maxWidth)
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const tdProps = _.splitProps(getTdProps(finalState, rowInfo, column, this))
const columnProps = _.splitProps(column.getProps(finalState, rowInfo, column, this))
@@ -523,7 +703,6 @@ export default React.createClass({
const makePadRow = (row, i) => {
const trGroupProps = getTrGroupProps(finalState, undefined, undefined, this)
const trProps = _.splitProps(getTrProps(finalState, undefined, undefined, this))
const tdProps = _.splitProps(getTdProps(finalState, undefined, undefined, this))
return (
<TrGroupComponent
key={i}
@@ -536,24 +715,11 @@ export default React.createClass({
)}
style={trProps.style || {}}
>
{SubComponent && (
<ThComponent
className={classnames(
'rt-expander-header',
tdProps.className
)}
style={{
...tdProps.style,
flex: `0 0 auto`,
width: `${expanderColumnWidth}px`
}}
{...tdProps.rest}
/>
)}
{allVisibleColumns.map((column, i2) => {
const resized = resizing.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(column.width, column.minWidth)
const maxWidth = _.getFirstDefined(column.width, column.maxWidth)
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const tdProps = _.splitProps(getTdProps(finalState, undefined, column, this))
const columnProps = _.splitProps(column.getProps(finalState, undefined, column, this))
@@ -613,9 +779,10 @@ export default React.createClass({
{...tFootTrProps.rest}
>
{allVisibleColumns.map((column, i2) => {
const resized = resizing.find(x => x.id === column.id) || {}
const show = typeof column.show === 'function' ? column.show() : column.show
const width = _.getFirstDefined(column.width, column.minWidth)
const maxWidth = _.getFirstDefined(column.width, column.maxWidth)
const width = _.getFirstDefined(resized.value, column.width, column.minWidth)
const maxWidth = _.getFirstDefined(resized.value, column.width, column.maxWidth)
const tFootTdProps = _.splitProps(getTfootTdProps(finalState, undefined, undefined, this))
const columnProps = _.splitProps(column.getProps(finalState, undefined, column, this))
const columnFooterProps = _.splitProps(column.getFooterProps(finalState, undefined, column, this))
@@ -693,7 +860,10 @@ export default React.createClass({
{...tFootTdProps.rest}
{...columnFooterProps.rest}
>
{_.normalizeComponent(column.footer)}
{_.normalizeComponent(column.footer, {
data: sortedData,
column: column
})}
</TdComponent>
)
})}
@@ -708,6 +878,7 @@ export default React.createClass({
const paginationProps = _.splitProps(getPaginationProps(finalState, undefined, undefined, this))
const loadingProps = getLoadingProps(finalState, undefined, undefined, this)
const noDataProps = getNoDataProps(finalState, undefined, undefined, this)
const resizerProps = getResizerProps(finalState, undefined, undefined, this)
const makeTable = () => (
<div
@@ -729,6 +900,7 @@ export default React.createClass({
>
{hasHeaderGroups ? makeHeaderGroups() : null}
{makeHeaders()}
{showFilters ? makeFilters() : null}
<TbodyComponent
className={classnames(tBodyProps.className)}
style={{
+35
View File
@@ -18,10 +18,21 @@ $expandSize = 7px
.rt-thead
display: flex
flex-direction: column
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&.-headerGroups
background: alpha(black, .03)
border-bottom: 1px solid alpha(black, .05)
&.-filters
border-bottom: 1px solid alpha(black, 0.05)
.rt-th
border-right: 1px solid alpha(black, 0.02)
&.-header
box-shadow: 0 2px 15px 0px alpha(black, .15)
@@ -30,6 +41,9 @@ $expandSize = 7px
.rt-th
.rt-td
padding: 5px 5px
line-height: normal
position: relative
border-right: 1px solid alpha(black, .05)
transition box-shadow .3s $easeOutBack
box-shadow:inset 0 0 0 0 transparent
@@ -41,6 +55,16 @@ $expandSize = 7px
cursor: pointer
&:last-child
border-right: 0
.rt-resizable-header
overflow: visible
&:last-child
overflow: hidden
.rt-resizable-header-content
overflow: hidden
text-overflow: ellipsis
.rt-tbody
display: flex
flex-direction: column
@@ -99,6 +123,17 @@ $expandSize = 7px
cursor: pointer
&.-open:after
transform: translate(-50%, -50%) rotate(0deg)
.rt-resizer
display: inline-block
position: absolute
width: 36px
top: 0
bottom: 0
right: -18px
cursor: col-resize
z-index: 10
.rt-tfoot
display: flex
flex-direction: column
+30 -10
View File
@@ -11,22 +11,26 @@ export default {
page: 0,
pageSize: this.props.defaultPageSize || 10,
sorting: this.props.defaultSorting,
expandedRows: {}
expandedRows: {},
filtering: this.props.defaultFiltering,
resizing: this.props.defaultResizing,
currentlyResizing: undefined,
skipNextSort: false
}
},
getResolvedState (props, state) {
const resolvedState = {
..._.compactObject(this.state),
..._.compactObject(state),
..._.compactObject(this.props),
..._.compactObject(state),
..._.compactObject(props)
}
return resolvedState
},
componentWillMount () {
this.setStateWithData(this.getDataModel())
this.setStateWithData(this.getDataModel(this.getResolvedState()))
},
componentDidMount () {
@@ -37,21 +41,32 @@ export default {
const oldState = this.getResolvedState()
const newState = this.getResolvedState(nextProps, nextState)
if (oldState.defaultSorting !== newState.defaultSorting) {
newState.sorting = newState.defaultSorting
}
if ((oldState.showFilters !== newState.showFilters) ||
(oldState.showFilters !== newState.showFilters)) {
newState.filtering = newState.defaultFiltering
}
// Props that trigger a data update
if (
oldState.data !== newState.data ||
oldState.columns !== newState.columns ||
oldState.pivotBy !== newState.pivotBy ||
oldState.sorting !== newState.sorting
oldState.sorting !== newState.sorting ||
oldState.showFilters !== newState.showFilters ||
oldState.filtering !== newState.filtering
) {
this.setStateWithData(this.getDataModel(nextProps, nextState))
this.setStateWithData(this.getDataModel(newState))
}
},
setStateWithData (newState, cb) {
const oldState = this.getResolvedState()
const newResolvedState = this.getResolvedState({}, newState)
const { freezeWhenExpanded } = newResolvedState
const {freezeWhenExpanded} = newResolvedState
// Default to unfrozen state
newResolvedState.frozen = false
@@ -73,12 +88,16 @@ export default {
if (
(oldState.frozen && !newResolvedState.frozen) ||
oldState.sorting !== newResolvedState.sorting ||
oldState.filtering !== newResolvedState.filtering ||
oldState.showFilters !== newResolvedState.showFilters ||
(!newResolvedState.frozen && oldState.resolvedData !== newResolvedState.resolvedData)
) {
// Handle collapseOnSortingChange & collapseOnPageChange
// Handle collapseOnSortingChange & collapseOnDataChange
if (
(oldState.sorting !== newResolvedState.sorting && this.props.collapseOnSortingChange) ||
(!newResolvedState.frozen && oldState.resolvedData !== newResolvedState.resolvedData && this.props.collapseOnPageChange)
(oldState.filtering !== newResolvedState.filtering) ||
(oldState.showFilters !== newResolvedState.showFilters) ||
(!newResolvedState.frozen && oldState.resolvedData !== newResolvedState.resolvedData && this.props.collapseOnDataChange)
) {
newResolvedState.expandedRows = {}
}
@@ -87,8 +106,9 @@ export default {
}
// Calculate pageSize all the time
if (newResolvedState.resolvedData) {
newResolvedState.pages = newResolvedState.manual ? newResolvedState.pages : Math.ceil(newResolvedState.resolvedData.length / newResolvedState.pageSize)
if (newResolvedState.sortedData) {
newResolvedState.pages = newResolvedState.manual ? newResolvedState.pages : Math.ceil(newResolvedState.sortedData.length / newResolvedState.pageSize)
newResolvedState.page = Math.max(newResolvedState.page >= newResolvedState.pages ? newResolvedState.pages - 1 : newResolvedState.page, 0)
}
return this.setState(newResolvedState, cb)
+210 -34
View File
@@ -1,7 +1,7 @@
import _ from './utils'
export default {
getDataModel (nextProps, nextState) {
getDataModel (newState) {
const {
columns,
pivotBy = [],
@@ -10,11 +10,8 @@ export default {
pivotValKey,
subRowsKey,
expanderColumnWidth,
SubComponent,
page,
pages,
pageSize
} = this.getResolvedState(nextProps, nextState)
SubComponent
} = newState
// Determine Header Groups
let hasHeaderGroups = false
@@ -192,15 +189,14 @@ export default {
// Group the rows together for this level
let groupedRows = Object.entries(
_.groupBy(rows, keys[i]))
.map(([key, value]) => {
return {
[pivotIDKey]: keys[i],
[pivotValKey]: key,
[keys[i]]: key,
[subRowsKey]: value
}
}
)
.map(([key, value]) => {
return {
[pivotIDKey]: keys[i],
[pivotValKey]: key,
[keys[i]]: key,
[subRowsKey]: value
}
})
// Recurse into the subRows
groupedRows = groupedRows.map(rowGroup => {
let subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1)
@@ -215,32 +211,32 @@ export default {
resolvedData = groupRecursively(resolvedData, pivotBy)
}
const newPages = _.getFirstDefined(pages, Math.ceil(resolvedData.length / pageSize))
const newPage = page > newPages ? newPage - 1 : page
return {
...newState,
resolvedData,
pivotColumn,
allVisibleColumns,
headerGroups,
allDecoratedColumns,
hasHeaderGroups,
page: Math.max(newPage, 0)
hasHeaderGroups
}
},
getSortedData (resolvedState) {
const {
manual,
sorting,
resolvedData
filtering,
showFilters,
defaultFilterMethod,
resolvedData,
allVisibleColumns
} = resolvedState
// Resolve the data from either manual data or sorted data
return {
sortedData: manual ? resolvedData : this.sortData(resolvedData, sorting)
sortedData: manual ? resolvedData : this.sortData(this.filterData(resolvedData, showFilters, filtering, defaultFilterMethod, allVisibleColumns), sorting)
}
},
fireOnChange () {
this.props.onChange(this.getResolvedState(), this)
},
@@ -250,10 +246,56 @@ export default {
getStateOrProp (key) {
return _.getFirstDefined(this.state[key], this.props[key])
},
filterData (data, showFilters, filtering, defaultFilterMethod, allVisibleColumns) {
let filteredData = data
if (showFilters && filtering.length) {
filteredData = filtering.reduce(
(filteredSoFar, nextFilter) => {
return filteredSoFar.filter(
(row) => {
let column
if (nextFilter.pivotId) {
const parentColumn = allVisibleColumns.find(x => x.id === nextFilter.id)
column = parentColumn.pivotColumns.find(x => x.id === nextFilter.pivotId)
} else {
column = allVisibleColumns.find(x => x.id === nextFilter.id)
}
const filterMethod = column.filterMethod || defaultFilterMethod
return filterMethod(nextFilter, row, column)
})
}
, filteredData
)
// Apply the filter to the subrows if we are pivoting, and then
// filter any rows without subcolumns because it would be strange to show
filteredData = filteredData.map(row => {
if (!row[this.props.subRowsKey]) {
return row
}
return {
...row,
[this.props.subRowsKey]: this.filterData(row[this.props.subRowsKey], showFilters, filtering, defaultFilterMethod, allVisibleColumns)
}
}).filter(row => {
if (!row[this.props.subRowsKey]) {
return true
}
return row[this.props.subRowsKey].length > 0
})
}
return filteredData
},
sortData (data, sorting) {
if (!sorting.length) {
return data
}
const sorted = _.orderBy(data, sorting.map(sort => {
return row => {
if (row[sort.id] === null || row[sort.id] === undefined) {
@@ -280,20 +322,23 @@ export default {
// User actions
onPageChange (page) {
const { onPageChange } = this.props
const {onPageChange, collapseOnPageChange} = this.props
if (onPageChange) {
return onPageChange(page)
}
this.setStateWithData({
expandedRows: {},
page
}, () => {
this.fireOnChange()
})
const newState = {page}
if (collapseOnPageChange) {
newState.expandedRows = {}
}
this.setStateWithData(
newState
, () => {
this.fireOnChange()
})
},
onPageSizeChange (newPageSize) {
const { onPageSizeChange } = this.props
const { pageSize, page } = this.getResolvedState()
const {onPageSizeChange} = this.props
const {pageSize, page} = this.getResolvedState()
// Normalize the page to display
const currentRow = pageSize * page
@@ -311,8 +356,20 @@ export default {
})
},
sortColumn (column, additive) {
const { sorting } = this.getResolvedState()
const { onSortingChange } = this.props
const {sorting, skipNextSort} = this.getResolvedState()
// we can't stop event propagation from the column resize move handlers
// attached to the document because of react's synthetic events
// so we have to prevent the sort function from actually sorting
// if we click on the column resize element within a header.
if (skipNextSort) {
this.setStateWithData({
skipNextSort: false
})
return
}
const {onSortingChange} = this.props
if (onSortingChange) {
return onSortingChange(column, additive)
}
@@ -394,5 +451,124 @@ export default {
}, () => {
this.fireOnChange()
})
},
filterColumn (column, value, pivotColumn) {
const {filtering} = this.getResolvedState()
const {onFilteringChange} = this.props
if (onFilteringChange) {
return onFilteringChange(column, value, pivotColumn)
}
// Remove old filter first if it exists
const newFiltering = (filtering || []).filter(x => {
if (x.id !== column.id) {
return true
}
if (x.pivotId) {
if (pivotColumn) {
return x.pivotId !== pivotColumn.id
}
return true
}
})
if (value !== '') {
newFiltering.push({
id: column.id,
value: value,
pivotId: pivotColumn ? pivotColumn.id : undefined
})
}
this.setStateWithData({
filtering: newFiltering
}, () => {
this.fireOnChange()
})
},
resizeColumnStart (column, event, isTouch) {
const {onResize} = this.props
if (onResize) {
return onResize(column, event, isTouch)
}
const parentWidth = event.target.parentElement.getBoundingClientRect().width
let pageX
if (isTouch) {
pageX = event.changedTouches[0].pageX
} else {
pageX = event.pageX
}
this.setStateWithData({
currentlyResizing: {
id: column.id,
startX: pageX,
parentWidth: parentWidth
}
}, () => {
if (isTouch) {
document.addEventListener('touchmove', this.resizeColumnMoving)
document.addEventListener('touchcancel', this.resizeColumnEnd)
document.addEventListener('touchend', this.resizeColumnEnd)
} else {
document.addEventListener('mousemove', this.resizeColumnMoving)
document.addEventListener('mouseup', this.resizeColumnEnd)
document.addEventListener('mouseleave', this.resizeColumnEnd)
}
})
},
resizeColumnEnd (event) {
let isTouch = event.type === 'touchend' || event.type === 'touchcancel'
if (isTouch) {
document.removeEventListener('touchmove', this.resizeColumnMoving)
document.removeEventListener('touchcancel', this.resizeColumnEnd)
document.removeEventListener('touchend', this.resizeColumnEnd)
}
// If its a touch event clear the mouse one's as well because sometimes
// the mouseDown event gets called as well, but the mouseUp event doesn't
document.removeEventListener('mousemove', this.resizeColumnMoving)
document.removeEventListener('mouseup', this.resizeColumnEnd)
document.removeEventListener('mouseleave', this.resizeColumnEnd)
// The touch events don't propagate up to the sorting's onMouseDown event so
// no need to prevent it from happening or else the first click after a touch
// event resize will not sort the column.
if (!isTouch) {
this.setStateWithData({
skipNextSort: true
})
}
},
resizeColumnMoving (event) {
const {resizing, currentlyResizing} = this.getResolvedState()
// Delete old value
const newResizing = resizing.filter(x => x.id !== currentlyResizing.id)
let pageX
if (event.type === 'touchmove') {
pageX = event.changedTouches[0].pageX
} else if (event.type === 'mousemove') {
pageX = event.pageX
}
// Set the min size to 10 to account for margin and border or else the group headers don't line up correctly
const newWidth = Math.max(currentlyResizing.parentWidth + pageX - currentlyResizing.startX, 11)
newResizing.push({
id: currentlyResizing.id,
value: newWidth
})
this.setStateWithData({
resizing: newResizing
})
}
}
+8 -5
View File
@@ -4,7 +4,7 @@ import classnames from 'classnames'
// import _ from './utils'
const defaultButton = (props) => (
<button {...props} className='-btn'>{props.children}</button>
<button type='button' {...props} className='-btn'>{props.children}</button>
)
export default React.createClass({
@@ -66,9 +66,7 @@ export default React.createClass({
<div className='-center'>
<span className='-pageInfo'>
{this.props.pageText} {showPageJump ? (
<form className='-pageJump'
onSubmit={this.applyPage}
>
<div className='-pageJump'>
<input
type={this.state.page === '' ? 'text' : 'number'}
onChange={e => {
@@ -81,8 +79,13 @@ export default React.createClass({
}}
value={this.state.page === '' ? '' : this.state.page + 1}
onBlur={this.applyPage}
onKeyPress={e => {
if (e.which === 13 || e.keyCode === 13) {
this.applyPage()
}
}}
/>
</form>
</div>
) : (
<span className='-currentPage'>{page + 1}</span>
)} {this.props.ofText} <span className='-totalPages'>{pages}</span>
+1 -1
View File
@@ -193,7 +193,7 @@ function isSortingDesc (d) {
function normalizeComponent (Comp, params = {}, fallback = Comp) {
return typeof Comp === 'function' ? (
Comp.prototype.isReactComponent ? (
Object.getPrototypeOf(Comp).isReactComponent ? (
<Comp
{...params}
/>
+155
View File
@@ -0,0 +1,155 @@
import React from 'react'
import _ from 'lodash'
import namor from 'namor'
import CodeHighlight from './components/codeHighlight'
import ReactTable from '../src/index'
const data = _.map(_.range(5553), d => {
return {
firstName: namor.generate({ words: 1, numLen: 0 }),
lastName: namor.generate({ words: 1, numLen: 0 }),
age: Math.floor(Math.random() * 30)
}
})
const columns = [{
header: 'Name',
columns: [{
header: 'First Name',
accessor: 'firstName'
}, {
header: 'Last Name',
id: 'lastName',
accessor: d => d.lastName
}]
}, {
header: 'Info',
columns: [{
header: 'Age',
accessor: 'age'
}]
}]
class ControlledTable extends React.Component {
constructor() {
super()
this.sortChange = this.sortChange.bind(this)
this.state = {
sorting: [],
page: 0,
pageSize: 10
}
}
sortChange(column, shift) {
if(shift)
alert('Shift click not implemented in this demo')
var sort = {id: column.id}
if(this.state.sorting.length && this.state.sorting[0].id == column.id)
this.state.sorting[0].asc ? sort.desc = true : sort.asc = true
else
sort.asc = true
this.setState({
sorting: [sort]
})
}
render() {
return (
<div>
<div className='table-wrap'>
<ReactTable
className='-striped -highlight'
data={data}
columns={columns}
sorting={this.state.sorting}
onSortingChange={this.sortChange}
page={this.state.page}
onPageChange={page => this.setState({page})}
pageSize={this.state.pageSize}
onPageSizeChange={(pageSize, page) => this.setState({page, pageSize})}
/>
</div>
<div style={{textAlign: 'center'}}>
<br />
<em>Tip: For simplicity, multi-sort is not implemented in this demo</em>
</div>
<CodeHighlight>{() => getCode()}</CodeHighlight>
</div>
)
}
}
function getCode () {
return `const data = _.map(_.range(5553), d => {
return {
firstName: namor.generate({ words: 1, numLen: 0 }),
lastName: namor.generate({ words: 1, numLen: 0 }),
age: Math.floor(Math.random() * 30)
}
})
const columns = [{
header: 'Name',
columns: [{
header: 'First Name',
accessor: 'firstName'
}, {
header: 'Last Name',
id: 'lastName',
accessor: d => d.lastName
}]
}, {
header: 'Info',
columns: [{
header: 'Age',
accessor: 'age'
}]
}]
class ControlledTable extends React.Component {
constructor() {
super()
this.sortChange = this.sortChange.bind(this)
this.pageChange = this.pageChange.bind(this)
this.pageSizeChange = this.pageSizeChange.bind(this)
this.state = {
sorting: [],
page: 0,
pageSize: 10
}
}
sortChange(column, shift) {
if(shift)
alert('Shift click not implemented in this demo')
var sort = {id: column.id}
if(this.state.sorting.length && this.state.sorting[0].id == column.id)
this.state.sorting[0].asc ? sort.desc = true : sort.asc = true
else
sort.asc = true
this.setState({
sorting: [sort]
})
}
render() {
return (
<ReactTable
className='-striped -highlight'
data={data}
columns={columns}
sorting={this.state.sorting}
onSortingChange={this.sortChange}
page={this.state.page}
onPageChange={page => this.setState({page})}
pageSize={this.state.pageSize}
onPageSizeChange={(pageSize, page) => this.setState({page, pageSize})}
/>
)
}
}`
}
export default () => <ControlledTable />
+264
View File
@@ -0,0 +1,264 @@
import React from 'react'
import _ from 'lodash'
import namor from 'namor'
import CodeHighlight from './components/codeHighlight'
import ReactTable from '../src/index'
class Filtering extends React.Component {
constructor (props) {
super(props)
const data = _.map(_.range(5553), d => {
return {
firstName: namor.generate({words: 1, numLen: 0}),
lastName: namor.generate({words: 1, numLen: 0}),
age: Math.floor(Math.random() * 30)
}
})
this.state = {
tableOptions: {
loading: false,
showPagination: true,
showPageSizeOptions: true,
showPageJump: true,
collapseOnSortingChange: true,
collapseOnPageChange: true,
collapseOnDataChange: true,
freezeWhenExpanded: false,
showFilters: true
},
data: data
}
this.setTableOption = this.setTableOption.bind(this)
}
render () {
const columns = [{
header: 'Name',
columns: [{
header: 'First Name',
accessor: 'firstName',
filterMethod: (filter, row) => (row[filter.id].startsWith(filter.value) && row[filter.id].endsWith(filter.value))
}, {
header: 'Last Name',
id: 'lastName',
accessor: d => d.lastName,
filterMethod: (filter, row) => (row[filter.id].includes(filter.value))
}]
}, {
header: 'Info',
columns: [{
header: 'Age',
accessor: 'age'
}, {
header: 'Over 21',
accessor: 'age',
id: 'over',
render: ({value}) => (value >= 21 ? 'Yes' : 'No'),
filterMethod: (filter, row) => {
if (filter.value === 'all') {
return true
}
if (filter.value === 'true') {
return row[filter.id] >= 21
}
return row[filter.id] < 21
},
filterRender: ({filter, onFilterChange}) => (
<select
onChange={event => onFilterChange(event.target.value)}
style={{width: '100%'}}
value={filter ? filter.value : 'all'}>
<option value="all"></option>
<option value="true">Can Drink</option>
<option value="false">Can't Drink</option>
</select>
)
}
]
}]
return (
<div>
<div style={{float: 'left'}}>
<h1>Table Options</h1>
<table>
<tbody>
{
Object.keys(this.state.tableOptions).map(optionKey => {
const optionValue = this.state.tableOptions[optionKey]
return (
<tr key={optionKey}>
<td>{optionKey}</td>
<td style={{paddingLeft: 10, paddingTop: 5}}>
<input type="checkbox"
name={optionKey}
checked={optionValue}
onChange={this.setTableOption}
/>
</td>
</tr>
)
})
}
</tbody>
</table>
</div>
<div className='table-wrap' style={{paddingLeft: 240}}>
<ReactTable
className='-striped -highlight'
data={this.state.data}
columns={columns}
defaultPageSize={10}
defaultFilterMethod={(filter, row) => (String(row[filter.id]) === filter.value)}
{...this.state.tableOptions}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>You can put any component you want here, even another React Table!</em>
<br />
<br />
<ReactTable
data={this.state.data}
columns={columns}
defaultPageSize={3}
showPagination={false}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>It even has access to the row data: </em>
<CodeHighlight>{() => JSON.stringify(row, null, 2)}</CodeHighlight>
</div>
)
}}
/>
</div>
)
}}
/>
</div>
<div style={{textAlign: 'center'}}>
<br />
<em>Tip: Hold shift when sorting to multi-sort!</em>
</div>
<div>
<h1>Custom Filters In This Example</h1>
<p>The default filter for all columns of a table if it is not specified in the configuration is set to match
on values that start with the filter text. Example: age.startsWith("2").</p>
<p>This example overrides the default filter behavior by setting
the <strong>defaultFilterMethod</strong> table option to match on values that are exactly equal to the
filter text. Example: age == "23")</p>
<p>Each column can also be customized with the column <strong>filterMethod</strong> option:</p>
<p>In this example the firstName column filters on the value starting with and ending with the filter
value.</p>
<p>In this example the lastName column filters on the value including the filter value anywhere in its
text.</p>
<p>To completely override the filter that is shown, you can set the <strong>filterRender</strong> column
option. Using this option you can specify the JSX that is shown. The option is passed
an <strong>onFilterChange</strong> method that must be called with the value that you wan't to
pass to the <strong>filterMethod</strong> option whenever the filter has changed.</p>
</div>
<CodeHighlight>{() => this.getCode()}</CodeHighlight>
</div>
)
}
setTableOption (event) {
const target = event.target
const value = target.type === 'checkbox' ? target.checked : target.value
const name = target.name
this.setState({
tableOptions: {
...this.state.tableOptions,
[name]: value
}
})
}
getCode () {
return `
const columns = [{
header: 'Name',
columns: [{
header: 'First Name',
accessor: 'firstName',
filterMethod: (filter, row) => (row[filter.id].startsWith(filter.value) && row[filter.id].endsWith(filter.value))
}, {
header: 'Last Name',
id: 'lastName',
accessor: d => d.lastName,
filterMethod: (filter, row) => (row[filter.id].includes(filter.value))
}]
}, {
header: 'Info',
columns: [{
header: 'Age',
accessor: 'age'
}, {
header: 'Over 21',
accessor: 'age',
id: 'over',
render: ({value}) => (value >= 21 ? 'Yes' : 'No'),
filterMethod: (filter, row) => {
if (filter.value === 'all') {
return true
}
if (filter.value === 'true') {
return row[filter.id] >= 21
}
return row[filter.id] < 21
},
filterRender: ({filter, onFilterChange}) => (
<select
onChange={event => onFilterChange(event.target.value)}
style={{width: '100%'}}
value={filter ? filter.value : 'all'}>
<option value="all"></option>
<option value="true">Can Drink</option>
<option value="false">Can't Drink</option>
</select>
)
}]
}]
export default (
<ReactTable
data={data}
columns={columns}
defaultPageSize={10}
defaultFilterMethod={(filter, row) => (String(row[filter.id]) === filter.value)}
{...otherOptions}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>You can put any component you want here, even another React Table!</em>
<br />
<br />
<ReactTable
data={data}
columns={columns}
defaultPageSize={3}
showPagination={false}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>It even has access to the row data: </em>
<CodeHighlight>{() => JSON.stringify(row, null, 2)}</CodeHighlight>
</div>
)
}}
/>
</div>
)
}}
/>
)
`
}
}
export default () => <Filtering />
+10 -4
View File
@@ -33,11 +33,13 @@ export default () => {
aggregate: vals => _.round(_.mean(vals)),
render: row => {
return <span>{row.aggregated ? `${row.value} (avg)` : row.value}</span>
}
},
filterMethod: (filter, row) => (filter.value == `${row[filter.id]} (avg)`)
}, {
header: 'Visits',
accessor: 'visits',
aggregate: vals => _.sum(vals)
aggregate: vals => _.sum(vals),
hideFilter: true
}]
}]
@@ -50,6 +52,7 @@ export default () => {
defaultPageSize={10}
className='-striped -highlight'
pivotBy={['firstName', 'lastName']}
showFilters={true}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
@@ -104,11 +107,13 @@ const columns = [{
aggregate: vals => _.round(_.mean(vals)),
render: row => {
return <span>{row.aggregated ? \`\${row.value} (avg)\` : row.value}</span>
}
},
filterMethod: (filter, row) => (filter.value == \`\${row[filter.id]} (avg)\`)
}, {
header: 'Visits',
accessor: 'visits',
aggregate: vals => _.sum(vals)
aggregate: vals => _.sum(vals),
hideFilter: true
}]
}]
@@ -119,6 +124,7 @@ return (
defaultPageSize={10}
className='-striped -highlight'
pivotBy={['firstName', 'lastName']}
showFilters={true}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
+21 -7
View File
@@ -14,23 +14,34 @@ const rawData = _.map(_.range(3424), d => {
})
// Now let's mock the server. It's job is simple: use the table model to sort and return the page data
const requestData = (pageSize, page, sorting) => {
const requestData = (pageSize, page, sorting, filtering) => {
return new Promise((resolve, reject) => {
// On the server, you'll likely use SQL or noSQL or some other query language to do this.
// For this mock, we'll just use lodash
const sortedData = _.orderBy(rawData, sorting.map(sort => {
let filteredData = rawData;
if (filtering.length) {
filteredData = filtering.reduce(
(filteredSoFar, nextFilter) => {
return filteredSoFar.filter(
(row) => {
return (row[nextFilter.id]+"").includes(nextFilter.value)
})
}
, filteredData)
}
const sortedData = _.orderBy(filteredData, sorting.map(sort => {
return row => {
if (row[sort.id] === null || row[sort.id] === undefined) {
return -Infinity
}
return typeof row[sort.id] === 'string' ? row[sort.id].toLowerCase() : row[sort.id]
}
}), sorting.map(d => d.asc ? 'asc' : 'desc'))
}), sorting.map(d => d.desc ? 'desc' : 'asc'))
// Be sure to send back the rows to be displayed and any other pertinent information, like how many pages there are total.
const res = {
rows: sortedData.slice(pageSize * page, (pageSize * page) + pageSize),
pages: Math.ceil(rawData.length / pageSize)
pages: Math.ceil(filteredData.length / pageSize)
}
// Here we'll simulate a server response with 500ms of delay.
@@ -48,13 +59,13 @@ const ServerSide = React.createClass({
}
},
fetchData (state, instance) {
console.log(state, instance)
// Whenever the table model changes, or the user sorts or changes pages, this method gets called and passed the current table model.
// You can set the `loading` prop of the table to true to use the built-in one or show you're own loading bar if you want.
this.setState({loading: true})
// Request the data however you want. Here, we'll use our mocked service we created earlier
requestData(state.pageSize, state.page, state.sorting)
requestData(state.pageSize, state.page, state.sorting, state.filtering)
.then((res) => {
console.log(res.rows)
// Now just get the rows of data to your React Table (and update anything else like total pages or loading)
this.setState({
data: res.rows,
@@ -82,6 +93,7 @@ const ServerSide = React.createClass({
}]}
manual // Forces table not to paginate or sort automatically, so we can handle it server-side
defaultPageSize={10}
showFilters={true}
data={this.state.data} // Set the rows to be displayed
pages={this.state.pages} // Display the total number of pages
loading={this.state.loading} // Display the loading overlay when we need it
@@ -124,7 +136,8 @@ export default React.createClass({
Axios.post('mysite.com/data', {
pageSize: state.pageSize,
page: state.page,
sorting: state.sorting
sorting: state.sorting,
filtering: state.filtering
})
.then((res) => {
// Now update your state!
@@ -151,6 +164,7 @@ export default React.createClass({
}]}
manual // Forces table not to paginate or sort automatically, so we can handle it server-side
defaultPageSize={10}
showFilters={true}
data={this.state.data} // Set the rows to be displayed
pages={this.state.pages} // Display the total number of pages
loading={this.state.loading} // Display the loading overlay when we need it
+131 -65
View File
@@ -5,77 +5,139 @@ import namor from 'namor'
import CodeHighlight from './components/codeHighlight'
import ReactTable from '../src/index'
export default () => {
const data = _.map(_.range(5553), d => {
return {
firstName: namor.generate({ words: 1, numLen: 0 }),
lastName: namor.generate({ words: 1, numLen: 0 }),
age: Math.floor(Math.random() * 30)
class SubComponents extends React.Component {
constructor(props) {
super(props)
const data = _.map(_.range(5553), d => {
return {
firstName: namor.generate({words: 1, numLen: 0}),
lastName: namor.generate({words: 1, numLen: 0}),
age: Math.floor(Math.random() * 30)
}
})
this.state = {
tableOptions: {
loading: false,
showPagination: true,
showPageSizeOptions: true,
showPageJump: true,
collapseOnSortingChange: true,
collapseOnPageChange: true,
collapseOnDataChange: true,
freezeWhenExpanded: false,
showFilters: false
},
data: data
}
})
const columns = [{
header: 'Name',
columns: [{
header: 'First Name',
accessor: 'firstName'
this.setTableOption = this.setTableOption.bind(this);
}
render() {
const columns = [{
header: 'Name',
columns: [{
header: 'First Name',
accessor: 'firstName'
}, {
header: 'Last Name',
id: 'lastName',
accessor: d => d.lastName
}]
}, {
header: 'Last Name',
id: 'lastName',
accessor: d => d.lastName
header: 'Info',
columns: [{
header: 'Age',
accessor: 'age'
}]
}]
}, {
header: 'Info',
columns: [{
header: 'Age',
accessor: 'age'
}]
}]
return (
<div>
<div className='table-wrap'>
<ReactTable
className='-striped -highlight'
data={data}
columns={columns}
defaultPageSize={10}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>You can put any component you want here, even another React Table!</em>
<br />
<br />
<ReactTable
data={data}
columns={columns}
defaultPageSize={3}
showPagination={false}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>It even has access to the row data: </em>
<CodeHighlight>{() => JSON.stringify(row, null, 2)}</CodeHighlight>
</div>
)
}}
/>
</div>
)
}}
/>
return (
<div>
<div style={{float: "left"}}>
<h1>Table Options</h1>
<table>
<tbody>
{
Object.keys(this.state.tableOptions).map(optionKey => {
const optionValue = this.state.tableOptions[optionKey];
return (
<tr key={optionKey}>
<td>{optionKey}</td>
<td style={{paddingLeft: 10, paddingTop: 5}}>
<input type="checkbox"
name={optionKey}
checked={optionValue}
onChange={this.setTableOption}
/>
</td>
</tr>
)
})
}
</tbody>
</table>
</div>
<div className='table-wrap' style={{paddingLeft: 240}}>
<ReactTable
className='-striped -highlight'
data={this.state.data}
columns={columns}
defaultPageSize={10}
{...this.state.tableOptions}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>You can put any component you want here, even another React Table!</em>
<br />
<br />
<ReactTable
data={this.state.data}
columns={columns}
defaultPageSize={3}
showPagination={false}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
<em>It even has access to the row data: </em>
<CodeHighlight>{() => JSON.stringify(row, null, 2)}</CodeHighlight>
</div>
)
}}
/>
</div>
)
}}
/>
</div>
<div style={{textAlign: 'center'}}>
<br />
<em>Tip: Hold shift when sorting to multi-sort!</em>
</div>
<CodeHighlight>{() => this.getCode()}</CodeHighlight>
</div>
<div style={{textAlign: 'center'}}>
<br />
<em>Tip: Hold shift when sorting to multi-sort!</em>
</div>
<CodeHighlight>{() => getCode()}</CodeHighlight>
</div>
)
}
)
}
function getCode () {
return `
setTableOption(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
tableOptions: {
...this.state.tableOptions,
[name]: value
}
})
}
getCode() {
return `
const columns = [{
header: 'Name',
columns: [{
@@ -99,6 +161,7 @@ export default (
data={data}
columns={columns}
defaultPageSize={10}
{...otherOptions}
SubComponent={(row) => {
return (
<div style={{padding: '20px'}}>
@@ -125,4 +188,7 @@ export default (
/>
)
`
}
}
}
export default () => <SubComponents/>
+7 -1
View File
@@ -2998,7 +2998,7 @@ indexof@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
inflight@^1.0.4, inflight@^1.0.6:
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
dependencies:
@@ -4964,6 +4964,12 @@ rimraf@2, rimraf@^2.2.8, rimraf@~2.5.1, rimraf@~2.5.4:
dependencies:
glob "^7.0.5"
rimraf@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
dependencies:
glob "^7.0.5"
ripemd160@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-0.2.0.tgz#2bf198bde167cacfa51c0a928e84b68bbe171fce"