From 76542c448e5b46b3a7ba99f2f087d35333b8b2d8 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 20 Oct 2016 15:08:27 -0600 Subject: [PATCH] Initial commit --- .gitignore | 1 + README.md | 162 +++++++++++++++++++ package.json | 56 +++++++ react-table.css | 1 + react-table.js | 10 ++ src/index.js | 411 ++++++++++++++++++++++++++++++++++++++++++++++++ src/index.styl | 191 ++++++++++++++++++++++ src/utils.js | 85 ++++++++++ 8 files changed, 917 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 react-table.css create mode 100644 react-table.js create mode 100644 src/index.js create mode 100644 src/index.styl create mode 100644 src/utils.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md index 826fe01..77841d0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,164 @@ # react-table A fast, lightweight, opinionated table and datagrid built on React + +[![Chart.js on Slack](https://img.shields.io/badge/slack-Chart.js-blue.svg)](https://react-table-slack.herokuapp.com/) + +## Features + +- Lightweight at 3kb (and just 2kb more for styles) +- No composition needed +- Uses customizable JSX and callbacks for everything +- Client-side pagination and sorting +- Server-side support +- Minimal Design & easy to theme + +Why you may **not** want to use `react-table`: +- No support for infinite scrolling. In our experience, infinite scrolling complicates data grids + +## Installation +```bash +$ npm install react-table +``` + +## Quick Usage +```javascript +import ReactTable from 'react-table' + +const data = [{ + name: 'Tanner Linsley', + age: 26, + friend: { + name: 'Jason Maurer', + age: 23, + } +},{ + ... +}] + +const columns = [{ + header: 'Name', + accessor: 'name', +}, { + header: 'Age', + accessor: 'age' +}, { + header: 'Friend Name', + accessor: d => d.friend.name +}, { + header: 'Friend Age', + accessor: 'friend.age' +}] + + +``` + +## Client-side Data +To use client-side data, simply pass the `data` prop an array. Client-side filtering and pagination is built in, and your table will update gracefully if you change any props. + +## Server-side Data +If you want to handle pagination, and sorting on the server, `react-table` makes it easy on you. Instead of passing the `data` prop an array, you provide a function instead. + +This function will be called on mount, pagination events, and sorting events. It also provides you all of the parameters to help you query and format your data. + +```javascript + { + + // params will give you all the info you need to query and sort your data + params == { + page: 0, // The page index the user is requesting + pageSize: 20, // The current pageSize + pages: -1, // The amount of existing pages (-1 means there is no page data yet) + sorting: [ // An array of column sort models (yes, you can multi-sort!) + { + id: 'columnID', // The columnID (usually the accessor string, but can be overridden for server-side or required if the column accessor is a function) + ascending: true or false + } + ] + } + + // Query your data however you'd like, then structure your response like so: + const result = { + rows: [...], // Your data for the current page/sorting model + pages: 10 // optionally provide how many pages exist (this is only needed if you choose to display page numbers, and only the first time you make the call or if the page count changes) + } + + // You can return a promise that resolve the result + return Axios.post('/myDataEnpoint', params) // resolves to `result` + + // or use the manual callback whenever you please + setTimeout(() => { + callback(result) + }, 5000) + + // That's it! + + }} +/> +``` + +## Multi-Sort +When clicking on a column header, hold shift to multi-sort! You can toggle `aascending` `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! + +## Default Props +```javascript +{ + className: '-striped -highlight', + pageSize: 20, + minRows: 0, + data: [], + previousComponent: , + nextComponent: , + previousText: 'Previous', + nextText: 'Next', + loadingComponent: Loading..., + column: { // default properties for every column's model + sortable: true, + show: true + } +} +``` + +You can easily override the core defaults like so: + +```javascript +import { ReactTableDefaults } from 'react-table' + +Object.assign(ReactTableDefaults, { + pageSize: 10, + minRows: 3, + // etc... +}) +``` + +Or just define them on the component + +```javascript +
Header Name
, + accessor: 'propertyName' or Accessor eg. (row) => row.propertyName, + + // Optional + id: 'myProperty', // A unique ID is needed if the accessor is not a string or if you would like to override the column name used in server-side calls + render: JSX eg. ({row, value, index}) => {value}, // Provide a JSX element or stateless function to render whatever you want as the column's cell with access to the entire row + sortable: true, + sort: 'asc' or 'desc', + show: true, + width: Number, // Locks the column width to this amount + minWidth: Number // Allows the column to flex above this minimum amount +}] +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..d2ac6e4 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "react-table", + "version": "0.0.1", + "description": "A fast, lightweight, opinionated table and datagrid built on React", + "license": "MIT", + "homepage": "https://github.com/tannerlinsley/react-table#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/tannerlinsley/react-table.git" + }, + "keywords": [ + "react", + "table", + "datagrid" + ], + "main": "src/index.js", + "scripts": { + "build:js": "rm -rf react-table.js && browserify src/index.js --external react -s react-table -t babelify -tg uglifyify -o react-table.js", + "build:css": "rm -rf react-table.css && stylus src/index.styl -o react-table.css --use ./node_modules/nib/lib/nib.js --compress", + "watch": "onchange 'src/**' -i -- npm-run-all build:*", + "prepublish": "npm-run-all build:*" + }, + "dependencies": { + "classnames": "^2.2.5", + "lodash": "^4.16.4" + }, + "devDependencies": { + "babel-cli": "6.14.0", + "babel-eslint": "6.1.2", + "babel-preset-es2015": "6.14.0", + "babel-preset-react": "6.11.1", + "babel-preset-stage-2": "6.13.0", + "babelify": "^7.3.0", + "browserify": "13.1.0", + "onchange": "^3.0.2", + "react": "15.3.1", + "rollupify": "^0.3.4", + "standard": "8.0.0", + "stylus": "^0.54.5", + "uglifyify": "3.0.3" + }, + "standard": { + "parser": "babel-eslint", + "ignore": [ + "lib", + "react-table.js" + ] + }, + "babel": { + "presets": [ + "es2015", + "stage-2", + "react" + ] + } +} diff --git a/react-table.css b/react-table.css new file mode 100644 index 0000000..aef5910 --- /dev/null +++ b/react-table.css @@ -0,0 +1 @@ +.ReactTable{position:relative;}.ReactTable table{width:100%;border-collapse:collapse;border:1px solid rgba(0,0,0,0.1);display:block;overflow-x:auto}.ReactTable .-td-inner,.ReactTable .-th-inner{white-space:nowrap;-o-text-overflow:ellipsis;text-overflow:ellipsis;padding:7px 5px;overflow:hidden;z-index:0;-webkit-transition:0.3s ease;-moz-transition:0.3s ease;-o-transition:0.3s ease;-ms-transition:0.3s ease;transition:0.3s ease;-webkit-transition-property:width, min-width, padding, opacity;-moz-transition-property:width, min-width, padding, opacity;-o-transition-property:width, min-width, padding, opacity;-ms-transition-property:width, min-width, padding, opacity;transition-property:width, min-width, padding, opacity}.ReactTable th:hover .-td-inner,.ReactTable td:hover .-td-inner,.ReactTable th:hover .-th-inner,.ReactTable td:hover .-th-inner{position:relative;overflow:visible;z-index:1;text-shadow:0 0 1px #fff,0 0 2px #fff,1px 0 3px #fff,1px 0 4px #fff,2px 0 5px #fff,2px 0 6px #fff,3px 0 7px #fff,3px 0 8px #fff,4px 0 9px #fff,4px 0 10px #fff,5px 0 11px #fff,5px 0 12px #fff,6px 0 13px #fff,6px 0 14px #fff,7px 0 15px #fff,7px 0 16px #fff,8px 0 17px #fff,8px 0 18px #fff,9px 0 19px #fff,9px 0 20px #fff;}.ReactTable th:hover .-td-inner > *,.ReactTable td:hover .-td-inner > *,.ReactTable th:hover .-th-inner > *,.ReactTable td:hover .-th-inner > *{text-shadow:none}.ReactTable th.hidden,.ReactTable td.hidden{border:0 !important;opacity:0 !important;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)" !important;filter:alpha(opacity=0) !important;}.ReactTable th.hidden .-th-inner,.ReactTable td.hidden .-th-inner,.ReactTable th.hidden .-td-inner,.ReactTable td.hidden .-td-inner{width:0 !important;min-width:0 !important;padding:0 !important}.ReactTable thead{-webkit-box-shadow:0 5px 20px 0 rgba(0,0,0,0.1);box-shadow:0 5px 20px 0 rgba(0,0,0,0.1);z-index:0;}.ReactTable thead.-headerGroups{border-bottom:1px solid rgba(0,0,0,0.05);}.ReactTable thead.-headerGroups th,.ReactTable thead.-headerGroups td{background:rgba(0,0,0,0.03)}.ReactTable thead tr{text-align:center}.ReactTable thead th,.ReactTable thead td{width:1%;background:#fff;font-weight:500;color:$darkColor;border-right:1px solid rgba(0,0,0,0.05);-webkit-transition:box-shadow 0.3s easeOutBack;-moz-transition:box-shadow 0.3s easeOutBack;-o-transition:box-shadow 0.3s easeOutBack;-ms-transition:box-shadow 0.3s easeOutBack;transition:box-shadow 0.3s easeOutBack;-webkit-box-shadow:inset 0 0 0 0 transparent;box-shadow:inset 0 0 0 0 transparent;}.ReactTable thead th.sort-asc,.ReactTable thead td.sort-asc{-webkit-box-shadow:inset 0 3px 0 0 rgba(0,0,0,0.6);box-shadow:inset 0 3px 0 0 rgba(0,0,0,0.6)}.ReactTable thead th.sort-desc,.ReactTable thead td.sort-desc{-webkit-box-shadow:inset 0 -3px 0 0 rgba(0,0,0,0.6);box-shadow:inset 0 -3px 0 0 rgba(0,0,0,0.6)}.ReactTable tbody{z-index:0;}.ReactTable tbody tr{border-bottom:solid 1px rgba(0,0,0,0.05);}.ReactTable tbody tr:last-child{border-bottom:0}.ReactTable tbody tr:first-child td{-webkit-box-shadow:inset 0 20px 20px -20px rgba(0,0,0,0.2);box-shadow:inset 0 20px 20px -20px rgba(0,0,0,0.2)}.ReactTable tbody td{border-right:1px solid rgba(0,0,0,0.02);}.ReactTable tbody td:last-child{border-right:0}.ReactTable.striped tbody tr:nth-child(even){background:rgba(0,0,0,0.03)}.ReactTable.highlight tbody tr:hover{background:rgba(0,0,0,0.05)}.ReactTable .-pagination{width:100%;display:-webkit-box;display:-moz-box;display:-webkit-flex;display:-ms-flexbox;display:box;display:flex;margin-top:5px;-webkit-box-pack:justify;-moz-box-pack:justify;-o-box-pack:justify;-ms-flex-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;-webkit-box-align:center;-moz-box-align:center;-o-box-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;-webkit-box-lines:multiple;-moz-box-lines:multiple;-o-box-lines:multiple;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}.ReactTable .-pagination .-btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;width:100%;border:0;border-radius:3px;padding:6px;font-size:1em;color:rgba(0,0,0,0.6);background:rgba(0,0,0,0.1);-webkit-transition:all 0.1s ease;-moz-transition:all 0.1s ease;-o-transition:all 0.1s ease;-ms-transition:all 0.1s ease;transition:all 0.1s ease;}.ReactTable .-pagination .-btn[disabled]{opacity:.5;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";filter:alpha(opacity=50);cursor:default}.ReactTable .-pagination .-btn:not([disabled]):hover{background:rgba(0,0,0,0.3);color:#fff}.ReactTable .-pagination .-left,.ReactTable .-pagination .-center,.ReactTable .-pagination .-right{-webkit-box-flex:1;-moz-box-flex:1;-o-box-flex:1;box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;margin-bottom:5px}.ReactTable .-pagination .-left,.ReactTable .-pagination .-right{text-align:center}.ReactTable .-pagination .-center{text-align:center;padding:0 5px}.ReactTable .-loading{display:block;position:absolute;left:0;right:0;top:0;bottom:0;background:rgba(255,255,255,0.8);-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;-ms-transition:all 0.3s ease;transition:all 0.3s ease;z-index:2;opacity:0;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0);pointer-events:none;}.ReactTable .-loading > div{position:absolute;display:block;text-align:center;width:100%;top:50%;left:0;font-size:15px;color:rgba(0,0,0,0.6);-webkit-transform:translateY(-52%);-moz-transform:translateY(-52%);-o-transform:translateY(-52%);-ms-transform:translateY(-52%);transform:translateY(-52%);-webkit-transition:0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);-moz-transition:0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);-o-transition:0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);-ms-transition:0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);transition:0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.ReactTable .-loading.-active{opacity:1;-ms-filter:none;filter:none;pointer-events:all;}.ReactTable .-loading.-active > div{-webkit-transform:translateY(50%);-moz-transform:translateY(50%);-o-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%)} \ No newline at end of file diff --git a/react-table.js b/react-table.js new file mode 100644 index 0000000..461dc98 --- /dev/null +++ b/react-table.js @@ -0,0 +1,10 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.reactTable = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o1&&void 0!==arguments[1]?arguments[1]:{};t.headerGroups.push(Object.assign({},s,{columns:e})),a=[]},n=function(e){var a=Object.assign({},t.props.column,e);return"string"==typeof a.accessor&&!function(){a.id=a.id||a.accessor;var e=a.accessor;a.accessor=function(t){return _utils2.default.get(t,e)}}(),a.id||console.warn("No column ID found for column: ",a),a.accessor||console.warn("No column accessor found for column: ",a),a};e.columns.forEach(function(e,r){e.columns?(e.columns.forEach(function(e){t.decoratedColumns.push(n(e))}),t.hasHeaderGroups&&(a.length>0&&s(a),s(_utils2.default.takeRight(t.decoratedColumns,e.columns.length),e))):(t.decoratedColumns.push(n(e)),a.push(_utils2.default.last(t.decoratedColumns)))}),this.hasHeaderGroups&&a.length>0&&s(a)},getInitSorting:function(){var e=this.decoratedColumns.filter(function(e){return"undefined"!=typeof e.sort}).map(function(e){return{id:e.id,asc:"asc"===e.sort}});return e.length?e:[{id:this.decoratedColumns[0].id,asc:!0}]},buildData:function(e,t){var a=this,s=t.sorting===!1?this.getInitSorting():t.sorting,n=function(e){a.setState({sorting:s,data:e,page:t.page,loading:!1})};if(this.isAsync){this.setState({loading:!0});var r=function(e){if(!e)return Promise.reject("Uh Oh! Nothing was returned in ReactTable's data callback!");e.pages&&a.setState({pages:e.pages});var t=a.accessData(e.rows);n(t)},i=e.data({sorting:s,page:t.page||0,pageSize:e.pageSize,pages:t.pages},r);i&&i.then&&i.then(r)}else{var o=this.accessData(e.data),c=this.sortData(o,s);n(c)}},accessData:function(e){var t=this;return e.map(function(e){var a={__original:e};return t.decoratedColumns.forEach(function(t){a[t.id]=t.accessor(e)}),a})},sortData:function(e,t){var a=t.length?t:this.getInitSorting();return _utils2.default.orderBy(e,a.map(function(e){return function(t){return null===t[e.id]||void 0===t[e.id]?-(1/0):"string"==typeof t[e.id]?t[e.id].toLowerCase():t[e.id]}}),a.map(function(e){return e.asc?"asc":"desc"}))},setPage:function(e){return this.isAsync?this.buildData(this.props,Object.assign({},this.state,{page:e})):void this.setState({page:e})},render:function(){var e=this,t=this.state.data?this.state.data:[],a=this.isAsync?this.state.pages:Math.ceil(t.length/this.props.pageSize),s=this.props.pageSize*this.state.page,n=s+this.props.pageSize,r=this.isAsync?t.slice(0,this.props.pageSize):t.slice(s,n),i=a>1?_utils2.default.range(this.props.pageSize-r.length):this.props.minRows?_utils2.default.range(Math.max(this.props.minRows-r.length,0)):[],o=this.state.page>0,c=this.state.page+11&&_react2.default.createElement("div",{className:"-pagination"},_react2.default.createElement("div",{className:"-left"},_react2.default.createElement(u,{onClick:o&&function(t){return e.previousPage(t)},disabled:!o},this.props.previousText)),_react2.default.createElement("div",{className:"-center"},"Page ",this.state.page+1," of ",a),_react2.default.createElement("div",{className:"-right"},_react2.default.createElement(l,{onClick:c&&function(t){return e.nextPage(t)},disabled:!c},this.props.nextText))),_react2.default.createElement("div",{className:(0,_classnames2.default)("-loading",{"-active":this.state.loading})},_react2.default.createElement("div",{className:"-loading-inner"},this.props.loadingComponent)))},sortColumn:function(e,t){var a=this.state.sorting||[],s=_utils2.default.clone(this.state.sorting||[]),n=s.findIndex(function(t){return t.id===e.id});if(n>-1){var r=s[n];r.asc?(r.asc=!1,t||(_utils2.default.remove(s,function(e){return e}),s.push(r))):t?s.splice(n,1):r.asc=!0}else t?s.push({id:e.id,asc:!0}):(_utils2.default.remove(s,function(e){return e}),s.push({id:e.id,asc:!0}));var i=0===n||!a.length&&s.length?0:this.state.page;this.buildData(this.props,Object.assign({},this.state,{page:i,sorting:s}))},nextPage:function(e){e.preventDefault(),this.setPage(this.state.page+1)},previousPage:function(e){e.preventDefault(),this.setPage(this.state.page-1)}}); + +},{"./utils":3,"classnames":1,"react":"react"}],3:[function(require,module,exports){ +"use strict";function remove(r,e){return r.filter(function(t,n){var u=e(t);return!!u&&(r.splice(n,1),!0)})}function get(r,e){return isArray(e)&&(e=e.join(".")),e.replace("[",".").replace("]","").split(".").reduce(function(r,e){return r[e]},r)}function takeRight(r,e){var t=e>r.length?0:r.length-e;return r.slice(t)}function last(r){return r[r.length-1]}function range(r){for(var e=[],t=0;ti)return 1;if(o ( + +) + +export const ReactTableDefaults = { + className: '-striped -highlight', + pageSize: 20, + minRows: 0, + data: [], + previousComponent: defaultButton, + nextComponent: defaultButton, + previousText: 'Previous', + nextText: 'Next', + loadingComponent: Loading..., + column: { + sortable: true, + show: true + } +} + +export default React.component({ + getDefaultProps () { + return ReactTableDefaults + }, + getInitialState () { + return { + sorting: false + } + }, + componentWillMount () { + this.update(this.props) + }, + componentWillReceiveProps (nextProps) { + this.update(nextProps) + }, + update (props) { + const resetState = { + loading: false, + page: 0, + pages: -1 + // columns: {} for column hiding in the future + } + this.setState(resetState) + const newState = Object.assign({}, this.state, resetState) + this.isAsync = typeof props.data === 'function' + this.buildColumns(props, newState) + this.buildData(props, newState) + }, + buildColumns (props) { + this.hasHeaderGroups = false + props.columns.forEach(column => { + if (column.columns) { + this.hasHeaderGroups = true + } + }) + + this.headerGroups = [] + this.decoratedColumns = [] + let currentSpan = [] + + const addHeader = (columns, column = {}) => { + this.headerGroups.push(Object.assign({}, column, { + columns: columns + })) + currentSpan = [] + } + const makeDecoratedColumn = (column) => { + const dcol = Object.assign({}, this.props.column, column) + if (typeof dcol.accessor === 'string') { + dcol.id = dcol.id || dcol.accessor + const accessorString = dcol.accessor + dcol.accessor = row => _.get(row, accessorString) + } + if (!dcol.id) { + console.warn('No column ID found for column: ', dcol) + } + if (!dcol.accessor) { + console.warn('No column accessor found for column: ', dcol) + } + return dcol + } + + props.columns.forEach((column, i) => { + if (column.columns) { + column.columns.forEach(nestedColumn => { + this.decoratedColumns.push(makeDecoratedColumn(nestedColumn)) + }) + if (this.hasHeaderGroups) { + if (currentSpan.length > 0) { + addHeader(currentSpan) + } + addHeader(_.takeRight(this.decoratedColumns, column.columns.length), column) + } + } else { + this.decoratedColumns.push(makeDecoratedColumn(column)) + currentSpan.push(_.last(this.decoratedColumns)) + } + }) + + if (this.hasHeaderGroups && currentSpan.length > 0) { + addHeader(currentSpan) + } + }, + getInitSorting () { + const initSorting = this.decoratedColumns.filter(d => { + return typeof d.sort !== 'undefined' + }).map(d => { + return { + id: d.id, + asc: d.sort === 'asc' + } + }) + + return initSorting.length ? initSorting : [{ + id: this.decoratedColumns[0].id, + asc: true + }] + }, + buildData (props, state) { + const sorting = state.sorting === false ? this.getInitSorting() : state.sorting + + const setData = (data) => { + this.setState({ + sorting, + data, + page: state.page, + loading: false + }) + } + + if (this.isAsync) { + this.setState({ + loading: true + }) + + const cb = (res) => { + if (!res) { + return Promise.reject('Uh Oh! Nothing was returned in ReactTable\'s data callback!') + } + if (res.pages) { + this.setState({ + pages: res.pages + }) + } + // Only access the data. Sorting is done server side. + const accessedData = this.accessData(res.rows) + setData(accessedData) + } + + // Fetch data with current state + const dataRes = props.data({ + sorting, + page: state.page || 0, + pageSize: props.pageSize, + pages: state.pages + }, cb) + + if (dataRes && dataRes.then) { + dataRes.then(cb) + } + } else { + // Return locally accessed, sorted data + const accessedData = this.accessData(props.data) + const sortedData = this.sortData(accessedData, sorting) + setData(sortedData) + } + }, + accessData (data) { + return data.map((d) => { + const row = { + __original: d + } + this.decoratedColumns.forEach(column => { + row[column.id] = column.accessor(d) + }) + return row + }) + }, + sortData (data, sorting) { + const resolvedSorting = sorting.length ? sorting : this.getInitSorting() + return _.orderBy(data, resolvedSorting.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] + } + }), resolvedSorting.map(d => d.asc ? 'asc' : 'desc')) + }, + setPage (page) { + if (this.isAsync) { + return this.buildData(this.props, Object.assign({}, this.state, {page})) + } + this.setState({ + page + }) + }, + + render () { + const data = this.state.data ? this.state.data : [] + + const pagesLength = this.isAsync ? this.state.pages : Math.ceil(data.length / this.props.pageSize) + const startRow = this.props.pageSize * this.state.page + const endRow = startRow + this.props.pageSize + const pageRows = this.isAsync ? data.slice(0, this.props.pageSize) : data.slice(startRow, endRow) + const padRows = pagesLength > 1 ? _.range(this.props.pageSize - pageRows.length) + : this.props.minRows ? _.range(Math.max(this.props.minRows - pageRows.length, 0)) + : [] + + const canPrevious = this.state.page > 0 + const canNext = this.state.page + 1 < pagesLength + + const PreviousComponent = this.props.previousComponent + const NextComponent = this.props.previousComponent + + return ( +
+ + {this.hasHeaderGroups && ( + + + {this.headerGroups.map((column, i) => { + return ( + + ) + })} + + + )} + + + {this.decoratedColumns.map((column, i) => { + const sort = this.state.sorting.find(d => d.id === column.id) + const show = typeof column.show === 'function' ? column.show() : column.show + return ( + + ) + })} + + + + {pageRows.map((row, i) => { + return ( + + {this.decoratedColumns.map((column, i2) => { + const Cell = column.render + const show = typeof column.show === 'function' ? column.show() : column.show + return ( + + ) + })} + + ) + })} + {padRows.map((row, i) => { + return ( + + {this.decoratedColumns.map((column, i2) => { + const show = typeof column.show === 'function' ? column.show() : column.show + return ( + + ) + })} + + ) + })} + +
+
+ {typeof column.header === 'function' ? ( + + ) : column.header} +
+
{ + column.sortable && this.sortColumn(column, e.shiftKey) + }}> +
+ {typeof column.header === 'function' ? ( + + ) : column.header} +
+
+
+ {typeof Cell === 'function' ? ( + + ) : typeof Cell !== 'undefined' ? Cell + : row[column.id]} +
+
+
 
+
+ {pagesLength > 1 && ( +
+
+ this.previousPage(e))} + disabled={!canPrevious}> + {this.props.previousText} + +
+
+ Page {this.state.page + 1} of {pagesLength} +
+
+ this.nextPage(e))} + disabled={!canNext}> + {this.props.nextText} + +
+
+ )} +
+
+ {this.props.loadingComponent} +
+
+
+ ) + }, + sortColumn (column, additive) { + const existingSorting = this.state.sorting || [] + const sorting = _.clone(this.state.sorting || []) + const existingIndex = sorting.findIndex(d => d.id === column.id) + if (existingIndex > -1) { + const existing = sorting[existingIndex] + if (existing.asc) { + existing.asc = false + if (!additive) { + _.remove(sorting, d => d) + sorting.push(existing) + } + } else { + if (additive) { + sorting.splice(existingIndex, 1) + } else { + existing.asc = true + } + } + } else { + if (additive) { + sorting.push({ + id: column.id, + asc: true + }) + } else { + _.remove(sorting, d => d) + sorting.push({ + id: column.id, + asc: true + }) + } + } + const page = (existingIndex === 0 || (!existingSorting.length && sorting.length)) ? 0 : this.state.page + this.buildData(this.props, Object.assign({}, this.state, {page, sorting})) + }, + nextPage (e) { + e.preventDefault() + this.setPage(this.state.page + 1) + }, + previousPage (e) { + e.preventDefault() + this.setPage(this.state.page - 1) + } +}) diff --git a/src/index.styl b/src/index.styl new file mode 100644 index 0000000..53198a3 --- /dev/null +++ b/src/index.styl @@ -0,0 +1,191 @@ +@import 'nib' + +$easeOutQuad = cubic-bezier(0.250, 0.460, 0.450, 0.940) + +.ReactTable + position:relative + table + width: 100% + border-collapse: collapse + border: 1px solid alpha(black, .1) + display: block + overflow-x: auto + + .-td + .-th + &-inner + white-space: nowrap + text-overflow: ellipsis + padding: 7px 5px + overflow: hidden + z-index:0 + transition: .3s ease + transition-property: width, min-width, padding, opacity + + th + td + &:hover + .-td + .-th + &-inner + position:relative + overflow:visible + z-index:1 + text-shadow: + 0 0 1px white, + 0 0 2px white, + 1px 0 3px white, + 1px 0 4px white, + 2px 0 5px white, + 2px 0 6px white, + 3px 0 7px white, + 3px 0 8px white, + 4px 0 9px white, + 4px 0 10px white, + 5px 0 11px white, + 5px 0 12px white, + 6px 0 13px white, + 6px 0 14px white, + 7px 0 15px white, + 7px 0 16px white, + 8px 0 17px white, + 8px 0 18px white, + 9px 0 19px white, + 9px 0 20px white + + > * + text-shadow: none + + &.hidden + border:0 !important + opacity: 0 !important + .-th-inner + .-td-inner + width: 0 !important + min-width: 0 !important + padding: 0 !important + + thead + box-shadow: 0px 5px 20px 0 alpha(black, .1) + z-index: 0 + + &.-headerGroups + border-bottom: 1px solid alpha(black, .05) + + th + td + background: alpha(black, .03) + + tr + text-align:center + + th + td + width: 1% + background: white + font-weight:500 + color: $darkColor + border-right: 1px solid alpha(black, .05) + transition box-shadow .3s easeOutBack + box-shadow:inset 0 0 0 0 transparent + &.sort-asc + box-shadow:inset 0 3px 0 0 alpha(black, .6) + &.sort-desc + box-shadow:inset 0 -3px 0 0 alpha(black, .6) + + tbody + z-index:0 + tr + border-bottom: solid 1px alpha(black, .05) + &:last-child + border-bottom: 0 + + &:first-child td + box-shadow: inset 0 20px 20px -20px alpha(black, .2) + td + border-right:1px solid alpha(black, .02) + &:last-child + border-right:0 + + &.striped + tbody tr:nth-child(even) + background: alpha(black, .03) + &.highlight + tbody tr:hover + background: alpha(black, .05) + + .-pagination + width:100% + display:flex + margin-top:5px + justify-content: space-between + align-items: center + flex-wrap: wrap + + .-btn + appearance:none + display:block + width:100% + border: 0 + border-radius: 3px + padding: 6px + font-size: 1em + color: alpha(black, .6) + background: alpha(black, .1) + transition: all .1s ease + + &[disabled] + opacity: .5 + cursor: default + + &:not([disabled]):hover + background: alpha(black, .3) + color: white + + .-left + .-center + .-right + flex: 1 + // min-width:150px + margin-bottom:5px + + .-left + .-right + text-align: center + + + .-center + text-align:center + padding: 0 5px + + + .-loading + display:block + position:absolute + left:0 + right:0 + top:0 + bottom:0 + background: alpha(white, .8) + transition: all .3s ease + z-index: 2 + opacity: 0 + pointer-events: none + + > div + position:absolute + display: block + text-align:center + width:100% + top:50% + left: 0 + font-size: 15px + color: alpha(black, .6) + transform: translateY(-52%) + transition: .3s $easeOutQuad + + &.-active + opacity: 1 + pointer-events: all + > div + transform: translateY(50%) diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..3fd61e2 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,85 @@ +export default { + get, + takeRight, + last, + orderBy, + range, + clone, + remove +} + +function remove (a, b) { + return a.filter(function (o, i) { + var r = b(o) + if (r) { + a.splice(i, 1) + return true + } + return false + }) +} + +function get (a, b) { + if (isArray(b)) { + b = b.join('.') + } + return b + .replace('[', '.').replace(']', '') + .split('.') + .reduce( + function (obj, property) { + return obj[property] + }, a + ) +} + +function takeRight (arr, n) { + const start = n > arr.length ? 0 : arr.length - n + return arr.slice(start) +} + +function last (arr) { + return arr[arr.length - 1] +} + +function range (n) { + const arr = [] + for (let i = 0; i < n; i++) { + arr.push(n) + } + return arr +} + +function orderBy (arr, funcs) { + return arr.sort((a, b) => { + for (let i = 0; i < funcs.length; i++) { + const comp = funcs[i] + const ca = comp(a) + const cb = comp(b) + if (ca > cb) { + return 1 + } + if (ca < cb) { + return -1 + } + } + return 0 + }) +} + +function clone (a) { + return JSON.parse(JSON.stringify(a, function (key, value) { + if (typeof value === 'function') { + return value.toString() + } + return value + })) +} + +// ######################################################################## +// Helpers +// ######################################################################## + +function isArray (a) { + return Array.isArray(a) +}