├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── docs ├── advance.md ├── faq.md ├── get-started.md ├── glossary.md ├── inline-editing.md └── selection.md ├── package.json ├── screenshots └── advance-table.png ├── src ├── AutoResizer.js ├── BaseTable.js ├── BaseTable.spec.js ├── Column.js ├── ColumnManager.js ├── ColumnResizer.js ├── ExpandIcon.js ├── GridTable.js ├── SortIndicator.js ├── SortOrder.js ├── TableCell.js ├── TableHeader.js ├── TableHeaderCell.js ├── TableHeaderRow.js ├── TableRow.js ├── _BaseTable.scss ├── __snapshots__ │ └── BaseTable.spec.js.snap ├── index.js └── utils.js ├── types └── index.d.ts ├── website ├── .eslintignore ├── .gitignore ├── .prettierrc ├── README.md ├── gatsby-config.js ├── gatsby-node.js ├── package.json ├── siteConfig.js ├── src │ ├── assets │ │ ├── external-url.svg │ │ ├── favicon.png │ │ └── mark-github.svg │ ├── components │ │ ├── ActionPanel.js │ │ ├── Anchor.js │ │ ├── CodeBlock.js │ │ ├── CodeEditor.js │ │ ├── CodePreview.js │ │ ├── CopyButton.js │ │ ├── CornerButton.js │ │ ├── Header.js │ │ ├── Html.js │ │ ├── Methods.js │ │ ├── Page.js │ │ ├── Pagination.js │ │ ├── Playground.js │ │ ├── Props.js │ │ └── Sidebar.js │ ├── examples │ │ ├── 10000-rows.js │ │ ├── auto-resize.js │ │ ├── col-span.js │ │ ├── column-hovering.js │ │ ├── components.js │ │ ├── custom-cell.js │ │ ├── default.js │ │ ├── detail-view.js │ │ ├── disabled.js │ │ ├── draggable-rows-frozen.js │ │ ├── draggable-rows.js │ │ ├── dynamic-row-heights.js │ │ ├── empty-renderer.js │ │ ├── expand-controlled.js │ │ ├── expand-icon-props.js │ │ ├── expand-uncontrolled.js │ │ ├── extra-props.js │ │ ├── fixed.js │ │ ├── flex-column.js │ │ ├── footer-renderer.js │ │ ├── frozen-columns.js │ │ ├── frozen-rows.js │ │ ├── header-renderer.js │ │ ├── hide-header.js │ │ ├── infinite-loading-loader.js │ │ ├── infinite-loading.js │ │ ├── inline-editing.js │ │ ├── is-scrolling.js │ │ ├── jsx-column.js │ │ ├── max-height.js │ │ ├── multi-header.js │ │ ├── multi-sort.js │ │ ├── overlay-renderer.js │ │ ├── resizable.js │ │ ├── row-event-handlers.js │ │ ├── row-header-height.js │ │ ├── row-key.js │ │ ├── row-renderer.js │ │ ├── row-span.js │ │ ├── scroll-to.js │ │ ├── scrollbar.js │ │ ├── selection.js │ │ ├── sortable.js │ │ ├── sticky-rows.js │ │ ├── tag-name.js │ │ ├── tooltip-cell.js │ │ └── width-height.js │ ├── pages │ │ ├── 404.js │ │ ├── api.js │ │ ├── docs.js │ │ ├── examples.js │ │ ├── index.js │ │ └── playground │ │ │ └── index.js │ ├── styles │ │ └── index.css │ ├── templates │ │ ├── api.js │ │ ├── doc.js │ │ └── example.js │ └── utils │ │ ├── actionChannel.js │ │ ├── baseScope.js │ │ ├── eventEmitter.js │ │ ├── sample.code │ │ └── urlHash.js └── yarn.lock └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": "commonjs", 7 | }, 8 | ], 9 | "@babel/preset-react", 10 | ], 11 | "plugins": [ 12 | "@babel/plugin-proposal-class-properties", 13 | "@babel/plugin-proposal-object-rest-spread", 14 | "@babel/plugin-transform-runtime" 15 | ], 16 | "env": { 17 | "es": { 18 | "presets": [ 19 | [ 20 | "@babel/preset-env", 21 | { 22 | "modules": false, 23 | "loose": true, 24 | }, 25 | ], 26 | ], 27 | }, 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | es/ 3 | website/ 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "plugin:prettier/recommended", "prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "react/prop-types": 2 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore annoying DS files 2 | .DS_Store 3 | *.DS_Store 4 | **.DS_Store 5 | 6 | # npm 7 | node_modules 8 | npm-debug.log 9 | npm-debug.log.* 10 | 11 | # yarn 12 | yarn-error.log 13 | yarn-error.log.* 14 | 15 | styles.css 16 | es/ 17 | lib/ 18 | coverage/ 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.16.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | es/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## NEXT VERSION 4 | 5 | ## v1.13.5 (2024-05-24) 6 | 7 | - fix: stop using React.Key in typings (#428) 8 | 9 | ## v1.13.4 (2023-03-15) 10 | 11 | - fix: allow any react node to be used as column title 12 | 13 | ## v1.13.3 (2023-03-13) 14 | 15 | - fix: pass extra sorting props to custom SortIndicator component 16 | 17 | ## v1.13.2 (2022-05-14) 18 | 19 | - fix: error imported by optimization render task 20 | 21 | ## v1.13.1 (2022-05-14) 22 | 23 | - fix: optimization render task performance 24 | 25 | ## v1.13.0 (2021-10-24) 26 | 27 | - fix: change propTypes for `BaseTable.components` 28 | - feat: add support for React 17 29 | 30 | ## v1.12.0 (2020-10-11) 31 | 32 | - feat: add the ability to pass function in `estimatedRowHeight` to determine the initial height of rows 33 | 34 | ## v1.11.3 (2020-08-24) 35 | 36 | - fix: remove propTypes for Column.key 37 | 38 | ## v1.11.2 (2020-08-18) 39 | 40 | - fix: add missing types for propTypes of `BaseTable` 41 | 42 | ## v1.11.1 (2020-08-17) 43 | 44 | - fix: add types folder into packages 45 | - chore: mark `Column.key` as required 46 | 47 | ## v1.11.0 (2020-08-17) 48 | 49 | - feat: add `ignoreFunctionInColumnCompare` to solve closure problem in renderers 50 | - chore: skip unnecessary cloneElement in `renderElement` 51 | - feat: add type declarations 52 | 53 | ## v1.10.9 (2020-08-13) 54 | 55 | - fix: input loses focus on unmount 56 | 57 | ## v1.10.8 (2020-08-11) 58 | 59 | - fix: scroll position would be reset to top if column becomes frozen 60 | 61 | ## v1.10.7 (2020-07-28) 62 | 63 | - fix: `getTotalRowsHeight` could be different before/after render in dynamic mode on initial render 64 | 65 | ## v1.10.6 (2020-07-25) 66 | 67 | - fix: `getTotalRowsHeight` could be different before/after render in dynamic mode 68 | 69 | ## v1.10.5 (2020-07-10) 70 | 71 | - chore: do not clear row height cache automatically 72 | 73 | ## v1.10.4 (2020-07-02) 74 | 75 | - fix: flicker on expanding in dynamic mode 76 | 77 | ## v1.10.3 (2020-06-30) 78 | 79 | - fix: horizontal scrollbar in flex mode with dynamic row height 80 | - chore: tweak row height measurement 81 | 82 | ## v1.10.2 (2020-06-26) 83 | 84 | - fix: regression of expansion with frozen columns 85 | - fix: dynamic rowHeight is not updated when `data` or `columns` changed 86 | 87 | ## v1.10.1 (2020-06-24) 88 | 89 | - fix: dynamic rowHeight is not calculated correctly with frozen columns 90 | - fix: dynamic rowHeight is not updated when resizing column 91 | 92 | ## v1.10.0 (2020-06-22) 93 | 94 | - feat: add `estimatedRowHeight` to support dynamic row height 95 | 96 | ## v1.9.4 (2020-06-22) 97 | 98 | - chore: loosen prop type check for `data` 99 | 100 | ## v1.9.3 (2020-05-26) 101 | 102 | - fix: wrong description for Column props 103 | 104 | ## v1.9.2 (2020-04-22) 105 | 106 | - fix: frozen data not shown with empty data 107 | 108 | ## v1.9.1 (2019-10-17) 109 | 110 | - reverted #80, now custom renderer doesn't support top level hooks, see #109 111 | 112 | ## v1.9.0 (2019-09-24) 113 | 114 | - feat: add `onColumnResizeEnd` prop to `BaseTable` 115 | 116 | ## v1.8.1 (2019-09-23) 117 | 118 | - fix: `unflatten` should not override the existing children 119 | 120 | ## v1.8.0 (2019-08-30) 121 | 122 | - feat: remove deprecated lifecycles for concurrent mode ready 123 | 124 | ## v1.7.3 (2019-08-27) 125 | 126 | - fix: fix possible memory leak in `ColumnResizer` 127 | - fix: bring back column resize on touch events support 128 | 129 | ## v1.7.2 (2019-08-26) 130 | 131 | - ~~fix: custom renderers should support function component with hooks~~ 132 | 133 | ## v1.7.1 (2019-08-22) 134 | 135 | - fix: `scrollToRow` doesn't work regression introduced in #73 136 | 137 | ## v1.7.0 (2019-08-06) 138 | 139 | - chore: remove the use of `Object.values` 140 | - feat: add `getColumnManager` and `getDOMNode` methods 141 | 142 | ## v1.6.5 (2019-07-11) 143 | 144 | - fix: style value `unset` is not supported on IE 145 | 146 | ## v1.6.4 (2019-07-09) 147 | 148 | - fix: content been selected when dragging on IE/FF (regression from #56) 149 | 150 | ## v1.6.3 (2019-07-09) 151 | 152 | - fix: `minWidth` is passed to dom node in `ColumnResizer` in #56 153 | 154 | ## v1.6.2 (2019-07-07) 155 | 156 | - fix: `getValue` returns the object itself if the path is not valid string 157 | 158 | ## v1.6.1 (2019-07-06) 159 | 160 | - feat: support infinite loading with `maxHeight` 161 | - chore: remove unused `$table-border-radius` variable 162 | - feat: add `sortState` to support multi sort 163 | 164 | ## v1.5.0 (2019-07-06) 165 | 166 | - refactor: remove dependent on `react-draggable` 167 | - fix: undefined parentId should be considered as root item 168 | - fix: `flattenOnKeys` not works with immutable data (regression from #23) 169 | 170 | ## v1.4.0 (2019-07-01) 171 | 172 | - refactor: remove dependent on `lodash`, and export `getValue` 173 | - build: use `@babel/plugin-transform-runtime` to reduce bundle size 174 | 175 | ## v1.3.3 (2019-06-27) 176 | 177 | - fix: use `PropTypes.elementType` for `tagName`'s type 178 | 179 | ## v1.3.2 (2019-06-14) 180 | 181 | - fix: resizing line rendered incorrectly when resizing the right frozen column 182 | 183 | ## v1.3.1 (2019-06-06) 184 | 185 | - chore: upgrade `react-window` to silence the deprecation 186 | 187 | ## v1.3.0 (2019-05-10) 188 | 189 | - feat: allow `rowKey` to be `number` 190 | - feat: add `getScrollbarSize` to `BaseTable` to custom scrollbar size measurement 191 | 192 | ## v1.2.2 (2019-05-03) 193 | 194 | - perf: optimize `unflatten` and `flattenOnKeys` to not use recursion 195 | 196 | ## v1.2.1 (2019-05-01) 197 | 198 | - fix: scrollbar size don't updated in SSR 199 | 200 | ## v1.2.0 (2019-04-29) 201 | 202 | - chore: more accurate `onScrollbarPresenceChange` 203 | - feat: replace `react-virtualized` with `react-window` 204 | - feat: add scroll direction to `onScroll` 205 | - feat: add `align` to `scrollToRow` 206 | 207 | ## v1.1.1 (2019-04-27) 208 | 209 | - fix: `flattenOnKeys` memoize is opt out because `this._depthMap` changes everytime 210 | - fix: hover state is out of sync in frozen rows, regression introduced in #9 211 | 212 | ## v1.1.0 (2019-04-26) 213 | 214 | - chore: stop using `Grid` for table's header 215 | - chore: add `role` to table's elements 216 | - chore: stop using `forceUpdate` to update the table 217 | - fix: table's header is re-rendered unnecessarily on row hovered 218 | 219 | ## v1.0.2 (2019-04-03) 220 | 221 | - fix: `onScroll` is called redundantly if there are frozen columns 222 | 223 | ## v1.0.1 (2019-03-28) 224 | 225 | - fix: header row's height doesn't update on `headerHeight` change 226 | 227 | ## v1.0.0 (2019-03-26) 228 | 229 | Initial public release 230 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions in any form are welcome. You can contribute by reporting issues, submitting pull requests, reviewing pull requests, participating in discussions on issues and pull requests and more. 4 | 5 | ## Reporting issues 6 | 7 | Issues can be questions, feature requests, enhancements, optimizations and more. Please use **Issues** to track them. 8 | 9 | When filing a bug, it would be helpful to provide a reproducible demonstration of the bug. 10 | 11 | ## Submitting pull requests 12 | 13 | When ready to contribute, fork this repository and submit a pull request that references the issue it resolves. Be sure to include a clear and detailed description of the changes you've made so that we can verify them and eventually merge. 14 | 15 | ## Maintainers 16 | 17 | Maintainers are responsible for responding to pull requests and issues, as well as guiding the direction of the project. 18 | 19 | If you've established yourself as an impactful contributor to the project, and are willing take on the extra work, we'd love to have your help maintaining it! Email the current maintainers for details. 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Autodesk, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-base-table 2 | 3 | BaseTable is a react table component to display large datasets with high performance and flexibility 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## Install 11 | 12 | ```bash 13 | # npm 14 | npm install react-base-table --save 15 | 16 | # yarn 17 | yarn add react-base-table 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```js 23 | import BaseTable, { Column } from 'react-base-table' 24 | import 'react-base-table/styles.css' 25 | // Important: if you fail to import react-base-table/styles.css then 26 | // BaseTable will not render as advertised in the included examples. 27 | // For advanced styling see link below: 28 | // https://github.com/Autodesk/react-base-table#advance 29 | ... 30 | 31 | 32 | 33 | ... 34 | 35 | ... 36 | ``` 37 | 38 | Learn more at the [website](https://autodesk.github.io/react-base-table/) 39 | 40 | ### unique key 41 | 42 | `key` is required for column definition or the column will be ignored 43 | 44 | Make sure each item in `data` is unique by a key, the default key is `id`, you can customize it via `rowKey` 45 | 46 | ### size 47 | 48 | `width` is required for column definition, but in flex mode(`fixed={false}`), you can set `width={0}` and `flexGrow={1}` to achieve flexible column width, checkout the [Flex Column](https://autodesk.github.io/react-base-table/examples/flex-column) example 49 | 50 | `width` and `height`(or `maxHeight`) are required to display the table properly 51 | 52 | In the [examples](https://autodesk.github.io/react-base-table/examples) 53 | we are using a wrapper `const Table = props => ` to do that 54 | 55 | If you want it responsive, you can use the [`AutoResizer`](https://autodesk.github.io/react-base-table/api/autoresizer) to make the table fill the container, checkout the [Auto Resize](https://autodesk.github.io/react-base-table/examples/auto-resize) example 56 | 57 | ### closure problem in custom renderers 58 | 59 | In practice we tend to write inline functions for custom renderers, which would make `shouldUpdateComponent` always true as the inline function will create a new instance on every re-render, to avoid "unnecessary" re-renders, **`BaseTable` ignores functions when comparing column definition by default**, it works well in most cases before, but if we use external data instead of reference state in custom renderers, we always get the staled initial value although the data has changed 60 | 61 | It's recommended to inject the external data in column definition to solve the problem, like ` { ... } } />`, the column definition will update on external data change, with this pattern we can easily move the custom renderers out of column definition for sharing, the downside is it would bloat the column definition and bug prone 62 | 63 | Things getting worse with the introduction of React hooks, we use primitive state instead of `this.state`, so it's easy to encounter the closure problem, but with React hooks, we can easily memoize functions via `useCallback` or `useMemo`, so the implicit optimization could be replaced with user land optimization which is more intuitive, to turn off the implicit optimization, set `ignoreFunctionInColumnCompare` to `false` which is introduced since `v1.11.0` 64 | 65 | Here is an [example](https://autodesk.github.io/react-base-table/playground#MYewdgzgLgBKA2BXAtpGBeGBzApmHATgIZQ4DCISqEAFAIwAMAlAFCiSwAmJRG2ehEjgAiPGghSQANDABMDZixY4AHgAcQBLjgBmRRPFg0mGAHwwA3ixhxw0GAG1QiMKQIyIOKBRduAunwASjhEwFAAdIieAMpQQjSKNuz2DgCWWGCaOABiLmGp4B5eAJIZWblg+eABmMGhEVE4sfFQBIg4rEl2sGlgAFY4YRRUYEVQxf2D3pSSNTB1YZExcaQ0evCenTAEXogEYDA01jYwADymxydnnKkAbjDQAJ7wOOgWFjBqRJw3YFgAXDAACwyG4QNTwIiPQEAch0LxUMJkfSiUFSOkeFFceCgsPBoRwAFoAEZeADuODwMJgAF8aRcrldTsTEFAoOAYOAyPBUsAANZvYxmB5eHzYgjiEC+QgwADUMDoTHpstOAHoWWzwAzGTZTpDSfBtTrdakwGpWZdjTYoI81K8AETAAAWgz5xJAKntlqtztdOE4b3SmR2FSqYBp3uNXKdRD+rwsOGFnnGZRDeTR4BoOHCcQIuAivv5-qVkauqqNxtKwcTOnTBQOptsI1syC+O1LZ1V+pwho7eqIBorOtOpvNUA7VxtdvQjpd-PdnonJ0LfP9gcmQxmqAjVqu0djuDeifQ5mTEwGm5GWZzRDzXnCK+LO935aX56mMFUrV43DiMHZTaSDAnC6KaqQZmAfZdgOPZDp2Ny3HBpwACoDi8MA6KkKj+sBPBvL+RA0jAQblHW4ATMMkgUK2t7xiRaaVBB9J9pRqBLhY4ScRI1AOAwfjPlaBEAOJeG4gomCetjSgQAnGs44rrhe0zNgA-FJ4owICLggZh+CcLJZZwbqrEHBxXFbpADh0PxMCvlapwmZYnEPhZEAOLINl2caDkWU55kjG5ADMnlGWcjlmS5AUOECIWRmqqHEi8FZqtqrARkAA) to demonstrate 66 | 67 | ## Browser Support 68 | 69 | `BaseTable` is well tested on all modern browsers and IE11. _You have to polyfill `Array.prototype.findIndex` to make it works on IE_ 70 | 71 | **The [examples](https://autodesk.github.io/react-base-table/examples) don't work on IE as they are powered by [react-runner](https://github.com/nihgwu/react-runner) which is a `react-live` like library but only for modern browsers.** 72 | 73 | ## Advance 74 | 75 | BaseTable is designed to be the base component to build your own complex table component 76 | 77 | ### Styling 78 | 79 | The simplest way is overriding the default styles (assuming you are using `scss`) 80 | 81 | ```scss 82 | // override default variables for BaseTable 83 | $table-prefix: AdvanceTable; 84 | 85 | $table-font-size: 13px; 86 | $table-padding-left: 15px; 87 | $table-padding-right: 15px; 88 | $column-padding: 7.5px; 89 | ... 90 | $show-frozen-rows-shadow: false; 91 | $show-frozen-columns-shadow: true; 92 | 93 | @import '~react-base-table/es/_BaseTable.scss'; 94 | 95 | .#{$table-prefix} { 96 | &:not(.#{$table-prefix}--show-left-shadow) { 97 | .#{$table-prefix}__table-frozen-left { 98 | box-shadow: none; 99 | } 100 | } 101 | 102 | &:not(.#{$table-prefix}--show-right-shadow) { 103 | .#{$table-prefix}__table-frozen-right { 104 | box-shadow: none; 105 | } 106 | } 107 | 108 | ... 109 | } 110 | ``` 111 | 112 | You can write your own styles from scratch or use CSS-in-JS solutions to achieve that 113 | 114 | ### Custom components 115 | 116 | ```jsx 117 | 127 | ``` 128 | 129 | ### Custom renderers & props 130 | 131 | There are a lot of highly flexible props like `xxxRenderer` and `xxxProps` for you to build your own table component, please check the [api](https://autodesk.github.io/react-base-table/api) and [examples](https://autodesk.github.io/react-base-table/examples) for more details 132 | 133 | ### Example 134 | 135 | We are using a advanced table component based on `BaseTable` internally, with much more features, including row selection, row grouping, data aggregation, column settings, column reordering, and column grouping, tooltip, inline editing. 136 | 137 | ![AdvanceTable](screenshots/advance-table.png) 138 | 139 | [In real products](https://blogs.autodesk.com/bim360-release-notes/2019/11/18/bim-360-cost-management-update-november-2019/) 140 | 141 | ## Development 142 | 143 | We use `Yarn` as the package manager, checkout this repo and run `yarn` under both root folder and `website` folder in install packages, then run `yarn start` to start the demo site powered by `Gatsby` 144 | 145 | ## Contributing 146 | 147 | Please check [guidelines](CONTRIBUTING.md) for more details 148 | -------------------------------------------------------------------------------- /docs/advance.md: -------------------------------------------------------------------------------- 1 | # Advance 2 | 3 | BaseTable is designed to be the base component to build your own complex table component 4 | 5 | ## Styling 6 | 7 | The simplest way is overriding the default styles (assuming you are using `scss`) 8 | 9 | ```scss 10 | // override default variables for BaseTable 11 | $table-prefix: AdvanceTable; 12 | 13 | $table-font-size: 13px; 14 | $table-padding-left: 15px; 15 | $table-padding-right: 15px; 16 | $column-padding: 7.5px; 17 | ... 18 | $show-frozen-rows-shadow: false; 19 | $show-frozen-columns-shadow: true; 20 | 21 | @import '~react-base-table/es/_BaseTable.scss'; 22 | 23 | .#{$table-prefix} { 24 | &:not(.#{$table-prefix}--show-left-shadow) { 25 | .#{$table-prefix}__table-frozen-left { 26 | box-shadow: none; 27 | } 28 | } 29 | 30 | &:not(.#{$table-prefix}--show-right-shadow) { 31 | .#{$table-prefix}__table-frozen-right { 32 | box-shadow: none; 33 | } 34 | } 35 | 36 | ... 37 | } 38 | ``` 39 | 40 | You can write your own styles from scratch or use CSS-in-JS solutions to achieve that 41 | 42 | ## Custom components 43 | 44 | ```jsx 45 | 55 | ``` 56 | 57 | ## Custom renderers & props 58 | 59 | There are a lot of highly flexible props like `xxxRenderer` and `xxxProps` for you to build your own table component, please check the [api](https://autodesk.github.io/react-base-table/api) and [examples](https://autodesk.github.io/react-base-table/examples) for more details 60 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | ### TODO 2 | -------------------------------------------------------------------------------- /docs/get-started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | BaseTable is a react table component to display large data set with high performance and flexibility 4 | 5 | ## Install 6 | 7 | ```bash 8 | # npm 9 | npm install react-base-table --save 10 | 11 | # yarn 12 | yarn add react-base-table 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | import BaseTable, { Column } from 'react-base-table' 19 | import 'react-base-table/styles.css' 20 | 21 | ... 22 | 23 | 24 | 25 | ... 26 | 27 | ... 28 | ``` 29 | 30 | ### unique key 31 | 32 | `key` is required for column definition or the column will be ignored 33 | 34 | Make sure each item in `data` is unique by a key, the default key is `id`, you can customize it via `rowKey` 35 | 36 | ### size 37 | 38 | `width` is required for column definition, but in flex mode(`fixed={false}`), you can set `width={0}` and `flexGrow={1}` to achieve flexible column width, checkout the [Flex Column](https://autodesk.github.io/react-base-table/examples/flex-column) example 39 | 40 | `width` and `height`(or `maxHeight`) are required to display the table properly 41 | 42 | In the [examples](https://autodesk.github.io/react-base-table/examples) 43 | we are using a wrapper `const Table = props => ` to do that 44 | 45 | If you want it responsive, you can use the [`AutoResizer`](https://autodesk.github.io/react-base-table/api/autoresizer) to make the table fill the container, checkout the [Auto Resize](https://autodesk.github.io/react-base-table/examples/auto-resize) example 46 | 47 | ### closure problem in custom renderers 48 | 49 | In practice we tend to write inline functions for custom renderers, which would make `shouldUpdateComponent` always true as the inline function will create a new instance on every re-render, to avoid "unnecessary" re-renders, **`BaseTable` ignores functions when comparing column definition by default**, it works well in most cases before, but if we use external data instead of reference state in custom renderers, we always get the staled initial value although the data has changed 50 | 51 | It's recommended to inject the external data in column definition to solve the problem, like ` { ... } } />`, the column definition will update on external data change, with this pattern we can easily move the custom renderers out of column definition for sharing, the downside is it would bloat the column definition and bug prone 52 | 53 | Things getting worse with the introduction of React hooks, we use primitive state instead of `this.state`, so it's easy to encounter the closure problem, but with React hooks, we can easily memoize functions via `useCallback` or `useMemo`, so the implicit optimization could be replaced with user land optimization which is more intuitive, to turn off the implicit optimization, set `ignoreFunctionInColumnCompare` to `false` which is introduced since `v1.11.0` 54 | 55 | Here is an [example](https://autodesk.github.io/react-base-table/playground#MYewdgzgLgBKA2BXAtpGBeGBzApmHATgIZQ4DCISqEAFAIwAMAlAFCiSwAmJRG2ehEjgAiPGghSQANDABMDZixY4AHgAcQBLjgBmRRPFg0mGAHwwA3ixhxw0GAG1QiMKQIyIOKBRduAunwASjhEwFAAdIieAMpQQjSKNuz2DgCWWGCaOABiLmGp4B5eAJIZWblg+eABmMGhEVE4sfFQBIg4rEl2sGlgAFY4YRRUYEVQxf2D3pSSNTB1YZExcaQ0evCenTAEXogEYDA01jYwADymxydnnKkAbjDQAJ7wOOgWFjBqRJw3YFgAXDAACwyG4QNTwIiPQEAch0LxUMJkfSiUFSOkeFFceCgsPBoRwAFoAEZeADuODwMJgAF8aRcrldTsTEFAoOAYOAyPBUsAANZvYxmB5eHzYgjiEC+QgwADUMDoTHpstOAHoWWzwAzGTZTpDSfBtTrdakwGpWZdjTYoI81K8AETAAAWgz5xJAKntlqtztdOE4b3SmR2FSqYBp3uNXKdRD+rwsOGFnnGZRDeTR4BoOHCcQIuAivv5-qVkauqqNxtKwcTOnTBQOptsI1syC+O1LZ1V+pwho7eqIBorOtOpvNUA7VxtdvQjpd-PdnonJ0LfP9gcmQxmqAjVqu0djuDeifQ5mTEwGm5GWZzRDzXnCK+LO935aX56mMFUrV43DiMHZTaSDAnC6KaqQZmAfZdgOPZDp2Ny3HBpwACoDi8MA6KkKj+sBPBvL+RA0jAQblHW4ATMMkgUK2t7xiRaaVBB9J9pRqBLhY4ScRI1AOAwfjPlaBEAOJeG4gomCetjSgQAnGs44rrhe0zNgA-FJ4owICLggZh+CcLJZZwbqrEHBxXFbpADh0PxMCvlapwmZYnEPhZEAOLINl2caDkWU55kjG5ADMnlGWcjlmS5AUOECIWRmqqHEi8FZqtqrARkAA) to demonstrate 56 | 57 | ## Browser Support 58 | 59 | `BaseTable` is well tested on all modern browsers and IE11. _You have to polyfill `Array.prototype.findIndex` to make it works on IE_ 60 | 61 | **The [examples](https://autodesk.github.io/react-base-table/examples) don't work on IE as they are powered by [react-runner](https://github.com/nihgwu/react-runner) which is a `react-live` like library but only for modern browsers.** 62 | 63 | ## Playground 64 | 65 | ```jsx live 66 | const columns = generateColumns(10); 67 | const data = generateData(columns, 200); 68 | 69 | export default () => ; 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/glossary.md: -------------------------------------------------------------------------------- 1 | ### TODO 2 | -------------------------------------------------------------------------------- /docs/inline-editing.md: -------------------------------------------------------------------------------- 1 | # Inline Editing 2 | 3 | `Inline Editing` is a very common feature in a table, but it's highly coupled with specific ui libraries, so it won't be a part of `BaseTable` itself. 4 | 5 | ## The Problem 6 | 7 | `Inline Editing` would be a bit tricky in `BaseTable` because of it's using the virtualization technology to render the rows, there is `overflow: hidden` for the table and rows and cells, so if your editing content is larger than the cell area, the content would be cut, you genius would find that you could override the `overflow: hidden` via style to prevent the content to be clipped, but **PLEASE DON'T DO THAT**, as there would be always problems with this solution, e.g. what would happens if it's in the last row? 8 | 9 | ## How 10 | 11 | The recommended solution is using something like `Portal` to render the editing content out of the cell, then it won't be constrained in the cell. As `Portal` needs a container to attach the target to, most of the custom renderers provide a param `container` to be used in this case, the `container` is the table itself. 12 | 13 | Internally we are using the `Overlay` component from [react-overlays](https://github.com/react-bootstrap/react-overlays) to do that, `react-overlays` is based on [Popper.js](https://github.com/FezVrasta/popper.js) which provides excellent positioning mechanism. 14 | 15 | If you are using fixed mode(fixed=true) with frozen columns, there will be a problem with the `Popper.js`. As the default `boundariesElement` for `preventOverflow` is `scrollParent`, but there would be three tables internal tables to implement the frozen feature, and those tables are all scrollable, then the positioning could be not expected, you could change the `boundariesElement` to `viewport` or the `container` to fix that. 16 | 17 | ## API Design 18 | 19 | `Column.editable: object | fun({ cellData, column, columnIndex, rowData, rowIndex })` 20 | 21 | You can use `callOrReturn` exported from this package to get the result, the result could be a `boolean` to indicate the specific cell is editable or not, or an object which include the options like `{ disabled, ...editorProps }`, the result would be used in your custom `TableCell` component. 22 | 23 | ## Recipe 24 | 25 | _The following is really a rough one, will improve it later_ 26 | 27 | ```jsx 28 | // import { Overlay } from 'react-overlays' 29 | const { Overlay } = ReactOverlays; 30 | 31 | const CellContainer = styled.div` 32 | display: flex; 33 | flex: 1 0 100%; 34 | align-items: center; 35 | height: 100%; 36 | overflow: hidden; 37 | margin: 0 -5px; 38 | padding: 5px; 39 | border: 1px dashed transparent; 40 | `; 41 | 42 | const GlobalStyle = createGlobalStyle` 43 | .BaseTable__row:hover, 44 | .BaseTable__row--hover { 45 | ${CellContainer} { 46 | border: 1px dashed #ccc; 47 | } 48 | } 49 | `; 50 | 51 | const Select = styled.select` 52 | width: 100%; 53 | height: 30px; 54 | margin-top: 10px; 55 | `; 56 | 57 | class EditableCell extends React.PureComponent { 58 | state = { 59 | value: this.props.cellData, 60 | editing: false, 61 | }; 62 | 63 | setTargetRef = ref => (this.targetRef = ref); 64 | 65 | getTargetRef = () => this.targetRef; 66 | 67 | handleClick = () => this.setState({ editing: true }); 68 | 69 | handleHide = () => this.setState({ editing: false }); 70 | 71 | handleChange = e => 72 | this.setState({ 73 | value: e.target.value, 74 | editing: false, 75 | }); 76 | 77 | render() { 78 | const { container, rowIndex, columnIndex } = this.props; 79 | const { value, editing } = this.state; 80 | 81 | return ( 82 | 83 | {!editing && value} 84 | {editing && this.targetRef && ( 85 | 86 | {({ props, placement }) => ( 87 |
95 | 101 |
102 | )} 103 |
104 | )} 105 |
106 | ); 107 | } 108 | } 109 | 110 | const columns = generateColumns(5); 111 | const data = generateData(columns, 100); 112 | 113 | columns[0].cellRenderer = EditableCell; 114 | columns[0].width = 300; 115 | 116 | export default () => ( 117 | <> 118 | 119 |
120 | 121 | ); 122 | ``` 123 | 124 | ## Example 125 | 126 | Check the live example [here](https://autodesk.github.io/react-base-table/examples/inline-editing). 127 | -------------------------------------------------------------------------------- /docs/selection.md: -------------------------------------------------------------------------------- 1 | # Selection 2 | 3 | There is a [PR](https://github.com/Autodesk/react-base-table/pull/39) to add selection feature, but I don't want to merge it with [good reasons](https://github.com/Autodesk/react-base-table/pull/39#pullrequestreview-241987600), so I'd like to share a recipe here for reference 4 | 5 | ## Recipe 6 | 7 | ```jsx 8 | const StyledTable = styled(BaseTable)` 9 | .row-selected { 10 | background-color: #e3e3e3; 11 | } 12 | `; 13 | 14 | class SelectionCell extends React.PureComponent { 15 | _handleChange = e => { 16 | const { rowData, rowIndex, column } = this.props; 17 | const { onChange } = column; 18 | 19 | onChange({ selected: e.target.checked, rowData, rowIndex }); 20 | }; 21 | 22 | render() { 23 | const { rowData, column } = this.props; 24 | const { selectedRowKeys, rowKey } = column; 25 | const checked = selectedRowKeys.includes(rowData[rowKey]); 26 | 27 | return ; 28 | } 29 | } 30 | 31 | class SelectableTable extends React.PureComponent { 32 | constructor(props) { 33 | super(props); 34 | 35 | const { selectedRowKeys, defaultSelectedRowKeys, expandedRowKeys, defaultExpandedRowKeys } = props; 36 | this.state = { 37 | selectedRowKeys: (selectedRowKeys !== undefined ? selectedRowKeys : defaultSelectedRowKeys) || [], 38 | expandedRowKeys: (expandedRowKeys !== undefined ? expandedRowKeys : defaultExpandedRowKeys) || [], 39 | }; 40 | } 41 | 42 | /** 43 | * Set `selectedRowKeys` manually. 44 | * This method is available only if `selectedRowKeys` is uncontrolled. 45 | * 46 | * @param {array} selectedRowKeys 47 | */ 48 | setSelectedRowKeys(selectedRowKeys) { 49 | // if `selectedRowKeys` is controlled 50 | if (this.props.selectedRowKeys !== undefined) return; 51 | 52 | this.setState({ 53 | selectedRowKeys: cloneArray(selectedRowKeys), 54 | }); 55 | } 56 | 57 | /** 58 | * See BaseTable#setExpandedRowKeys 59 | */ 60 | setExpandedRowKeys(expandedRowKeys) { 61 | // if `expandedRowKeys` is controlled 62 | if (this.props.expandedRowKeys !== undefined) return; 63 | 64 | this.setState({ 65 | expandedRowKeys: cloneArray(expandedRowKeys), 66 | }); 67 | } 68 | 69 | /* some other custom methods and proxy methods */ 70 | 71 | /** 72 | * Remove rowKeys from inner state manually, it's useful to purge dirty state after rows removed. 73 | * This method is available only if `selectedRowKeys` or `expandedRowKeys` is uncontrolled. 74 | * 75 | * @param {array} rowKeys 76 | */ 77 | removeRowKeysFromState(rowKeys) { 78 | if (!Array.isArray(rowKeys)) return; 79 | 80 | const state = {}; 81 | if (this.props.selectedRowKeys === undefined && this.state.selectedRowKeys.length > 0) { 82 | state.selectedRowKeys = this.state.selectedRowKeys.filter(key => !rowKeys.includes(key)); 83 | } 84 | if (this.props.expandedRowKeys === undefined && this.state.expandedRowKeys.length > 0) { 85 | state.expandedRowKeys = this.state.expandedRowKeys.filter(key => !rowKeys.includes(key)); 86 | } 87 | if (state.selectedRowKeys || state.expandedRowKeys) { 88 | this.setState(state); 89 | } 90 | } 91 | 92 | _handleSelectChange = ({ selected, rowData, rowIndex }) => { 93 | const selectedRowKeys = [...this.state.selectedRowKeys]; 94 | const key = rowData[this.props.rowKey]; 95 | 96 | if (selected) { 97 | if (!selectedRowKeys.includes(key)) selectedRowKeys.push(key); 98 | } else { 99 | const index = selectedRowKeys.indexOf(key); 100 | if (index > -1) { 101 | selectedRowKeys.splice(index, 1); 102 | } 103 | } 104 | 105 | // if `selectedRowKeys` is uncontrolled, update internal state 106 | if (this.props.selectedRowKeys === undefined) { 107 | this.setState({ selectedRowKeys }); 108 | } 109 | this.props.onRowSelect({ selected, rowData, rowIndex }); 110 | this.props.onSelectedRowsChange(selectedRowKeys); 111 | }; 112 | 113 | _rowClassName = ({ rowData, rowIndex }) => { 114 | const { rowClassName, rowKey } = this.props; 115 | const { selectedRowKeys } = this.state; 116 | 117 | const rowClass = rowClassName ? callOrReturn(rowClassName, { rowData, rowIndex }) : ''; 118 | const key = rowData[rowKey]; 119 | 120 | return [rowClass, selectedRowKeys.includes(key) && 'row-selected'].filter(Boolean).concat(' '); 121 | }; 122 | 123 | render() { 124 | const { columns, children, selectable, selectionColumnProps, ...rest } = this.props; 125 | const { selectedRowKeys } = this.state; 126 | 127 | // you'd better memoize this operation 128 | let _columns = columns || normalizeColumns(children); 129 | if (selectable) { 130 | const selectionColumn = { 131 | width: 40, 132 | flexShrink: 0, 133 | resizable: false, 134 | frozen: Column.FrozenDirection.LEFT, 135 | cellRenderer: SelectionCell, 136 | ...selectionColumnProps, 137 | key: '__selection__', 138 | rowKey: this.props.rowKey, 139 | selectedRowKeys: selectedRowKeys, 140 | onChange: this._handleSelectChange, 141 | }; 142 | _columns = [selectionColumn, ..._columns]; 143 | } 144 | 145 | return ; 146 | } 147 | } 148 | 149 | SelectableTable.defaultProps = { 150 | ...BaseTable.defaultProps, 151 | onRowSelect: noop, 152 | onSelectedRowsChange: noop, 153 | }; 154 | ``` 155 | 156 | ## Example 157 | 158 | Check the live example [here](https://autodesk.github.io/react-base-table/examples/selection). 159 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-base-table", 3 | "version": "1.13.5", 4 | "description": "a react table component to display large data set with high performance and flexibility", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "types": "types/index.d.ts", 8 | "files": [ 9 | "lib/", 10 | "es/", 11 | "types/", 12 | "styles.css" 13 | ], 14 | "author": "Neo Nie ", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Autodesk/react-base-table.git" 19 | }, 20 | "scripts": { 21 | "start": "cd website && npm start", 22 | "deploy": "cd website && npm run deploy", 23 | "lint": "eslint ./src/**/*.js", 24 | "clean": "rimraf lib es styles.css", 25 | "build:js": "cross-env NODE_ENV=production babel src -d lib --ignore '**/*.spec.js','__snapshots__' --copy-files --source-maps", 26 | "build:es": "cross-env BABEL_ENV=es NODE_ENV=production babel src -d es --ignore '**/*.spec.js','__snapshots__' --copy-files --source-maps", 27 | "build:css": "node-sass src/_BaseTable.scss ./styles.css --output-style expanded", 28 | "build": "npm run build:js && npm run build:es && npm run build:css", 29 | "format": "prettier --write 'src/**/*.{js,scss}'", 30 | "prebuild": "npm run clean", 31 | "precommit": "lint-staged", 32 | "prepush": "npm run test", 33 | "prepublish": "npm run build && npm run test", 34 | "test": "jest" 35 | }, 36 | "lint-staged": { 37 | "packages/**/*.scss": [ 38 | "prettier --write", 39 | "git add" 40 | ], 41 | "packages/**/*.js": [ 42 | "prettier --write", 43 | "eslint -c .eslintrc", 44 | "git add" 45 | ] 46 | }, 47 | "dependencies": { 48 | "@babel/runtime": "^7.0.0", 49 | "classnames": "^2.2.5", 50 | "memoize-one": "^5.0.0", 51 | "prop-types": "^15.7.0", 52 | "react-virtualized-auto-sizer": "^1.0.2", 53 | "react-window": "^1.8.2" 54 | }, 55 | "peerDependencies": { 56 | "react": "^16.0.0 || ^17.0.0", 57 | "react-dom": "^16.0.0 || ^17.0.0" 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "^7.0.0", 61 | "@babel/core": "^7.0.0", 62 | "@babel/plugin-proposal-class-properties": "^7.0.0", 63 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 64 | "@babel/plugin-transform-runtime": "^7.0.0", 65 | "@babel/preset-env": "^7.0.0", 66 | "@babel/preset-react": "^7.0.0", 67 | "@types/react": "^16.9.46", 68 | "babel-core": "^7.0.0-bridge.0", 69 | "babel-eslint": "^9.0.0", 70 | "babel-jest": "^23.4.2", 71 | "babel-plugin-syntax-trailing-function-commas": "^6.22.0", 72 | "cross-env": "^5.2.0", 73 | "eslint": "^5.6.0", 74 | "eslint-config-prettier": "^3.0.1", 75 | "eslint-config-react-app": "^3.0.8", 76 | "eslint-plugin-flowtype": "^3.4.2", 77 | "eslint-plugin-import": "^2.14.0", 78 | "eslint-plugin-jsx-a11y": "^6.1.1", 79 | "eslint-plugin-prettier": "^2.6.2", 80 | "eslint-plugin-react": "^7.11.1", 81 | "husky": "^0.14.3", 82 | "jest": "^23.5.0", 83 | "lerna": "^3.2.1", 84 | "lint-staged": "^7.2.2", 85 | "node-sass": "^4.9.3", 86 | "prettier": "^1.14.2", 87 | "react": "^17.0.2", 88 | "react-dom": "^17.0.2", 89 | "react-test-renderer": "^17.0.2", 90 | "rimraf": "^2.6.2" 91 | }, 92 | "jest": { 93 | "roots": [ 94 | "/src" 95 | ], 96 | "testRegex": ".*.spec\\.js$", 97 | "transform": { 98 | "^.+\\.jsx?$": "babel-jest" 99 | }, 100 | "transformIgnorePatterns": [ 101 | "/node_modules/" 102 | ] 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /screenshots/advance-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk/react-base-table/ef4f9e34488e5a149aa750d429a8adaf071275ae/screenshots/advance-table.png -------------------------------------------------------------------------------- /src/AutoResizer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import AutoSizer from 'react-virtualized-auto-sizer'; 4 | 5 | /** 6 | * Decorator component that automatically adjusts the width and height of a single child 7 | */ 8 | const AutoResizer = ({ className, width, height, children, onResize }) => { 9 | const disableWidth = typeof width === 'number'; 10 | const disableHeight = typeof height === 'number'; 11 | 12 | if (disableWidth && disableHeight) { 13 | return ( 14 |
15 | {children({ width, height })} 16 |
17 | ); 18 | } 19 | 20 | return ( 21 | 22 | {size => 23 | children({ 24 | width: disableWidth ? width : size.width, 25 | height: disableHeight ? height : size.height, 26 | }) 27 | } 28 | 29 | ); 30 | }; 31 | 32 | AutoResizer.propTypes = { 33 | /** 34 | * Class name for the component 35 | */ 36 | className: PropTypes.string, 37 | /** 38 | * the width of the component, will be the container's width if not set 39 | */ 40 | width: PropTypes.number, 41 | /** 42 | * the height of the component, will be the container's width if not set 43 | */ 44 | height: PropTypes.number, 45 | /** 46 | * A callback function to render the children component 47 | * The handler is of the shape of `({ width, height }) => node` 48 | */ 49 | children: PropTypes.func.isRequired, 50 | /** 51 | * A callback function when the size of the table container changed if the width and height are not set 52 | * The handler is of the shape of `({ width, height }) => *` 53 | */ 54 | onResize: PropTypes.func, 55 | }; 56 | 57 | export default AutoResizer; 58 | -------------------------------------------------------------------------------- /src/BaseTable.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import BaseTable from './BaseTable'; 5 | 6 | const RENDERER = () => null; 7 | 8 | const columns = [ 9 | { 10 | key: 'code', 11 | title: 'code', 12 | dataKey: 'code', 13 | width: 50, 14 | }, 15 | { 16 | key: 'name', 17 | title: 'name', 18 | dataKey: 'name', 19 | width: 50, 20 | }, 21 | ]; 22 | 23 | const data = [ 24 | { 25 | id: '1', 26 | code: '1', 27 | name: '1', 28 | }, 29 | { 30 | id: '2', 31 | code: '2', 32 | name: '2', 33 | }, 34 | ]; 35 | 36 | const Table = props => ; 37 | 38 | describe('Table', function() { 39 | test('renders correctly', () => { 40 | const tree = renderer.create(
).toJSON(); 41 | expect(tree).toMatchSnapshot(); 42 | }); 43 | 44 | test('table can receive className', () => { 45 | const tree = renderer.create(
).toJSON(); 46 | expect(tree).toMatchSnapshot(); 47 | }); 48 | 49 | test('table can receive style', () => { 50 | const tree = renderer.create(
).toJSON(); 51 | expect(tree).toMatchSnapshot(); 52 | }); 53 | 54 | test('table can receive children', () => { 55 | const tree = renderer 56 | .create( 57 |
58 | 59 | 60 |
61 | ) 62 | .toJSON(); 63 | expect(tree).toMatchSnapshot(); 64 | }); 65 | 66 | test('table can receive empty data', () => { 67 | const tree = renderer.create().toJSON(); 68 | expect(tree).toMatchSnapshot(); 69 | }); 70 | 71 | test('table can specific a different rowKey', () => { 72 | const tree = renderer.create(
).toJSON(); 73 | expect(tree).toMatchSnapshot(); 74 | }); 75 | 76 | test('table can receive width', () => { 77 | const tree = renderer.create(
).toJSON(); 78 | expect(tree).toMatchSnapshot(); 79 | }); 80 | 81 | test('table can receive height', () => { 82 | const tree = renderer.create(
).toJSON(); 83 | expect(tree).toMatchSnapshot(); 84 | }); 85 | 86 | test('table can receive rowHeight', () => { 87 | const tree = renderer.create(
).toJSON(); 88 | expect(tree).toMatchSnapshot(); 89 | }); 90 | 91 | test('table can receive headerHeight', () => { 92 | const tree = renderer.create(
).toJSON(); 93 | expect(tree).toMatchSnapshot(); 94 | }); 95 | 96 | test('table can be set to fixed', () => { 97 | const tree = renderer.create(
).toJSON(); 98 | expect(tree).toMatchSnapshot(); 99 | }); 100 | 101 | test('table can be set to disabled', () => { 102 | const tree = renderer.create(
).toJSON(); 103 | expect(tree).toMatchSnapshot(); 104 | }); 105 | 106 | test('table can hide the header', () => { 107 | const tree = renderer.create(
).toJSON(); 108 | expect(tree).toMatchSnapshot(); 109 | }); 110 | 111 | test('table can freeze rows', () => { 112 | const tree = renderer.create(
).toJSON(); 113 | expect(tree).toMatchSnapshot(); 114 | }); 115 | 116 | test('table can receive an emptyRenderer callback', () => { 117 | const tree = renderer.create(
).toJSON(); 118 | expect(tree).toMatchSnapshot(); 119 | }); 120 | 121 | test('table can receive an headerRenderer callback', () => { 122 | const tree = renderer.create(
).toJSON(); 123 | expect(tree).toMatchSnapshot(); 124 | }); 125 | 126 | test('table can receive an rowRenderer callback', () => { 127 | const tree = renderer.create(
).toJSON(); 128 | expect(tree).toMatchSnapshot(); 129 | }); 130 | 131 | test('table can receive headerClassName', () => { 132 | const tree = renderer.create(
).toJSON(); 133 | expect(tree).toMatchSnapshot(); 134 | }); 135 | 136 | test('table can receive rowClassName', () => { 137 | const tree = renderer.create(
).toJSON(); 138 | expect(tree).toMatchSnapshot(); 139 | }); 140 | 141 | test('table can receive expandColumnKey', () => { 142 | const tree = renderer.create(
).toJSON(); 143 | expect(tree).toMatchSnapshot(); 144 | }); 145 | 146 | test('table can receive defaultExpandedRowKeys', () => { 147 | const tree = renderer.create(
).toJSON(); 148 | expect(tree).toMatchSnapshot(); 149 | }); 150 | 151 | test('table can receive expandedRowKeys', () => { 152 | const tree = renderer.create(
).toJSON(); 153 | expect(tree).toMatchSnapshot(); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/Column.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const Alignment = { 5 | LEFT: 'left', 6 | CENTER: 'center', 7 | RIGHT: 'right', 8 | }; 9 | 10 | export const FrozenDirection = { 11 | LEFT: 'left', 12 | RIGHT: 'right', 13 | DEFAULT: true, 14 | NONE: false, 15 | }; 16 | 17 | /** 18 | * Column for BaseTable 19 | */ 20 | class Column extends React.Component {} 21 | 22 | Column.propTypes = { 23 | /** 24 | * Class name for the column cell, could be a callback to return the class name 25 | * The callback is of the shape of `({ cellData, columns, column, columnIndex, rowData, rowIndex }) => string` 26 | */ 27 | className: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 28 | /** 29 | * Class name for the column header, could be a callback to return the class name 30 | * The callback is of the shape of `({ columns, column, columnIndex, headerIndex }) => string` 31 | */ 32 | headerClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 33 | /** 34 | * Custom style for the column cell, including the header cells 35 | */ 36 | style: PropTypes.object, 37 | /** 38 | * Title for the column header 39 | */ 40 | title: PropTypes.node, 41 | /** 42 | * Data key for the column cell, could be "a.b.c" 43 | */ 44 | dataKey: PropTypes.string, 45 | /** 46 | * Custom cell data getter 47 | * The handler is of the shape of `({ columns, column, columnIndex, rowData, rowIndex }) => node` 48 | */ 49 | dataGetter: PropTypes.func, 50 | /** 51 | * Alignment of the column cell 52 | */ 53 | align: PropTypes.oneOf(['left', 'center', 'right']), 54 | /** 55 | * Flex grow style, defaults to 0 56 | */ 57 | flexGrow: PropTypes.number, 58 | /** 59 | * Flex shrink style, defaults to 1 for flexible table and 0 for fixed table 60 | */ 61 | flexShrink: PropTypes.number, 62 | /** 63 | * The width of the column, gutter width is not included 64 | */ 65 | width: PropTypes.number.isRequired, 66 | /** 67 | * Maximum width of the column, used if the column is resizable 68 | */ 69 | maxWidth: PropTypes.number, 70 | /** 71 | * Minimum width of the column, used if the column is resizable 72 | */ 73 | minWidth: PropTypes.number, 74 | /** 75 | * Whether the column is frozen and what's the frozen side 76 | */ 77 | frozen: PropTypes.oneOf(['left', 'right', true, false]), 78 | /** 79 | * Whether the column is hidden 80 | */ 81 | hidden: PropTypes.bool, 82 | /** 83 | * Whether the column is resizable, defaults to false 84 | */ 85 | resizable: PropTypes.bool, 86 | /** 87 | * Whether the column is sortable, defaults to false 88 | */ 89 | sortable: PropTypes.bool, 90 | /** 91 | * Custom column cell renderer 92 | * The renderer receives props `{ cellData, columns, column, columnIndex, rowData, rowIndex, container, isScrolling }` 93 | */ 94 | cellRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 95 | /** 96 | * Custom column header renderer 97 | * The renderer receives props `{ columns, column, columnIndex, headerIndex, container }` 98 | */ 99 | headerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 100 | }; 101 | 102 | Column.Alignment = Alignment; 103 | Column.FrozenDirection = FrozenDirection; 104 | 105 | export default Column; 106 | -------------------------------------------------------------------------------- /src/ColumnManager.js: -------------------------------------------------------------------------------- 1 | import { FrozenDirection } from './Column'; 2 | 3 | export default class ColumnManager { 4 | constructor(columns, fixed) { 5 | this._origColumns = []; 6 | this.reset(columns, fixed); 7 | } 8 | 9 | _cache(key, fn) { 10 | if (key in this._cached) return this._cached[key]; 11 | this._cached[key] = fn(); 12 | return this._cached[key]; 13 | } 14 | 15 | reset(columns, fixed) { 16 | this._columns = columns.map(column => { 17 | let width = column.width; 18 | if (column.resizable) { 19 | // don't reset column's `width` if `width` prop doesn't change 20 | const idx = this._origColumns.findIndex(x => x.key === column.key); 21 | if (idx >= 0 && this._origColumns[idx].width === column.width) { 22 | width = this._columns[idx].width; 23 | } 24 | } 25 | return { ...column, width }; 26 | }); 27 | this._origColumns = columns; 28 | this._fixed = fixed; 29 | this._cached = {}; 30 | this._columnStyles = this.recomputeColumnStyles(); 31 | } 32 | 33 | resetCache() { 34 | this._cached = {}; 35 | } 36 | 37 | getOriginalColumns() { 38 | return this._origColumns; 39 | } 40 | 41 | getColumns() { 42 | return this._columns; 43 | } 44 | 45 | getVisibleColumns() { 46 | return this._cache('visibleColumns', () => { 47 | return this._columns.filter(column => !column.hidden); 48 | }); 49 | } 50 | 51 | hasFrozenColumns() { 52 | return this._cache('hasFrozenColumns', () => { 53 | return this._fixed && this.getVisibleColumns().some(column => !!column.frozen); 54 | }); 55 | } 56 | 57 | hasLeftFrozenColumns() { 58 | return this._cache('hasLeftFrozenColumns', () => { 59 | return ( 60 | this._fixed && 61 | this.getVisibleColumns().some(column => column.frozen === FrozenDirection.LEFT || column.frozen === true) 62 | ); 63 | }); 64 | } 65 | 66 | hasRightFrozenColumns() { 67 | return this._cache('hasRightFrozenColumns', () => { 68 | return this._fixed && this.getVisibleColumns().some(column => column.frozen === FrozenDirection.RIGHT); 69 | }); 70 | } 71 | 72 | getMainColumns() { 73 | return this._cache('mainColumns', () => { 74 | const columns = this.getVisibleColumns(); 75 | if (!this.hasFrozenColumns()) return columns; 76 | 77 | const mainColumns = []; 78 | this.getLeftFrozenColumns().forEach(column => { 79 | //columns placeholder for the fixed table above them 80 | mainColumns.push({ ...column, [ColumnManager.PlaceholderKey]: true }); 81 | }); 82 | this.getVisibleColumns().forEach(column => { 83 | if (!column.frozen) mainColumns.push(column); 84 | }); 85 | this.getRightFrozenColumns().forEach(column => { 86 | mainColumns.push({ ...column, [ColumnManager.PlaceholderKey]: true }); 87 | }); 88 | 89 | return mainColumns; 90 | }); 91 | } 92 | 93 | getLeftFrozenColumns() { 94 | return this._cache('leftFrozenColumns', () => { 95 | if (!this._fixed) return []; 96 | return this.getVisibleColumns().filter( 97 | column => column.frozen === FrozenDirection.LEFT || column.frozen === true 98 | ); 99 | }); 100 | } 101 | 102 | getRightFrozenColumns() { 103 | return this._cache('rightFrozenColumns', () => { 104 | if (!this._fixed) return []; 105 | return this.getVisibleColumns().filter(column => column.frozen === FrozenDirection.RIGHT); 106 | }); 107 | } 108 | 109 | getColumn(key) { 110 | const idx = this._columns.findIndex(column => column.key === key); 111 | return this._columns[idx]; 112 | } 113 | 114 | getColumnsWidth() { 115 | return this._cache('columnsWidth', () => { 116 | return this.recomputeColumnsWidth(this.getVisibleColumns()); 117 | }); 118 | } 119 | 120 | getLeftFrozenColumnsWidth() { 121 | return this._cache('leftFrozenColumnsWidth', () => { 122 | return this.recomputeColumnsWidth(this.getLeftFrozenColumns()); 123 | }); 124 | } 125 | 126 | getRightFrozenColumnsWidth() { 127 | return this._cache('rightFrozenColumnsWidth', () => { 128 | return this.recomputeColumnsWidth(this.getRightFrozenColumns()); 129 | }); 130 | } 131 | 132 | recomputeColumnsWidth(columns) { 133 | return columns.reduce((width, column) => width + column.width, 0); 134 | } 135 | 136 | setColumnWidth(key, width) { 137 | const column = this.getColumn(key); 138 | column.width = width; 139 | this._cached = {}; 140 | this._columnStyles[column.key] = this.recomputeColumnStyle(column); 141 | } 142 | 143 | getColumnStyle(key) { 144 | return this._columnStyles[key]; 145 | } 146 | 147 | getColumnStyles() { 148 | return this._columnStyles; 149 | } 150 | 151 | recomputeColumnStyle(column) { 152 | let flexGrow = 0; 153 | let flexShrink = 0; 154 | if (!this._fixed) { 155 | flexGrow = typeof column.flexGrow === 'number' ? column.flexGrow : 0; 156 | flexShrink = typeof column.flexShrink === 'number' ? column.flexShrink : 1; 157 | } 158 | // workaround for Flex bug on IE: https://github.com/philipwalton/flexbugs#flexbug-7 159 | const flexValue = `${flexGrow} ${flexShrink} auto`; 160 | 161 | const style = { 162 | ...column.style, 163 | flex: flexValue, 164 | msFlex: flexValue, 165 | WebkitFlex: flexValue, 166 | width: column.width, 167 | overflow: 'hidden', 168 | }; 169 | 170 | if (!this._fixed && column.maxWidth) { 171 | style.maxWidth = column.maxWidth; 172 | } 173 | if (!this._fixed && column.minWidth) { 174 | style.minWidth = column.minWidth; 175 | } 176 | 177 | return style; 178 | } 179 | 180 | recomputeColumnStyles() { 181 | return this._columns.reduce((styles, column) => { 182 | styles[column.key] = this.recomputeColumnStyle(column); 183 | return styles; 184 | }, {}); 185 | } 186 | } 187 | 188 | ColumnManager.PlaceholderKey = '__placeholder__'; 189 | -------------------------------------------------------------------------------- /src/ColumnResizer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { noop, addClassName, removeClassName } from './utils'; 5 | 6 | const INVALID_VALUE = null; 7 | 8 | // copied from https://github.com/mzabriskie/react-draggable/blob/master/lib/utils/domFns.js 9 | export function addUserSelectStyles(doc) { 10 | if (!doc) return; 11 | let styleEl = doc.getElementById('react-draggable-style-el'); 12 | if (!styleEl) { 13 | styleEl = doc.createElement('style'); 14 | styleEl.type = 'text/css'; 15 | styleEl.id = 'react-draggable-style-el'; 16 | styleEl.innerHTML = '.react-draggable-transparent-selection *::-moz-selection {all: inherit;}\n'; 17 | styleEl.innerHTML += '.react-draggable-transparent-selection *::selection {all: inherit;}\n'; 18 | doc.getElementsByTagName('head')[0].appendChild(styleEl); 19 | } 20 | if (doc.body) addClassName(doc.body, 'react-draggable-transparent-selection'); 21 | } 22 | 23 | export function removeUserSelectStyles(doc) { 24 | if (!doc) return; 25 | try { 26 | if (doc.body) removeClassName(doc.body, 'react-draggable-transparent-selection'); 27 | if (doc.selection) { 28 | doc.selection.empty(); 29 | } else { 30 | // Remove selection caused by scroll, unless it's a focused input 31 | // (we use doc.defaultView in case we're in an iframe) 32 | const selection = (doc.defaultView || window).getSelection(); 33 | if (selection && selection.type !== 'Caret') { 34 | selection.removeAllRanges(); 35 | } 36 | } 37 | } catch (e) { 38 | // probably IE 39 | } 40 | } 41 | 42 | const eventsFor = { 43 | touch: { 44 | start: 'touchstart', 45 | move: 'touchmove', 46 | stop: 'touchend', 47 | }, 48 | mouse: { 49 | start: 'mousedown', 50 | move: 'mousemove', 51 | stop: 'mouseup', 52 | }, 53 | }; 54 | 55 | let dragEventFor = eventsFor.mouse; 56 | 57 | /** 58 | * ColumnResizer for BaseTable 59 | */ 60 | class ColumnResizer extends React.PureComponent { 61 | constructor(props) { 62 | super(props); 63 | 64 | this.isDragging = false; 65 | this.lastX = INVALID_VALUE; 66 | this.width = 0; 67 | 68 | this._setHandleRef = this._setHandleRef.bind(this); 69 | this._handleClick = this._handleClick.bind(this); 70 | this._handleMouseDown = this._handleMouseDown.bind(this); 71 | this._handleMouseUp = this._handleMouseUp.bind(this); 72 | this._handleTouchStart = this._handleTouchStart.bind(this); 73 | this._handleTouchEnd = this._handleTouchEnd.bind(this); 74 | this._handleDragStart = this._handleDragStart.bind(this); 75 | this._handleDragStop = this._handleDragStop.bind(this); 76 | this._handleDrag = this._handleDrag.bind(this); 77 | } 78 | 79 | componentWillUnmount() { 80 | if (this.handleRef) { 81 | const { ownerDocument } = this.handleRef; 82 | ownerDocument.removeEventListener(eventsFor.mouse.move, this._handleDrag); 83 | ownerDocument.removeEventListener(eventsFor.mouse.stop, this._handleDragStop); 84 | ownerDocument.removeEventListener(eventsFor.touch.move, this._handleDrag); 85 | ownerDocument.removeEventListener(eventsFor.touch.stop, this._handleDragStop); 86 | removeUserSelectStyles(ownerDocument); 87 | } 88 | } 89 | 90 | render() { 91 | const { style, column, onResizeStart, onResize, onResizeStop, minWidth, ...rest } = this.props; 92 | 93 | return ( 94 |
113 | ); 114 | } 115 | 116 | _setHandleRef(ref) { 117 | this.handleRef = ref; 118 | } 119 | 120 | _handleClick(e) { 121 | e.stopPropagation(); 122 | } 123 | 124 | _handleMouseDown(e) { 125 | dragEventFor = eventsFor.mouse; 126 | this._handleDragStart(e); 127 | } 128 | 129 | _handleMouseUp(e) { 130 | dragEventFor = eventsFor.mouse; 131 | this._handleDragStop(e); 132 | } 133 | 134 | _handleTouchStart(e) { 135 | dragEventFor = eventsFor.touch; 136 | this._handleDragStart(e); 137 | } 138 | 139 | _handleTouchEnd(e) { 140 | dragEventFor = eventsFor.touch; 141 | this._handleDragStop(e); 142 | } 143 | 144 | _handleDragStart(e) { 145 | if (typeof e.button === 'number' && e.button !== 0) return; 146 | 147 | this.isDragging = true; 148 | this.lastX = INVALID_VALUE; 149 | this.width = this.props.column.width; 150 | this.props.onResizeStart(this.props.column); 151 | 152 | const { ownerDocument } = this.handleRef; 153 | addUserSelectStyles(ownerDocument); 154 | ownerDocument.addEventListener(dragEventFor.move, this._handleDrag); 155 | ownerDocument.addEventListener(dragEventFor.stop, this._handleDragStop); 156 | } 157 | 158 | _handleDragStop(e) { 159 | if (!this.isDragging) return; 160 | this.isDragging = false; 161 | 162 | this.props.onResizeStop(this.props.column); 163 | 164 | const { ownerDocument } = this.handleRef; 165 | removeUserSelectStyles(ownerDocument); 166 | ownerDocument.removeEventListener(dragEventFor.move, this._handleDrag); 167 | ownerDocument.removeEventListener(dragEventFor.stop, this._handleDragStop); 168 | } 169 | 170 | _handleDrag(e) { 171 | let clientX = e.clientX; 172 | if (e.type === eventsFor.touch.move) { 173 | e.preventDefault(); 174 | if (e.targetTouches && e.targetTouches[0]) clientX = e.targetTouches[0].clientX; 175 | } 176 | 177 | const { offsetParent } = this.handleRef; 178 | const offsetParentRect = offsetParent.getBoundingClientRect(); 179 | const x = clientX + offsetParent.scrollLeft - offsetParentRect.left; 180 | 181 | if (this.lastX === INVALID_VALUE) { 182 | this.lastX = x; 183 | return; 184 | } 185 | 186 | const { column, minWidth: MIN_WIDTH } = this.props; 187 | const { width, maxWidth, minWidth = MIN_WIDTH } = column; 188 | const movedX = x - this.lastX; 189 | if (!movedX) return; 190 | 191 | this.width = this.width + movedX; 192 | this.lastX = x; 193 | 194 | let newWidth = this.width; 195 | if (maxWidth && newWidth > maxWidth) { 196 | newWidth = maxWidth; 197 | } else if (newWidth < minWidth) { 198 | newWidth = minWidth; 199 | } 200 | 201 | if (newWidth === width) return; 202 | this.props.onResize(column, newWidth); 203 | } 204 | } 205 | 206 | ColumnResizer.defaultProps = { 207 | onResizeStart: noop, 208 | onResize: noop, 209 | onResizeStop: noop, 210 | minWidth: 30, 211 | }; 212 | 213 | ColumnResizer.propTypes = { 214 | /** 215 | * Custom style for the drag handler 216 | */ 217 | style: PropTypes.object, 218 | /** 219 | * The column object to be dragged 220 | */ 221 | column: PropTypes.object, 222 | /** 223 | * A callback function when resizing started 224 | * The callback is of the shape of `(column) => *` 225 | */ 226 | onResizeStart: PropTypes.func, 227 | /** 228 | * A callback function when resizing the column 229 | * The callback is of the shape of `(column, width) => *` 230 | */ 231 | onResize: PropTypes.func, 232 | /** 233 | * A callback function when resizing stopped 234 | * The callback is of the shape of `(column) => *` 235 | */ 236 | onResizeStop: PropTypes.func, 237 | /** 238 | * Minimum width of the column could be resized to if the column's `minWidth` is not set 239 | */ 240 | minWidth: PropTypes.number, 241 | }; 242 | 243 | export default ColumnResizer; 244 | -------------------------------------------------------------------------------- /src/ExpandIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import cn from 'classnames'; 4 | 5 | /** 6 | * default ExpandIcon for BaseTable 7 | */ 8 | class ExpandIcon extends React.PureComponent { 9 | constructor(props) { 10 | super(props); 11 | 12 | this._handleClick = this._handleClick.bind(this); 13 | } 14 | 15 | render() { 16 | const { expandable, expanded, indentSize, depth, onExpand, ...rest } = this.props; 17 | if (!expandable && indentSize === 0) return null; 18 | 19 | const cls = cn('BaseTable__expand-icon', { 20 | 'BaseTable__expand-icon--expanded': expanded, 21 | }); 22 | return ( 23 |
42 | {expandable && '\u25B8'} 43 |
44 | ); 45 | } 46 | 47 | _handleClick(e) { 48 | e.stopPropagation(); 49 | e.preventDefault(); 50 | const { onExpand, expanded } = this.props; 51 | onExpand(!expanded); 52 | } 53 | } 54 | 55 | ExpandIcon.defaultProps = { 56 | depth: 0, 57 | indentSize: 16, 58 | }; 59 | 60 | ExpandIcon.propTypes = { 61 | expandable: PropTypes.bool, 62 | expanded: PropTypes.bool, 63 | indentSize: PropTypes.number, 64 | depth: PropTypes.number, 65 | onExpand: PropTypes.func, 66 | }; 67 | 68 | export default ExpandIcon; 69 | -------------------------------------------------------------------------------- /src/SortIndicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import cn from 'classnames'; 4 | 5 | import SortOrder from './SortOrder'; 6 | 7 | /** 8 | * default SortIndicator for BaseTable 9 | */ 10 | const SortIndicator = ({ sortOrder, className, style }) => { 11 | const cls = cn('BaseTable__sort-indicator', className, { 12 | 'BaseTable__sort-indicator--descending': sortOrder === SortOrder.DESC, 13 | }); 14 | return ( 15 |
26 | {sortOrder === SortOrder.DESC ? '\u2193' : '\u2191'} 27 |
28 | ); 29 | }; 30 | 31 | SortIndicator.propTypes = { 32 | sortOrder: PropTypes.oneOf([SortOrder.ASC, SortOrder.DESC]), 33 | className: PropTypes.string, 34 | style: PropTypes.object, 35 | }; 36 | 37 | export default SortIndicator; 38 | -------------------------------------------------------------------------------- /src/SortOrder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sort order for BaseTable 3 | */ 4 | const SortOrder = { 5 | /** 6 | * Sort data in ascending order 7 | */ 8 | ASC: 'asc', 9 | /** 10 | * Sort data in descending order 11 | */ 12 | DESC: 'desc', 13 | }; 14 | 15 | export default SortOrder; 16 | -------------------------------------------------------------------------------- /src/TableCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { toString } from './utils'; 4 | 5 | /** 6 | * Cell component for BaseTable 7 | */ 8 | const TableCell = ({ className, cellData, column, columnIndex, rowData, rowIndex }) => ( 9 |
{React.isValidElement(cellData) ? cellData : toString(cellData)}
10 | ); 11 | 12 | TableCell.propTypes = { 13 | className: PropTypes.string, 14 | cellData: PropTypes.any, 15 | column: PropTypes.object, 16 | columnIndex: PropTypes.number, 17 | rowData: PropTypes.object, 18 | rowIndex: PropTypes.number, 19 | }; 20 | 21 | export default TableCell; 22 | -------------------------------------------------------------------------------- /src/TableHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class TableHeader extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.renderHeaderRow = this.renderHeaderRow.bind(this); 9 | this.renderFrozenRow = this.renderFrozenRow.bind(this); 10 | this._setRef = this._setRef.bind(this); 11 | } 12 | 13 | scrollTo(offset) { 14 | requestAnimationFrame(() => { 15 | if (this.headerRef) this.headerRef.scrollLeft = offset; 16 | }); 17 | } 18 | 19 | renderHeaderRow(height, index) { 20 | const { columns, headerRenderer } = this.props; 21 | if (height <= 0) return null; 22 | 23 | const style = { width: '100%', height }; 24 | return headerRenderer({ style, columns, headerIndex: index }); 25 | } 26 | 27 | renderFrozenRow(rowData, index) { 28 | const { columns, rowHeight, rowRenderer } = this.props; 29 | const style = { width: '100%', height: rowHeight }; 30 | // for frozen row the `rowIndex` is negative 31 | const rowIndex = -index - 1; 32 | return rowRenderer({ style, columns, rowData, rowIndex }); 33 | } 34 | 35 | render() { 36 | const { className, width, height, rowWidth, headerHeight, frozenData } = this.props; 37 | if (height <= 0) return null; 38 | 39 | const style = { 40 | width, 41 | height: height, 42 | position: 'relative', 43 | overflow: 'hidden', 44 | }; 45 | 46 | const innerStyle = { 47 | width: rowWidth, 48 | height, 49 | }; 50 | 51 | const rowHeights = Array.isArray(headerHeight) ? headerHeight : [headerHeight]; 52 | return ( 53 |
54 |
55 | {rowHeights.map(this.renderHeaderRow)} 56 | {frozenData.map(this.renderFrozenRow)} 57 |
58 |
59 | ); 60 | } 61 | 62 | _setRef(ref) { 63 | this.headerRef = ref; 64 | } 65 | } 66 | 67 | TableHeader.propTypes = { 68 | className: PropTypes.string, 69 | width: PropTypes.number.isRequired, 70 | height: PropTypes.number.isRequired, 71 | headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, 72 | rowWidth: PropTypes.number.isRequired, 73 | rowHeight: PropTypes.number.isRequired, 74 | columns: PropTypes.arrayOf(PropTypes.object).isRequired, 75 | data: PropTypes.array.isRequired, 76 | frozenData: PropTypes.array, 77 | headerRenderer: PropTypes.func.isRequired, 78 | rowRenderer: PropTypes.func.isRequired, 79 | }; 80 | 81 | export default TableHeader; 82 | -------------------------------------------------------------------------------- /src/TableHeaderCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * HeaderCell component for BaseTable 6 | */ 7 | const TableHeaderCell = ({ className, column, columnIndex }) =>
{column.title}
; 8 | 9 | TableHeaderCell.propTypes = { 10 | className: PropTypes.string, 11 | column: PropTypes.object, 12 | columnIndex: PropTypes.number, 13 | }; 14 | 15 | export default TableHeaderCell; 16 | -------------------------------------------------------------------------------- /src/TableHeaderRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { renderElement } from './utils'; 5 | 6 | /** 7 | * HeaderRow component for BaseTable 8 | */ 9 | const TableHeaderRow = ({ 10 | className, 11 | style, 12 | columns, 13 | headerIndex, 14 | cellRenderer, 15 | headerRenderer, 16 | expandColumnKey, 17 | expandIcon: ExpandIcon, 18 | tagName: Tag, 19 | ...rest 20 | }) => { 21 | let cells = columns.map((column, columnIndex) => 22 | cellRenderer({ 23 | columns, 24 | column, 25 | columnIndex, 26 | headerIndex, 27 | expandIcon: column.key === expandColumnKey && , 28 | }) 29 | ); 30 | 31 | if (headerRenderer) { 32 | cells = renderElement(headerRenderer, { cells, columns, headerIndex }); 33 | } 34 | 35 | return ( 36 | 37 | {cells} 38 | 39 | ); 40 | }; 41 | 42 | TableHeaderRow.defaultProps = { 43 | tagName: 'div', 44 | }; 45 | 46 | TableHeaderRow.propTypes = { 47 | isScrolling: PropTypes.bool, 48 | className: PropTypes.string, 49 | style: PropTypes.object, 50 | columns: PropTypes.arrayOf(PropTypes.object).isRequired, 51 | headerIndex: PropTypes.number, 52 | cellRenderer: PropTypes.func, 53 | headerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 54 | expandColumnKey: PropTypes.string, 55 | expandIcon: PropTypes.func, 56 | tagName: PropTypes.elementType, 57 | }; 58 | 59 | export default TableHeaderRow; 60 | -------------------------------------------------------------------------------- /src/TableRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { renderElement } from './utils'; 5 | 6 | /** 7 | * Row component for BaseTable 8 | */ 9 | class TableRow extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | measured: false, 15 | }; 16 | 17 | this._setRef = this._setRef.bind(this); 18 | this._handleExpand = this._handleExpand.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | this.props.estimatedRowHeight && this.props.rowIndex >= 0 && this._measureHeight(true); 23 | } 24 | 25 | componentDidUpdate(prevProps, prevState) { 26 | if ( 27 | this.props.estimatedRowHeight && 28 | this.props.rowIndex >= 0 && 29 | // should not re-measure if it's updated after measured and reset 30 | !this.props.getIsResetting() && 31 | this.state.measured && 32 | prevState.measured 33 | ) { 34 | this.setState({ measured: false }, () => this._measureHeight()); 35 | } 36 | } 37 | 38 | render() { 39 | /* eslint-disable no-unused-vars */ 40 | const { 41 | isScrolling, 42 | className, 43 | style, 44 | columns, 45 | rowIndex, 46 | rowData, 47 | expandColumnKey, 48 | depth, 49 | rowEventHandlers, 50 | estimatedRowHeight, 51 | rowRenderer, 52 | cellRenderer, 53 | expandIconRenderer, 54 | tagName: Tag, 55 | // omit the following from rest 56 | rowKey, 57 | getIsResetting, 58 | onRowHover, 59 | onRowExpand, 60 | onRowHeightChange, 61 | ...rest 62 | } = this.props; 63 | /* eslint-enable no-unused-vars */ 64 | 65 | const expandIcon = expandIconRenderer({ rowData, rowIndex, depth, onExpand: this._handleExpand }); 66 | let cells = columns.map((column, columnIndex) => 67 | cellRenderer({ 68 | isScrolling, 69 | columns, 70 | column, 71 | columnIndex, 72 | rowData, 73 | rowIndex, 74 | expandIcon: column.key === expandColumnKey && expandIcon, 75 | }) 76 | ); 77 | 78 | if (rowRenderer) { 79 | cells = renderElement(rowRenderer, { isScrolling, cells, columns, rowData, rowIndex, depth }); 80 | } 81 | 82 | const eventHandlers = this._getEventHandlers(rowEventHandlers); 83 | 84 | if (estimatedRowHeight && rowIndex >= 0) { 85 | const { height, ...otherStyles } = style; 86 | return ( 87 | 94 | {cells} 95 | 96 | ); 97 | } 98 | 99 | return ( 100 | 101 | {cells} 102 | 103 | ); 104 | } 105 | 106 | _setRef(ref) { 107 | this.ref = ref; 108 | } 109 | 110 | _handleExpand(expanded) { 111 | const { onRowExpand, rowData, rowIndex, rowKey } = this.props; 112 | onRowExpand && onRowExpand({ expanded, rowData, rowIndex, rowKey }); 113 | } 114 | 115 | _measureHeight(initialMeasure) { 116 | if (!this.ref) return; 117 | 118 | const { style, rowKey, onRowHeightChange, rowIndex, columns } = this.props; 119 | const height = this.ref.getBoundingClientRect().height; 120 | this.setState({ measured: true }, () => { 121 | if (initialMeasure || height !== style.height) 122 | onRowHeightChange(rowKey, height, rowIndex, columns[0] && !columns[0].__placeholder__ && columns[0].frozen); 123 | }); 124 | } 125 | 126 | _getEventHandlers(handlers = {}) { 127 | const { rowData, rowIndex, rowKey, onRowHover } = this.props; 128 | const eventHandlers = {}; 129 | Object.keys(handlers).forEach(eventKey => { 130 | const callback = handlers[eventKey]; 131 | if (typeof callback === 'function') { 132 | eventHandlers[eventKey] = event => { 133 | callback({ rowData, rowIndex, rowKey, event }); 134 | }; 135 | } 136 | }); 137 | 138 | if (onRowHover) { 139 | const mouseEnterHandler = eventHandlers['onMouseEnter']; 140 | eventHandlers['onMouseEnter'] = event => { 141 | onRowHover({ 142 | hovered: true, 143 | rowData, 144 | rowIndex, 145 | rowKey, 146 | event, 147 | }); 148 | mouseEnterHandler && mouseEnterHandler(event); 149 | }; 150 | 151 | const mouseLeaveHandler = eventHandlers['onMouseLeave']; 152 | eventHandlers['onMouseLeave'] = event => { 153 | onRowHover({ 154 | hovered: false, 155 | rowData, 156 | rowIndex, 157 | rowKey, 158 | event, 159 | }); 160 | mouseLeaveHandler && mouseLeaveHandler(event); 161 | }; 162 | } 163 | 164 | return eventHandlers; 165 | } 166 | } 167 | 168 | TableRow.defaultProps = { 169 | tagName: 'div', 170 | }; 171 | 172 | TableRow.propTypes = { 173 | isScrolling: PropTypes.bool, 174 | className: PropTypes.string, 175 | style: PropTypes.object, 176 | columns: PropTypes.arrayOf(PropTypes.object).isRequired, 177 | rowData: PropTypes.object.isRequired, 178 | rowIndex: PropTypes.number.isRequired, 179 | rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 180 | expandColumnKey: PropTypes.string, 181 | depth: PropTypes.number, 182 | rowEventHandlers: PropTypes.object, 183 | rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), 184 | cellRenderer: PropTypes.func, 185 | expandIconRenderer: PropTypes.func, 186 | estimatedRowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 187 | getIsResetting: PropTypes.func, 188 | onRowHover: PropTypes.func, 189 | onRowExpand: PropTypes.func, 190 | onRowHeightChange: PropTypes.func, 191 | tagName: PropTypes.elementType, 192 | }; 193 | 194 | export default TableRow; 195 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './BaseTable'; 2 | 3 | export { default as Column, Alignment, FrozenDirection } from './Column'; 4 | export { default as SortOrder } from './SortOrder'; 5 | export { default as AutoResizer } from './AutoResizer'; 6 | export { default as TableHeader } from './TableHeader'; 7 | export { default as TableRow } from './TableRow'; 8 | 9 | export { 10 | renderElement, 11 | normalizeColumns, 12 | isObjectEqual, 13 | callOrReturn, 14 | hasChildren, 15 | unflatten, 16 | flattenOnKeys, 17 | getScrollbarSize, 18 | getValue, 19 | } from './utils'; 20 | -------------------------------------------------------------------------------- /website/.eslintignore: -------------------------------------------------------------------------------- 1 | src/examples/ 2 | 3 | react-base-table/ 4 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 3 | .cache 4 | 5 | # Build directory 6 | /public 7 | 8 | # Ignore annoying DS files 9 | .DS_Store 10 | *.DS_Store 11 | **.DS_Store 12 | 13 | # npm 14 | node_modules 15 | npm-debug.log 16 | npm-debug.log.* 17 | 18 | # yarn 19 | yarn-error.log 20 | yarn-error.log.* 21 | -------------------------------------------------------------------------------- /website/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # react-base-table 2 | 3 | [BaseTable website](https://autodesk.github.io/react-base-table/) 4 | 5 | ## Start 6 | 7 | ```bash 8 | git clone https://github.com/Autodesk/react-base-table.git 9 | cd react-base-table 10 | cd website 11 | yarn # install dependencies 12 | ``` 13 | 14 | use `npm install` if you don't get `yarn` installed 15 | 16 | ## Develop 17 | 18 | ```bash 19 | yarn start 20 | ``` 21 | 22 | visit http://localhost:8000 after server started, change any file in `src` folder and the webpage will hot reload the changes synchronously 23 | 24 | ## Deploy 25 | 26 | ```bash 27 | yarn deploy 28 | ``` 29 | 30 | then your changes will be automatically deployed to the Github Pages 31 | -------------------------------------------------------------------------------- /website/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pathPrefix: '/react-base-table', 3 | siteMetadata: { 4 | title: 'BaseTable', 5 | description: 'BaseTable website', 6 | keywords: 'react, component, table, basetable', 7 | author: 'Neo Nie (nihgwu@live.com)', 8 | }, 9 | plugins: [ 10 | { 11 | resolve: `gatsby-plugin-google-analytics`, 12 | options: { 13 | trackingId: 'UA-138491668-1', 14 | }, 15 | }, 16 | { 17 | resolve: 'gatsby-plugin-manifest', 18 | options: { 19 | name: 'BaseTable', 20 | short_name: 'BaseTable', 21 | start_url: '/', 22 | background_color: '#fff', 23 | theme_color: '#fff', 24 | display: 'minimal-ui', 25 | icon: 'src/assets/favicon.png', 26 | }, 27 | }, 28 | { 29 | resolve: 'gatsby-plugin-nprogress', 30 | options: { 31 | color: 'tomato', 32 | }, 33 | }, 34 | 'gatsby-plugin-react-helmet', 35 | 'gatsby-plugin-remove-trailing-slashes', 36 | 'gatsby-plugin-catch-links', 37 | 'gatsby-plugin-lodash', 38 | { 39 | resolve: 'gatsby-plugin-styled-components', 40 | options: { 41 | displayName: false, 42 | }, 43 | }, 44 | { 45 | resolve: 'gatsby-source-filesystem', 46 | options: { 47 | name: 'examples', 48 | path: `${__dirname}/src/examples`, 49 | }, 50 | }, 51 | { 52 | resolve: 'gatsby-transformer-code', 53 | options: { 54 | name: 'examples', 55 | }, 56 | }, 57 | { 58 | resolve: 'gatsby-source-filesystem', 59 | options: { 60 | name: 'docs', 61 | path: `${__dirname}/../docs`, 62 | }, 63 | }, 64 | { 65 | resolve: 'gatsby-source-filesystem', 66 | options: { 67 | name: 'api', 68 | ignore: ['**/*.snap', '**/*.scss'], 69 | path: `${__dirname}/../src`, 70 | }, 71 | }, 72 | { 73 | resolve: 'gatsby-transformer-remark', 74 | options: { 75 | plugins: ['gatsby-remark-copy-linked-files'], 76 | }, 77 | }, 78 | 'gatsby-transformer-react-docgen', 79 | ], 80 | } 81 | -------------------------------------------------------------------------------- /website/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const _ = require('lodash') 3 | 4 | const siteConfig = require('./siteConfig') 5 | 6 | exports.onCreateWebpackConfig = ({ stage, getConfig, actions }) => { 7 | const config = getConfig() 8 | 9 | config.resolve.alias = { 10 | ...config.resolve.alias, 11 | assets: path.resolve(__dirname, 'src/assets'), 12 | components: path.resolve(__dirname, 'src/components'), 13 | utils: path.resolve(__dirname, 'src/utils'), 14 | siteConfig: path.resolve(__dirname, 'siteConfig'), 15 | 'react-base-table/package.json': path.resolve(__dirname, '../package.json'), 16 | 'react-base-table/styles.css': path.resolve(__dirname, '../styles.css'), 17 | 'react-base-table': path.resolve(__dirname, '../src'), 18 | } 19 | 20 | actions.replaceWebpackConfig(config) 21 | } 22 | 23 | exports.onCreateNode = ({ node, actions, getNode, createNodeId }) => { 24 | const { createNodeField, createNode, createParentChildLink } = actions 25 | if (node.internal.type === 'MarkdownRemark') { 26 | let slug 27 | const fileNode = getNode(node.parent) 28 | if (!fileNode.relativePath) return 29 | 30 | const parsedFilePath = path.parse(fileNode.relativePath) 31 | if (parsedFilePath.name !== 'index' && parsedFilePath.dir !== '') { 32 | slug = `/${parsedFilePath.dir}/${parsedFilePath.name}` 33 | } else if (parsedFilePath.dir === '') { 34 | slug = `/${parsedFilePath.name}` 35 | } else { 36 | slug = `/${parsedFilePath.dir}` 37 | } 38 | slug = `/${fileNode.sourceInstanceName}${slug}` 39 | 40 | // Add slug as a field on the node. 41 | createNodeField({ node, name: 'slug', value: slug }) 42 | } else if ( 43 | node.internal.type === 'ComponentMetadata' && 44 | node.methods.length 45 | ) { 46 | node.methods 47 | .filter(method => method.docblock) 48 | .map(method => { 49 | const methodNode = { 50 | id: createNodeId(`${node.id} >>> ${method.name}`), 51 | parent: node.id, 52 | children: [], 53 | name: method.name, 54 | params: method.params, 55 | description: method.description, 56 | internal: { 57 | type: `ComponentMethodExt`, 58 | mediaType: `text/markdown`, 59 | content: method.description, 60 | contentDigest: method.description, 61 | }, 62 | } 63 | 64 | createNode(methodNode) 65 | createParentChildLink({ parent: node, child: methodNode }) 66 | }) 67 | } 68 | } 69 | 70 | exports.createPages = async ({ graphql, actions, getNode }) => { 71 | const { createPage } = actions 72 | 73 | const docPage = path.resolve('src/templates/doc.js') 74 | const apiPage = path.resolve('src/templates/api.js') 75 | const examplePage = path.resolve('src/templates/example.js') 76 | 77 | const result = await graphql( 78 | ` 79 | { 80 | allMarkdownRemark { 81 | edges { 82 | node { 83 | fields { 84 | slug 85 | } 86 | } 87 | } 88 | } 89 | allComponentMetadata { 90 | edges { 91 | node { 92 | parent { 93 | id 94 | } 95 | displayName 96 | docblock 97 | } 98 | } 99 | } 100 | allRawCode { 101 | edges { 102 | node { 103 | name 104 | } 105 | } 106 | } 107 | } 108 | ` 109 | ) 110 | if (result.errors) { 111 | throw new Error(result.errors) 112 | } 113 | 114 | result.data.allMarkdownRemark.edges.forEach(edge => { 115 | const slug = _.get(edge, 'node.fields.slug') 116 | if (!slug || !slug.includes('docs')) return 117 | createPage({ 118 | path: slug, 119 | component: docPage, 120 | context: { 121 | slug, 122 | }, 123 | }) 124 | }) 125 | 126 | result.data.allComponentMetadata.edges.forEach(edge => { 127 | const node = edge.node 128 | const fileNode = getNode(node.parent.id) 129 | if (fileNode.sourceInstanceName !== 'api') return 130 | const { displayName: name, docblock } = node 131 | if (!docblock) return 132 | createPage({ 133 | path: `/api/${name.toLowerCase()}`, 134 | component: apiPage, 135 | context: { 136 | name, 137 | }, 138 | }) 139 | }) 140 | 141 | result.data.allRawCode.edges.forEach(edge => { 142 | const name = edge.node.name 143 | createPage({ 144 | path: `/examples/${name}`, 145 | component: examplePage, 146 | context: { 147 | name, 148 | }, 149 | }) 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-base-table-website", 3 | "description": "BaseTable website", 4 | "version": "1.0.0", 5 | "author": "Neo Nie ", 6 | "homepage": "https://autodesk.github.io/react-base-table/", 7 | "dependencies": { 8 | "classnames": "^2.2.6", 9 | "clipboard": "^2.0.4", 10 | "faker": "^4.1.0", 11 | "gatsby": "^2.13.1", 12 | "gatsby-plugin-catch-links": "^2.1.0", 13 | "gatsby-plugin-google-analytics": "^2.1.1", 14 | "gatsby-plugin-lodash": "^3.1.0", 15 | "gatsby-plugin-manifest": "^2.2.1", 16 | "gatsby-plugin-nprogress": "^2.1.0", 17 | "gatsby-plugin-react-helmet": "^3.1.0", 18 | "gatsby-plugin-remove-trailing-slashes": "^2.1.0", 19 | "gatsby-plugin-styled-components": "^3.1.0", 20 | "gatsby-remark-copy-linked-files": "^2.1.0", 21 | "gatsby-source-filesystem": "^2.1.2", 22 | "gatsby-transformer-code": "^0.1.0", 23 | "gatsby-transformer-react-docgen": "^4.1.0", 24 | "gatsby-transformer-remark": "^2.6.0", 25 | "lz-string": "^1.4.4", 26 | "minireset.css": "^0.0.5", 27 | "prop-types": "^15.7.2", 28 | "react": "^16.8.5", 29 | "react-dom": "^16.8.5", 30 | "react-helmet": "^5.2.0", 31 | "react-inspector": "^2.3.1", 32 | "react-live-runner": "^0.7.3", 33 | "react-overlays": "^1.2.0", 34 | "react-sortable-hoc": "^1.9.1", 35 | "react-texty": "^0.1.0", 36 | "rehype-react": "^3.1.0", 37 | "seamless-immutable": "^7.1.4", 38 | "slugify": "^1.3.4", 39 | "styled-components": "^4.3.2" 40 | }, 41 | "devDependencies": { 42 | "babel-plugin-styled-components": "^1.10.6", 43 | "gh-pages": "^2.0.1", 44 | "lint-staged": "^9.0.1", 45 | "prettier": "^1.18.2", 46 | "rimraf": "^2.6.3" 47 | }, 48 | "browserslist": [ 49 | "> 1%", 50 | "IE >= 9", 51 | "last 2 versions" 52 | ], 53 | "scripts": { 54 | "start": "gatsby develop", 55 | "build": "gatsby build", 56 | "clean": "rimraf .cache public", 57 | "deploy": "rimraf .cache public && gatsby build --prefix-paths && gh-pages -d public", 58 | "develop": "gatsby develop", 59 | "serve": "gatsby serve", 60 | "format": "prettier --write 'src/**/*.{js,css}'", 61 | "test": "echo \"Error: no test specified\" && exit 1" 62 | }, 63 | "lint-staged": { 64 | "*.js": [ 65 | "prettier --write", 66 | "git add" 67 | ], 68 | "*.css": [ 69 | "prettier --write", 70 | "git add" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /website/siteConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | docs: [ 3 | { 4 | title: 'Get Started', 5 | path: '/docs/get-started', 6 | }, 7 | { 8 | title: 'Advance', 9 | path: '/docs/advance', 10 | }, 11 | { 12 | title: 'Selection', 13 | path: '/docs/selection', 14 | }, 15 | { 16 | title: 'Inline Editing', 17 | path: '/docs/inline-editing', 18 | }, 19 | { 20 | title: 'Glossary', 21 | path: '/docs/glossary', 22 | }, 23 | { 24 | title: 'FAQ', 25 | path: '/docs/faq', 26 | }, 27 | ], 28 | api: [ 29 | { 30 | title: 'BaseTable', 31 | path: '/api/basetable', 32 | }, 33 | { 34 | title: 'Column', 35 | path: '/api/column', 36 | }, 37 | { 38 | title: 'AutoResizer', 39 | path: '/api/autoresizer', 40 | }, 41 | { 42 | title: 'ColumnResizer', 43 | path: '/api/columnresizer', 44 | }, 45 | ], 46 | examples: [ 47 | { 48 | title: 'Default', 49 | path: '/examples/default', 50 | }, 51 | { 52 | title: 'JSX Column', 53 | path: '/examples/jsx-column', 54 | }, 55 | { 56 | title: 'Flex Column(column width in ratio)', 57 | path: '/examples/flex-column', 58 | }, 59 | { 60 | title: 'Custom Cell', 61 | path: '/examples/custom-cell', 62 | }, 63 | { 64 | title: 'Dynamic Row Height', 65 | path: '/examples/dynamic-row-heights', 66 | }, 67 | { 68 | title: 'Tooltip Cell', 69 | path: '/examples/tooltip-cell', 70 | }, 71 | { 72 | title: 'Auto Resize', 73 | path: '/examples/auto-resize', 74 | }, 75 | { 76 | title: 'Fixed Table', 77 | path: '/examples/fixed', 78 | }, 79 | { 80 | title: 'Frozen Columns', 81 | path: '/examples/frozen-columns', 82 | }, 83 | { 84 | title: 'Frozen Rows', 85 | path: '/examples/frozen-rows', 86 | }, 87 | { 88 | title: 'Sticky Rows', 89 | path: '/examples/sticky-rows', 90 | }, 91 | { 92 | title: '10000 Rows', 93 | path: '/examples/10000-rows', 94 | }, 95 | { 96 | title: 'Disabled', 97 | path: '/examples/disabled', 98 | }, 99 | { 100 | title: 'Resizable', 101 | path: '/examples/resizable', 102 | }, 103 | { 104 | title: 'Hide Header', 105 | path: '/examples/hide-header', 106 | }, 107 | { 108 | title: 'Draggable Rows', 109 | path: '/examples/draggable-rows', 110 | }, 111 | { 112 | title: 'Draggable Rows with frozen columns', 113 | path: '/examples/draggable-rows-frozen', 114 | }, 115 | { 116 | title: 'Multi Header', 117 | path: '/examples/multi-header', 118 | }, 119 | { 120 | title: 'Infinite Loading', 121 | path: '/examples/infinite-loading', 122 | }, 123 | { 124 | title: 'Infinite Loading with Loader', 125 | path: '/examples/infinite-loading-loader', 126 | }, 127 | { 128 | title: 'Col Span', 129 | path: '/examples/col-span', 130 | }, 131 | { 132 | title: 'Row Span', 133 | path: '/examples/row-span', 134 | }, 135 | { 136 | title: 'Custom Components', 137 | path: '/examples/components', 138 | }, 139 | { 140 | title: 'Custom Row', 141 | path: '/examples/row-renderer', 142 | }, 143 | { 144 | title: 'Custom Header', 145 | path: '/examples/header-renderer', 146 | }, 147 | { 148 | title: 'Custom Footer', 149 | path: '/examples/footer-renderer', 150 | }, 151 | { 152 | title: 'Custom Empty', 153 | path: '/examples/empty-renderer', 154 | }, 155 | { 156 | title: 'Custom Overlay', 157 | path: '/examples/overlay-renderer', 158 | }, 159 | { 160 | title: 'Custom Width/Height', 161 | path: '/examples/width-height', 162 | }, 163 | { 164 | title: 'Custom Scrollbar', 165 | path: '/examples/scrollbar', 166 | }, 167 | { 168 | title: 'Max Height', 169 | path: '/examples/max-height', 170 | }, 171 | { 172 | title: 'Custom Row/Header Height', 173 | path: '/examples/row-header-height', 174 | }, 175 | { 176 | title: 'Row Event Handlers', 177 | path: '/examples/row-event-handlers', 178 | }, 179 | { 180 | title: 'Sortable', 181 | path: '/examples/sortable', 182 | }, 183 | { 184 | title: 'Multi Sort', 185 | path: '/examples/multi-sort', 186 | }, 187 | { 188 | title: 'Selection', 189 | path: '/examples/selection', 190 | }, 191 | { 192 | title: 'Expand - Uncontrolled', 193 | path: '/examples/expand-uncontrolled', 194 | }, 195 | { 196 | title: 'Expand - Controlled', 197 | path: '/examples/expand-controlled', 198 | }, 199 | { 200 | title: 'Detail View', 201 | path: '/examples/detail-view', 202 | }, 203 | { 204 | title: 'Use IsScrolling', 205 | path: '/examples/is-scrolling', 206 | }, 207 | { 208 | title: 'Custom RowKey', 209 | path: '/examples/row-key', 210 | }, 211 | { 212 | title: 'Extra Props', 213 | path: '/examples/extra-props', 214 | }, 215 | { 216 | title: 'ExpandIcon Props', 217 | path: '/examples/expand-icon-props', 218 | }, 219 | { 220 | title: 'Tag Name', 221 | path: '/examples/tag-name', 222 | }, 223 | { 224 | title: 'Inline Editing', 225 | path: '/examples/inline-editing', 226 | }, 227 | { 228 | title: 'Scroll Methods', 229 | path: '/examples/scroll-to', 230 | }, 231 | { 232 | title: 'Column Hovering', 233 | path: '/examples/column-hovering', 234 | }, 235 | ], 236 | } 237 | -------------------------------------------------------------------------------- /website/src/assets/external-url.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /website/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk/react-base-table/ef4f9e34488e5a149aa750d429a8adaf071275ae/website/src/assets/favicon.png -------------------------------------------------------------------------------- /website/src/assets/mark-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /website/src/components/ActionPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Inspector from 'react-inspector' 4 | 5 | import CornerButton from './CornerButton' 6 | 7 | const Container = styled.div` 8 | position: relative; 9 | outline: 1px solid #edf0f2; 10 | padding: 15px 0; 11 | ` 12 | 13 | const ActionsContainer = styled.div` 14 | max-height: 200px; 15 | overflow-y: auto; 16 | padding: 0 15px; 17 | ` 18 | 19 | const ClearButton = styled(CornerButton)` 20 | background-color: transparent; 21 | ` 22 | 23 | class ActionPanel extends React.Component { 24 | constructor(props) { 25 | super(props) 26 | this.props.channel.on(this.onAction) 27 | } 28 | 29 | state = { 30 | actions: [], 31 | } 32 | 33 | componentWillUnmount() { 34 | this.props.channel.off(this.onAction) 35 | } 36 | 37 | render() { 38 | const { actions } = this.state 39 | if (!actions.length) return null 40 | return ( 41 | 42 | 43 | {actions.map((action, idx) => ( 44 | 50 | ))} 51 | 52 | clear 53 | 54 | ) 55 | } 56 | 57 | onAction = action => { 58 | this.setState(({ actions }) => ({ 59 | actions: [action, ...actions.slice(0, 99)], 60 | })) 61 | } 62 | 63 | onClear = () => { 64 | this.setState({ 65 | actions: [], 66 | }) 67 | } 68 | } 69 | 70 | export default ActionPanel 71 | -------------------------------------------------------------------------------- /website/src/components/Anchor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import slugify from 'slugify' 4 | 5 | const Container = styled.div` 6 | position: relative; 7 | ` 8 | 9 | const Span = styled.span` 10 | display: block; 11 | height: 70px; 12 | margin-top: -70px; 13 | visibility: hidden; 14 | ` 15 | 16 | const Link = styled.a` 17 | position: absolute; 18 | left: -1.6rem; 19 | 20 | &::before { 21 | content: '#'; 22 | visibility: hidden; 23 | } 24 | 25 | &:hover { 26 | &::before { 27 | visibility: visible; 28 | } 29 | } 30 | ` 31 | 32 | const Anchor = ({ tagName = 'h2', children, title, link, ...rest }) => { 33 | if (!title && !children) return null 34 | 35 | const slug = link || slugify(title || children, { lower: true }) 36 | return ( 37 | 38 | 39 | 40 | {children || title} 41 | 42 | ) 43 | } 44 | 45 | export default Anchor 46 | -------------------------------------------------------------------------------- /website/src/components/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { CodeBlock as Code } from 'react-live-runner' 4 | 5 | import CopyButton from './CopyButton' 6 | 7 | const Container = styled.div` 8 | position: relative; 9 | overflow: hidden; 10 | border-radius: 0.3rem; 11 | ` 12 | 13 | const Scroll = styled.div` 14 | overflow: auto; 15 | ` 16 | 17 | const StyledCode = styled(Code)` 18 | float: left; 19 | min-width: 100%; 20 | ` 21 | 22 | const CodeBlock = ({ code = '', language = 'jsx', ...rest }) => ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | 31 | export default CodeBlock 32 | -------------------------------------------------------------------------------- /website/src/components/CodeEditor.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo, useEffect } from 'react' 2 | import styled from 'styled-components' 3 | import { CodeEditor as Editor } from 'react-live-runner' 4 | import { debounce } from 'lodash' 5 | 6 | import CopyButton from './CopyButton' 7 | 8 | const Container = styled.div` 9 | position: relative; 10 | overflow: hidden; 11 | ` 12 | 13 | const EditorContainer = styled.div` 14 | overflow: auto; 15 | height: 100%; 16 | ` 17 | 18 | const StyledEditor = styled(Editor)` 19 | font-family: source-code-pro, Menlo, Monaco, Consolas, Courier New, monospace; 20 | font-size: 1.4rem; 21 | white-space: pre; 22 | background: #222; 23 | caret-color: #fff; 24 | min-width: 100%; 25 | min-height: 100%; 26 | float: left; 27 | 28 | & > textarea, 29 | & > pre { 30 | outline: none; 31 | white-space: pre !important; 32 | } 33 | ` 34 | 35 | const CodeEditor = ({ sourceCode, language, onChange, ...rest }) => { 36 | const [code, setCode] = useState(sourceCode) 37 | const debouncedChange = useMemo(() => debounce(onChange, 300), [onChange]) 38 | const handleChange = useCallback(code => { 39 | setCode(code) 40 | debouncedChange(code) 41 | }, [debouncedChange]) 42 | useEffect(() => setCode(sourceCode), [sourceCode]) 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | ) 52 | } 53 | 54 | export default CodeEditor 55 | -------------------------------------------------------------------------------- /website/src/components/CodePreview.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import styled from 'styled-components' 3 | import { useLiveRunner } from 'react-live-runner' 4 | 5 | import ActionPanel from './ActionPanel' 6 | import CodeEditor from './CodeEditor' 7 | import { createActionChannel } from 'utils/actionChannel' 8 | import baseScope from 'utils/baseScope' 9 | 10 | const Container = styled.div` 11 | height: 100%; 12 | ` 13 | 14 | const StyledEditor = styled(CodeEditor)` 15 | height: 30rem; 16 | border-radius: 0.3rem; 17 | ` 18 | 19 | const PreviewContainer = styled.div` 20 | min-height: 40rem; 21 | position: relative; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: center; 26 | overflow: auto; 27 | padding: 1rem; 28 | margin-bottom: 1rem; 29 | box-shadow: 0 0 0.8rem 0 lightsteelblue; 30 | ` 31 | 32 | const Preview = styled.div` 33 | margin: auto; 34 | ` 35 | 36 | const Error = styled.div` 37 | background: #fcc; 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | min-width: 100%; 42 | margin: 0; 43 | padding: 1rem; 44 | color: #f00; 45 | white-space: pre-wrap; 46 | ` 47 | 48 | const CodePreview = ({ 49 | code: sourceCode, 50 | scope: _scope, 51 | language, 52 | type, 53 | editorHeight = 300, 54 | ...rest 55 | }) => { 56 | const { action, channel } = useMemo(createActionChannel, []) 57 | const scope = useMemo(() => ({ ...baseScope, action, ..._scope }), [ 58 | action, 59 | _scope, 60 | ]) 61 | const { element, error, onChange } = useLiveRunner({ 62 | sourceCode, 63 | scope, 64 | type, 65 | }) 66 | 67 | return ( 68 | 69 | 70 | {error && {error}} 71 | {element} 72 | 73 | 74 | 80 | 81 | ) 82 | } 83 | 84 | export default CodePreview 85 | -------------------------------------------------------------------------------- /website/src/components/CopyButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import clipboard from 'clipboard' 3 | 4 | import CornerButton from './CornerButton' 5 | 6 | class CopyButton extends React.PureComponent { 7 | state = { 8 | text: this.props.text, 9 | } 10 | 11 | handleRef = ref => (this.ref = ref) 12 | 13 | onSuccess = () => { 14 | this.clearTimer() 15 | this.setState({ text: 'copied' }, () => { 16 | this.timer = setTimeout(() => { 17 | this.setState({ text: this.props.text }) 18 | }, 300) 19 | }) 20 | } 21 | 22 | onError = () => { 23 | this.clearTimer() 24 | this.setState({ text: 'failed' }, () => { 25 | this.timer = setTimeout(() => { 26 | this.setState({ text: this.props.text }) 27 | }, 300) 28 | }) 29 | } 30 | 31 | clearTimer = () => { 32 | this.timer && clearTimeout(this.timer) 33 | } 34 | 35 | componentDidMount() { 36 | this.clearTimer() 37 | this.clipboard = new clipboard(this.ref) 38 | 39 | this.clipboard.on('success', this.onSuccess) 40 | this.clipboard.on('error', this.onError) 41 | } 42 | 43 | componentWillUnmount() { 44 | this.clipboard && this.clipboard.destroy() 45 | } 46 | 47 | render() { 48 | const { content, ...rest } = this.props 49 | const { text } = this.state 50 | return ( 51 | 56 | {text} 57 | 58 | ) 59 | } 60 | } 61 | 62 | CopyButton.defaultProps = { 63 | text: 'copy', 64 | } 65 | 66 | export default CopyButton 67 | -------------------------------------------------------------------------------- /website/src/components/CornerButton.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const CornerButton = styled.button` 4 | position: absolute; 5 | top: 0; 6 | right: 0; 7 | border-bottom-left-radius: 0.3em; 8 | padding: 0.2em 0.8em; 9 | outline: none; 10 | border: 0; 11 | opacity: 0.5; 12 | font-size: 1.4rem; 13 | color: #000; 14 | transition: opacity 0.15s; 15 | 16 | &:hover { 17 | opacity: 0.8; 18 | cursor: pointer; 19 | } 20 | ` 21 | 22 | export default CornerButton 23 | -------------------------------------------------------------------------------- /website/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import styled from 'styled-components' 4 | import pkg from 'react-base-table/package.json' 5 | 6 | import linkIcon from 'assets/mark-github.svg' 7 | 8 | const Container = styled.div` 9 | background-color: #182a3d; 10 | position: fixed; 11 | top: 0; 12 | width: 100%; 13 | z-index: 1001; 14 | ` 15 | 16 | const Nav = styled.div` 17 | margin: 0 auto; 18 | max-width: 100rem; 19 | height: 5rem; 20 | padding: 0 2rem; 21 | display: flex; 22 | align-items: center; 23 | justify-content: space-between; 24 | ` 25 | 26 | const Title = styled(Link)` 27 | text-decoration: none; 28 | font-size: 2.4rem; 29 | line-height: 1; 30 | padding: 1rem 0; 31 | &, 32 | &:hover, 33 | &:focus { 34 | color: #fff; 35 | } 36 | ` 37 | 38 | const Spacer = styled.div` 39 | flex: 1; 40 | ` 41 | 42 | const NavLink = styled(Link).attrs({ 43 | partiallyActive: true, 44 | })` 45 | color: #bcc9d1; 46 | text-decoration: none; 47 | padding: 1rem; 48 | line-height: 1; 49 | &:hover { 50 | color: #fff; 51 | } 52 | &, 53 | &:focus { 54 | color: ${props => 55 | props.pathname && props.pathname.includes(props.to) ? '#fff' : '#bcc9d1'}; 56 | } 57 | &:last-child { 58 | padding-right: 0; 59 | display: inline-block; 60 | } 61 | ` 62 | 63 | const ExternalLink = NavLink.withComponent('a') 64 | 65 | const LinkIcon = styled.img` 66 | width: 2rem; 67 | height: 2rem; 68 | ` 69 | 70 | const Version = styled(ExternalLink)` 71 | font-size: 1.4rem; 72 | padding: 1rem; 73 | margin-top: 1rem; 74 | ` 75 | 76 | const Header = ({ pathname }) => { 77 | return ( 78 | 79 | 104 | 105 | ) 106 | } 107 | 108 | export default Header 109 | -------------------------------------------------------------------------------- /website/src/components/Html.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import rehypeReact from 'rehype-react' 3 | 4 | import CodeBlock from './CodeBlock' 5 | import CodePreview from './CodePreview' 6 | 7 | const parseMeta = meta => { 8 | const options = {} 9 | if (!meta) return options 10 | 11 | const items = meta.split(/\s+/) 12 | items.forEach(item => { 13 | if (/^[\w-]+=?$/.test(item)) options[item] = true 14 | else if (/^[\w-]+=[^=]+$/.test(item)) { 15 | const [key, value] = item.split('=') 16 | let parsed = value 17 | if (value === 'true') parsed = true 18 | else if (value === 'false') parsed = false 19 | else if (/^\d+$/.test(value)) parsed = parseInt(value, 10) 20 | else if (/^\d*\.\d+$/.test(value)) parsed = parseFloat(value) 21 | else if (/^['"].*['"]$/.test(value)) 22 | parsed = value.substr(1, value.length - 2) 23 | else if (/^{.*}$/.test(value)) { 24 | try { 25 | // eslint-disable-next-line no-eval 26 | parsed = eval(`(${value})`) 27 | } catch (err) {} 28 | } 29 | options[key] = parsed 30 | } 31 | }) 32 | return options 33 | } 34 | 35 | const Pre = props => { 36 | if (!props.children[0]) return
37 | 
38 |   const { children, className } = props.children[0].props
39 |   const language = className && className.split('-')[1]
40 |   const code = children[0]
41 | 
42 |   const meta = parseMeta(props.children[0].props['data-meta'])
43 |   const { live, ...rest } = meta
44 |   const Component = live ? CodePreview : CodeBlock
45 |   return (
46 |     
52 |   )
53 | }
54 | 
55 | const renderAst = new rehypeReact({
56 |   createElement: React.createElement,
57 |   components: {
58 |     pre: Pre,
59 |   },
60 | }).Compiler
61 | 
62 | const Html = ({ html, htmlAst, ...rest }) => {
63 |   if (htmlAst) {
64 |     return 
{renderAst(htmlAst)}
65 | } 66 | 67 | if (html) { 68 | return
69 | } 70 | 71 | return null 72 | } 73 | 74 | export default Html 75 | -------------------------------------------------------------------------------- /website/src/components/Methods.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import Html from './Html' 5 | import Anchor from './Anchor' 6 | 7 | const Method = styled.div` 8 | margin-bottom: 1.6rem; 9 | ` 10 | 11 | const Name = styled.div` 12 | font-weight: 600; 13 | ` 14 | 15 | const Tag = styled.span` 16 | font-size: 0.8em; 17 | padding: 0.1em 0.4em; 18 | margin: 0 0.4em; 19 | border-radius: 0.2em; 20 | background-color: #daf0f9; 21 | color: #819099; 22 | ` 23 | 24 | const Block = styled(Html)` 25 | color: #666; 26 | ` 27 | 28 | const getSignature = params => 29 | `(${params 30 | .map(x => `${x.name}${x.type ? `: ${x.type.name}` : ''}`) 31 | .join(', ')})` 32 | 33 | const Methods = ({ title = 'Methods', methods, ...rest }) => ( 34 |
35 | {title} 36 | {Array.isArray(methods) && 37 | methods.map((method, idx) => ( 38 | 39 | 40 | {method.name} 41 | {getSignature(method.params)} 42 | 43 | 44 | 45 | ))} 46 |
47 | ) 48 | 49 | export default Methods 50 | -------------------------------------------------------------------------------- /website/src/components/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Helmet from 'react-helmet' 3 | import styled, { css } from 'styled-components' 4 | import { StaticQuery, graphql } from 'gatsby' 5 | 6 | import Header from './Header' 7 | import Sidebar from './Sidebar' 8 | 9 | import '../styles/index.css' 10 | import 'react-base-table/styles.css' 11 | 12 | const pageMixin = css` 13 | margin: 0 auto; 14 | max-width: 100rem; 15 | ` 16 | 17 | const Container = styled.div` 18 | position: relative; 19 | padding: 70px 20px 20px; 20 | ${props => !props.full && pageMixin}; 21 | ` 22 | 23 | const Content = styled.div` 24 | margin-left: 240px; 25 | ` 26 | 27 | const Page = ({ title, location = {}, children, links, ...rest }) => ( 28 | ( 31 | 32 | 41 |
42 | 43 | {links ? ( 44 | 45 | 46 | {children} 47 | 48 | ) : ( 49 | children 50 | )} 51 | 52 | 53 | )} 54 | /> 55 | ) 56 | 57 | export default Page 58 | 59 | const detailsQuery = graphql` 60 | query { 61 | site { 62 | config: siteMetadata { 63 | title 64 | description 65 | keywords 66 | author 67 | } 68 | } 69 | } 70 | ` 71 | -------------------------------------------------------------------------------- /website/src/components/Pagination.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Link } from 'gatsby' 4 | 5 | const Container = styled.div` 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | height: 5rem; 10 | margin-top: 2rem; 11 | border-top: 1px solid #edf0f2; 12 | ` 13 | 14 | const StyledLink = styled(Link)` 15 | display: block; 16 | text-decoration: none; 17 | white-space: nowrap; 18 | text-overflow: ellipsis; 19 | overflow: hidden; 20 | color: #888; 21 | 22 | &:hover, 23 | &:active { 24 | color: #222; 25 | } 26 | ` 27 | 28 | const Pagination = ({ links, link, ...rest }) => { 29 | const index = links.indexOf(link) 30 | if (index < 0) return null 31 | const prevLink = index === 0 ? null : links[index - 1] 32 | const nextLink = index === links.length - 1 ? null : links[index + 1] 33 | 34 | return ( 35 | 36 |
37 | {prevLink && ( 38 | ← {prevLink.title} 39 | )} 40 |
41 |
42 | {nextLink && ( 43 | {nextLink.title}→ 44 | )} 45 |
46 |
47 | ) 48 | } 49 | 50 | export default Pagination 51 | -------------------------------------------------------------------------------- /website/src/components/Playground.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useCallback, useState, useEffect } from 'react' 2 | import styled from 'styled-components' 3 | import { useLiveRunner } from 'react-live-runner' 4 | import CodeEditor from './CodeEditor' 5 | import CopyButton from './CopyButton' 6 | 7 | import baseScope from 'utils/baseScope' 8 | import { getCode, replaceState } from 'utils/urlHash' 9 | 10 | const Container = styled.div` 11 | position: relative; 12 | display: flex; 13 | box-shadow: 0 0 8px 0 lightsteelblue; 14 | height: 100%; 15 | ` 16 | 17 | const StyledEditor = styled(CodeEditor)` 18 | flex: 0 1 60rem; 19 | ` 20 | 21 | const PreviewContainer = styled.div` 22 | position: relative; 23 | flex: 1 1 60rem; 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | background: #f3f3f3; 29 | overflow: auto; 30 | background: #fff; 31 | ` 32 | 33 | const Preview = styled.div` 34 | margin: auto; 35 | ` 36 | 37 | const Error = styled.div` 38 | background: #fcc; 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | min-width: 100%; 43 | margin: 0; 44 | padding: 1rem; 45 | color: #f00; 46 | white-space: pre-wrap; 47 | ` 48 | 49 | const Playground = ({ scope: _scope, language, type, ...rest }) => { 50 | const scope = useMemo(() => ({ ...baseScope, ..._scope }), [_scope]) 51 | const [sourceCode, setSourceCode] = useState(getCode) 52 | const { element, error, onChange } = useLiveRunner({ 53 | sourceCode, 54 | scope, 55 | type, 56 | }) 57 | const handleChange = useCallback( 58 | code => { 59 | onChange(code) 60 | replaceState(code) 61 | }, 62 | [onChange] 63 | ) 64 | 65 | const canUseDOM = typeof document !== 'undefined' 66 | useEffect(() => { 67 | setSourceCode(getCode) 68 | }, [canUseDOM]) 69 | 70 | return ( 71 | 72 | 77 | 78 | {error && {error}} 79 | {element} 80 | 81 | {typeof document !== 'undefined' && ( 82 | 83 | )} 84 | 85 | ) 86 | } 87 | 88 | export default Playground 89 | -------------------------------------------------------------------------------- /website/src/components/Props.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import Html from './Html' 5 | import Anchor from './Anchor' 6 | 7 | const Prop = styled.div` 8 | margin-bottom: 1.6rem; 9 | ` 10 | 11 | const Name = styled.div` 12 | font-weight: 600; 13 | ` 14 | 15 | const Tag = styled.span` 16 | font-size: 0.8em; 17 | padding: 0.1em 0.4em; 18 | margin: 0 0.4em; 19 | border-radius: 0.2em; 20 | background-color: #daf0f9; 21 | color: #819099; 22 | ` 23 | 24 | const Required = styled(Tag)` 25 | background-color: #182a3d; 26 | color: #fff; 27 | margin: 0; 28 | ` 29 | 30 | const DefaultValue = styled.span` 31 | color: #819099; 32 | ` 33 | 34 | const Block = styled(Html)` 35 | color: #666; 36 | ` 37 | 38 | const parseType = type => { 39 | if (!type) return 'unknown' 40 | 41 | if (type.name === 'enum') { 42 | if (typeof type.value === 'string') return type.value 43 | return type.value.map(x => x.value).join(' | ') 44 | } 45 | 46 | if (type.name === 'union') { 47 | return type.value.map(x => x.name).join(' | ') 48 | } 49 | 50 | return type.name 51 | } 52 | 53 | const Props = ({ title = 'Props', props, ...rest }) => { 54 | return ( 55 |
56 | {title} 57 | {Array.isArray(props) && 58 | props.map(prop => ( 59 | 60 | 61 | {prop.name} 62 | {parseType(prop.type)} 63 | {prop.defaultValue && ( 64 | 65 | defaults to 66 | {prop.defaultValue.value} 67 | 68 | )} 69 | {prop.required && required} 70 | 71 | {prop.description && ( 72 | 73 | )} 74 | 75 | ))} 76 |
77 | ) 78 | } 79 | 80 | export default Props 81 | -------------------------------------------------------------------------------- /website/src/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Link } from 'gatsby' 4 | 5 | import linkIcon from 'assets/external-url.svg' 6 | 7 | const Container = styled.div` 8 | position: fixed; 9 | top: 7rem; 10 | bottom: 2rem; 11 | overflow-y: auto; 12 | width: 22rem; 13 | min-width: 22rem; 14 | padding-right: 2rem; 15 | border-right: 1px solid #edf0f2; 16 | ` 17 | 18 | const Ul = styled.ul` 19 | padding-top: 1rem; 20 | ` 21 | 22 | const Li = styled.li` 23 | padding-bottom: 1rem; 24 | ` 25 | 26 | const StyledLink = styled(Link).attrs({ 27 | activeStyle: { 28 | fontWeight: 700, 29 | borderRight: '3px solid #0696d7', 30 | }, 31 | })` 32 | display: block; 33 | text-decoration: none; 34 | white-space: nowrap; 35 | text-overflow: ellipsis; 36 | overflow: hidden; 37 | color: #222; 38 | ` 39 | 40 | const ExternalLink = StyledLink.withComponent('a') 41 | 42 | const LinkIcon = styled.img` 43 | width: 1.4rem; 44 | height: 1.4rem; 45 | margin-left: 0.4rem; 46 | ` 47 | 48 | const Sidebar = ({ links }) => ( 49 | 50 |
    51 | {links.map(({ key, to, title, external }) => ( 52 |
  • 53 | {external ? ( 54 | 55 | {title} 56 | 57 | 58 | ) : ( 59 | {title} 60 | )} 61 |
  • 62 | ))} 63 |
64 |
65 | ) 66 | 67 | export default Sidebar 68 | -------------------------------------------------------------------------------- /website/src/examples/10000-rows.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 10000) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | export default () =>
12 | -------------------------------------------------------------------------------- /website/src/examples/auto-resize.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const Container = styled.div` 5 | width: calc(50vw + 220px); 6 | height: 50vh; 7 | ` 8 | 9 | const Hint = styled.div` 10 | font-size: 16px; 11 | font-weight: 700; 12 | color: #336699; 13 | margin-bottom: 10px; 14 | ` 15 | 16 | export default () => ( 17 | <> 18 | Resize your browser and see 19 | 20 | 21 | {({ width, height }) => ( 22 | 28 | )} 29 | 30 | 31 | 32 | ) 33 | -------------------------------------------------------------------------------- /website/src/examples/col-span.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10, undefined, { resizable: true }) 2 | const data = generateData(columns, 200) 3 | 4 | const spanIndex = 1 5 | columns[spanIndex].colSpan = ({ rowData, rowIndex }) => (rowIndex % 4) + 1 6 | columns[spanIndex].align = Column.Alignment.CENTER 7 | 8 | const rowRenderer = ({ rowData, rowIndex, cells, columns }) => { 9 | const span = columns[spanIndex].colSpan({ rowData, rowIndex }) 10 | if (span > 1) { 11 | let width = cells[spanIndex].props.style.width 12 | for (let i = 1; i < span; i++) { 13 | width += cells[spanIndex + i].props.style.width 14 | cells[spanIndex + i] = null 15 | } 16 | const style = { 17 | ...cells[spanIndex].props.style, 18 | width, 19 | backgroundColor: 'lightgray', 20 | } 21 | cells[spanIndex] = React.cloneElement(cells[spanIndex], { style }) 22 | } 23 | return cells 24 | } 25 | 26 | export default () => ( 27 |
28 | ) 29 | -------------------------------------------------------------------------------- /website/src/examples/column-hovering.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | const tableRef = React.createRef() 4 | 5 | const GlobalStyle = createGlobalStyle` 6 | .BaseTable.active-col-0 [data-col-idx="0"], 7 | .BaseTable.active-col-1 [data-col-idx="1"], 8 | .BaseTable.active-col-2 [data-col-idx="2"], 9 | .BaseTable.active-col-3 [data-col-idx="3"], 10 | .BaseTable.active-col-4 [data-col-idx="4"], 11 | .BaseTable.active-col-5 [data-col-idx="5"], 12 | .BaseTable.active-col-6 [data-col-idx="6"], 13 | .BaseTable.active-col-7 [data-col-idx="7"], 14 | .BaseTable.active-col-8 [data-col-idx="8"], 15 | .BaseTable.active-col-9 [data-col-idx="9"] { 16 | background: #f3f3f3; 17 | } 18 | ` 19 | 20 | const cellProps = ({ columnIndex }) => ({ 21 | 'data-col-idx': columnIndex, 22 | onMouseEnter: () => { 23 | const table = tableRef.current.getDOMNode() 24 | table.classList.add(`active-col-${columnIndex}`) 25 | }, 26 | onMouseLeave: () => { 27 | const table = tableRef.current.getDOMNode() 28 | table.classList.remove(`active-col-${columnIndex}`) 29 | }, 30 | }) 31 | 32 | const headerCellProps = ({ columnIndex }) => ({ 33 | 'data-col-idx': columnIndex, 34 | }) 35 | 36 | export default () => ( 37 | <> 38 | 39 |
46 | 47 | ) 48 | -------------------------------------------------------------------------------- /website/src/examples/components.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | fixedColumns[0].format = 'checkbox' 12 | fixedColumns[1].format = 'contact' 13 | 14 | const Contact = styled.div` 15 | font-weight: 700; 16 | color: orange; 17 | ` 18 | 19 | const stringRenderer = ({ className, cellData }) => ( 20 |
{cellData}
21 | ) 22 | const checkboxRenderer = ({ rowIndex }) => ( 23 | 24 | ) 25 | const contactRenderer = ({ cellData }) => {cellData} 26 | 27 | const renderers = { 28 | string: stringRenderer, 29 | checkbox: checkboxRenderer, 30 | contact: contactRenderer, 31 | } 32 | 33 | const Cell = cellProps => { 34 | const format = cellProps.column.format || 'string' 35 | const renderer = renderers[format] || renderers.string 36 | 37 | return renderer(cellProps) 38 | } 39 | 40 | const components = { 41 | TableCell: Cell, 42 | } 43 | 44 | const expandColumnKey = 'column-1' 45 | const treeData = unflatten(data) 46 | 47 | export default () => ( 48 |
56 | ) 57 | -------------------------------------------------------------------------------- /website/src/examples/custom-cell.js: -------------------------------------------------------------------------------- 1 | const dataGenerator = () => ({ 2 | id: faker.random.uuid(), 3 | name: faker.name.findName(), 4 | gender: faker.random.boolean() ? 'male' : 'female', 5 | score: { 6 | math: faker.random.number(70) + 30, 7 | }, 8 | birthday: faker.date.between(1995, 2005), 9 | attachments: faker.random.number(5), 10 | description: faker.lorem.sentence(), 11 | email: faker.internet.email(), 12 | country: faker.address.country(), 13 | address: { 14 | street: faker.address.streetAddress(), 15 | city: faker.address.city(), 16 | zipCode: faker.address.zipCode(), 17 | }, 18 | }) 19 | 20 | const GenderContainer = styled.div` 21 | background-color: ${props => 22 | props.gender === 'male' ? 'lightblue' : 'pink'}; 23 | color: white; 24 | border-radius: 3px; 25 | width: 20px; 26 | height: 20px; 27 | font-size: 16px; 28 | font-weight: bold; 29 | line-height: 20px; 30 | text-align: center; 31 | ` 32 | 33 | const Gender = ({ gender }) => ( 34 | 35 | {gender === 'male' ? '♂' : '♀'} 36 | 37 | ) 38 | 39 | const Score = styled.span` 40 | color: ${props => (props.score >= 60 ? 'green' : 'red')}; 41 | ` 42 | 43 | const Attachment = styled.div` 44 | background-color: lightgray; 45 | width: 20px; 46 | height: 20px; 47 | line-height: 20px; 48 | text-align: center; 49 | border-radius: 4px; 50 | color: gray; 51 | ` 52 | 53 | const defaultData = new Array(5000) 54 | .fill(0) 55 | .map(dataGenerator) 56 | .sort((a, b) => (a.name > b.name ? 1 : -1)) 57 | 58 | const defaultSort = { key: 'name', order: SortOrder.ASC } 59 | 60 | export default class App extends React.Component { 61 | state = { 62 | data: defaultData, 63 | sortBy: defaultSort, 64 | } 65 | 66 | columns = [ 67 | { 68 | key: 'name', 69 | title: 'Name', 70 | dataKey: 'name', 71 | width: 150, 72 | resizable: true, 73 | sortable: true, 74 | frozen: Column.FrozenDirection.LEFT, 75 | }, 76 | { 77 | key: 'score', 78 | title: 'Score', 79 | dataKey: 'score.math', 80 | width: 60, 81 | align: Column.Alignment.CENTER, 82 | sortable: false, 83 | cellRenderer: ({ cellData: score }) => {score}, 84 | }, 85 | { 86 | key: 'gender', 87 | title: '♂♀', 88 | dataKey: 'gender', 89 | cellRenderer: ({ cellData: gender }) => , 90 | width: 60, 91 | align: Column.Alignment.CENTER, 92 | sortable: true, 93 | }, 94 | { 95 | key: 'birthday', 96 | title: 'Birthday', 97 | dataKey: 'birthday', 98 | dataGetter: ({ column, rowData }) => 99 | rowData[column.dataKey].toLocaleDateString(), 100 | width: 100, 101 | align: Column.Alignment.RIGHT, 102 | sortable: true, 103 | }, 104 | { 105 | key: 'attachments', 106 | title: 'Attachments', 107 | dataKey: 'attachments', 108 | width: 60, 109 | align: Column.Alignment.CENTER, 110 | headerRenderer: () => ?, 111 | cellRenderer: ({ cellData }) => {cellData}, 112 | }, 113 | { 114 | key: 'description', 115 | title: 'Description', 116 | dataKey: 'description', 117 | width: 200, 118 | resizable: true, 119 | sortable: true, 120 | cellRenderer: ({ cellData }) => {cellData}, 121 | }, 122 | { 123 | key: 'email', 124 | title: 'Email', 125 | dataKey: 'email', 126 | width: 200, 127 | resizable: true, 128 | sortable: true, 129 | }, 130 | { 131 | key: 'country', 132 | title: 'Country', 133 | dataKey: 'country', 134 | width: 100, 135 | resizable: true, 136 | sortable: true, 137 | }, 138 | { 139 | key: 'address', 140 | title: 'Address', 141 | dataKey: 'address.street', 142 | width: 200, 143 | resizable: true, 144 | }, 145 | { 146 | key: 'action', 147 | width: 100, 148 | align: Column.Alignment.CENTER, 149 | frozen: Column.FrozenDirection.RIGHT, 150 | cellRenderer: ({ rowData }) => ( 151 | 160 | ), 161 | }, 162 | ] 163 | 164 | onColumnSort = sortBy => { 165 | const order = sortBy.order === SortOrder.ASC ? 1 : -1 166 | const data = [...this.state.data] 167 | data.sort((a, b) => (a[sortBy.key] > b[sortBy.key] ? order : -order)) 168 | this.setState({ 169 | sortBy, 170 | data, 171 | }) 172 | } 173 | 174 | render() { 175 | const { data, sortBy } = this.state 176 | return ( 177 |
184 | ) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /website/src/examples/default.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | export default () =>
5 | -------------------------------------------------------------------------------- /website/src/examples/detail-view.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | data.forEach(x => { 5 | x.children = [ 6 | { 7 | id: `${x.id}-detail`, 8 | content: faker.lorem.paragraphs(), 9 | }, 10 | ] 11 | }) 12 | 13 | const GlobalStyle = createGlobalStyle` 14 | .BaseTable__row--depth-0 { 15 | height: 50px; 16 | } 17 | 18 | .BaseTable__row--depth-0 .BaseTable__row-cell-text { 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | white-space: nowrap; 22 | } 23 | ` 24 | 25 | const Row = styled.div` 26 | padding: 15px; 27 | ` 28 | const rowRenderer = ({ rowData, cells }) => { 29 | if (rowData.content) return {rowData.content} 30 | return cells 31 | } 32 | 33 | export default () => ( 34 | <> 35 | 36 |
43 | 44 | ) 45 | -------------------------------------------------------------------------------- /website/src/examples/disabled.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | export default () =>
5 | -------------------------------------------------------------------------------- /website/src/examples/draggable-rows-frozen.js: -------------------------------------------------------------------------------- 1 | // import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc' 2 | const { sortableContainer, sortableElement, sortableHandle } = ReactSortableHoc 3 | const DraggableContainer = sortableContainer(({ children }) => children) 4 | const DraggableElement = sortableElement(({ children }) => children) 5 | const DraggableHandle = sortableHandle(({ children }) => children) 6 | 7 | const Handle = styled.div` 8 | flex: none; 9 | width: 7.5px; 10 | height: 100%; 11 | 12 | &::before { 13 | content: ''; 14 | border-left: 4px dotted #ccc; 15 | display: block; 16 | height: 20px; 17 | margin: 15px 3px; 18 | } 19 | 20 | &:hover::before { 21 | border-color: #888; 22 | } 23 | ` 24 | 25 | const Row = ({ key, index, children, ...rest }) => { 26 | // if the children's length is not equal the columns' length, then it's rendering row for frozen table 27 | if (children.length !== 10) 28 | return ( 29 | 30 |
31 | 32 | 33 | 34 | {children} 35 |
36 |
37 | ) 38 | 39 | return
{children}
40 | } 41 | 42 | const rowProps = ({ rowIndex }) => ({ 43 | tagName: Row, 44 | index: rowIndex, 45 | }) 46 | 47 | class DraggableTable extends React.PureComponent { 48 | state = { 49 | data: this.props.data, 50 | } 51 | 52 | table = React.createRef() 53 | 54 | getContainer = () => { 55 | // for fixed table with frozen columns, the drag handle is in the left frozen table 56 | return this.table.current 57 | .getDOMNode() 58 | .querySelector('.BaseTable__table-frozen-left .BaseTable__body') 59 | } 60 | 61 | getHelperContainer = () => { 62 | return this.table.current 63 | .getDOMNode() 64 | .querySelector('.BaseTable__table-frozen-left') 65 | } 66 | 67 | rowProps = args => { 68 | // don't forget to passing the incoming rowProps 69 | const extraProps = callOrReturn(this.props.rowProps) 70 | return { 71 | ...extraProps, 72 | tagName: Row, 73 | index: args.rowIndex, 74 | } 75 | } 76 | 77 | handleSortEnd = ({ oldIndex, newIndex }) => { 78 | const data = [...this.state.data] 79 | const [removed] = data.splice(oldIndex, 1) 80 | data.splice(newIndex, 0, removed) 81 | this.setState({ data }) 82 | } 83 | 84 | render() { 85 | return ( 86 | 92 |
99 | 100 | ) 101 | } 102 | } 103 | 104 | const Hint = styled.div` 105 | font-size: 16px; 106 | font-weight: 700; 107 | color: #336699; 108 | margin-bottom: 10px; 109 | ` 110 | 111 | const columns = generateColumns(10) 112 | const data = generateData(columns, 200) 113 | columns[0].minWidth = 150 114 | columns[0].frozen = true 115 | 116 | export default () => ( 117 | <> 118 | Drag the dots, only works in fixed mode(fixed=true) 119 | 120 | 121 | ) 122 | -------------------------------------------------------------------------------- /website/src/examples/draggable-rows.js: -------------------------------------------------------------------------------- 1 | // import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc' 2 | const { sortableContainer, sortableElement, sortableHandle } = ReactSortableHoc 3 | const DraggableContainer = sortableContainer(({ children }) => children) 4 | const DraggableElement = sortableElement(({ children }) => children) 5 | const DraggableHandle = sortableHandle(({ children }) => children) 6 | 7 | const Handle = styled.div` 8 | flex: none; 9 | width: 7.5px; 10 | height: 100%; 11 | 12 | &::before { 13 | content: ''; 14 | border-left: 4px dotted #ccc; 15 | display: block; 16 | height: 20px; 17 | margin: 15px 3px; 18 | } 19 | 20 | &:hover::before { 21 | border-color: #888; 22 | } 23 | ` 24 | 25 | const Row = ({ key, index, children, ...rest }) => ( 26 | 27 |
28 | 29 | 30 | 31 | {children} 32 |
33 |
34 | ) 35 | 36 | const rowProps = ({ rowIndex }) => ({ 37 | tagName: Row, 38 | index: rowIndex, 39 | }) 40 | 41 | class DraggableTable extends React.PureComponent { 42 | state = { 43 | data: this.props.data, 44 | } 45 | 46 | table = React.createRef() 47 | 48 | getContainer = () => { 49 | return this.table.current.getDOMNode().querySelector('.BaseTable__body') 50 | } 51 | 52 | getHelperContainer = () => { 53 | return this.table.current.getDOMNode().querySelector('.BaseTable__table') 54 | } 55 | 56 | rowProps = args => { 57 | // don't forget to passing the incoming rowProps 58 | const extraProps = callOrReturn(this.props.rowProps) 59 | return { 60 | ...extraProps, 61 | tagName: Row, 62 | index: args.rowIndex, 63 | } 64 | } 65 | 66 | handleSortEnd = ({ oldIndex, newIndex }) => { 67 | const data = [...this.state.data] 68 | const [removed] = data.splice(oldIndex, 1) 69 | data.splice(newIndex, 0, removed) 70 | this.setState({ data }) 71 | } 72 | 73 | render() { 74 | return ( 75 | 81 |
88 | 89 | ) 90 | } 91 | } 92 | 93 | const Hint = styled.div` 94 | font-size: 16px; 95 | font-weight: 700; 96 | color: #336699; 97 | margin-bottom: 10px; 98 | ` 99 | 100 | const columns = generateColumns(10) 101 | const data = generateData(columns, 200) 102 | columns[0].minWidth = 150 103 | 104 | export default () => ( 105 | <> 106 | Drag the dots, only works in flex mode(fixed=false) 107 | 108 | 109 | ) 110 | -------------------------------------------------------------------------------- /website/src/examples/dynamic-row-heights.js: -------------------------------------------------------------------------------- 1 | const dataGenerator = () => ({ 2 | id: faker.random.uuid(), 3 | name: faker.name.findName(), 4 | gender: faker.random.boolean() ? 'male' : 'female', 5 | score: { 6 | math: faker.random.number(70) + 30, 7 | }, 8 | birthday: faker.date.between(1995, 2005), 9 | attachments: faker.random.number(5), 10 | description: faker.lorem.sentence(), 11 | email: faker.internet.email(), 12 | country: faker.address.country(), 13 | address: { 14 | street: faker.address.streetAddress(), 15 | city: faker.address.city(), 16 | zipCode: faker.address.zipCode(), 17 | }, 18 | }) 19 | 20 | const GenderContainer = styled.div` 21 | background-color: ${props => 22 | props.gender === 'male' ? 'lightblue' : 'pink'}; 23 | color: white; 24 | border-radius: 3px; 25 | width: 20px; 26 | height: 20px; 27 | font-size: 16px; 28 | font-weight: bold; 29 | line-height: 20px; 30 | text-align: center; 31 | ` 32 | 33 | const Gender = ({ gender }) => ( 34 | 35 | {gender === 'male' ? '♂' : '♀'} 36 | 37 | ) 38 | 39 | const Score = styled.span` 40 | color: ${props => (props.score >= 60 ? 'green' : 'red')}; 41 | ` 42 | 43 | const Attachment = styled.div` 44 | background-color: lightgray; 45 | width: 20px; 46 | height: 20px; 47 | line-height: 20px; 48 | text-align: center; 49 | border-radius: 4px; 50 | color: gray; 51 | ` 52 | 53 | const defaultData = new Array(5000) 54 | .fill(0) 55 | .map(dataGenerator) 56 | .sort((a, b) => (a.name > b.name ? 1 : -1)) 57 | 58 | const defaultSort = { key: 'name', order: SortOrder.ASC } 59 | 60 | export default class App extends React.Component { 61 | state = { 62 | data: defaultData, 63 | sortBy: defaultSort, 64 | } 65 | 66 | columns = [ 67 | { 68 | key: 'name', 69 | title: 'Name', 70 | dataKey: 'name', 71 | width: 150, 72 | resizable: true, 73 | sortable: true, 74 | frozen: Column.FrozenDirection.LEFT, 75 | }, 76 | { 77 | key: 'score', 78 | title: 'Score', 79 | dataKey: 'score.math', 80 | width: 60, 81 | align: Column.Alignment.CENTER, 82 | sortable: false, 83 | }, 84 | { 85 | key: 'gender', 86 | title: '♂♀', 87 | dataKey: 'gender', 88 | cellRenderer: ({ cellData: gender }) => , 89 | width: 60, 90 | align: Column.Alignment.CENTER, 91 | sortable: true, 92 | }, 93 | { 94 | key: 'birthday', 95 | title: 'Birthday', 96 | dataKey: 'birthday', 97 | dataGetter: ({ column, rowData }) => 98 | rowData[column.dataKey].toLocaleDateString(), 99 | width: 100, 100 | align: Column.Alignment.RIGHT, 101 | sortable: true, 102 | }, 103 | { 104 | key: 'attachments', 105 | title: 'Attachments', 106 | dataKey: 'attachments', 107 | width: 60, 108 | align: Column.Alignment.CENTER, 109 | headerRenderer: () => ?, 110 | cellRenderer: ({ cellData }) => {cellData}, 111 | }, 112 | { 113 | key: 'description', 114 | title: 'Description', 115 | dataKey: 'description', 116 | width: 200, 117 | resizable: true, 118 | sortable: true, 119 | }, 120 | { 121 | key: 'email', 122 | title: 'Email', 123 | dataKey: 'email', 124 | width: 200, 125 | resizable: true, 126 | sortable: true, 127 | }, 128 | { 129 | key: 'country', 130 | title: 'Country', 131 | dataKey: 'country', 132 | width: 100, 133 | resizable: true, 134 | sortable: true, 135 | }, 136 | { 137 | key: 'address', 138 | title: 'Address', 139 | dataKey: 'address.street', 140 | width: 200, 141 | resizable: true, 142 | }, 143 | { 144 | key: 'action', 145 | width: 100, 146 | align: Column.Alignment.CENTER, 147 | frozen: Column.FrozenDirection.RIGHT, 148 | cellRenderer: ({ rowData }) => ( 149 | 158 | ), 159 | }, 160 | ] 161 | 162 | onColumnSort = sortBy => { 163 | const order = sortBy.order === SortOrder.ASC ? 1 : -1 164 | const data = [...this.state.data] 165 | data.sort((a, b) => (a[sortBy.key] > b[sortBy.key] ? order : -order)) 166 | this.setState({ 167 | sortBy, 168 | data, 169 | }) 170 | } 171 | 172 | render() { 173 | const { data, sortBy } = this.state 174 | return ( 175 | <> 176 | 185 | 194 |
204 | 205 | ) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /website/src/examples/empty-renderer.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | 3 | const Empty = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | height: 100%; 8 | font-size: 16px; 9 | ` 10 | 11 | export default () => ( 12 |
Table is empty} 16 | /> 17 | ) 18 | -------------------------------------------------------------------------------- /website/src/examples/expand-controlled.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | const expandColumnKey = 'column-0' 12 | 13 | // add some sub items 14 | for (let i = 0; i < 5; i++) { 15 | data.push({ 16 | ...data[0], 17 | id: `${data[0].id}-sub-${i}`, 18 | parentId: data[0].id, 19 | [expandColumnKey]: `Sub ${i}`, 20 | }) 21 | data.push({ 22 | ...data[2], 23 | id: `${data[2].id}-sub-${i}`, 24 | parentId: data[2].id, 25 | [expandColumnKey]: `Sub ${i}`, 26 | }) 27 | data.push({ 28 | ...data[2], 29 | id: `${data[2].id}-sub-sub-${i}`, 30 | parentId: `${data[2].id}-sub-${i}`, 31 | [expandColumnKey]: `Sub-Sub ${i}`, 32 | }) 33 | } 34 | 35 | const treeData = unflatten(data) 36 | 37 | export default () => ( 38 |
47 | ) 48 | -------------------------------------------------------------------------------- /website/src/examples/expand-icon-props.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | const expandColumnKey = 'column-0' 12 | 13 | // add some sub items 14 | for (let i = 0; i < 3; i++) { 15 | data.push({ 16 | ...data[0], 17 | id: `${data[0].id}-sub-${i}`, 18 | parentId: data[0].id, 19 | [expandColumnKey]: `Sub ${i}`, 20 | }) 21 | data.push({ 22 | ...data[2], 23 | id: `${data[2].id}-sub-${i}`, 24 | parentId: data[2].id, 25 | [expandColumnKey]: `Sub ${i}`, 26 | }) 27 | data.push({ 28 | ...data[2], 29 | id: `${data[2].id}-sub-sub-${i}`, 30 | parentId: `${data[2].id}-sub-${i}`, 31 | [expandColumnKey]: `Sub-Sub ${i}`, 32 | }) 33 | } 34 | 35 | const treeData = unflatten(data) 36 | 37 | const rotate = keyframes` 38 | from { 39 | transform: rotate(0deg); 40 | } 41 | 42 | to { 43 | transform: rotate(360deg); 44 | } 45 | ` 46 | 47 | const Loader = styled.div` 48 | display: inline-block; 49 | border-radius: 100%; 50 | margin: 2px; 51 | border: 2px solid #0696d7; 52 | border-bottom-color: transparent; 53 | margin: 2px; 54 | width: 12px; 55 | height: 12px; 56 | animation: ${rotate} 0.75s linear infinite; 57 | margin-left: ${props => props.depth * 16}px; 58 | ` 59 | 60 | const ExpandIcon = ({ expanding, ...rest }) => 61 | expanding ? : 62 | 63 | const components = { 64 | ExpandIcon, 65 | } 66 | 67 | const expandIconProps = ({ rowData }) => ({ 68 | expanding: !rowData.children || rowData.children.length === 0, 69 | }) 70 | 71 | export default () => ( 72 |
83 | ) 84 | -------------------------------------------------------------------------------- /website/src/examples/expand-uncontrolled.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | const expandColumnKey = 'column-0' 12 | 13 | // add some sub items 14 | for (let i = 0; i < 1000; i++) { 15 | data.push({ 16 | ...data[0], 17 | id: `${data[0].id}-sub-${i}`, 18 | parentId: data[0].id, 19 | [expandColumnKey]: `Sub ${i}`, 20 | }) 21 | data.push({ 22 | ...data[2], 23 | id: `${data[2].id}-sub-${i}`, 24 | parentId: data[2].id, 25 | [expandColumnKey]: `Sub ${i}`, 26 | }) 27 | data.push({ 28 | ...data[2], 29 | id: `${data[2].id}-sub-sub-${i}`, 30 | parentId: `${data[2].id}-sub-${i}`, 31 | [expandColumnKey]: `Sub-Sub ${i}`, 32 | }) 33 | } 34 | 35 | const treeData = unflatten(data) 36 | 37 | export default () => ( 38 |
47 | ) 48 | -------------------------------------------------------------------------------- /website/src/examples/extra-props.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const Row = styled.a` 5 | color: black; 6 | &:hover { 7 | color: red; 8 | } 9 | ` 10 | 11 | const rowProps = { 12 | tagName: Row, 13 | href: 'https://www.google.com', 14 | target: '_blank', 15 | } 16 | const cellProps = ({ rowIndex, columnIndex }) => 17 | rowIndex % 2 === 0 && { 18 | tagName: 'button', 19 | onClick: e => { 20 | e.preventDefault() 21 | e.stopPropagation() 22 | alert(`You clicked row ${rowIndex} column ${columnIndex}`) 23 | }, 24 | } 25 | 26 | export default () => ( 27 |
33 | ) 34 | -------------------------------------------------------------------------------- /website/src/examples/fixed.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | export default () =>
5 | -------------------------------------------------------------------------------- /website/src/examples/flex-column.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | export default () => ( 5 |
6 | 7 | 8 | 9 | 10 | 11 |
12 | ) 13 | -------------------------------------------------------------------------------- /website/src/examples/footer-renderer.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | const Footer = styled.div` 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | height: 100%; 16 | font-size: 16px; 17 | ` 18 | 19 | export default () => ( 20 | Custom Footer} 26 | /> 27 | ) 28 | -------------------------------------------------------------------------------- /website/src/examples/frozen-columns.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | export default () =>
12 | -------------------------------------------------------------------------------- /website/src/examples/frozen-rows.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | const frozenData = generateData(columns, 3, 'frozen-row-') 4 | 5 | const fixedColumns = columns.map((column, columnIndex) => { 6 | let frozen 7 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 8 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 9 | return { ...column, frozen } 10 | }) 11 | 12 | const expandColumnKey = 'column-0' 13 | 14 | // add some sub items 15 | for (let i = 0; i < 3; i++) { 16 | data.push({ 17 | ...data[0], 18 | id: `${data[0].id}-sub-${i}`, 19 | parentId: data[0].id, 20 | [expandColumnKey]: `Sub ${i}`, 21 | }) 22 | data.push({ 23 | ...data[2], 24 | id: `${data[2].id}-sub-${i}`, 25 | parentId: data[2].id, 26 | [expandColumnKey]: `Sub ${i}`, 27 | }) 28 | data.push({ 29 | ...data[2], 30 | id: `${data[2].id}-sub-sub-${i}`, 31 | parentId: `${data[2].id}-sub-${i}`, 32 | [expandColumnKey]: `Sub-Sub ${i}`, 33 | }) 34 | } 35 | 36 | const treeData = unflatten(data) 37 | 38 | export default () => ( 39 |
46 | ) 47 | -------------------------------------------------------------------------------- /website/src/examples/header-renderer.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const Row = styled.div` 5 | display: flex; 6 | align-items: center; 7 | width: 100%; 8 | height: 100%; 9 | ` 10 | 11 | const CustomHeader = styled.div` 12 | display: flex; 13 | flex-grow: 1; 14 | padding: 0 7.5px; 15 | ` 16 | 17 | columns[0].frozen = Column.FrozenDirection.LEFT 18 | columns[1].frozen = Column.FrozenDirection.LEFT 19 | columns[2].frozen = Column.FrozenDirection.RIGHT 20 | 21 | const headerRenderer = ({ cells, columns }) => { 22 | // frozen table's header 23 | if (columns.every(x => x.frozen)) return cells 24 | 25 | // scrollalbe table's header, as there are placeholders for the frozen cells 26 | // we have to keep them to make sure the custom content display in the right palce 27 | const leftPlaceholders = [] 28 | const rightPlaceholders = [] 29 | columns.forEach((column, idx) => { 30 | if (column.frozen === Column.FrozenDirection.RIGHT) 31 | rightPlaceholders.push(cells[idx]) 32 | else if (column.frozen) leftPlaceholders.push(cells[idx]) 33 | }) 34 | 35 | return ( 36 | 37 | {leftPlaceholders} 38 | This is a custom Header 39 | {rightPlaceholders} 40 | 41 | ) 42 | } 43 | 44 | export default () => ( 45 |
46 | ) 47 | -------------------------------------------------------------------------------- /website/src/examples/hide-header.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | export default () =>
5 | -------------------------------------------------------------------------------- /website/src/examples/infinite-loading-loader.js: -------------------------------------------------------------------------------- 1 | const TOTAL_SIZE = 1005 2 | const PAGE_SIZE = 50 3 | 4 | const columns = generateColumns(10) 5 | const DATA = generateData(columns, TOTAL_SIZE) 6 | 7 | const fixedColumns = columns.map((column, columnIndex) => { 8 | let frozen 9 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 10 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 11 | return { ...column, frozen } 12 | }) 13 | 14 | const Empty = styled.div` 15 | height: 100%; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | ` 20 | 21 | const rotate = keyframes` 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | 26 | to { 27 | transform: rotate(360deg); 28 | } 29 | ` 30 | 31 | const Loader = styled.div` 32 | display: inline-block; 33 | border-radius: 100%; 34 | margin: 2px; 35 | border: 2px solid #0696d7; 36 | border-bottom-color: transparent; 37 | margin: 2px; 38 | width: 22px; 39 | height: 22px; 40 | animation: ${rotate} 0.75s linear infinite; 41 | ` 42 | 43 | const Toolbar = styled.div` 44 | display: flex; 45 | align-items: center; 46 | justify-content: space-between; 47 | margin-bottom: 10px; 48 | ` 49 | 50 | const Footer = styled.div` 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | height: 100%; 55 | ` 56 | 57 | export default class App extends React.Component { 58 | state = { 59 | data: [], 60 | loading: true, 61 | loadingMore: false, 62 | loadedAll: false, 63 | } 64 | 65 | fetchData(offset = 0, limit = PAGE_SIZE) { 66 | return delay(3000).then(() => { 67 | return DATA.slice(offset, offset + limit) 68 | }) 69 | } 70 | 71 | loadData() { 72 | this.fetchData(0, Math.random() < 0.2 ? 0 : PAGE_SIZE).then(data => { 73 | if (!this._isMount) return 74 | this.setState({ 75 | data, 76 | loading: false, 77 | loadedAll: data.length < PAGE_SIZE, 78 | }) 79 | }) 80 | } 81 | 82 | loadMore() { 83 | this.setState({ loadingMore: true }) 84 | this.fetchData(this.state.data.length).then(data => { 85 | if (!this._isMount) return 86 | this.setState({ 87 | data: [...this.state.data, ...data], 88 | loadingMore: false, 89 | loadedAll: data.length < PAGE_SIZE, 90 | }) 91 | }) 92 | } 93 | 94 | handleEndReached = args => { 95 | action('onEndReached')(args) 96 | const { loading, loadingMore, loadedAll } = this.state 97 | if (loading || loadingMore || loadedAll) return 98 | this.loadMore() 99 | } 100 | 101 | handleReload = () => { 102 | this.setState({ 103 | data: [], 104 | loading: true, 105 | }) 106 | this.loadData() 107 | } 108 | 109 | renderFooter = () => { 110 | if (!this.state.loadingMore) return null 111 | return ( 112 |
113 | 114 |
115 | ) 116 | } 117 | 118 | renderEmpty = () => { 119 | if (this.state.loading) 120 | return ( 121 | 122 | 123 | 124 | ) 125 | if (this.state.data.length === 0) return No data available 126 | } 127 | 128 | componentDidMount() { 129 | this._isMount = true 130 | this.loadData() 131 | } 132 | 133 | componentWillUnmount() { 134 | this._isMount = false 135 | } 136 | 137 | render() { 138 | const { data, loading, loadingMore, loadedAll } = this.state 139 | return ( 140 | <> 141 | 142 | Loaded data length: {data.length} 143 | All data loaded: {loadedAll.toString()} 144 | 145 | 146 |
157 | 158 | ) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /website/src/examples/infinite-loading.js: -------------------------------------------------------------------------------- 1 | const TOTAL_SIZE = 1005 2 | const PAGE_SIZE = 50 3 | 4 | const columns = generateColumns(10) 5 | const DATA = generateData(columns, TOTAL_SIZE) 6 | 7 | const fixedColumns = columns.map((column, columnIndex) => { 8 | let frozen 9 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 10 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 11 | return { ...column, frozen } 12 | }) 13 | 14 | const Empty = styled.div` 15 | height: 100%; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | ` 20 | 21 | const rotate = keyframes` 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | 26 | to { 27 | transform: rotate(360deg); 28 | } 29 | ` 30 | 31 | const Loader = styled.div` 32 | display: inline-block; 33 | border-radius: 100%; 34 | margin: 2px; 35 | border: 2px solid #0696d7; 36 | border-bottom-color: transparent; 37 | margin: 2px; 38 | width: ${props => (props.small ? 12 : 22)}px; 39 | height: ${props => (props.small ? 12 : 22)}px; 40 | animation: ${rotate} 0.75s linear infinite; 41 | ` 42 | 43 | const LoadingLayer = styled.div` 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | background-color: rgba(255, 255, 255, 0.3); 48 | margin: 0; 49 | width: 100%; 50 | height: 100%; 51 | ` 52 | 53 | const LoadingMoreLayer = styled.div` 54 | pointer-events: none; 55 | background: rgba(32, 60, 94, 0.3); 56 | position: absolute; 57 | bottom: 30px; 58 | left: 50%; 59 | transform: translateX(-50%); 60 | padding: 5px 15px; 61 | border-radius: 10px; 62 | display: flex; 63 | align-items: center; 64 | ` 65 | 66 | const LoadingMoreText = styled.span` 67 | color: #fff; 68 | margin-right: 5px; 69 | ` 70 | 71 | const Toolbar = styled.div` 72 | display: flex; 73 | align-items: center; 74 | justify-content: space-between; 75 | margin-bottom: 10px; 76 | ` 77 | 78 | const Footer = styled.div` 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | height: 100%; 83 | ` 84 | 85 | export default class App extends React.Component { 86 | state = { 87 | data: [], 88 | loading: true, 89 | loadingMore: false, 90 | loadedAll: false, 91 | } 92 | 93 | fetchData(offset = 0, limit = PAGE_SIZE) { 94 | return delay(3000).then(() => { 95 | return DATA.slice(offset, offset + limit) 96 | }) 97 | } 98 | 99 | loadData() { 100 | this.fetchData(0, Math.random() < 0.2 ? 0 : PAGE_SIZE).then(data => { 101 | if (!this._isMount) return 102 | this.setState({ 103 | data, 104 | loading: false, 105 | loadedAll: data.length < PAGE_SIZE, 106 | }) 107 | }) 108 | } 109 | 110 | loadMore() { 111 | this.setState({ loadingMore: true }) 112 | this.fetchData(this.state.data.length).then(data => { 113 | if (!this._isMount) return 114 | this.setState({ 115 | data: [...this.state.data, ...data], 116 | loadingMore: false, 117 | loadedAll: data.length < PAGE_SIZE, 118 | }) 119 | }) 120 | } 121 | 122 | handleEndReached = args => { 123 | action('onEndReached')(args) 124 | const { loading, loadingMore, loadedAll } = this.state 125 | if (loading || loadingMore || loadedAll) return 126 | this.loadMore() 127 | } 128 | 129 | handleReload = () => { 130 | this.setState({ 131 | data: [], 132 | loading: true, 133 | }) 134 | this.loadData() 135 | } 136 | 137 | renderEmpty = () => { 138 | if (this.state.loading) return null 139 | return No data available 140 | } 141 | 142 | renderOverlay = () => { 143 | const { loading, loadingMore } = this.state 144 | 145 | if (loadingMore) 146 | return ( 147 | 148 | Loading More 149 | 150 | 151 | ) 152 | if (loading) 153 | return ( 154 | 155 | 156 | 157 | ) 158 | 159 | return null 160 | } 161 | 162 | componentDidMount() { 163 | this._isMount = true 164 | this.loadData() 165 | } 166 | 167 | componentWillUnmount() { 168 | this._isMount = false 169 | } 170 | 171 | render() { 172 | const { data, loading, loadingMore, loadedAll } = this.state 173 | return ( 174 | <> 175 | 176 | Loaded data length: {data.length} 177 | All data loaded: {loadedAll.toString()} 178 | 179 | 180 |
191 | 192 | ) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /website/src/examples/inline-editing.js: -------------------------------------------------------------------------------- 1 | // import { Overlay } from 'react-overlays' 2 | const { Overlay } = ReactOverlays 3 | 4 | const CellContainer = styled.div` 5 | display: flex; 6 | flex: 1 0 100%; 7 | align-items: center; 8 | height: 100%; 9 | overflow: hidden; 10 | margin: 0 -5px; 11 | padding: 5px; 12 | border: 1px dashed transparent; 13 | ` 14 | 15 | const GlobalStyle = createGlobalStyle` 16 | .BaseTable__row:hover, 17 | .BaseTable__row--hover { 18 | ${CellContainer} { 19 | border: 1px dashed #ccc; 20 | } 21 | } 22 | ` 23 | 24 | const Select = styled.select` 25 | width: 100%; 26 | height: 30px; 27 | margin-top: 10px; 28 | ` 29 | 30 | class EditableCell extends React.PureComponent { 31 | state = { 32 | value: this.props.cellData, 33 | editing: false, 34 | } 35 | 36 | setTargetRef = ref => (this.targetRef = ref) 37 | 38 | getTargetRef = () => this.targetRef 39 | 40 | handleClick = () => this.setState({ editing: true }) 41 | 42 | handleHide = () => this.setState({ editing: false }) 43 | 44 | handleChange = e => 45 | this.setState({ 46 | value: e.target.value, 47 | editing: false, 48 | }) 49 | 50 | render() { 51 | const { container, rowIndex, columnIndex } = this.props 52 | const { value, editing } = this.state 53 | 54 | return ( 55 | 56 | {!editing && value} 57 | {editing && this.targetRef && ( 58 | 66 | {({ props, placement }) => ( 67 |
78 | 84 |
85 | )} 86 |
87 | )} 88 |
89 | ) 90 | } 91 | } 92 | 93 | const columns = generateColumns(5) 94 | const data = generateData(columns, 100) 95 | 96 | columns[0].cellRenderer = EditableCell 97 | columns[0].width = 300 98 | 99 | export default () => ( 100 | <> 101 | 102 |
103 | 104 | ) 105 | -------------------------------------------------------------------------------- /website/src/examples/is-scrolling.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const Loading = styled.div` 5 | padding-left: 15px; 6 | color: gray; 7 | ` 8 | 9 | const rowRenderer = ({ isScrolling, cells }) => { 10 | if (isScrolling) return Scrolling 11 | return cells 12 | } 13 | 14 | export default () => ( 15 |
21 | ) 22 | -------------------------------------------------------------------------------- /website/src/examples/jsx-column.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | export default () => ( 5 |
6 | {columns.map(column => ( 7 | 8 | ))} 9 |
10 | ) 11 | -------------------------------------------------------------------------------- /website/src/examples/max-height.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 3) 3 | const frozenData = generateData(columns, 1, 'frozen-row-') 4 | 5 | const fixedColumns = columns.map((column, columnIndex) => { 6 | let frozen 7 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 8 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 9 | return { ...column, frozen } 10 | }) 11 | 12 | const expandColumnKey = 'column-0' 13 | 14 | // add some sub items 15 | for (let i = 0; i < 3; i++) { 16 | data.push({ 17 | ...data[0], 18 | id: `${data[0].id}-sub-${i}`, 19 | parentId: data[0].id, 20 | [expandColumnKey]: `Sub ${i}`, 21 | }) 22 | data.push({ 23 | ...data[2], 24 | id: `${data[2].id}-sub-${i}`, 25 | parentId: data[2].id, 26 | [expandColumnKey]: `Sub ${i}`, 27 | }) 28 | data.push({ 29 | ...data[2], 30 | id: `${data[2].id}-sub-sub-${i}`, 31 | parentId: `${data[2].id}-sub-${i}`, 32 | [expandColumnKey]: `Sub-Sub ${i}`, 33 | }) 34 | } 35 | 36 | const treeData = unflatten(data) 37 | 38 | export default () => ( 39 | 48 | ) 49 | -------------------------------------------------------------------------------- /website/src/examples/multi-header.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(15) 2 | const data = generateData(columns, 200) 3 | 4 | const GroupCell = styled.div` 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | height: 100%; 9 | 10 | &:not(:last-child) { 11 | border-right: 1px solid lightgray; 12 | } 13 | ` 14 | 15 | const fixedColumns = columns.map((column, columnIndex) => { 16 | let frozen 17 | if (columnIndex < 3) frozen = Column.FrozenDirection.LEFT 18 | if (columnIndex > 12) frozen = Column.FrozenDirection.RIGHT 19 | return { ...column, frozen, width: 100 } 20 | }) 21 | 22 | const headerRenderer = ({ cells, columns, headerIndex }) => { 23 | if (headerIndex === 2) return cells 24 | 25 | const groupCells = [] 26 | let width = 0 27 | let idx = 0 28 | 29 | columns.forEach((column, columnIndex) => { 30 | // if there are frozen columns, there will be some placeholders for the frozen cells 31 | if (column[Table.PlaceholderKey]) groupCells.push(cells[columnIndex]) 32 | else { 33 | width += cells[columnIndex].props.style.width 34 | idx++ 35 | 36 | const nextColumn = columns[columnIndex + 1] 37 | if ( 38 | columnIndex === columns.length - 1 || 39 | nextColumn[Table.PlaceholderKey] || 40 | idx === (headerIndex === 0 ? 4 : 2) 41 | ) { 42 | groupCells.push( 43 | 47 | Group width {width} 48 | 49 | ) 50 | width = 0 51 | idx = 0 52 | } 53 | } 54 | }) 55 | return groupCells 56 | } 57 | 58 | export default () => ( 59 |
66 | ) 67 | -------------------------------------------------------------------------------- /website/src/examples/multi-sort.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10, undefined, { sortable: true }) 2 | const data = generateData(columns, 200) 3 | 4 | const defaultSort = { 5 | 'column-0': SortOrder.ASC, 6 | 'column-1': SortOrder.DESC, 7 | 'column-2': SortOrder.ASC, 8 | } 9 | 10 | export default class App extends React.Component { 11 | state = { 12 | data, 13 | sortState: defaultSort, 14 | } 15 | 16 | onColumnSort = ({ key, order }) => { 17 | const { data, sortState } = this.state 18 | this.setState({ 19 | // clear the sort state if the previous order is desc 20 | sortState: { 21 | ...sortState, 22 | [key]: sortState[key] === SortOrder.DESC ? null : order, 23 | }, 24 | data: this.state.data.reverse(), 25 | }) 26 | } 27 | 28 | render() { 29 | return ( 30 |
37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /website/src/examples/overlay-renderer.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | const Overlay = styled.div` 12 | background: lightgray; 13 | position: absolute; 14 | top: 50%; 15 | left: 50%; 16 | transform: translateX(-50%) translateY(-50%); 17 | padding: 5px 15px; 18 | border-radius: 10px; 19 | color: white; 20 | ` 21 | 22 | export default () => ( 23 |
29 | Custom Overlay 30 | 31 | } 32 | /> 33 | ) 34 | -------------------------------------------------------------------------------- /website/src/examples/resizable.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | const frozenData = generateData(columns, 1, 'frozen-row-') 4 | 5 | const fixedColumns = columns.map((column, columnIndex) => { 6 | let frozen 7 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 8 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 9 | return { ...column, frozen, resizable: true, maxWidth: 300 } 10 | }) 11 | 12 | const expandColumnKey = 'column-0' 13 | 14 | // add some sub items 15 | for (let i = 0; i < 3; i++) { 16 | data.push({ 17 | ...data[0], 18 | id: `${data[0].id}-sub-${i}`, 19 | parentId: data[0].id, 20 | [expandColumnKey]: `Sub ${i}`, 21 | }) 22 | data.push({ 23 | ...data[2], 24 | id: `${data[2].id}-sub-${i}`, 25 | parentId: data[2].id, 26 | [expandColumnKey]: `Sub ${i}`, 27 | }) 28 | data.push({ 29 | ...data[2], 30 | id: `${data[2].id}-sub-sub-${i}`, 31 | parentId: `${data[2].id}-sub-${i}`, 32 | [expandColumnKey]: `Sub-Sub ${i}`, 33 | }) 34 | } 35 | 36 | const treeData = unflatten(data) 37 | 38 | export default () => ( 39 |
48 | ) 49 | -------------------------------------------------------------------------------- /website/src/examples/row-event-handlers.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | const frozenData = generateData(columns, 1, 'frozen-row-') 4 | 5 | const fixedColumns = columns.map((column, columnIndex) => { 6 | let frozen 7 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 8 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 9 | return { ...column, frozen } 10 | }) 11 | 12 | const rowEventHandlers = { 13 | onClick: action('click'), 14 | onDoubleClick: action('double click'), 15 | onContextMenu: action('context menu'), 16 | onMouseEnter: action('mouse enter'), 17 | onMouseLeave: action('mouse leave'), 18 | } 19 | 20 | export default () => ( 21 |
28 | ) 29 | -------------------------------------------------------------------------------- /website/src/examples/row-header-height.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | export default () => ( 5 |
6 | ) 7 | -------------------------------------------------------------------------------- /website/src/examples/row-key.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | const expandColumnKey = 'column-0' 12 | 13 | // add some sub items 14 | for (let i = 0; i < 3; i++) { 15 | data.push({ 16 | ...data[0], 17 | ['column-0']: `${data[0]['column-0']}-sub-${i}`, 18 | parentId: data[0]['column-0'], 19 | [expandColumnKey]: `Sub ${i}`, 20 | }) 21 | data.push({ 22 | ...data[2], 23 | ['column-0']: `${data[2]['column-0']}-sub-${i}`, 24 | parentId: data[2]['column-0'], 25 | [expandColumnKey]: `Sub ${i}`, 26 | }) 27 | data.push({ 28 | ...data[2], 29 | ['column-0']: `${data[2]['column-0']}-sub-sub-${i}`, 30 | parentId: `${data[2]['column-0']}-sub-${i}`, 31 | [expandColumnKey]: `Sub-Sub ${i}`, 32 | }) 33 | } 34 | 35 | const treeData = unflatten(data, null, 'column-0') 36 | 37 | export default () => ( 38 |
47 | ) 48 | -------------------------------------------------------------------------------- /website/src/examples/row-renderer.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const Title = styled.h4` 5 | font-size: 16px; 6 | color: #819099; 7 | margin-top: 20px; 8 | margin-bottom: 10px; 9 | ` 10 | 11 | const Row = styled.div` 12 | padding: 0 15px; 13 | ` 14 | 15 | const rowRenderer = ({ rowData, ...rest }) => ( 16 | {Object.values(rowData).join(' | ')} 17 | ) 18 | 19 | class RowComponent extends React.Component { 20 | render() { 21 | const { rowData, ...rest } = this.props 22 | return {Object.values(rowData).join(' | ')} 23 | } 24 | } 25 | 26 | export default () => ( 27 | 28 | Function as renderer 29 |
30 | Component as renderer 31 |
32 | Element as renderer 33 |
} /> 34 | 35 | ) 36 | -------------------------------------------------------------------------------- /website/src/examples/row-span.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10, undefined, { resizable: true }) 2 | const data = generateData(columns, 200) 3 | 4 | const colSpanIndex = 1 5 | columns[colSpanIndex].colSpan = ({ rowData, rowIndex }) => (rowIndex % 4) + 1 6 | columns[colSpanIndex].align = Column.Alignment.CENTER 7 | 8 | const rowSpanIndex = 0 9 | columns[rowSpanIndex].rowSpan = ({ rowData, rowIndex }) => 10 | rowIndex % 2 === 0 && rowIndex <= data.length - 2 ? 2 : 1 11 | 12 | const rowRenderer = ({ rowData, rowIndex, cells, columns }) => { 13 | const colSpan = columns[colSpanIndex].colSpan({ rowData, rowIndex }) 14 | if (colSpan > 1) { 15 | let width = cells[colSpanIndex].props.style.width 16 | for (let i = 1; i < colSpan; i++) { 17 | width += cells[colSpanIndex + i].props.style.width 18 | cells[colSpanIndex + i] = null 19 | } 20 | const style = { 21 | ...cells[colSpanIndex].props.style, 22 | width, 23 | backgroundColor: 'lightgray', 24 | } 25 | cells[colSpanIndex] = React.cloneElement(cells[colSpanIndex], { style }) 26 | } 27 | 28 | const rowSpan = columns[rowSpanIndex].rowSpan({ rowData, rowIndex }) 29 | if (rowSpan > 1) { 30 | const cell = cells[rowSpanIndex] 31 | const style = { 32 | ...cell.props.style, 33 | backgroundColor: 'darkgray', 34 | height: rowSpan * 50 - 1, 35 | alignSelf: 'flex-start', 36 | zIndex: 1, 37 | } 38 | cells[rowSpanIndex] = React.cloneElement(cell, { style }) 39 | } 40 | return cells 41 | } 42 | 43 | export default () => ( 44 |
51 | ) 52 | -------------------------------------------------------------------------------- /website/src/examples/scroll-to.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 500) 3 | 4 | const Button = styled.button` 5 | padding: 4px 8px; 6 | margin: 10px; 7 | ` 8 | 9 | export default class App extends React.Component { 10 | setRef = ref => (this.table = ref) 11 | 12 | render() { 13 | return ( 14 | <> 15 | 18 | 21 | 24 | 27 | 30 | 33 | 40 |
41 | 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /website/src/examples/scrollbar.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | const frozenData = generateData(columns, 3, 'frozen-row-') 4 | 5 | const fixedColumns = columns.map((column, columnIndex) => { 6 | let frozen 7 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 8 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 9 | return { ...column, frozen } 10 | }) 11 | 12 | const expandColumnKey = 'column-0' 13 | 14 | // add some sub items 15 | for (let i = 0; i < 3; i++) { 16 | data.push({ 17 | ...data[0], 18 | id: `${data[0].id}-sub-${i}`, 19 | parentId: data[0].id, 20 | [expandColumnKey]: `Sub ${i}`, 21 | }) 22 | data.push({ 23 | ...data[2], 24 | id: `${data[2].id}-sub-${i}`, 25 | parentId: data[2].id, 26 | [expandColumnKey]: `Sub ${i}`, 27 | }) 28 | data.push({ 29 | ...data[2], 30 | id: `${data[2].id}-sub-sub-${i}`, 31 | parentId: `${data[2].id}-sub-${i}`, 32 | [expandColumnKey]: `Sub-Sub ${i}`, 33 | }) 34 | } 35 | 36 | const treeData = unflatten(data) 37 | 38 | const StyledTable = styled(Table)` 39 | .BaseTable__table .BaseTable__body { 40 | ::-webkit-scrollbar { 41 | -webkit-appearance: none; 42 | background-color: #e3e3e3; 43 | } 44 | 45 | ::-webkit-scrollbar:vertical { 46 | width: 10px; 47 | } 48 | 49 | ::-webkit-scrollbar:horizontal { 50 | height: 10px; 51 | } 52 | 53 | ::-webkit-scrollbar-thumb { 54 | border-radius: 10px; 55 | border: 2px solid #e3e3e3; 56 | background-color: #999; 57 | 58 | &:hover { 59 | background-color: #666; 60 | } 61 | } 62 | 63 | ::-webkit-resizer { 64 | display: none; 65 | } 66 | } 67 | ` 68 | 69 | const Tip = styled.div` 70 | font-weight: 600; 71 | ` 72 | 73 | const getScrollbarSize = () => 10 74 | 75 | export default () => ( 76 | <> 77 | works only on WebKit based browser 78 | 86 | 87 | ) 88 | -------------------------------------------------------------------------------- /website/src/examples/selection.js: -------------------------------------------------------------------------------- 1 | const StyledTable = styled(BaseTable)` 2 | .row-selected { 3 | background-color: #e3e3e3; 4 | } 5 | ` 6 | 7 | class SelectionCell extends React.PureComponent { 8 | _handleChange = e => { 9 | const { rowData, rowIndex, column } = this.props 10 | const { onChange } = column 11 | 12 | onChange({ selected: e.target.checked, rowData, rowIndex }) 13 | } 14 | 15 | render() { 16 | const { rowData, column } = this.props 17 | const { selectedRowKeys, rowKey } = column 18 | const checked = selectedRowKeys.includes(rowData[rowKey]) 19 | 20 | return ( 21 | 22 | ) 23 | } 24 | } 25 | 26 | class SelectableTable extends React.PureComponent { 27 | constructor(props) { 28 | super(props) 29 | 30 | const { 31 | selectedRowKeys, 32 | defaultSelectedRowKeys, 33 | expandedRowKeys, 34 | defaultExpandedRowKeys, 35 | } = props 36 | this.state = { 37 | selectedRowKeys: 38 | (selectedRowKeys !== undefined 39 | ? selectedRowKeys 40 | : defaultSelectedRowKeys) || [], 41 | expandedRowKeys: 42 | (expandedRowKeys !== undefined 43 | ? expandedRowKeys 44 | : defaultExpandedRowKeys) || [], 45 | } 46 | } 47 | 48 | /** 49 | * Set `selectedRowKeys` manually. 50 | * This method is available only if `selectedRowKeys` is uncontrolled. 51 | * 52 | * @param {array} selectedRowKeys 53 | */ 54 | setSelectedRowKeys(selectedRowKeys) { 55 | // if `selectedRowKeys` is controlled 56 | if (this.props.selectedRowKeys !== undefined) return 57 | 58 | this.setState({ 59 | selectedRowKeys: cloneArray(selectedRowKeys), 60 | }) 61 | } 62 | 63 | /** 64 | * See BaseTable#setExpandedRowKeys 65 | */ 66 | setExpandedRowKeys(expandedRowKeys) { 67 | // if `expandedRowKeys` is controlled 68 | if (this.props.expandedRowKeys !== undefined) return 69 | 70 | this.setState({ 71 | expandedRowKeys: cloneArray(expandedRowKeys), 72 | }) 73 | } 74 | 75 | /* some other custom methods and proxy methods */ 76 | 77 | /** 78 | * Remove rowKeys from inner state manually, it's useful to purge dirty state after rows removed. 79 | * This method is available only if `selectedRowKeys` or `expandedRowKeys` is uncontrolled. 80 | * 81 | * @param {array} rowKeys 82 | */ 83 | removeRowKeysFromState(rowKeys) { 84 | if (!Array.isArray(rowKeys)) return 85 | 86 | const state = {} 87 | if ( 88 | this.props.selectedRowKeys === undefined && 89 | this.state.selectedRowKeys.length > 0 90 | ) { 91 | state.selectedRowKeys = this.state.selectedRowKeys.filter( 92 | key => !rowKeys.includes(key) 93 | ) 94 | } 95 | if ( 96 | this.props.expandedRowKeys === undefined && 97 | this.state.expandedRowKeys.length > 0 98 | ) { 99 | state.expandedRowKeys = this.state.expandedRowKeys.filter( 100 | key => !rowKeys.includes(key) 101 | ) 102 | } 103 | if (state.selectedRowKeys || state.expandedRowKeys) { 104 | this.setState(state) 105 | } 106 | } 107 | 108 | _handleSelectChange = ({ selected, rowData, rowIndex }) => { 109 | const selectedRowKeys = [...this.state.selectedRowKeys] 110 | const key = rowData[this.props.rowKey] 111 | 112 | if (selected) { 113 | if (!selectedRowKeys.includes(key)) selectedRowKeys.push(key) 114 | } else { 115 | const index = selectedRowKeys.indexOf(key) 116 | if (index > -1) { 117 | selectedRowKeys.splice(index, 1) 118 | } 119 | } 120 | 121 | // if `selectedRowKeys` is uncontrolled, update internal state 122 | if (this.props.selectedRowKeys === undefined) { 123 | this.setState({ selectedRowKeys }) 124 | } 125 | this.props.onRowSelect({ selected, rowData, rowIndex }) 126 | this.props.onSelectedRowsChange(selectedRowKeys) 127 | } 128 | 129 | _rowClassName = ({ rowData, rowIndex }) => { 130 | const { rowClassName, rowKey } = this.props 131 | const { selectedRowKeys } = this.state 132 | 133 | const rowClass = rowClassName 134 | ? callOrReturn(rowClassName, { rowData, rowIndex }) 135 | : '' 136 | const key = rowData[rowKey] 137 | 138 | return [rowClass, selectedRowKeys.includes(key) && 'row-selected'] 139 | .filter(Boolean) 140 | .concat(' ') 141 | } 142 | 143 | render() { 144 | const { 145 | columns, 146 | children, 147 | selectable, 148 | selectionColumnProps, 149 | ...rest 150 | } = this.props 151 | const { selectedRowKeys } = this.state 152 | 153 | // you'd better memoize this operation 154 | let _columns = columns || normalizeColumns(children) 155 | if (selectable) { 156 | const selectionColumn = { 157 | width: 40, 158 | flexShrink: 0, 159 | resizable: false, 160 | frozen: Column.FrozenDirection.LEFT, 161 | cellRenderer: SelectionCell, 162 | ...selectionColumnProps, 163 | key: '__selection__', 164 | rowKey: this.props.rowKey, 165 | selectedRowKeys: selectedRowKeys, 166 | onChange: this._handleSelectChange, 167 | } 168 | _columns = [selectionColumn, ..._columns] 169 | } 170 | 171 | return ( 172 | 177 | ) 178 | } 179 | } 180 | 181 | SelectableTable.defaultProps = { 182 | ...BaseTable.defaultProps, 183 | onRowSelect: noop, 184 | onSelectedRowsChange: noop, 185 | } 186 | 187 | // Use case 188 | const columns = generateColumns(10) 189 | const data = generateData(columns, 200) 190 | 191 | export default () => ( 192 | 201 | ) 202 | -------------------------------------------------------------------------------- /website/src/examples/sortable.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | for (let i = 0; i < 3; i++) columns[i].sortable = true 5 | 6 | const defaultSort = { key: 'column-0', order: SortOrder.ASC } 7 | 8 | export default class App extends React.Component { 9 | state = { 10 | data, 11 | sortBy: defaultSort, 12 | } 13 | 14 | onColumnSort = sortBy => { 15 | this.setState({ 16 | sortBy, 17 | data: this.state.data.reverse(), 18 | }) 19 | } 20 | 21 | render() { 22 | return ( 23 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /website/src/examples/sticky-rows.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const fixedColumns = columns.map((column, columnIndex) => { 5 | let frozen 6 | if (columnIndex < 2) frozen = Column.FrozenDirection.LEFT 7 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 8 | return { ...column, frozen } 9 | }) 10 | 11 | const GlobalStyle = createGlobalStyle` 12 | .sticky-row.BaseTable__row { 13 | background-color: #f3f3f3; 14 | } 15 | ` 16 | 17 | export default () => { 18 | const [stickyIndex, setStickyIndex] = React.useState(0) 19 | const tableData = data.slice(1) 20 | const frozenData = data.slice(stickyIndex, stickyIndex + 1) 21 | 22 | const rowClassName = React.useCallback(({ rowIndex }) => { 23 | // we sliced the original data by 1, so we have to correct the index back 24 | if (rowIndex < 0 || (rowIndex + 1) % 5 === 0) return 'sticky-row' 25 | }) 26 | const handleScroll = React.useCallback(({ scrollTop }) => { 27 | setStickyIndex(Math.floor(scrollTop / 250) * 5) 28 | }) 29 | return ( 30 | <> 31 | 32 |
40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /website/src/examples/tag-name.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | const move = keyframes` 5 | from { 6 | transform: translateX(-100%); 7 | } 8 | 9 | to { 10 | transform: translateX(100%); 11 | } 12 | ` 13 | 14 | const InlineLoader = styled.div` 15 | overflow: hidden; 16 | height: 100%; 17 | width: 100%; 18 | position: relative; 19 | background-color: #eee; 20 | 21 | &::after { 22 | content: ''; 23 | position: absolute; 24 | left: 0; 25 | right: 0; 26 | top: 0; 27 | bottom: 0; 28 | background-position: left top; 29 | background-repeat: no-repeat; 30 | background-image: linear-gradient(to right, transparent, #ccc, transparent); 31 | animation: ${move} 1.5s linear infinite; 32 | } 33 | ` 34 | 35 | const CellLoader = styled(InlineLoader)` 36 | height: 16px !important; 37 | ` 38 | 39 | const Cell = props => ( 40 |
41 | 42 |
43 | ) 44 | 45 | const RowLoader = styled(InlineLoader)` 46 | height: 16px !important; 47 | margin: 0 15px; 48 | ` 49 | 50 | const Row = props => ( 51 |
52 | 53 |
54 | ) 55 | 56 | const cellProps = ({ rowIndex, columnIndex }) => 57 | rowIndex % 3 === 1 && columnIndex > 0 && { tagName: Cell } 58 | 59 | const rowProps = ({ rowIndex }) => rowIndex % 3 === 2 && { tagName: Row } 60 | 61 | export default () => ( 62 |
68 | ) 69 | -------------------------------------------------------------------------------- /website/src/examples/tooltip-cell.js: -------------------------------------------------------------------------------- 1 | // import Text from 'react-texty' 2 | const Text = ReactTexty 3 | 4 | const TableCell = ({ className, cellData }) => ( 5 | {cellData} 6 | ) 7 | 8 | const TableHeaderCell = ({ className, column }) => ( 9 | {column.title} 10 | ) 11 | 12 | const columns = generateColumns(10) 13 | const data = generateData(columns, 200) 14 | columns[3].title = 'No tooltip' 15 | columns[3].minWidth = 150 16 | 17 | export default () => ( 18 |
23 | ) 24 | -------------------------------------------------------------------------------- /website/src/examples/width-height.js: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 200) 3 | 4 | export default () => ( 5 |
6 | ) 7 | -------------------------------------------------------------------------------- /website/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Page from 'components/Page' 4 | 5 | export default () => ( 6 | 7 |

PAGE NOT FOUND

8 |

You just hit a route that doesn't exist... the sadness.

9 |
10 | ) 11 | -------------------------------------------------------------------------------- /website/src/pages/api.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from '@reach/router' 3 | import { withPrefix } from 'gatsby-link' 4 | 5 | import Page from 'components/Page' 6 | 7 | const API = () => ( 8 | 9 | 14 | 15 | ) 16 | 17 | export default API 18 | -------------------------------------------------------------------------------- /website/src/pages/docs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from '@reach/router' 3 | import { withPrefix } from 'gatsby-link' 4 | 5 | import Page from 'components/Page' 6 | 7 | const Docs = () => ( 8 | 9 | 14 | 15 | ) 16 | 17 | export default Docs 18 | -------------------------------------------------------------------------------- /website/src/pages/examples.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from '@reach/router' 3 | import { withPrefix } from 'gatsby-link' 4 | 5 | import Page from 'components/Page' 6 | 7 | const Examples = () => ( 8 | 9 | 14 | 15 | ) 16 | 17 | export default Examples 18 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import styled from 'styled-components' 4 | 5 | import Page from 'components/Page' 6 | 7 | const Container = styled(Page).attrs({ full: true })` 8 | padding: 0; 9 | ` 10 | 11 | const Hero = styled.div` 12 | background-color: #182a3d; 13 | color: #fff; 14 | padding: 10rem; 15 | text-align: center; 16 | ` 17 | 18 | const Description = styled.p` 19 | color: #bcc9d1; 20 | max-width: 60rem; 21 | margin: 2rem auto 4rem; 22 | ` 23 | 24 | const Content = styled.div` 25 | margin: 0 auto; 26 | max-width: 96rem; 27 | padding: 2rem; 28 | position: relative; 29 | ` 30 | 31 | const StyledLink = styled(Link)` 32 | display: block; 33 | margin-top: 1rem; 34 | font-size: 1.8rem; 35 | font-weight: 500; 36 | ` 37 | 38 | const ExternalLink = StyledLink.withComponent('a') 39 | 40 | const StartLink = styled(Link)` 41 | background-color: #0696d7; 42 | color: #fff; 43 | padding: 0.5em 1em; 44 | border-radius: 0.2em; 45 | &:hover { 46 | background-color: #fff; 47 | } 48 | ` 49 | 50 | const ExampleLink = styled(StartLink)` 51 | background-color: transparent; 52 | &:hover { 53 | background-color: transparent; 54 | } 55 | ` 56 | 57 | export default () => ( 58 | 59 | 60 |

BaseTable

61 | 62 | BaseTable is a react table component to display large data set with high 63 | performance and flexibility 64 | 65 | Get Started 66 | View Examples 67 |
68 | 69 | Docs 70 | API 71 | Examples 72 | 77 | Github 78 | 79 | 80 |
81 | ) 82 | -------------------------------------------------------------------------------- /website/src/pages/playground/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import Page from 'components/Page' 5 | import Playground from 'components/Playground' 6 | 7 | const Container = styled(Page).attrs({ full: true })` 8 | max-width: 100%; 9 | height: 100vh; 10 | ` 11 | 12 | export default ({ location }) => ( 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /website/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import '~minireset.css'; 2 | @import '~react-texty/styles.css'; 3 | 4 | html { 5 | font-size: 62.5%; 6 | font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 7 | Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, 8 | Noto Color Emoji; 9 | } 10 | 11 | body { 12 | -webkit-font-smoothing: antialiased; 13 | /* Currently ems cause chrome bug misinterpreting rems on body element */ 14 | font-size: 1.6em; 15 | line-height: 1.6; 16 | color: #333; 17 | font-weight: 400; 18 | } 19 | 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | font-weight: 400; 27 | } 28 | 29 | h1 { 30 | font-size: 3rem; 31 | margin: 0 0 1.6rem; 32 | } 33 | 34 | h2 { 35 | font-size: 2.6rem; 36 | margin: 0 0 1.2rem; 37 | } 38 | 39 | h3 { 40 | font-size: 2.2rem; 41 | margin: 0 0 0.8rem; 42 | } 43 | 44 | h4 { 45 | font-size: 1.8rem; 46 | margin: 0 0 0.4rem; 47 | } 48 | 49 | h5, 50 | h6 { 51 | font-size: 1.6rem; 52 | margin: 0; 53 | } 54 | 55 | p { 56 | font-size: 1.6rem; 57 | margin: 0 0 1.2rem; 58 | } 59 | 60 | a { 61 | color: #0696d7; 62 | text-decoration: none; 63 | } 64 | 65 | a:hover, 66 | a:focus { 67 | color: #38abdf; 68 | } 69 | 70 | code, 71 | kbd, 72 | pre, 73 | samp { 74 | font-family: source-code-pro, Menlo, Monaco, Consolas, Courier New, monospace; 75 | } 76 | 77 | pre { 78 | font-size: 1.4rem; 79 | } 80 | 81 | :not(pre) > code { 82 | padding: 0.2em 0.4em; 83 | font-size: 85%; 84 | color: #c7254e; 85 | background-color: #f3f3f3; 86 | border-radius: 0.3em; 87 | } 88 | 89 | [data-texty-tooltip] { 90 | font-size: 1.2rem; 91 | padding: 0.2rem 0.8rem; 92 | } 93 | -------------------------------------------------------------------------------- /website/src/templates/api.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | 4 | import Page from 'components/Page' 5 | import Html from 'components/Html' 6 | import Props from 'components/Props' 7 | import Methods from 'components/Methods' 8 | import Pagination from 'components/Pagination' 9 | 10 | import siteConfig from 'siteConfig' 11 | 12 | const links = siteConfig.api.map(item => ({ 13 | key: item.title, 14 | title: item.title, 15 | to: item.path, 16 | })) 17 | 18 | class ApiTemplate extends React.Component { 19 | render() { 20 | const { data, pageContext, location } = this.props 21 | const metaData = data.componentMetadata 22 | const methods = metaData.childrenComponentMethodExt 23 | const name = pageContext.name 24 | const link = links.find(link => link.to === `/api/${name.toLowerCase()}`) 25 | 26 | return ( 27 | 32 |

{metaData.displayName}

33 | {metaData.description && ( 34 | 35 | )} 36 | 37 | {methods.length > 0 && } 38 | 39 |
40 | ) 41 | } 42 | } 43 | 44 | export default ApiTemplate 45 | 46 | export const pageQuery = graphql` 47 | query ApiByName($name: String!) { 48 | componentMetadata(displayName: { eq: $name }) { 49 | displayName 50 | description { 51 | childMarkdownRemark { 52 | htmlAst 53 | } 54 | } 55 | props { 56 | name 57 | type { 58 | name 59 | value 60 | raw 61 | } 62 | required 63 | description { 64 | childMarkdownRemark { 65 | htmlAst 66 | } 67 | } 68 | defaultValue { 69 | value 70 | } 71 | } 72 | childrenComponentMethodExt { 73 | name 74 | params { 75 | name 76 | type { 77 | name 78 | } 79 | } 80 | childMarkdownRemark { 81 | htmlAst 82 | } 83 | } 84 | } 85 | } 86 | ` 87 | -------------------------------------------------------------------------------- /website/src/templates/doc.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | 4 | import Html from 'components/Html' 5 | import Page from 'components/Page' 6 | import Pagination from 'components/Pagination' 7 | 8 | import siteConfig from 'siteConfig' 9 | 10 | const links = siteConfig.docs.map(item => ({ 11 | key: item.title, 12 | title: item.title, 13 | to: item.path, 14 | })) 15 | 16 | class DocumentTemplate extends React.Component { 17 | render() { 18 | const { data, pageContext, location } = this.props 19 | const doc = data.markdownRemark 20 | const link = links.find(link => link.to === pageContext.slug) 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | } 30 | 31 | export default DocumentTemplate 32 | 33 | export const pageQuery = graphql` 34 | query DocumentBySlug($slug: String!) { 35 | markdownRemark(fields: { slug: { eq: $slug } }) { 36 | htmlAst 37 | } 38 | } 39 | ` 40 | -------------------------------------------------------------------------------- /website/src/templates/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | 4 | import Page from 'components/Page' 5 | import CodePreview from 'components/CodePreview' 6 | import Pagination from 'components/Pagination' 7 | 8 | import siteConfig from 'siteConfig' 9 | 10 | const links = siteConfig.examples.map(item => ({ 11 | key: item.title, 12 | title: item.title, 13 | to: item.path, 14 | })) 15 | 16 | class ComponentTemplate extends React.Component { 17 | render() { 18 | const { data, pageContext, location } = this.props 19 | const code = data.rawCode.content 20 | const name = pageContext.name 21 | const link = links.find(link => link.to === `/examples/${name}`) 22 | return ( 23 | 24 |

{link.title}

25 | 26 | 27 |
28 | ) 29 | } 30 | } 31 | 32 | export default ComponentTemplate 33 | 34 | export const pageQuery = graphql` 35 | query ExampleByName($name: String!) { 36 | rawCode(name: { eq: $name }) { 37 | content 38 | } 39 | } 40 | ` 41 | -------------------------------------------------------------------------------- /website/src/utils/actionChannel.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from './eventEmitter' 2 | 3 | export const actionEmitter = new EventEmitter() 4 | 5 | export default class ActionChannel { 6 | constructor(channel) { 7 | if (!channel) throw new Error('"channel" is required for ActionChannel') 8 | this.channel = channel 9 | } 10 | 11 | on(handler) { 12 | actionEmitter.on(this.channel, handler) 13 | } 14 | 15 | off(handler) { 16 | actionEmitter.off(this.channel, handler) 17 | } 18 | 19 | emit(event) { 20 | actionEmitter.emit(this.channel, event) 21 | } 22 | } 23 | 24 | export const createAction = channel => { 25 | return name => args => actionEmitter.emit(channel, { name, args }) 26 | } 27 | 28 | let sequence = 0 29 | export const createActionChannel = name => { 30 | const channel = name || `${Date.now()}-${sequence++}` 31 | return { 32 | action: createAction(channel), 33 | channel: new ActionChannel(channel), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /website/src/utils/baseScope.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import styled, { css, keyframes, createGlobalStyle } from 'styled-components' 4 | import * as ReactSortableHoc from 'react-sortable-hoc' 5 | import * as ReactOverlays from 'react-overlays' 6 | import ReactTexty from 'react-texty' 7 | import faker from 'faker'; 8 | 9 | import BaseTable, { 10 | Column, 11 | SortOrder, 12 | AutoResizer, 13 | normalizeColumns, 14 | callOrReturn, 15 | unflatten, 16 | TableHeader as BaseTableHeader, 17 | TableRow as BaseTableRow, 18 | } from 'react-base-table' 19 | import BaseTableExpandIcon from 'react-base-table/ExpandIcon' 20 | 21 | const generateColumns = (count = 10, prefix = 'column-', props) => 22 | new Array(count).fill(0).map((column, columnIndex) => ({ 23 | ...props, 24 | key: `${prefix}${columnIndex}`, 25 | dataKey: `${prefix}${columnIndex}`, 26 | title: `Column ${columnIndex}`, 27 | width: 150, 28 | })) 29 | 30 | const generateData = (columns, count = 200, prefix = 'row-') => 31 | new Array(count).fill(0).map((row, rowIndex) => { 32 | return columns.reduce( 33 | (rowData, column, columnIndex) => { 34 | rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}` 35 | return rowData 36 | }, 37 | { 38 | id: `${prefix}${rowIndex}`, 39 | parentId: null, 40 | } 41 | ) 42 | }) 43 | 44 | const noop = () => {} 45 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) 46 | const action = message => args => console.log(message, args) 47 | 48 | const Table = React.forwardRef((props, ref) => ( 49 | 50 | )) 51 | Table.Column = Column 52 | Table.PlaceholderKey = BaseTable.PlaceholderKey 53 | 54 | export default { 55 | React, 56 | ReactDOM, 57 | 58 | styled, 59 | css, 60 | keyframes, 61 | createGlobalStyle, 62 | 63 | ReactSortableHoc, 64 | ReactOverlays, 65 | ReactTexty, 66 | faker, 67 | 68 | BaseTable, 69 | Column, 70 | SortOrder, 71 | AutoResizer, 72 | normalizeColumns, 73 | callOrReturn, 74 | unflatten, 75 | BaseTableRow, 76 | BaseTableHeader, 77 | BaseTableExpandIcon, 78 | 79 | generateColumns, 80 | generateData, 81 | noop, 82 | delay, 83 | action, 84 | Table, 85 | } 86 | -------------------------------------------------------------------------------- /website/src/utils/eventEmitter.js: -------------------------------------------------------------------------------- 1 | export default class EventEmitter { 2 | handlersMap = {} 3 | 4 | on(type, handler) { 5 | const handlers = this.handlersMap[type] 6 | if (!handlers) this.handlersMap[type] = [handler] 7 | else handlers.push(handler) 8 | } 9 | 10 | off(type, handler) { 11 | const handlers = this.handlersMap[type] || [] 12 | const index = handlers.indexOf(handler) 13 | if (index >= 0) handlers.splice(index, 1) 14 | } 15 | 16 | emit(type, event) { 17 | const handlers = this.handlersMap[type] || [] 18 | handlers.forEach(handler => handler(event)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /website/src/utils/sample.code: -------------------------------------------------------------------------------- 1 | const columns = generateColumns(10) 2 | const data = generateData(columns, 2000) 3 | const frozenData = generateData(columns, 1, 'frozen-row-') 4 | 5 | const fixedColumns = columns.map((column, columnIndex) => { 6 | let frozen 7 | if (columnIndex < 1) frozen = Column.FrozenDirection.LEFT 8 | if (columnIndex > 8) frozen = Column.FrozenDirection.RIGHT 9 | return { ...column, frozen } 10 | }) 11 | 12 | const expandColumnKey = 'column-0' 13 | 14 | // add some sub items 15 | for (let i = 0; i < 100; i++) { 16 | data.push({ 17 | ...data[0], 18 | id: `${data[0].id}-sub-${i}`, 19 | parentId: data[0].id, 20 | [expandColumnKey]: `Sub ${i}`, 21 | }) 22 | data.push({ 23 | ...data[2], 24 | id: `${data[2].id}-sub-${i}`, 25 | parentId: data[2].id, 26 | [expandColumnKey]: `Sub ${i}`, 27 | }) 28 | data.push({ 29 | ...data[2], 30 | id: `${data[2].id}-sub-sub-${i}`, 31 | parentId: `${data[2].id}-sub-${i}`, 32 | [expandColumnKey]: `Sub-Sub ${i}`, 33 | }) 34 | } 35 | 36 | const treeData = unflatten(data) 37 | 38 | export default () => ( 39 |
46 | ) 47 | -------------------------------------------------------------------------------- /website/src/utils/urlHash.js: -------------------------------------------------------------------------------- 1 | import LZString from 'lz-string' 2 | 3 | // eslint-disable-next-line 4 | const sampleCode = require('!raw-loader!./sample.code') 5 | 6 | export const getCode = () => { 7 | if (typeof document === 'undefined') return sampleCode 8 | 9 | const hash = document.location.hash.slice(1) 10 | if (!hash) return sampleCode 11 | 12 | return ( 13 | LZString.decompressFromEncodedURIComponent(hash) || decodeURIComponent(hash) 14 | ) 15 | } 16 | 17 | export const replaceState = code => { 18 | const hash = code ? LZString.compressToEncodedURIComponent(code) : '' 19 | 20 | if ( 21 | typeof URL === 'function' && 22 | typeof window.history === 'object' && 23 | typeof window.history.replaceState === 'function' 24 | ) { 25 | const url = new URL(document.location) 26 | url.hash = hash 27 | window.history.replaceState(null, null, url) 28 | } else { 29 | document.location.hash = hash 30 | } 31 | } 32 | --------------------------------------------------------------------------------