├── .prettierignore ├── demo ├── dist │ ├── favicon.ico │ └── index.html ├── src │ ├── components │ │ ├── index.js │ │ ├── cells │ │ │ ├── UsernameEditorCell.jsx │ │ │ ├── index.js │ │ │ ├── GenderEditorCell.jsx │ │ │ ├── ButtonsCell.jsx │ │ │ ├── UsernameCell.jsx │ │ │ └── ButtonsEditorCell.jsx │ │ ├── ControllerWrappper.jsx │ │ ├── ColumnsControllers.jsx │ │ ├── TableControllers.jsx │ │ └── ControllersDrawer.jsx │ ├── index.css │ ├── getColumns.js │ └── views │ │ ├── sync.js │ │ ├── async.js │ │ ├── asyncControlled.js │ │ └── asyncManaged.js └── webpack.config.js ├── src ├── defaults │ ├── index.js │ ├── texts.js │ └── icons.js ├── utils │ ├── index.js │ ├── uuid.js │ └── getHighlightedText.jsx ├── drag-and-drop │ ├── SortableContainer │ │ ├── defaultGetHelperDimensions.js │ │ ├── defaultShouldCancelStart.js │ │ └── props.js │ ├── index.js │ ├── SortableHandle │ │ └── index.js │ ├── Manager │ │ └── index.js │ ├── SortableElement │ │ └── index.js │ ├── AutoScroller │ │ └── index.js │ └── utils.js ├── components │ ├── Loader.jsx │ ├── NoResults.jsx │ ├── PlaceHolderCell.jsx │ ├── Cell.jsx │ ├── HeaderCell.jsx │ ├── SelectionCell.jsx │ ├── HeaderSelectionCell.jsx │ ├── Search.jsx │ ├── EditorCell.jsx │ ├── index.js │ ├── PageSize.jsx │ ├── Header.jsx │ ├── PopoverButton.jsx │ ├── Footer.jsx │ ├── ColumnVisibility.jsx │ ├── Information.jsx │ ├── Pagination.jsx │ ├── Row.jsx │ ├── CellContainer.jsx │ └── HeaderCellContainer.jsx ├── hooks │ ├── useColumnsVisibility.jsx │ ├── useRequestDebounce.jsx │ ├── useDetectClickOutside.jsx │ ├── useRowVirtualizer.jsx │ ├── index.js │ ├── useRows.jsx │ ├── useColumnsReorder.jsx │ ├── useResizeEvents.jsx │ ├── useRowEdit.jsx │ ├── useSearch.jsx │ ├── useSort.jsx │ ├── usePagination.jsx │ ├── useTableManager.jsx │ ├── useColumns.jsx │ ├── useColumnsResize.jsx │ ├── useRowSelection.jsx │ └── useAsync.jsx ├── index.js └── index.css ├── .gitignore ├── .eslintrc.json ├── LICENSE ├── webpack.config.js └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /demo/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NadavShaar/react-grid-table/HEAD/demo/dist/favicon.ico -------------------------------------------------------------------------------- /src/defaults/index.js: -------------------------------------------------------------------------------- 1 | import icons from "./icons"; 2 | import texts from "./texts"; 3 | 4 | export { icons, texts }; 5 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import getHighlightedText from "./getHighlightedText"; 2 | import uuid from "./uuid"; 3 | 4 | export { getHighlightedText, uuid }; 5 | -------------------------------------------------------------------------------- /demo/src/components/index.js: -------------------------------------------------------------------------------- 1 | import ControllerWrappper from "./ControllerWrappper"; 2 | import ControllersDrawer from "./ControllersDrawer"; 3 | 4 | export { ControllerWrappper, ControllersDrawer }; 5 | -------------------------------------------------------------------------------- /src/drag-and-drop/SortableContainer/defaultGetHelperDimensions.js: -------------------------------------------------------------------------------- 1 | export default function defaultGetHelperDimensions({ node }) { 2 | return { 3 | height: node.offsetHeight, 4 | width: node.offsetWidth, 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /demo/src/components/cells/UsernameEditorCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { UsernameCell } from "./"; 3 | 4 | const UsernameEditorCell = (props) => ; 5 | 6 | export default UsernameEditorCell; 7 | -------------------------------------------------------------------------------- /src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | const Loader = ({ tableManager }) => { 2 | let { 3 | config: { 4 | icons: { loader: loaderIcon }, 5 | }, 6 | } = tableManager; 7 | 8 | return loaderIcon; 9 | }; 10 | 11 | export default Loader; 12 | -------------------------------------------------------------------------------- /src/components/NoResults.jsx: -------------------------------------------------------------------------------- 1 | const NoResults = ({ tableManager }) => { 2 | let { 3 | config: { 4 | texts: { noResults }, 5 | }, 6 | } = tableManager; 7 | 8 | return noResults; 9 | }; 10 | 11 | export default NoResults; 12 | -------------------------------------------------------------------------------- /src/utils/uuid.js: -------------------------------------------------------------------------------- 1 | const uuid = () => { 2 | return ( 3 | "rgt-" + 4 | ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (a) => 5 | (a ^ ((Math.random() * 16) >> (a / 4))).toString(16) 6 | ) 7 | ); 8 | }; 9 | 10 | export default uuid; 11 | -------------------------------------------------------------------------------- /demo/src/components/cells/index.js: -------------------------------------------------------------------------------- 1 | export { default as UsernameCell } from "./UsernameCell"; 2 | export { default as UsernameEditorCell } from "./UsernameEditorCell"; 3 | export { default as GenderEditorCell } from "./GenderEditorCell"; 4 | export { default as ButtonsCell } from "./ButtonsCell"; 5 | export { default as ButtonsEditorCell } from "./ButtonsEditorCell"; 6 | -------------------------------------------------------------------------------- /src/defaults/texts.js: -------------------------------------------------------------------------------- 1 | export default { 2 | search: "Search:", 3 | totalRows: "Total rows:", 4 | rows: "Rows:", 5 | selected: "Selected", 6 | rowsPerPage: "Rows per page:", 7 | page: "Page:", 8 | of: "of", 9 | prev: "Prev", 10 | next: "Next", 11 | columnVisibility: "Column visibility", 12 | noResults: "No Results found", 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | /dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | debug.log* 24 | 25 | package-lock.json -------------------------------------------------------------------------------- /demo/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react-grid-table Demo 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/drag-and-drop/index.js: -------------------------------------------------------------------------------- 1 | export { default as SortableContainer } from "./SortableContainer"; 2 | export { default as SortableElement } from "./SortableElement"; 3 | export { default as SortableHandle } from "./SortableHandle"; 4 | 5 | export { default as sortableContainer } from "./SortableContainer"; 6 | export { default as sortableElement } from "./SortableElement"; 7 | export { default as sortableHandle } from "./SortableHandle"; 8 | 9 | export { arrayMove } from "./utils"; 10 | -------------------------------------------------------------------------------- /src/components/PlaceHolderCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PlaceHolderCell = ({ tableManager }) => { 4 | const { 5 | config: { 6 | additionalProps: { placeHolderCell: additionalProps = {} }, 7 | }, 8 | } = tableManager; 9 | 10 | let classNames = ( 11 | "rgt-placeholder-cell " + (additionalProps.className || "") 12 | ).trim(); 13 | 14 | return ; 15 | }; 16 | 17 | export default PlaceHolderCell; 18 | -------------------------------------------------------------------------------- /demo/src/components/cells/GenderEditorCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const styles = { 4 | select: { margin: "0 20px" }, 5 | }; 6 | 7 | const GenderEditorCell = ({ value, data, column, onChange }) => ( 8 | 16 | ); 17 | 18 | export default GenderEditorCell; 19 | -------------------------------------------------------------------------------- /src/components/Cell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Cell = ({ value, textValue, tableManager }) => { 4 | const { 5 | config: { 6 | additionalProps: { cell: additionalProps = {} }, 7 | }, 8 | } = tableManager; 9 | 10 | const classNames = ( 11 | "rgt-cell-inner rgt-text-truncate " + (additionalProps.className || "") 12 | ).trim(); 13 | 14 | return ( 15 |
16 | {value} 17 |
18 | ); 19 | }; 20 | 21 | export default Cell; 22 | -------------------------------------------------------------------------------- /src/components/HeaderCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const HeaderCell = ({ column, tableManager }) => { 4 | const { 5 | config: { 6 | additionalProps: { headerCell: additionalProps = {} }, 7 | }, 8 | } = tableManager; 9 | 10 | let classNames = ( 11 | "rgt-text-truncate " + (additionalProps.className || "") 12 | ).trim(); 13 | 14 | return ( 15 | 20 | {column.label} 21 | 22 | ); 23 | }; 24 | 25 | export default HeaderCell; 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended", "plugin:react-hooks/recommended"], 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": ["react", "prettier"], 16 | "rules": { 17 | "prettier/prettier": ["error", { "endOfLine": "auto", "tabWidth": 4 }], 18 | "react/prop-types": 0, 19 | "react-hooks/rules-of-hooks": "error", 20 | "react-hooks/exhaustive-deps": "warn" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useColumnsVisibility.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | const useColumnsVisibility = (props, tableManager) => { 4 | const { 5 | columnsApi: { columns, setColumns }, 6 | } = tableManager; 7 | 8 | const columnsVisibilityApi = useRef({}).current; 9 | 10 | columnsVisibilityApi.toggleColumnVisibility = (columnId) => { 11 | const newColumns = [...columns]; 12 | const colIndex = newColumns.findIndex( 13 | (column) => column.id === columnId 14 | ); 15 | 16 | newColumns[colIndex].visible = !newColumns[colIndex].visible; 17 | setColumns(newColumns); 18 | }; 19 | 20 | return columnsVisibilityApi; 21 | }; 22 | 23 | export default useColumnsVisibility; 24 | -------------------------------------------------------------------------------- /src/drag-and-drop/SortableContainer/defaultShouldCancelStart.js: -------------------------------------------------------------------------------- 1 | import { NodeType, closest } from "../utils"; 2 | 3 | export default function defaultShouldCancelStart(event) { 4 | // Cancel sorting if the event target is an `input`, `textarea`, `select` or `option` 5 | const interactiveElements = [ 6 | NodeType.Input, 7 | NodeType.Textarea, 8 | NodeType.Select, 9 | NodeType.Option, 10 | NodeType.Button, 11 | ]; 12 | 13 | if (interactiveElements.indexOf(event.target.tagName) !== -1) { 14 | // Return true to cancel sorting 15 | return true; 16 | } 17 | 18 | if (closest(event.target, (el) => el.contentEditable === "true")) { 19 | return true; 20 | } 21 | 22 | return false; 23 | } 24 | -------------------------------------------------------------------------------- /src/hooks/useRequestDebounce.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | const useRequestDebounce = (callback, wait) => { 4 | const params = useRef({ timeout: null, lastData: {} }).current; 5 | 6 | params.wait = wait; 7 | 8 | return function () { 9 | if ( 10 | arguments[0].from === params.lastData.from && 11 | arguments[0].to === params.lastData.to 12 | ) 13 | return; 14 | 15 | params.lastData = arguments[0]; 16 | 17 | clearTimeout(params.timeout); 18 | params.timeout = setTimeout(() => { 19 | params.timeout = null; 20 | callback(...arguments); 21 | params.lastData = {}; 22 | }, params.wait); 23 | }; 24 | }; 25 | 26 | export default useRequestDebounce; 27 | -------------------------------------------------------------------------------- /demo/src/components/ControllerWrappper.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const styles = { 4 | wrapper: { 5 | display: "flex", 6 | justifyContent: "space-between", 7 | padding: "7px 0", 8 | alignItems: "center", 9 | }, 10 | label: { 11 | whiteSpace: "nowrap", 12 | overflow: "hidden", 13 | textOverflow: "ellipsis", 14 | }, 15 | children: { 16 | display: "flex", 17 | flexDirection: "column", 18 | }, 19 | }; 20 | 21 | const ControllerWrappper = ({ label, children }) => { 22 | return ( 23 |
24 | {label}: 25 |
{children}
26 |
27 | ); 28 | }; 29 | 30 | export default ControllerWrappper; 31 | -------------------------------------------------------------------------------- /src/components/SelectionCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SelectionCell = ({ value, disabled, onChange, tableManager }) => { 4 | const { 5 | config: { 6 | additionalProps: { selectionCell: additionalProps = {} }, 7 | }, 8 | } = tableManager; 9 | 10 | let classNames = `${disabled ? "rgt-disabled" : "rgt-clickable"} ${ 11 | additionalProps.className || "" 12 | }`.trim(); 13 | 14 | return ( 15 | event.stopPropagation()} 21 | checked={value} 22 | disabled={disabled} 23 | /> 24 | ); 25 | }; 26 | 27 | export default SelectionCell; 28 | -------------------------------------------------------------------------------- /src/hooks/useDetectClickOutside.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | 3 | const useDetectClickOutside = (initialIsVisible) => { 4 | const [isComponentVisible, setIsComponentVisible] = 5 | useState(initialIsVisible); 6 | const ref = useRef(null); 7 | 8 | useEffect(() => { 9 | const handleClickOutside = (event) => { 10 | if (ref.current && !ref.current.contains(event.target)) { 11 | setIsComponentVisible(false); 12 | } 13 | }; 14 | 15 | document.addEventListener("click", handleClickOutside, true); 16 | 17 | return () => 18 | document.removeEventListener("click", handleClickOutside, true); 19 | }, []); 20 | 21 | return { ref, isComponentVisible, setIsComponentVisible }; 22 | }; 23 | 24 | export default useDetectClickOutside; 25 | -------------------------------------------------------------------------------- /src/drag-and-drop/SortableHandle/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { provideDisplayName } from "../utils"; 4 | 5 | export default function sortableHandle(WrappedComponent) { 6 | return class WithSortableHandle extends React.Component { 7 | static displayName = provideDisplayName( 8 | "sortableHandle", 9 | WrappedComponent 10 | ); 11 | 12 | componentDidMount() { 13 | this.wrappedInstance.current.sortableHandle = true; 14 | } 15 | 16 | getWrappedInstance() { 17 | return this.wrappedInstance.current; 18 | } 19 | 20 | wrappedInstance = React.createRef(); 21 | 22 | render() { 23 | return ( 24 | 25 | ); 26 | } 27 | }; 28 | } 29 | 30 | export function isSortableHandle(node) { 31 | return node.sortableHandle != null; 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/useRowVirtualizer.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { useVirtual } from "react-virtual"; 3 | 4 | const useRowVirtualizer = (props, tableManager) => { 5 | const { 6 | config: { 7 | isPaginated, 8 | isVirtualScroll, 9 | additionalProps: { rowVirtualizer: rowVirtualizerProps }, 10 | }, 11 | refs: { tableRef }, 12 | paginationApi: { page, pageSize, totalPages }, 13 | rowsApi: { totalRows }, 14 | } = tableManager; 15 | 16 | const rowVirtualizer = useRef({}).current; 17 | 18 | const useVirtualProps = { 19 | size: isPaginated 20 | ? totalPages === page 21 | ? totalRows - (totalPages - 1) * pageSize 22 | : pageSize 23 | : totalRows, 24 | overscan: 20, 25 | parentRef: isVirtualScroll ? tableRef : {}, 26 | ...rowVirtualizerProps, 27 | }; 28 | 29 | Object.assign(rowVirtualizer, useVirtual(useVirtualProps)); 30 | 31 | return rowVirtualizer; 32 | }; 33 | 34 | export default useRowVirtualizer; 35 | -------------------------------------------------------------------------------- /src/components/HeaderSelectionCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const HeaderSelectionCell = ({ 4 | tableManager, 5 | ref = tableManager.rowSelectionApi.selectAll.ref, 6 | onChange = tableManager.rowSelectionApi.selectAll.onChange, 7 | checked = tableManager.rowSelectionApi.selectAll.checked, 8 | disabled = tableManager.rowSelectionApi.selectAll.disabled, 9 | }) => { 10 | const { 11 | config: { 12 | additionalProps: { headerSelectionCell: additionalProps = {} }, 13 | }, 14 | } = tableManager; 15 | 16 | let classNames = ( 17 | disabled 18 | ? "rgt-disabled" 19 | : "rgt-clickable" + " " + additionalProps.className || "" 20 | ).trim(); 21 | 22 | return ( 23 | 32 | ); 33 | }; 34 | 35 | export default HeaderSelectionCell; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nadav Shaar 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 | -------------------------------------------------------------------------------- /src/components/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Search = ({ 4 | tableManager, 5 | value = tableManager.searchApi.searchText, 6 | onChange = tableManager.searchApi.setSearchText, 7 | }) => { 8 | const { 9 | config: { 10 | texts: { search: searchText }, 11 | icons: { search: searchIcon }, 12 | additionalProps: { search: additionalProps = {} }, 13 | }, 14 | } = tableManager; 15 | 16 | let classNames = ( 17 | "rgt-search-container " + (additionalProps.className || "") 18 | ).trim(); 19 | 20 | return ( 21 |
22 | 26 | onChange(event.target.value)} 31 | className="rgt-search-input" 32 | /> 33 |
34 | ); 35 | }; 36 | 37 | export default Search; 38 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | import useDetectClickOutside from "./useDetectClickOutside"; 2 | import useResizeEvents from "./useResizeEvents"; 3 | import useTableManager from "./useTableManager"; 4 | import useRowVirtualizer from "./useRowVirtualizer"; 5 | import useColumns from "./useColumns"; 6 | import useSort from "./useSort"; 7 | import useSearch from "./useSearch"; 8 | import usePagination from "./usePagination"; 9 | import useRowSelection from "./useRowSelection"; 10 | import useRowEdit from "./useRowEdit"; 11 | import useRows from "./useRows"; 12 | import useAsync from "./useAsync"; 13 | import useColumnsReorder from "./useColumnsReorder"; 14 | import useColumnsVisibility from "./useColumnsVisibility"; 15 | import useColumnsResize from "./useColumnsResize"; 16 | import useRequestDebounce from "./useRequestDebounce"; 17 | 18 | export { 19 | useDetectClickOutside, 20 | useResizeEvents, 21 | useTableManager, 22 | useRowVirtualizer, 23 | useColumns, 24 | useSort, 25 | useSearch, 26 | usePagination, 27 | useRowSelection, 28 | useRowEdit, 29 | useRows, 30 | useAsync, 31 | useColumnsReorder, 32 | useColumnsVisibility, 33 | useColumnsResize, 34 | useRequestDebounce, 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/EditorCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const EditorCell = ({ 4 | tableManager, 5 | value, 6 | data, 7 | column, 8 | onChange, 9 | isFirstEditableCell, 10 | }) => { 11 | const { 12 | config: { 13 | additionalProps: { editorCell: additionalProps = {} }, 14 | }, 15 | } = tableManager; 16 | 17 | const classNames = ( 18 | "rgt-cell-inner rgt-cell-editor " + (additionalProps.className || "") 19 | ).trim(); 20 | 21 | return ( 22 |
23 |
24 | 31 | onChange({ 32 | ...data, 33 | [column.field]: event.target.value, 34 | }) 35 | } 36 | /> 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default EditorCell; 43 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import CellContainer from "./CellContainer"; 2 | import HeaderCellContainer from "./HeaderCellContainer"; 3 | import Cell from "./Cell"; 4 | import EditorCell from "./EditorCell"; 5 | import SelectionCell from "./SelectionCell"; 6 | import ColumnVisibility from "./ColumnVisibility"; 7 | import Footer from "./Footer"; 8 | import Header from "./Header"; 9 | import HeaderCell from "./HeaderCell"; 10 | import HeaderSelectionCell from "./HeaderSelectionCell"; 11 | import PlaceHolderCell from "./PlaceHolderCell"; 12 | import Loader from "./Loader"; 13 | import NoResults from "./NoResults"; 14 | import PopoverButton from "./PopoverButton"; 15 | import Row from "./Row"; 16 | import Search from "./Search"; 17 | import Information from "./Information"; 18 | import PageSize from "./PageSize"; 19 | import Pagination from "./Pagination"; 20 | 21 | export { 22 | CellContainer, 23 | HeaderCellContainer, 24 | Cell, 25 | EditorCell, 26 | SelectionCell, 27 | ColumnVisibility, 28 | Footer, 29 | Header, 30 | HeaderCell, 31 | HeaderSelectionCell, 32 | PlaceHolderCell, 33 | Loader, 34 | NoResults, 35 | PopoverButton, 36 | Row, 37 | Search, 38 | Information, 39 | PageSize, 40 | Pagination, 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/PageSize.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PageSize = ({ 4 | tableManager, 5 | value = tableManager.paginationApi.pageSize, 6 | onChange = tableManager.paginationApi.setPageSize, 7 | options = tableManager.config.pageSizes, 8 | }) => { 9 | const { 10 | config: { 11 | texts: { rowsPerPage: rowsPerPageText }, 12 | additionalProps: { pageSize: additionalProps = {} }, 13 | }, 14 | } = tableManager; 15 | 16 | let classNames = ( 17 | "rgt-footer-page-size " + (additionalProps.className || "") 18 | ).trim(); 19 | 20 | return ( 21 |
22 | {rowsPerPageText} 23 | 36 |
37 | ); 38 | }; 39 | 40 | export default PageSize; 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 3 | module.exports = { 4 | entry: { 5 | index: "./src/index.js", 6 | }, 7 | output: { 8 | library: 'GridTable', 9 | libraryTarget: 'umd', 10 | path: __dirname + '/dist', 11 | filename: '[name].js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-env', '@babel/preset-react'], 21 | plugins: ['@babel/plugin-proposal-object-rest-spread'] 22 | } 23 | }, 24 | exclude: /node_modules/ 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: ["style-loader", "css-loader"] 29 | } 30 | ] 31 | }, 32 | plugins: [ 33 | new CleanWebpackPlugin() 34 | ], 35 | resolve: { 36 | extensions: [".js", ".jsx"] 37 | }, 38 | externals: { 39 | 'react': 'react', 40 | 'react-dom': 'react-dom', 41 | 'prop-types': 'prop-types' 42 | } 43 | } -------------------------------------------------------------------------------- /src/drag-and-drop/Manager/index.js: -------------------------------------------------------------------------------- 1 | export default class Manager { 2 | refs = {}; 3 | 4 | add(collection, ref) { 5 | if (!this.refs[collection]) { 6 | this.refs[collection] = []; 7 | } 8 | 9 | this.refs[collection].push(ref); 10 | } 11 | 12 | remove(collection, ref) { 13 | const index = this.getIndex(collection, ref); 14 | 15 | if (index !== -1) { 16 | this.refs[collection].splice(index, 1); 17 | } 18 | } 19 | 20 | isActive() { 21 | return this.active; 22 | } 23 | 24 | getActive() { 25 | return this.refs[this.active.collection].find( 26 | // eslint-disable-next-line eqeqeq 27 | ({ node }) => node.sortableInfo.index == this.active.index 28 | ); 29 | } 30 | 31 | getIndex(collection, ref) { 32 | return this.refs[collection].indexOf(ref); 33 | } 34 | 35 | getOrderedRefs(collection = this.active.collection) { 36 | return this.refs[collection].sort(sortByIndex); 37 | } 38 | } 39 | 40 | function sortByIndex( 41 | { 42 | node: { 43 | sortableInfo: { index: index1 }, 44 | }, 45 | }, 46 | { 47 | node: { 48 | sortableInfo: { index: index2 }, 49 | }, 50 | } 51 | ) { 52 | return index1 - index2; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Header = ({ tableManager }) => { 4 | const { 5 | config: { 6 | showColumnVisibilityManager, 7 | components: { ColumnVisibility, Search }, 8 | additionalProps: { header: additionalProps = {} }, 9 | showSearch, 10 | }, 11 | columnsApi: { columns }, 12 | columnsVisibilityApi: { toggleColumnVisibility }, 13 | searchApi: { setSearchText, searchText }, 14 | } = tableManager; 15 | 16 | const classNames = ( 17 | "rgt-header-container " + (additionalProps.className || "") 18 | ).trim(); 19 | 20 | return ( 21 |
22 | {showSearch !== false ? ( 23 | 28 | ) : ( 29 | 30 | )} 31 | {showColumnVisibilityManager !== false ? ( 32 | 37 | ) : ( 38 | 39 | )} 40 |
41 | ); 42 | }; 43 | 44 | export default Header; 45 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 3 | const target = process.env.TARGET || 'sync'; 4 | module.exports = { 5 | entry: { 6 | bundle: __dirname + `/src/views/${target}.js`, 7 | }, 8 | mode: 'development', 9 | devtool: 'source-map', 10 | output: { 11 | path: __dirname + '/dist/build', 12 | filename: '[name].js' 13 | }, 14 | devServer: { 15 | publicPath: path.join(__dirname, '/dist'), 16 | contentBase: path.join(__dirname, '/dist'), 17 | port: 9000, 18 | writeToDisk: true 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(js|jsx)$/, 24 | use: { 25 | loader: 'babel-loader', 26 | options: { 27 | presets: ['@babel/preset-env', '@babel/preset-react'], 28 | plugins: ['@babel/plugin-proposal-object-rest-spread', "@babel/plugin-transform-runtime"] 29 | } 30 | }, 31 | exclude: /node_modules/ 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: ["style-loader", "css-loader"] 36 | } 37 | ] 38 | }, 39 | plugins: [ 40 | new CleanWebpackPlugin() 41 | ], 42 | resolve: { 43 | extensions: [".js", ".jsx"] 44 | }, 45 | externals: { 46 | } 47 | } -------------------------------------------------------------------------------- /src/hooks/useRows.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef, useState } from "react"; 2 | 3 | const useRows = (props, tableManager) => { 4 | const { 5 | mode, 6 | searchApi: { searchRows }, 7 | sortApi: { sortRows }, 8 | } = tableManager; 9 | 10 | const rowsApi = useRef({}).current; 11 | let [rows, setRows] = useState([]); 12 | const [totalRows, setTotalRows] = useState(null); 13 | 14 | Object.defineProperty(rowsApi, "onRowClick", { 15 | enumerable: false, 16 | writable: true, 17 | }); 18 | 19 | rowsApi.originalRows = props.rows ?? rows; 20 | 21 | rowsApi.rows = useMemo(() => { 22 | let newRows = rowsApi.originalRows; 23 | 24 | if (mode === "sync") { 25 | newRows = searchRows(newRows); 26 | newRows = sortRows(newRows); 27 | } 28 | 29 | return newRows; 30 | }, [rowsApi.originalRows, mode, searchRows, sortRows]); 31 | 32 | rowsApi.onRowClick = props.onRowClick; 33 | rowsApi.totalRows = 34 | mode === "sync" ? rowsApi.rows?.length : props.totalRows ?? totalRows; 35 | 36 | rowsApi.setRows = (rows) => { 37 | if (props.onRowsChange === undefined) setRows(rows); 38 | props.onRowsChange?.(rows, tableManager); 39 | }; 40 | 41 | rowsApi.setTotalRows = (totalRows) => { 42 | if (props.onTotalRowsChange === undefined) setTotalRows(totalRows); 43 | props.onTotalRowsChange?.(totalRows, tableManager); 44 | }; 45 | 46 | return rowsApi; 47 | }; 48 | 49 | export default useRows; 50 | -------------------------------------------------------------------------------- /src/hooks/useColumnsReorder.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | const useColumnsReorder = (props, tableManager) => { 4 | const columnsReorderApi = useRef({ isColumnReordering: false }).current; 5 | 6 | Object.defineProperty(columnsReorderApi, "onColumnReorderStart", { 7 | enumerable: false, 8 | writable: true, 9 | }); 10 | Object.defineProperty(columnsReorderApi, "onColumnReorderEnd", { 11 | enumerable: false, 12 | writable: true, 13 | }); 14 | 15 | columnsReorderApi.onColumnReorderStart = (sortData) => { 16 | columnsReorderApi.isColumnReordering = true; 17 | 18 | sortData.helper.classList.add("rgt-column-sort-ghost"); 19 | 20 | props.onColumnReorderStart?.(sortData, tableManager); 21 | }; 22 | 23 | columnsReorderApi.onColumnReorderEnd = (sortData) => { 24 | const { 25 | columnsApi: { columns, visibleColumns, setColumns }, 26 | } = tableManager; 27 | 28 | setTimeout(() => (columnsReorderApi.isColumnReordering = false), 0); 29 | 30 | if (sortData.oldIndex === sortData.newIndex) return; 31 | 32 | const newColumns = [...columns]; 33 | newColumns.splice( 34 | visibleColumns[sortData.newIndex].index, 35 | 0, 36 | ...newColumns.splice(visibleColumns[sortData.oldIndex].index, 1) 37 | ); 38 | 39 | setColumns(newColumns); 40 | 41 | props.onColumnReorderEnd?.(sortData, tableManager); 42 | }; 43 | 44 | return columnsReorderApi; 45 | }; 46 | 47 | export default useColumnsReorder; 48 | -------------------------------------------------------------------------------- /src/components/PopoverButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDetectClickOutside } from "../hooks/"; 3 | 4 | const PopoverButton = ({ 5 | title, 6 | buttonChildren, 7 | popoverChildren, 8 | className, 9 | ...rest 10 | }) => { 11 | const { ref, isComponentVisible, setIsComponentVisible } = 12 | useDetectClickOutside(false); 13 | 14 | let classNames = ( 15 | "rgt-columns-manager-wrapper " + (className || "") 16 | ).trim(); 17 | 18 | return ( 19 |
20 | 30 |
37 | 38 | {title} 39 | 40 |
41 | {popoverChildren} 42 |
43 |
44 |
45 | ); 46 | }; 47 | 48 | export default PopoverButton; 49 | -------------------------------------------------------------------------------- /src/utils/getHighlightedText.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const getHighlightedText = (text, searchTerm) => { 4 | if (text === searchTerm) 5 | return {text}; 6 | 7 | const regex = new RegExp(searchTerm, "gi"); 8 | const restArr = text.split(regex, text.length); 9 | let restItemsLength = 0; 10 | 11 | const highlightedSearch = restArr.map((textSlice, idx) => { 12 | restItemsLength += textSlice.length; 13 | let element = null; 14 | 15 | if (textSlice) { 16 | element = ( 17 | 18 | {textSlice} 19 | {restArr.length !== idx + 1 ? ( 20 | 21 | {text.slice( 22 | restItemsLength, 23 | searchTerm.length + restItemsLength 24 | )} 25 | 26 | ) : null} 27 | 28 | ); 29 | } else if (restArr.length !== idx + 1) { 30 | element = ( 31 | 32 | {text.slice( 33 | restItemsLength, 34 | searchTerm.length + restItemsLength 35 | )} 36 | 37 | ); 38 | } 39 | 40 | restItemsLength += searchTerm.length; 41 | 42 | return element; 43 | }); 44 | 45 | return {highlightedSearch}; 46 | }; 47 | 48 | export default getHighlightedText; 49 | -------------------------------------------------------------------------------- /demo/src/components/cells/ButtonsCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const EDIT_SVG = ( 4 | 10 | 11 | 15 | 16 | 17 | 18 | ); 19 | 20 | const styles = { 21 | buttonsCellContainer: { 22 | padding: "0 20px", 23 | width: "100%", 24 | height: "100%", 25 | display: "flex", 26 | justifyContent: "flex-end", 27 | alignItems: "center", 28 | }, 29 | editButton: { 30 | background: "#f3f3f3", 31 | outline: "none", 32 | cursor: "pointer", 33 | padding: 4, 34 | display: "inline-flex", 35 | border: "none", 36 | borderRadius: "50%", 37 | boxShadow: "1px 1px 2px 0px rgb(0 0 0 / .3)", 38 | }, 39 | }; 40 | 41 | const ButtonsCell = ({ tableManager, data }) => ( 42 |
43 | 53 |
54 | ); 55 | 56 | export default ButtonsCell; 57 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer = ({ tableManager }) => { 4 | const { 5 | config: { 6 | isPaginated, 7 | showRowsInformation, 8 | pageSizes, 9 | components: { Information, PageSize, Pagination }, 10 | additionalProps: { footer: additionalProps = {} }, 11 | }, 12 | rowsApi: { totalRows }, 13 | rowSelectionApi: { selectedRowsIds }, 14 | paginationApi: { page, pageSize, setPage, setPageSize, pageRows }, 15 | } = tableManager; 16 | 17 | const classNames = ( 18 | "rgt-footer " + (additionalProps.className || "") 19 | ).trim(); 20 | 21 | return ( 22 |
23 | {showRowsInformation !== false ? ( 24 | 31 | ) : ( 32 | 33 | )} 34 | {isPaginated ? ( 35 |
36 | 42 | 47 |
48 | ) : null} 49 |
50 | ); 51 | }; 52 | 53 | export default Footer; 54 | -------------------------------------------------------------------------------- /src/hooks/useResizeEvents.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback } from "react"; 2 | 3 | const useResizeEvents = ( 4 | resizeHandleRef, 5 | column, 6 | onResizeStart, 7 | onResize, 8 | onResizeEnd 9 | ) => { 10 | useEffect(() => { 11 | const resizeEl = resizeHandleRef.current; 12 | if (resizeEl) resizeEl.addEventListener("mousedown", onMouseDown); 13 | 14 | return () => { 15 | if (resizeEl) 16 | resizeEl.removeEventListener("mousedown", onMouseDown); 17 | window.removeEventListener("mousemove", onMouseMove); 18 | window.removeEventListener("mouseup", onMouseUp); 19 | }; 20 | }, [ 21 | column, 22 | onResizeStart, 23 | onResize, 24 | onResizeEnd, 25 | resizeHandleRef, 26 | onMouseDown, 27 | onMouseMove, 28 | onMouseUp, 29 | ]); 30 | 31 | const onMouseDown = useCallback( 32 | (event) => { 33 | onResizeStart({ event, target: resizeHandleRef.current, column }); 34 | window.addEventListener("mousemove", onMouseMove); 35 | window.addEventListener("mouseup", onMouseUp); 36 | }, 37 | [column, onMouseMove, onMouseUp, onResizeStart, resizeHandleRef] 38 | ); 39 | 40 | const onMouseMove = useCallback( 41 | (event) => { 42 | onResize({ event, target: resizeHandleRef.current, column }); 43 | }, 44 | [column, onResize, resizeHandleRef] 45 | ); 46 | 47 | const onMouseUp = useCallback( 48 | (event) => { 49 | onResizeEnd({ event, target: resizeHandleRef.current, column }); 50 | window.removeEventListener("mousemove", onMouseMove); 51 | window.removeEventListener("mouseup", onMouseUp); 52 | }, 53 | [column, onMouseMove, onResizeEnd, resizeHandleRef] 54 | ); 55 | }; 56 | 57 | export default useResizeEvents; 58 | -------------------------------------------------------------------------------- /src/hooks/useRowEdit.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | 3 | const useRowEdit = (props, tableManager) => { 4 | const { 5 | config: { rowIdField }, 6 | paginationApi: { pageRows }, 7 | } = tableManager; 8 | 9 | const rowEditApi = useRef({}).current; 10 | const [editRow, setEditRow] = useState(null); 11 | const [editRowId, setEditRowId] = useState(null); 12 | 13 | rowEditApi.editRowId = props.editRowId ?? editRowId; 14 | rowEditApi.setEditRow = setEditRow; 15 | rowEditApi.editRow = editRow; 16 | rowEditApi.getIsRowEditable = props.getIsRowEditable; 17 | 18 | rowEditApi.setEditRowId = (rowEditId) => { 19 | if ( 20 | props.rowEditId === undefined || 21 | props.onEditRowIdChange === undefined 22 | ) 23 | setEditRowId(rowEditId); 24 | props.onEditRowIdChange?.(rowEditId, tableManager); 25 | }; 26 | 27 | useEffect(() => { 28 | if (rowEditApi.editRow?.[rowIdField] === rowEditApi.editRowId) return; 29 | 30 | rowEditApi.setEditRow( 31 | pageRows.find( 32 | (item) => item?.[rowIdField] === rowEditApi.editRowId 33 | ) || null 34 | ); 35 | }, [pageRows, rowEditApi, rowEditApi.editRowId, rowIdField]); 36 | 37 | // reset edit row 38 | useEffect(() => { 39 | if ( 40 | !tableManager.paginationApi.pageRows.find( 41 | (row, i) => 42 | (row?.[tableManager.config.rowIdField] || i) === 43 | rowEditApi.editRowId 44 | ) 45 | ) 46 | tableManager.rowEditApi.setEditRowId(null); 47 | }, [ 48 | rowEditApi.editRowId, 49 | tableManager.config.rowIdField, 50 | tableManager.paginationApi.pageRows, 51 | tableManager.rowEditApi, 52 | ]); 53 | 54 | return rowEditApi; 55 | }; 56 | 57 | export default useRowEdit; 58 | -------------------------------------------------------------------------------- /src/components/ColumnVisibility.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PopoverButton } from "./"; 3 | 4 | const ColumnVisibility = ({ 5 | tableManager, 6 | columns = tableManager.columnsApi.columns, 7 | onChange = tableManager.columnsVisibilityApi.toggleColumnVisibility, 8 | }) => { 9 | const { 10 | config: { 11 | additionalProps: { columnVisibility: additionalProps = {} }, 12 | texts: { columnVisibility: columnVisibilityText }, 13 | icons: { columnVisibility: columnVisibilityIcon }, 14 | }, 15 | } = tableManager; 16 | 17 | return ( 18 | column.label) 23 | .map((column, idx) => ( 24 |
28 | 36 | {}} 41 | checked={column.visible !== false} 42 | /> 43 |
44 | ))} 45 | {...additionalProps} 46 | /> 47 | ); 48 | }; 49 | 50 | export default ColumnVisibility; 51 | -------------------------------------------------------------------------------- /demo/src/components/cells/UsernameCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const styles = { 4 | root: { 5 | position: "relative", 6 | padding: "0 20px", 7 | display: "flex", 8 | width: "100%", 9 | height: "100%", 10 | alignItems: "center", 11 | }, 12 | img: { minWidth: 32 }, 13 | input: { 14 | position: "absolute", 15 | height: 28, 16 | width: "calc(100% - 82px)", 17 | top: 10, 18 | right: 20, 19 | bottom: 0, 20 | border: "none", 21 | borderBottom: "1px solid #eee", 22 | outline: "none", 23 | fontSize: 16, 24 | padding: 0, 25 | fontFamily: "inherit", 26 | }, 27 | text: { 28 | marginLeft: 10, 29 | whiteSpace: "nowrap", 30 | overflow: "hidden", 31 | textOverflow: "ellipsis", 32 | }, 33 | }; 34 | const UsernameCell = ({ 35 | value, 36 | onChange, 37 | isEdit, 38 | data, 39 | column, 40 | isFirstEditableCell, 41 | }) => { 42 | return ( 43 |
44 | {isEdit ? ( 45 | 46 | avatar 47 | 53 | onChange({ 54 | ...data, 55 | [column.field]: e.target.value, 56 | }) 57 | } 58 | /> 59 | 60 | ) : ( 61 | 62 | avatar 63 | 64 | {value} 65 | 66 | 67 | )} 68 |
69 | ); 70 | }; 71 | 72 | export default UsernameCell; 73 | -------------------------------------------------------------------------------- /src/components/Information.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Information = ({ 4 | tableManager, 5 | totalCount = tableManager.rowsApi.totalRows, 6 | pageSize = tableManager.paginationApi.pageSize, 7 | pageCount = tableManager.paginationApi.pageRows.length, 8 | selectedCount = tableManager.rowSelectionApi.selectedRowsIds.length, 9 | }) => { 10 | const { 11 | config: { 12 | isPaginated, 13 | tableHasSelection, 14 | texts: { 15 | totalRows: totalRowsText, 16 | rows: rowsText, 17 | selected: selectedText, 18 | }, 19 | icons: { clearSelection: clearSelectionIcon }, 20 | additionalProps: { information: additionalProps = {} }, 21 | }, 22 | paginationApi: { page }, 23 | rowSelectionApi: { setSelectedRowsIds }, 24 | } = tableManager; 25 | 26 | let classNames = ( 27 | "rgt-footer-items-information " + (additionalProps.className || "") 28 | ).trim(); 29 | 30 | return ( 31 |
32 | {totalRowsText} {totalCount || 0}  33 | {!isPaginated 34 | ? "" 35 | : `| ${rowsText} ${ 36 | !pageCount 37 | ? "0" 38 | : `${pageSize * (page - 1) + 1} - ${ 39 | pageSize * (page - 1) + pageCount 40 | }` 41 | }`}{" "} 42 | {tableHasSelection ? ( 43 | 44 | {`| ${selectedCount} ${selectedText}`} 45 | {selectedCount ? ( 46 | setSelectedRowsIds([])} 49 | > 50 | {clearSelectionIcon} 51 | 52 | ) : null} 53 | 54 | ) : ( 55 | "" 56 | )} 57 |
58 | ); 59 | }; 60 | 61 | export default Information; 62 | -------------------------------------------------------------------------------- /src/components/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Pagination = ({ 4 | tableManager, 5 | page = tableManager.paginationApi.page, 6 | onChange = tableManager.paginationApi.setPage, 7 | }) => { 8 | const { 9 | config: { 10 | texts: { 11 | prev: prevText, 12 | page: pageText, 13 | next: nextText, 14 | of: ofText, 15 | }, 16 | additionalProps: { pagination: additionalProps = {} }, 17 | }, 18 | paginationApi: { totalPages }, 19 | } = tableManager; 20 | 21 | let backButtonDisabled = page - 1 < 1; 22 | let nextButtonDisabled = page + 1 > totalPages; 23 | 24 | let classNames = ( 25 | "rgt-footer-pagination " + (additionalProps.className || "") 26 | ).trim(); 27 | 28 | return ( 29 |
30 | 39 | 40 |
41 | {pageText} 42 | event.target.select()} 44 | className="rgt-footer-page-input" 45 | type="number" 46 | value={totalPages ? page : 0} 47 | onChange={(event) => onChange(event.target.value * 1)} 48 | /> 49 | 50 | {ofText} {totalPages} 51 | 52 |
53 | 54 | 63 |
64 | ); 65 | }; 66 | 67 | export default Pagination; 68 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | width: 100%; 13 | height: 100%; 14 | overflow: hidden; 15 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 16 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 17 | sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | #root { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | 27 | .demo { 28 | display: flex; 29 | width: 100%; 30 | height: 100%; 31 | overflow: hidden; 32 | background-color: #861657; 33 | background-image: linear-gradient(326deg, #861657, #ffa69e 74%); 34 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 35 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 36 | sans-serif; 37 | -webkit-font-smoothing: antialiased; 38 | -moz-osx-font-smoothing: grayscale; 39 | } 40 | 41 | .demo ::-webkit-scrollbar-track { 42 | background-color: #f5f5f5; 43 | } 44 | 45 | .demo ::-webkit-scrollbar { 46 | width: 8px; 47 | height: 8px; 48 | background-color: #f5f5f5; 49 | } 50 | 51 | .demo ::-webkit-scrollbar-thumb { 52 | background-color: #ddd; 53 | border: 2px solid #d8d8d8; 54 | } 55 | 56 | .tableWrapper { 57 | height: 100%; 58 | padding: 20px; 59 | overflow: auto; 60 | flex: 1; 61 | margin: 0; 62 | } 63 | 64 | .rgt-cell:not(.rgt-row-edit):not(.rgt-row-not-selectable) { 65 | cursor: pointer; 66 | } 67 | 68 | .rgt-row-selected { 69 | background: #f3f6f9; 70 | } 71 | 72 | .rgt-row-hover { 73 | background: #f7f7f7; 74 | } 75 | 76 | .settingsDrawer input[type="checkbox"] { 77 | cursor: pointer; 78 | } 79 | 80 | .settingsDrawer input[type="text"], 81 | .settingsDrawer input[type="number"], 82 | .settingsDrawer select { 83 | background: #eef2f5; 84 | outline: none; 85 | border: none; 86 | padding: 5px 10px; 87 | border-radius: 4px; 88 | width: 120px; 89 | } 90 | 91 | @media (min-width: 1025px) { 92 | .tableWrapper { 93 | padding: 40px; 94 | } 95 | .settingsDrawer { 96 | position: unset !important; 97 | transform: translate3d(0px, 0px, 0px) !important; 98 | } 99 | .settingsDrawerButton { 100 | display: none !important; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/components/Row.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CellContainer } from "./"; 3 | 4 | const Row = ({ index, data, tableManager, measureRef }) => { 5 | const { 6 | config: { isVirtualScroll, rowIdField }, 7 | rowEditApi: { editRow, getIsRowEditable }, 8 | rowSelectionApi: { getIsRowSelectable, selectedRowsIds }, 9 | columnsApi: { visibleColumns }, 10 | paginationApi: { page, pageSize }, 11 | rowVirtualizer: { virtualItems, totalSize }, 12 | } = tableManager; 13 | 14 | if (isVirtualScroll) { 15 | if (index === "virtual-start") { 16 | return visibleColumns.map((visibleColumn) => ( 17 |
21 | )); 22 | } 23 | if (index === "virtual-end") { 24 | return visibleColumns.map((visibleColumn) => ( 25 |
33 | )); 34 | } 35 | } 36 | 37 | let rowIndex = index + 1 + pageSize * (page - 1); 38 | let rowId = data?.[rowIdField] || rowIndex; 39 | let disableSelection = !data || !getIsRowSelectable(data); 40 | let isSelected = 41 | !!data && 42 | !!selectedRowsIds.find((selectedRowId) => selectedRowId === rowId); 43 | let isEdit = 44 | !!data && editRow?.[rowIdField] === rowId && !!getIsRowEditable(data); 45 | 46 | return visibleColumns.map((visibleColumn, colIndex) => ( 47 | 60 | )); 61 | }; 62 | 63 | export default Row; 64 | -------------------------------------------------------------------------------- /src/defaults/icons.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LOADER = ( 4 | 10 | 11 | 12 | 19 | 20 | 28 | 29 | 30 | 31 | 32 | ); 33 | 34 | const CLEAR_ICON = ( 35 | 41 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | 58 | const MENU_ICON = ( 59 | 65 | 66 | 67 | ); 68 | 69 | const SORT_ASCENDING_ICON = ; 70 | 71 | const SORT_DESCENDING_ICON = ; 72 | 73 | const SEARCH_ICON = ; 74 | 75 | export default { 76 | loader: LOADER, 77 | clearSelection: CLEAR_ICON, 78 | columnVisibility: MENU_ICON, 79 | sortAscending: SORT_ASCENDING_ICON, 80 | sortDescending: SORT_DESCENDING_ICON, 81 | search: SEARCH_ICON, 82 | }; 83 | -------------------------------------------------------------------------------- /src/drag-and-drop/SortableContainer/props.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | import { KEYCODE } from "../utils"; 4 | import defaultGetHelperDimensions from "./defaultGetHelperDimensions"; 5 | import defaultShouldCancelStart from "./defaultShouldCancelStart"; 6 | 7 | export const propTypes = { 8 | axis: PropTypes.oneOf(["x", "y", "xy"]), 9 | contentWindow: PropTypes.any, 10 | disableAutoscroll: PropTypes.bool, 11 | distance: PropTypes.number, 12 | getContainer: PropTypes.func, 13 | getHelperDimensions: PropTypes.func, 14 | helperClass: PropTypes.string, 15 | helperContainer: PropTypes.oneOfType([ 16 | PropTypes.func, 17 | typeof HTMLElement === "undefined" 18 | ? PropTypes.any 19 | : PropTypes.instanceOf(HTMLElement), 20 | ]), 21 | hideSortableGhost: PropTypes.bool, 22 | keyboardSortingTransitionDuration: PropTypes.number, 23 | lockAxis: PropTypes.string, 24 | lockOffset: PropTypes.oneOfType([ 25 | PropTypes.number, 26 | PropTypes.string, 27 | PropTypes.arrayOf( 28 | PropTypes.oneOfType([PropTypes.number, PropTypes.string]) 29 | ), 30 | ]), 31 | lockToContainerEdges: PropTypes.bool, 32 | onSortEnd: PropTypes.func, 33 | onSortMove: PropTypes.func, 34 | onSortOver: PropTypes.func, 35 | onSortStart: PropTypes.func, 36 | pressDelay: PropTypes.number, 37 | pressThreshold: PropTypes.number, 38 | keyCodes: PropTypes.shape({ 39 | lift: PropTypes.arrayOf(PropTypes.number), 40 | drop: PropTypes.arrayOf(PropTypes.number), 41 | cancel: PropTypes.arrayOf(PropTypes.number), 42 | up: PropTypes.arrayOf(PropTypes.number), 43 | down: PropTypes.arrayOf(PropTypes.number), 44 | }), 45 | shouldCancelStart: PropTypes.func, 46 | transitionDuration: PropTypes.number, 47 | updateBeforeSortStart: PropTypes.func, 48 | useDragHandle: PropTypes.bool, 49 | useWindowAsScrollContainer: PropTypes.bool, 50 | }; 51 | 52 | export const defaultKeyCodes = { 53 | lift: [KEYCODE.SPACE], 54 | drop: [KEYCODE.SPACE], 55 | cancel: [KEYCODE.ESC], 56 | up: [KEYCODE.UP, KEYCODE.LEFT], 57 | down: [KEYCODE.DOWN, KEYCODE.RIGHT], 58 | }; 59 | 60 | export const defaultProps = { 61 | axis: "y", 62 | disableAutoscroll: false, 63 | distance: 0, 64 | getHelperDimensions: defaultGetHelperDimensions, 65 | hideSortableGhost: true, 66 | lockOffset: "50%", 67 | lockToContainerEdges: false, 68 | pressDelay: 0, 69 | pressThreshold: 5, 70 | keyCodes: defaultKeyCodes, 71 | shouldCancelStart: defaultShouldCancelStart, 72 | transitionDuration: 300, 73 | useWindowAsScrollContainer: false, 74 | }; 75 | 76 | export const omittedProps = Object.keys(propTypes); 77 | -------------------------------------------------------------------------------- /src/drag-and-drop/SortableElement/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { SortableContext } from "../SortableContainer"; 4 | 5 | import { provideDisplayName, omit } from "../utils"; 6 | 7 | const propTypes = { 8 | index: PropTypes.number.isRequired, 9 | collection: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 10 | disabled: PropTypes.bool, 11 | }; 12 | 13 | const omittedProps = Object.keys(propTypes); 14 | 15 | export default function sortableElement(WrappedComponent) { 16 | return class WithSortableElement extends React.Component { 17 | static displayName = provideDisplayName( 18 | "sortableElement", 19 | WrappedComponent 20 | ); 21 | 22 | static contextType = SortableContext; 23 | 24 | static propTypes = propTypes; 25 | 26 | static defaultProps = { 27 | collection: 0, 28 | }; 29 | 30 | componentDidMount() { 31 | this.register(); 32 | } 33 | 34 | componentDidUpdate(prevProps) { 35 | if (this.node) { 36 | if (prevProps.index !== this.props.index) { 37 | this.node.sortableInfo.index = this.props.index; 38 | } 39 | 40 | if (prevProps.disabled !== this.props.disabled) { 41 | this.node.sortableInfo.disabled = this.props.disabled; 42 | } 43 | } 44 | 45 | if (prevProps.collection !== this.props.collection) { 46 | this.unregister(prevProps.collection); 47 | this.register(); 48 | } 49 | } 50 | 51 | componentWillUnmount() { 52 | this.unregister(); 53 | } 54 | 55 | register() { 56 | const { collection, disabled, index } = this.props; 57 | const node = this.wrappedInstance.current; 58 | 59 | node.sortableInfo = { 60 | collection, 61 | disabled, 62 | index, 63 | manager: this.context.manager, 64 | }; 65 | 66 | this.node = node; 67 | this.ref = { node }; 68 | 69 | this.context.manager.add(collection, this.ref); 70 | } 71 | 72 | unregister(collection = this.props.collection) { 73 | this.context.manager.remove(collection, this.ref); 74 | } 75 | 76 | getWrappedInstance() { 77 | return this.wrappedInstance.current; 78 | } 79 | 80 | wrappedInstance = React.createRef(); 81 | 82 | render() { 83 | return ( 84 | 88 | ); 89 | } 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/hooks/useSearch.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useRef } from "react"; 2 | 3 | const useSearch = (props, tableManager) => { 4 | const { 5 | config: { minSearchChars }, 6 | columnsApi: { columns }, 7 | } = tableManager; 8 | 9 | const searchApi = useRef({}).current; 10 | const [searchText, setSearchText] = useState(""); 11 | 12 | searchApi.searchText = props.searchText ?? searchText; 13 | searchApi.validSearchText = 14 | searchApi.searchText.length >= minSearchChars 15 | ? searchApi.searchText 16 | : ""; 17 | 18 | searchApi.setSearchText = (searchText) => { 19 | if ( 20 | props.searchText === undefined || 21 | props.onSearchTextChange === undefined 22 | ) 23 | setSearchText(searchText); 24 | props.onSearchTextChange?.(searchText, tableManager); 25 | }; 26 | 27 | searchApi.valuePassesSearch = (value, column) => { 28 | if (!value) return false; 29 | if (!column?.searchable) return false; 30 | if (!searchApi.validSearchText) return false; 31 | 32 | return column.search({ 33 | value: value.toString(), 34 | searchText: searchApi.validSearchText, 35 | }); 36 | }; 37 | 38 | searchApi.searchRows = useCallback( 39 | (rows) => { 40 | if (searchApi.validSearchText) { 41 | rows = rows.filter((item) => 42 | Object.keys(item).some((key) => { 43 | var cols = columns.filter( 44 | (column) => 45 | column.searchable && column.field === key 46 | ); 47 | 48 | let isValid = false; 49 | 50 | for (let index = 0; index < cols.length; index++) { 51 | const currentColumn = cols[index]; 52 | const value = currentColumn.getValue({ 53 | tableManager, 54 | value: item[key], 55 | column: currentColumn, 56 | rowData: item, 57 | }); 58 | isValid = currentColumn.search({ 59 | value: value?.toString() || "", 60 | searchText: searchApi.validSearchText, 61 | }); 62 | 63 | if (isValid) break; 64 | } 65 | 66 | return isValid; 67 | }) 68 | ); 69 | } 70 | 71 | return rows; 72 | }, 73 | [columns, searchApi.validSearchText, tableManager] 74 | ); 75 | 76 | return searchApi; 77 | }; 78 | 79 | export default useSearch; 80 | -------------------------------------------------------------------------------- /demo/src/components/cells/ButtonsEditorCell.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CANCEL_SVG = ( 4 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | const SAVE_SVG = ( 17 | 23 | 29 | 30 | ); 31 | 32 | const styles = { 33 | buttonsCellEditorContainer: { 34 | height: "100%", 35 | width: "100%", 36 | display: "inline-flex", 37 | padding: "0 20px", 38 | justifyContent: "flex-end", 39 | alignItems: "center", 40 | }, 41 | cancelButton: { 42 | background: "#f3f3f3", 43 | outline: "none", 44 | cursor: "pointer", 45 | marginRight: 10, 46 | padding: 2, 47 | display: "inline-flex", 48 | border: "none", 49 | borderRadius: "50%", 50 | boxShadow: "1px 1px 2px 0px rgb(0 0 0 / .3)", 51 | }, 52 | saveButton: { 53 | background: "#f3f3f3", 54 | outline: "none", 55 | cursor: "pointer", 56 | padding: 2, 57 | display: "inline-flex", 58 | border: "none", 59 | borderRadius: "50%", 60 | boxShadow: "1px 1px 2px 0px rgb(0 0 0 / .3)", 61 | }, 62 | }; 63 | 64 | const ButtonsEditorCell = ({ tableManager, data, setRowsData }) => ( 65 |
66 | 76 | 92 |
93 | ); 94 | 95 | export default ButtonsEditorCell; 96 | -------------------------------------------------------------------------------- /src/hooks/useSort.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useRef } from "react"; 2 | 3 | const useSort = (props, tableManager) => { 4 | const { 5 | columnsApi: { columns }, 6 | } = tableManager; 7 | 8 | const sortApi = useRef({}).current; 9 | const [sort, setSort] = useState({ colId: null, isAsc: true }); 10 | 11 | sortApi.sort = props.sort ?? sort; 12 | if ( 13 | !columns.some( 14 | (column) => column.id === sortApi.sort.colId && column.sortable 15 | ) 16 | ) 17 | sortApi.sort = { colId: null, isAsc: true }; 18 | 19 | sortApi.setSort = ({ colId, isAsc }) => { 20 | const { 21 | columnsReorderApi: { isColumnReordering }, 22 | columnsResizeApi: { isColumnResizing }, 23 | } = tableManager; 24 | 25 | if (isColumnReordering) return; 26 | if (isColumnResizing) return; 27 | 28 | if (props.sort === undefined || props.onSortChange === undefined) 29 | setSort({ colId, isAsc }); 30 | props.onSortChange?.({ colId, isAsc }, tableManager); 31 | }; 32 | 33 | sortApi.sortRows = useCallback( 34 | (rows) => { 35 | var cols = columns.reduce((conf, coldef) => { 36 | conf[coldef.id] = coldef; 37 | return conf; 38 | }, {}); 39 | 40 | if (sortApi.sort?.colId) { 41 | rows = [...rows]; 42 | rows.sort((a, b) => { 43 | const aVal = cols[sortApi.sort.colId].getValue({ 44 | tableManager, 45 | value: a[cols[sortApi.sort.colId].field], 46 | column: cols[sortApi.sort.colId], 47 | rowData: a, 48 | }); 49 | const bVal = cols[sortApi.sort.colId].getValue({ 50 | tableManager, 51 | value: b[cols[sortApi.sort.colId].field], 52 | column: cols[sortApi.sort.colId], 53 | rowData: b, 54 | }); 55 | 56 | if (cols[sortApi.sort.colId].sortable === false) return 0; 57 | return cols[sortApi.sort.colId].sort({ 58 | a: aVal, 59 | b: bVal, 60 | isAscending: sortApi.sort.isAsc, 61 | }); 62 | }); 63 | } 64 | 65 | return rows; 66 | }, 67 | [sortApi.sort, columns, tableManager] 68 | ); 69 | 70 | sortApi.toggleSort = (colId) => { 71 | let isAsc = true; 72 | if (sortApi.sort.colId === colId) { 73 | if (sortApi.sort.isAsc) isAsc = false; 74 | else { 75 | colId = null; 76 | isAsc = true; 77 | } 78 | } 79 | 80 | sortApi.setSort({ colId, isAsc }); 81 | }; 82 | 83 | return sortApi; 84 | }; 85 | 86 | export default useSort; 87 | -------------------------------------------------------------------------------- /src/drag-and-drop/AutoScroller/index.js: -------------------------------------------------------------------------------- 1 | export default class AutoScroller { 2 | constructor(container, onScrollCallback) { 3 | this.container = container; 4 | this.onScrollCallback = onScrollCallback; 5 | } 6 | 7 | clear() { 8 | if (this.interval == null) { 9 | return; 10 | } 11 | 12 | clearInterval(this.interval); 13 | this.interval = null; 14 | } 15 | 16 | update({ translate, minTranslate, maxTranslate, width, height }) { 17 | const direction = { 18 | x: 0, 19 | y: 0, 20 | }; 21 | const speed = { 22 | x: 1, 23 | y: 1, 24 | }; 25 | const acceleration = { 26 | x: 10, 27 | y: 10, 28 | }; 29 | 30 | const { 31 | scrollTop, 32 | scrollLeft, 33 | scrollHeight, 34 | scrollWidth, 35 | clientHeight, 36 | clientWidth, 37 | } = this.container; 38 | 39 | const isTop = scrollTop === 0; 40 | const isBottom = scrollHeight - scrollTop - clientHeight === 0; 41 | const isLeft = scrollLeft === 0; 42 | const isRight = scrollWidth - scrollLeft - clientWidth === 0; 43 | 44 | if (translate.y >= maxTranslate.y - height / 2 && !isBottom) { 45 | // Scroll Down 46 | direction.y = 1; 47 | speed.y = 48 | acceleration.y * 49 | Math.abs((maxTranslate.y - height / 2 - translate.y) / height); 50 | } else if (translate.x >= maxTranslate.x - width / 2 && !isRight) { 51 | // Scroll Right 52 | direction.x = 1; 53 | speed.x = 54 | acceleration.x * 55 | Math.abs((maxTranslate.x - width / 2 - translate.x) / width); 56 | } else if (translate.y <= minTranslate.y + height / 2 && !isTop) { 57 | // Scroll Up 58 | direction.y = -1; 59 | speed.y = 60 | acceleration.y * 61 | Math.abs((translate.y - height / 2 - minTranslate.y) / height); 62 | } else if (translate.x <= minTranslate.x + width / 2 && !isLeft) { 63 | // Scroll Left 64 | direction.x = -1; 65 | speed.x = 66 | acceleration.x * 67 | Math.abs((translate.x - width / 2 - minTranslate.x) / width); 68 | } 69 | 70 | if (this.interval) { 71 | this.clear(); 72 | this.isAutoScrolling = false; 73 | } 74 | 75 | if (direction.x !== 0 || direction.y !== 0) { 76 | this.interval = setInterval(() => { 77 | this.isAutoScrolling = true; 78 | const offset = { 79 | left: speed.x * direction.x, 80 | top: speed.y * direction.y, 81 | }; 82 | this.container.scrollTop += offset.top; 83 | this.container.scrollLeft += offset.left; 84 | 85 | this.onScrollCallback(offset); 86 | }, 5); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/hooks/usePagination.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useMemo, useEffect } from "react"; 2 | 3 | const usePagination = (props, tableManager) => { 4 | const { 5 | mode, 6 | config: { isPaginated, pageSizes }, 7 | rowsApi: { rows, totalRows }, 8 | } = tableManager; 9 | 10 | const paginationApi = useRef({}).current; 11 | const [page, setPage] = useState(props.page || 1); 12 | const [pageSize, setPageSize] = useState( 13 | props.pageSize || pageSizes[0] || 20 14 | ); 15 | 16 | paginationApi.pageSize = props.pageSize ?? pageSize; 17 | paginationApi.totalPages = Math.ceil(totalRows / paginationApi.pageSize); 18 | paginationApi.page = Math.max( 19 | 1, 20 | Math.min(paginationApi.totalPages, props.page ?? page) 21 | ); 22 | paginationApi.pageRows = useMemo(() => { 23 | if (!isPaginated) return rows; 24 | 25 | const pageRows = rows.slice( 26 | paginationApi.pageSize * paginationApi.page - 27 | paginationApi.pageSize, 28 | paginationApi.pageSize * paginationApi.page 29 | ); 30 | 31 | // fill missing page rows with nulls - makes sure we display PlaceHolderCells when moving to a new page (while not using virtual scroll) 32 | if (mode !== "sync" && pageRows.length < paginationApi.pageSize) { 33 | let totalMissingRows = paginationApi.pageSize - pageRows.length; 34 | if (paginationApi.page === Math.max(paginationApi.totalPages, 1)) 35 | totalMissingRows = 36 | (totalRows % paginationApi.pageSize) - pageRows.length; 37 | for (let i = 0; i < totalMissingRows; i++) { 38 | pageRows.push(null); 39 | } 40 | } 41 | 42 | return pageRows; 43 | }, [ 44 | isPaginated, 45 | rows, 46 | paginationApi.pageSize, 47 | paginationApi.page, 48 | paginationApi.totalPages, 49 | mode, 50 | totalRows, 51 | ]); 52 | 53 | paginationApi.setPage = (page) => { 54 | page = ~~page; 55 | page = Math.max(1, Math.min(paginationApi.totalPages, page)); 56 | if (paginationApi.page === page) return; 57 | 58 | if (props.page === undefined || props.onPageChange === undefined) 59 | setPage(page); 60 | props.onPageChange?.(page, tableManager); 61 | 62 | setTimeout(() => (tableManager.refs.tableRef.current.scrollTop = 0), 0); 63 | }; 64 | 65 | paginationApi.setPageSize = (pageSize) => { 66 | pageSize = ~~pageSize; 67 | 68 | if ( 69 | props.pageSize === undefined || 70 | props.onPageSizeChange === undefined 71 | ) 72 | setPageSize(pageSize); 73 | props.onPageSizeChange?.(pageSize, tableManager); 74 | }; 75 | 76 | // reset page number 77 | useEffect(() => { 78 | if (!tableManager.isInitialized) return; 79 | if (tableManager.paginationApi.page === 1) return; 80 | 81 | tableManager.paginationApi.setPage(1); 82 | }, [ 83 | tableManager.searchApi.validSearchText, 84 | tableManager.isInitialized, 85 | paginationApi, 86 | paginationApi.pageSize, 87 | tableManager.paginationApi, 88 | ]); 89 | 90 | return paginationApi; 91 | }; 92 | 93 | export default usePagination; 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nadavshaar/react-grid-table", 3 | "version": "1.1.2", 4 | "description": "A modular table, based on a CSS grid layout, optimized for customization.", 5 | "author": "Nadav Shaar", 6 | "license": "MIT", 7 | "repository": "NadavShaar/react-grid-table", 8 | "homepage": "https://nadavshaar.github.io/react-grid-table/", 9 | "contributors": [ 10 | { 11 | "name": "Nadav Shaar", 12 | "email": "nadavshaar@gamil.com", 13 | "url": "https://github.com/NadavShaar" 14 | }, 15 | { 16 | "name": "Ilay Ofir", 17 | "email": "ilayofir@gmail.com", 18 | "url": "https://github.com/IlayOfir" 19 | } 20 | ], 21 | "keywords": [ 22 | "react", 23 | "table", 24 | "grid", 25 | "css-grid", 26 | "list", 27 | "modular", 28 | "sort", 29 | "resize", 30 | "reorder", 31 | "pin", 32 | "columns", 33 | "rows", 34 | "sticky-header", 35 | "row-selection", 36 | "inline-editing", 37 | "dynamic-row-height", 38 | "visibility-management", 39 | "search", 40 | "header", 41 | "footer", 42 | "cells", 43 | "pagination", 44 | "async", 45 | "row-virtualizer" 46 | ], 47 | "main": "dist/index.js", 48 | "source": "src/index.js", 49 | "engines": { 50 | "node": ">=10" 51 | }, 52 | "scripts": { 53 | "start": "webpack serve --config demo/webpack.config.js", 54 | "start-async": "cross-env TARGET=async webpack serve --config demo/webpack.config.js", 55 | "start-async-controlled": "cross-env TARGET=asyncControlled webpack serve --config demo/webpack.config.js", 56 | "start-async-managed": "cross-env TARGET=asyncManaged webpack serve --config demo/webpack.config.js", 57 | "watch": "webpack -w", 58 | "build": "webpack", 59 | "build-demo": "webpack --config demo/webpack.config.js", 60 | "deploy-demo": "gh-pages -d demo/dist", 61 | "test": "" 62 | }, 63 | "peerDependencies": { 64 | "react": ">17.0.1", 65 | "react-dom": ">17.0.1", 66 | "prop-types": "^15.7.2" 67 | }, 68 | "devDependencies": { 69 | "@babel/core": "^7.12.3", 70 | "@babel/plugin-transform-runtime": "^7.12.10", 71 | "@babel/preset-env": "^7.12.1", 72 | "@babel/preset-react": "^7.12.5", 73 | "babel-eslint": "^10.1.0", 74 | "babel-loader": "^8.2.1", 75 | "clean-webpack-plugin": "^3.0.0", 76 | "cross-env": "^7.0.3", 77 | "css-loader": "^5.0.1", 78 | "eslint": "^7.26.0", 79 | "eslint-config-prettier": "^8.3.0", 80 | "eslint-plugin-prettier": "^3.4.0", 81 | "eslint-plugin-react": "^7.23.2", 82 | "eslint-plugin-react-hooks": "^4.2.0", 83 | "gh-pages": "^3.1.0", 84 | "prettier": "^2.3.0", 85 | "prop-types": "^15.7.2", 86 | "react": "^17.0.1", 87 | "react-dom": "^17.0.1", 88 | "react-is": "^17.0.1", 89 | "style-loader": "^2.0.0", 90 | "webpack": "^5.11.0", 91 | "webpack-cli": "^4.3.1", 92 | "webpack-dev-server": "^3.11.0" 93 | }, 94 | "files": [ 95 | "dist" 96 | ], 97 | "dependencies": { 98 | "react-virtual": "^2.3.0" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /demo/src/getColumns.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | UsernameCell, 4 | UsernameEditorCell, 5 | GenderEditorCell, 6 | ButtonsCell, 7 | ButtonsEditorCell as BaseButtonsEditorCell, 8 | } from "./components/cells"; 9 | 10 | const getColumns = ({ setRowsData }) => { 11 | const ButtonsEditorCell = (props) => ( 12 | 13 | ); 14 | 15 | return [ 16 | { 17 | id: "checkbox", 18 | visible: true, 19 | pinned: true, 20 | width: "54px", 21 | }, 22 | { 23 | id: "2", 24 | field: "username", 25 | label: "Username", 26 | visible: true, 27 | searchable: true, 28 | editable: true, 29 | sortable: true, 30 | resizable: true, 31 | cellRenderer: UsernameCell, 32 | editorCellRenderer: UsernameEditorCell, 33 | }, 34 | { 35 | id: "3", 36 | field: "first_name", 37 | label: "First Name", 38 | visible: true, 39 | searchable: true, 40 | editable: true, 41 | sortable: true, 42 | resizable: true, 43 | }, 44 | { 45 | id: "4", 46 | field: "last_name", 47 | label: "Last Name", 48 | visible: true, 49 | searchable: true, 50 | editable: true, 51 | sortable: true, 52 | resizable: true, 53 | }, 54 | { 55 | id: "5", 56 | field: "email", 57 | label: "Email", 58 | visible: true, 59 | searchable: true, 60 | editable: true, 61 | sortable: true, 62 | resizable: true, 63 | }, 64 | { 65 | id: "6", 66 | field: "gender", 67 | label: "Gender", 68 | visible: true, 69 | searchable: true, 70 | editable: true, 71 | sortable: true, 72 | resizable: true, 73 | editorCellRenderer: GenderEditorCell, 74 | }, 75 | { 76 | id: "7", 77 | field: "ip_address", 78 | label: "IP Address", 79 | visible: true, 80 | searchable: true, 81 | editable: true, 82 | sortable: true, 83 | resizable: true, 84 | }, 85 | { 86 | id: "8", 87 | field: "last_visited", 88 | label: "Last Visited", 89 | visible: true, 90 | searchable: true, 91 | editable: true, 92 | sortable: true, 93 | resizable: true, 94 | sort: ({ a, b, isAscending }) => { 95 | let aa = a.split("/").reverse().join(), 96 | bb = b.split("/").reverse().join(); 97 | return aa < bb 98 | ? isAscending 99 | ? -1 100 | : 1 101 | : aa > bb 102 | ? isAscending 103 | ? 1 104 | : -1 105 | : 0; 106 | }, 107 | }, 108 | { 109 | id: "buttons", 110 | width: "max-content", 111 | visible: true, 112 | pinned: true, 113 | sortable: false, 114 | resizable: false, 115 | cellRenderer: ButtonsCell, 116 | editorCellRenderer: ButtonsEditorCell, 117 | }, 118 | ]; 119 | }; 120 | 121 | export default getColumns; 122 | -------------------------------------------------------------------------------- /src/hooks/useTableManager.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import * as components from "../components"; 3 | import { icons, texts } from "../defaults"; 4 | import { uuid } from "../utils"; 5 | import { 6 | useRowVirtualizer, 7 | useColumns, 8 | useSort, 9 | useSearch, 10 | usePagination, 11 | useRowSelection, 12 | useRowEdit, 13 | useRows, 14 | useAsync, 15 | useColumnsReorder, 16 | useColumnsVisibility, 17 | useColumnsResize, 18 | } from "../hooks/"; 19 | 20 | const useTableManager = (props) => { 21 | const tableManager = useRef({ 22 | id: props.id || uuid(), 23 | isMounted: false, 24 | isInitialized: false, 25 | }).current; 26 | 27 | Object.defineProperty(tableManager, "columnsReorderApi", { 28 | enumerable: false, 29 | writable: true, 30 | }); 31 | Object.defineProperty(tableManager, "columnsResizeApi", { 32 | enumerable: false, 33 | writable: true, 34 | }); 35 | 36 | // initialization 37 | useEffect(() => { 38 | tableManager.isMounted = true; 39 | props.onLoad?.(tableManager); 40 | 41 | return () => (tableManager.isMounted = false); 42 | }, [props, tableManager]); 43 | 44 | tableManager.mode = !props.onRowsRequest ? "sync" : "async"; 45 | tableManager.config = { 46 | rowIdField: props.rowIdField, 47 | minColumnResizeWidth: props.minColumnResizeWidth, 48 | minSearchChars: props.minSearchChars, 49 | isHeaderSticky: props.isHeaderSticky, 50 | isPaginated: props.isPaginated, 51 | enableColumnsReorder: props.enableColumnsReorder, 52 | highlightSearch: props.highlightSearch, 53 | showSearch: props.showSearch, 54 | showRowsInformation: props.showRowsInformation, 55 | showColumnVisibilityManager: props.showColumnVisibilityManager, 56 | pageSizes: props.pageSizes, 57 | requestDebounceTimeout: props.requestDebounceTimeout, 58 | isVirtualScroll: 59 | props.isVirtualScroll || 60 | (!props.isPaginated && tableManager.mode !== "sync"), 61 | tableHasSelection: !!props.columns.find((cd) => cd.id === "checkbox"), 62 | components: { ...components, ...props.components }, 63 | additionalProps: props.additionalProps || {}, 64 | icons: { ...icons, ...props.icons }, 65 | texts: { ...texts, ...props.texts }, 66 | }; 67 | 68 | tableManager.refs = { 69 | tableRef: useRef(null), 70 | rgtRef: useRef(null), 71 | }; 72 | tableManager.columnsApi = useColumns(props, tableManager); 73 | tableManager.columnsReorderApi = useColumnsReorder(props, tableManager); 74 | tableManager.columnsResizeApi = useColumnsResize(props, tableManager); 75 | tableManager.columnsVisibilityApi = useColumnsVisibility( 76 | props, 77 | tableManager 78 | ); 79 | tableManager.searchApi = useSearch(props, tableManager); 80 | tableManager.sortApi = useSort(props, tableManager); 81 | tableManager.rowsApi = useRows(props, tableManager); 82 | tableManager.paginationApi = usePagination(props, tableManager); 83 | tableManager.rowSelectionApi = useRowSelection(props, tableManager); 84 | tableManager.rowEditApi = useRowEdit(props, tableManager); 85 | tableManager.rowVirtualizer = useRowVirtualizer(props, tableManager); 86 | tableManager.asyncApi = useAsync(props, tableManager); 87 | tableManager.isLoading = 88 | props.isLoading ?? 89 | (tableManager.mode !== "sync" && tableManager.asyncApi.isLoading); 90 | 91 | // initialization completion 92 | useEffect(() => { 93 | tableManager.isInitialized = true; 94 | }, [tableManager]); 95 | 96 | return tableManager; 97 | }; 98 | 99 | export default useTableManager; 100 | -------------------------------------------------------------------------------- /src/hooks/useColumns.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useRef } from "react"; 2 | 3 | const useColumns = (props, tableManager) => { 4 | const { 5 | config: { 6 | components: { 7 | Cell, 8 | EditorCell, 9 | SelectionCell, 10 | HeaderCell, 11 | HeaderSelectionCell, 12 | PlaceHolderCell, 13 | }, 14 | }, 15 | } = tableManager; 16 | 17 | const columnsApi = useRef({}).current; 18 | let [columns, setColumns] = useState(props.columns); 19 | 20 | columnsApi.columns = useMemo(() => { 21 | const newColumns = props.onColumnsChange ? props.columns : columns; 22 | 23 | return newColumns.map((column, idx) => { 24 | const isPinnedColumn = 25 | (idx === 0 && column.pinned) || 26 | (idx === newColumns.length - 1 && column.pinned); 27 | const isVisibleColumn = column.visible !== false; 28 | 29 | if (column.id === "checkbox") 30 | return { 31 | className: "", 32 | width: "max-content", 33 | minResizeWidth: 0, 34 | maxResizeWidth: null, 35 | resizable: false, 36 | cellRenderer: SelectionCell, 37 | headerCellRenderer: HeaderSelectionCell, 38 | ...column, 39 | searchable: false, 40 | editable: false, 41 | sortable: false, 42 | pinned: isPinnedColumn, 43 | visible: isVisibleColumn, 44 | index: idx, 45 | }; 46 | 47 | return { 48 | label: column.field, 49 | className: "", 50 | width: "200px", 51 | minResizeWidth: null, 52 | maxResizeWidth: null, 53 | getValue: ({ value }) => value, 54 | setValue: ({ value, data, setRow, column }) => { 55 | setRow({ ...data, [column.field]: value }); 56 | }, 57 | searchable: true, 58 | editable: true, 59 | sortable: true, 60 | resizable: true, 61 | search: ({ value, searchText }) => 62 | value 63 | .toString() 64 | .toLowerCase() 65 | .includes(searchText.toLowerCase()), 66 | sort: ({ a, b, isAscending }) => { 67 | const aa = typeof a === "string" ? a.toLowerCase() : a; 68 | const bb = typeof b === "string" ? b.toLowerCase() : b; 69 | if (aa > bb) return isAscending ? 1 : -1; 70 | else if (aa < bb) return isAscending ? -1 : 1; 71 | return 0; 72 | }, 73 | cellRenderer: Cell, 74 | editorCellRenderer: EditorCell, 75 | headerCellRenderer: HeaderCell, 76 | placeHolderRenderer: PlaceHolderCell, 77 | ...column, 78 | pinned: isPinnedColumn, 79 | visible: isVisibleColumn, 80 | index: idx, 81 | }; 82 | }); 83 | }, [ 84 | props.onColumnsChange, 85 | props.columns, 86 | columns, 87 | SelectionCell, 88 | HeaderSelectionCell, 89 | Cell, 90 | EditorCell, 91 | HeaderCell, 92 | PlaceHolderCell, 93 | ]); 94 | 95 | columnsApi.visibleColumns = useMemo(() => { 96 | const visibleColumns = columnsApi.columns.filter( 97 | (column) => column.visible 98 | ); 99 | 100 | const virtualColIndex = visibleColumns[visibleColumns.length - 1] 101 | ?.pinned 102 | ? visibleColumns.length - 1 103 | : visibleColumns.length; 104 | 105 | visibleColumns.splice(virtualColIndex, 0, { 106 | id: "virtual", 107 | visible: true, 108 | width: "auto", 109 | }); 110 | 111 | return visibleColumns; 112 | }, [columnsApi.columns]); 113 | 114 | columnsApi.setColumns = (cols) => { 115 | if (!props.onColumnsChange) setColumns(cols); 116 | else props.onColumnsChange(cols, tableManager); 117 | }; 118 | 119 | return columnsApi; 120 | }; 121 | 122 | export default useColumns; 123 | -------------------------------------------------------------------------------- /src/hooks/useColumnsResize.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { useResizeEvents } from "./"; 3 | 4 | const useColumnsResize = (props, tableManager) => { 5 | const { 6 | config: { minColumnResizeWidth }, 7 | refs: { tableRef }, 8 | columnsApi: { columns, visibleColumns, setColumns }, 9 | } = tableManager; 10 | 11 | const columnsResizeApi = useRef({ isColumnResizing: false }).current; 12 | const lastPos = useRef(null); 13 | 14 | Object.defineProperty(columnsResizeApi, "onResizeStart", { 15 | enumerable: false, 16 | writable: true, 17 | }); 18 | Object.defineProperty(columnsResizeApi, "onResize", { 19 | enumerable: false, 20 | writable: true, 21 | }); 22 | Object.defineProperty(columnsResizeApi, "onResizeEnd", { 23 | enumerable: false, 24 | writable: true, 25 | }); 26 | Object.defineProperty(columnsResizeApi, "useResizeRef", { 27 | enumerable: false, 28 | writable: true, 29 | }); 30 | 31 | columnsResizeApi.onResizeStart = ({ event, target, column }) => { 32 | columnsResizeApi.isColumnResizing = true; 33 | props.onColumnResizeStart?.({ event, target, column }, tableManager); 34 | }; 35 | 36 | columnsResizeApi.onResize = ({ event, target, column }) => { 37 | const containerEl = tableRef.current; 38 | const gridTemplateColumns = containerEl.style.gridTemplateColumns; 39 | const currentColWidth = target.offsetParent.clientWidth; 40 | lastPos.current = lastPos.current ?? event.clientX; 41 | 42 | const diff = event.clientX - lastPos.current; 43 | 44 | if (!diff) return; 45 | 46 | const minResizeWidth = column.minResizeWidth ?? minColumnResizeWidth; 47 | let newColWidth = currentColWidth + diff; 48 | if (minResizeWidth && newColWidth < minResizeWidth) 49 | newColWidth = minResizeWidth; 50 | if (column.maxResizeWidth && column.maxResizeWidth < newColWidth) 51 | newColWidth = column.maxResizeWidth; 52 | 53 | const colIndex = visibleColumns.findIndex((cd) => cd.id === column.id); 54 | const gtcArr = gridTemplateColumns.split(" ").reduce((gtcArr, gtc) => { 55 | if ( 56 | gtcArr[gtcArr.length - 1] && 57 | gtcArr[gtcArr.length - 1][ 58 | gtcArr[gtcArr.length - 1].length - 1 59 | ] === "," 60 | ) { 61 | gtcArr[gtcArr.length - 1] = gtcArr[gtcArr.length - 1] + gtc; 62 | return gtcArr; 63 | } 64 | return gtcArr.concat(gtc); 65 | }, []); 66 | gtcArr[colIndex] = `${newColWidth}px`; 67 | 68 | containerEl.style.gridTemplateColumns = gtcArr.join(" "); 69 | 70 | lastPos.current = event.clientX; 71 | props.onColumnResize?.({ event, target, column }, tableManager); 72 | }; 73 | 74 | columnsResizeApi.onResizeEnd = ({ event, target, column }) => { 75 | setTimeout(() => (columnsResizeApi.isColumnResizing = false), 0); 76 | 77 | lastPos.current = null; 78 | const containerEl = tableRef.current; 79 | const gtcArr = containerEl.style.gridTemplateColumns 80 | .split(" ") 81 | .reduce((gtcArr, gtc) => { 82 | if ( 83 | gtcArr[gtcArr.length - 1] && 84 | gtcArr[gtcArr.length - 1][ 85 | gtcArr[gtcArr.length - 1].length - 1 86 | ] === "," 87 | ) { 88 | gtcArr[gtcArr.length - 1] = gtcArr[gtcArr.length - 1] + gtc; 89 | return gtcArr; 90 | } 91 | return gtcArr.concat(gtc); 92 | }, []); 93 | 94 | columns.forEach((column) => { 95 | if (!column.visible) return; 96 | 97 | const colIndex = visibleColumns.findIndex( 98 | (cd) => cd.id === column.id 99 | ); 100 | column.width = gtcArr[colIndex]; 101 | }); 102 | 103 | setColumns(columns); 104 | props.onColumnResizeEnd?.({ event, target, column }, tableManager); 105 | }; 106 | 107 | const useResizeRef = (column) => { 108 | const resizeHandleRef = useRef(null); 109 | 110 | useResizeEvents( 111 | resizeHandleRef, 112 | column, 113 | columnsResizeApi.onResizeStart, 114 | columnsResizeApi.onResize, 115 | columnsResizeApi.onResizeEnd 116 | ); 117 | 118 | return resizeHandleRef; 119 | }; 120 | 121 | columnsResizeApi.useResizeRef = useResizeRef; 122 | 123 | return columnsResizeApi; 124 | }; 125 | 126 | export default useColumnsResize; 127 | -------------------------------------------------------------------------------- /src/hooks/useRowSelection.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect, useMemo } from "react"; 2 | 3 | const useRowSelection = (props, tableManager) => { 4 | const { 5 | config: { rowIdField }, 6 | rowsApi: { rows }, 7 | paginationApi: { pageRows }, 8 | } = tableManager; 9 | 10 | const rowSelectionApi = useRef({}).current; 11 | const [selectedRowsIds, setSelectedRowsIds] = useState([]); 12 | 13 | rowSelectionApi.selectedRowsIds = props.selectedRowsIds ?? selectedRowsIds; 14 | rowSelectionApi.getIsRowSelectable = props.getIsRowSelectable; 15 | 16 | rowSelectionApi.setSelectedRowsIds = (newSelectedItems) => { 17 | if ( 18 | props.selectedRowsIds === undefined || 19 | props.onSelectedRowsChange === undefined 20 | ) 21 | setSelectedRowsIds(newSelectedItems); 22 | props.onSelectedRowsChange?.(newSelectedItems, tableManager); 23 | }; 24 | 25 | rowSelectionApi.toggleRowSelection = (rowId) => { 26 | const newSelectedRowsIds = [...rowSelectionApi.selectedRowsIds]; 27 | 28 | const itemIndex = newSelectedRowsIds.findIndex((s) => s === rowId); 29 | 30 | if (itemIndex !== -1) newSelectedRowsIds.splice(itemIndex, 1); 31 | else newSelectedRowsIds.push(rowId); 32 | 33 | rowSelectionApi.setSelectedRowsIds(newSelectedRowsIds); 34 | }; 35 | 36 | const selectAllRef = useRef(null); 37 | 38 | const { 39 | selectedRowsIds: selectedRows, 40 | setSelectedRowsIds: setSelectedRows, 41 | getIsRowSelectable, 42 | } = rowSelectionApi; 43 | 44 | rowSelectionApi.selectAll = useMemo(() => { 45 | const mode = props.selectAllMode; 46 | const allRows = mode === "all" ? rows : pageRows; 47 | const selectableItemsIds = allRows 48 | .filter((row) => row) 49 | .filter(getIsRowSelectable) 50 | .map((item) => item[rowIdField]); 51 | const checked = 52 | selectableItemsIds.length && 53 | selectableItemsIds.every((selectableItemId) => 54 | selectedRows.find((id) => selectableItemId === id) 55 | ); 56 | const disabled = !selectableItemsIds.length; 57 | const indeterminate = !!( 58 | selectedRows.length && 59 | !checked && 60 | selectableItemsIds.some((selectableItemId) => 61 | selectedRows.find((id) => selectableItemId === id) 62 | ) 63 | ); 64 | 65 | return { 66 | mode, 67 | ref: selectAllRef, 68 | checked, 69 | disabled, 70 | indeterminate, 71 | onChange: () => { 72 | let newSelectedRowsIds = [...selectedRows]; 73 | 74 | if (checked || indeterminate) 75 | newSelectedRowsIds = newSelectedRowsIds.filter( 76 | (si) => 77 | !selectableItemsIds.find((itemId) => si === itemId) 78 | ); 79 | else 80 | selectableItemsIds.forEach((s) => 81 | newSelectedRowsIds.push(s) 82 | ); 83 | 84 | setSelectedRows(newSelectedRowsIds); 85 | }, 86 | }; 87 | }, [ 88 | props.selectAllMode, 89 | rows, 90 | pageRows, 91 | getIsRowSelectable, 92 | selectedRows, 93 | rowIdField, 94 | setSelectedRows, 95 | ]); 96 | 97 | useEffect(() => { 98 | if (!selectAllRef.current) return; 99 | 100 | selectAllRef.current.indeterminate = 101 | rowSelectionApi.selectAll.indeterminate; 102 | }, [rowSelectionApi.selectAll.indeterminate]); 103 | 104 | // filter selectedRows if their ids no longer exist in the rows 105 | useEffect(() => { 106 | if (!tableManager.isInitialized) return; 107 | 108 | const filteredSelectedRows = rowSelectionApi.selectedRowsIds.filter( 109 | (selectedRowId) => 110 | tableManager.rowsApi.originalRows.find( 111 | (row, i) => 112 | (row[tableManager.config.rowIdField] || i) === 113 | selectedRowId 114 | ) 115 | ); 116 | if ( 117 | filteredSelectedRows.length !== 118 | rowSelectionApi.selectedRowsIds.length 119 | ) { 120 | rowSelectionApi.setSelectedRowsIds(filteredSelectedRows); 121 | } 122 | }, [ 123 | tableManager.config.rowIdField, 124 | tableManager.isInitialized, 125 | tableManager.rowEditApi, 126 | rowSelectionApi, 127 | tableManager.rowsApi.originalRows, 128 | ]); 129 | 130 | return rowSelectionApi; 131 | }; 132 | 133 | export default useRowSelection; 134 | -------------------------------------------------------------------------------- /demo/src/components/ColumnsControllers.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ControllerWrappper from "./ControllerWrappper"; 3 | 4 | const styles = { 5 | column: { 6 | display: "flex", 7 | flexDirection: "column", 8 | paddingBottom: 5, 9 | borderBottom: "1px solid #eee", 10 | }, 11 | label: { 12 | fontWeight: "bold", 13 | padding: "10px 0", 14 | color: "#125082", 15 | fontSize: 16, 16 | }, 17 | }; 18 | 19 | const ColumnsControllers = ({ controllers }) => { 20 | let columns = [...controllers.columns[0]]; 21 | const setColumns = controllers.columns[1]; 22 | 23 | const setLabel = (column, newLabel) => { 24 | column.label = newLabel; 25 | setColumns(columns); 26 | }; 27 | 28 | const setVisible = (column) => { 29 | column.visible = !column.visible; 30 | setColumns(columns); 31 | }; 32 | 33 | const setPinned = (column) => { 34 | column.pinned = !column.pinned; 35 | setColumns(columns); 36 | }; 37 | 38 | const setSearchable = (column) => { 39 | column.searchable = !column.searchable; 40 | setColumns(columns); 41 | }; 42 | 43 | const setSortable = (column) => { 44 | column.sortable = !column.sortable; 45 | setColumns(columns); 46 | }; 47 | 48 | const setEditable = (column) => { 49 | column.editable = !column.editable; 50 | setColumns(columns); 51 | }; 52 | 53 | const setResizable = (column) => { 54 | column.resizable = !column.resizable; 55 | setColumns(columns); 56 | }; 57 | 58 | return ( 59 | 60 | {columns.map((column, idx) => ( 61 |
62 | 63 | {column.label || column.id} 64 | 65 | {column.id !== "checkbox" && column.id !== "buttons" ? ( 66 | 67 | 71 | setLabel(column, e.target.value) 72 | } 73 | /> 74 | 75 | ) : null} 76 | 77 | setVisible(column)} 81 | /> 82 | 83 | {idx === 0 || idx === columns.length - 1 ? ( 84 | 85 | setPinned(column)} 89 | /> 90 | 91 | ) : null} 92 | {column.id !== "checkbox" && column.id !== "buttons" ? ( 93 | 94 | setSearchable(column)} 98 | /> 99 | 100 | ) : null} 101 | {column.id !== "checkbox" && column.id !== "buttons" ? ( 102 | 103 | setSortable(column)} 107 | /> 108 | 109 | ) : null} 110 | {column.id !== "checkbox" && column.id !== "buttons" ? ( 111 | 112 | setEditable(column)} 116 | /> 117 | 118 | ) : null} 119 | 120 | setResizable(column)} 124 | /> 125 | 126 |
127 | ))} 128 |
129 | ); 130 | }; 131 | 132 | export default ColumnsControllers; 133 | -------------------------------------------------------------------------------- /src/hooks/useAsync.jsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | import { uuid } from "../utils"; 3 | import { useRequestDebounce } from "."; 4 | 5 | function getRowsRequest(tableManager, rowsRequests) { 6 | const { 7 | config: { isPaginated, isVirtualScroll }, 8 | rowsApi: { totalRows }, 9 | searchApi: { searchText }, 10 | sortApi: { sort }, 11 | paginationApi: { page, pageSize }, 12 | rowVirtualizer: { virtualItems }, 13 | asyncApi: { batchSize }, 14 | } = tableManager; 15 | 16 | // get starting indexes (100, 100) 17 | let from = isPaginated ? (page - 1) * pageSize : 0; 18 | let to = from; 19 | 20 | // get exact indexes via virtualItems (113, 157) 21 | if (isVirtualScroll) { 22 | from += virtualItems[0]?.index || 0; 23 | to += virtualItems[virtualItems.length - 1]?.index || 0; 24 | } 25 | 26 | // get the required batch limits (100, 200) 27 | from -= from % batchSize; 28 | to += batchSize - (to % batchSize); 29 | 30 | // make sure "to" does not exceed "totalRows" 31 | if (rowsRequests.length) { 32 | to = Math.min(to, totalRows); 33 | } 34 | 35 | // make sure "from" does not overlap previous requests 36 | rowsRequests.forEach((request) => { 37 | if (request.from <= from && from <= request.to) { 38 | from = request.to; 39 | } 40 | }); 41 | 42 | // make sure "to" does not overlap previous requests 43 | // make sure no previous requests are between "from" & "to" 44 | rowsRequests 45 | .slice() 46 | .reverse() 47 | .find((request) => { 48 | if (request.from <= to && to <= request.to) { 49 | to = request.from; 50 | } 51 | if (from < request.from && request.to < to) { 52 | to = request.from; 53 | } 54 | }); 55 | 56 | // make sure "to" does not exceed "batchSize" 57 | to = Math.min(to, from + batchSize); 58 | 59 | return { 60 | from, 61 | to, 62 | searchText, 63 | sort, 64 | id: uuid(), 65 | }; 66 | } 67 | 68 | const useAsync = (props, tableManager) => { 69 | const { 70 | mode, 71 | config: { requestDebounceTimeout }, 72 | rowsApi: { rows, totalRows }, 73 | paginationApi: { pageSize }, 74 | searchApi: { validSearchText }, 75 | } = tableManager; 76 | 77 | const asyncApi = useRef({}).current; 78 | const rowsRequests = useRef([]); 79 | 80 | asyncApi.batchSize = props.batchSize ?? pageSize; 81 | asyncApi.isLoading = (() => { 82 | if (!rowsRequests.current.length) return true; 83 | if (totalRows === 0) return false; 84 | if (!rowsRequests.current.every((request) => rows[request.from])) 85 | return true; 86 | })(); 87 | 88 | const onRowsRequest = async (rowsRequest) => { 89 | rowsRequests.current = [...rowsRequests.current, rowsRequest]; 90 | asyncApi.lastRowsRequestId = rowsRequest.id; 91 | 92 | const result = await props.onRowsRequest(rowsRequest, tableManager); 93 | 94 | if ( 95 | !rowsRequests.current.find( 96 | (request) => request.id === rowsRequest.id 97 | ) 98 | ) 99 | return; 100 | 101 | const { 102 | rowsApi: { rows, setRows, setTotalRows }, 103 | } = tableManager; 104 | 105 | if (result?.rows) { 106 | const newRows = asyncApi.mergeRowsAt( 107 | rows, 108 | result.rows, 109 | rowsRequest.from 110 | ); 111 | setRows(newRows); 112 | } 113 | if (result?.totalRows !== undefined) setTotalRows(result.totalRows); 114 | }; 115 | 116 | const debouncedOnRowsRequest = useRequestDebounce( 117 | onRowsRequest, 118 | requestDebounceTimeout 119 | ); 120 | 121 | asyncApi.resetRows = () => { 122 | if (mode === "sync") return; 123 | 124 | const { 125 | rowsApi: { setRows, setTotalRows }, 126 | rowSelectionApi: { setSelectedRowsIds }, 127 | rowEditApi: { editRow, setEditRowId }, 128 | } = tableManager; 129 | 130 | setSelectedRowsIds([]); 131 | if (editRow) setEditRowId(null); 132 | 133 | rowsRequests.current = []; 134 | if (props.onRowsReset) props.onRowsReset(tableManager); 135 | else { 136 | setRows([]); 137 | setTotalRows(null); 138 | } 139 | }; 140 | 141 | asyncApi.mergeRowsAt = (rows, newRows, at) => { 142 | const holes = []; 143 | holes.length = Math.max(at - rows.length, 0); 144 | holes.fill(null); 145 | 146 | rows = rows.concat(holes); 147 | rows.splice(at, newRows.length, ...newRows); 148 | return rows; 149 | }; 150 | 151 | // reset rows 152 | useEffect(() => { 153 | if (!tableManager.isInitialized) return; 154 | if (mode === "sync") return; 155 | 156 | asyncApi.resetRows(); 157 | }, [ 158 | validSearchText, 159 | asyncApi, 160 | mode, 161 | tableManager.isInitialized, 162 | tableManager.sortApi.sort.colId, 163 | tableManager.sortApi.sort.isAsc, 164 | ]); 165 | 166 | useEffect(() => { 167 | if (mode === "sync") return; 168 | 169 | const rowsRequest = getRowsRequest(tableManager, rowsRequests.current); 170 | 171 | if (rowsRequest.to <= rowsRequest.from) return; 172 | 173 | const isFirstRequest = !rowsRequests.current.length; 174 | if (isFirstRequest) onRowsRequest(rowsRequest); 175 | else debouncedOnRowsRequest(rowsRequest); 176 | }); 177 | 178 | return asyncApi; 179 | }; 180 | 181 | export default useAsync; 182 | -------------------------------------------------------------------------------- /demo/src/views/sync.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import GridTable from "../../../src"; 4 | import { ControllersDrawer } from "../components"; 5 | import getColumns from "../getColumns"; 6 | import MOCK_DATA from "../MOCK_DATA.json"; 7 | import "../index.css"; 8 | 9 | const MyAwesomeTable = () => { 10 | const [, setTableManager] = useState(null); 11 | const [rowsData, setRowsData] = useState([]); 12 | const [isLoading, setLoading] = useState(false); 13 | const [editRowId, setEditRowId] = useState(null); 14 | let [searchText, setSearchText] = useState(""); 15 | let [selectedRowsIds, setSelectedRowsIds] = useState([]); 16 | let [sort, setSort] = useState({ colId: null, isAsc: true }); 17 | let [page, setPage] = useState(1); 18 | let [pageSize, setPageSize] = useState(20); 19 | let [pageSizes, setPageSizes] = useState([20, 50, 100]); 20 | let [enableColumnsReorder, setEnableColumnsReorder] = useState(true); 21 | let [highlightSearch, setHighlightSearch] = useState(true); 22 | let [showSearch, setShowSearch] = useState(true); 23 | let [showRowsInformation, setShowRowsInformation] = useState(true); 24 | let [showColumnVisibilityManager, setShowColumnVisibilityManager] = 25 | useState(true); 26 | let [isHeaderSticky, setIsHeaderSticky] = useState(true); 27 | let [isVirtualScroll, setIsVirtualScroll] = useState(true); 28 | let [isPaginated, setIsPaginated] = useState(true); 29 | let [minSearchChars, setMinSearchChars] = useState(2); 30 | let [minColumnResizeWidth, setMinColumnWidth] = useState(70); 31 | let [columns, setColumns] = useState(getColumns({ setRowsData })); 32 | let [isSettingsOpen, setIsSettingsOpen] = useState(false); 33 | let [selectAllMode, setSelectAllMode] = useState("page"); 34 | 35 | const controllers = { 36 | columns: [columns, setColumns], 37 | editRowId: [editRowId, setEditRowId], 38 | searchText: [searchText, setSearchText], 39 | selectedRowsIds: [selectedRowsIds, setSelectedRowsIds], 40 | sort: [sort, setSort], 41 | page: [page, setPage], 42 | pageSize: [pageSize, setPageSize], 43 | pageSizes: [pageSizes, setPageSizes], 44 | enableColumnsReorder: [enableColumnsReorder, setEnableColumnsReorder], 45 | highlightSearch: [highlightSearch, setHighlightSearch], 46 | showSearch: [showSearch, setShowSearch], 47 | showRowsInformation: [showRowsInformation, setShowRowsInformation], 48 | showColumnVisibilityManager: [ 49 | showColumnVisibilityManager, 50 | setShowColumnVisibilityManager, 51 | ], 52 | isHeaderSticky: [isHeaderSticky, setIsHeaderSticky], 53 | isVirtualScroll: [isVirtualScroll, setIsVirtualScroll], 54 | isPaginated: [isPaginated, setIsPaginated], 55 | minSearchChars: [minSearchChars, setMinSearchChars], 56 | minColumnResizeWidth: [minColumnResizeWidth, setMinColumnWidth], 57 | selectAllMode: [selectAllMode, setSelectAllMode], 58 | }; 59 | 60 | useEffect(() => { 61 | setLoading(true); 62 | setTimeout(() => { 63 | setRowsData(MOCK_DATA); 64 | setLoading(false); 65 | }, 1500); 66 | }, []); 67 | 68 | return ( 69 |
70 | 75 |
76 | 86 | !isEdit && 87 | tableManager.rowSelectionApi.getIsRowSelectable( 88 | data.id 89 | ) && 90 | tableManager.rowSelectionApi.toggleRowSelection(data.id) 91 | } 92 | style={{ 93 | boxShadow: "rgb(0 0 0 / 30%) 0px 40px 40px -20px", 94 | border: "none", 95 | }} 96 | onLoad={setTableManager} 97 | searchText={searchText} 98 | onSearchTextChange={setSearchText} 99 | sort={sort} 100 | onSortChange={setSort} 101 | page={page} 102 | onPageChange={setPage} 103 | pageSize={pageSize} 104 | onPageSizeChange={setPageSize} 105 | pageSizes={pageSizes} 106 | enableColumnsReorder={enableColumnsReorder} 107 | highlightSearch={highlightSearch} 108 | showSearch={showSearch} 109 | showRowsInformation={showRowsInformation} 110 | showColumnVisibilityManager={showColumnVisibilityManager} 111 | isHeaderSticky={isHeaderSticky} 112 | isVirtualScroll={isVirtualScroll} 113 | isPaginated={isPaginated} 114 | minSearchChars={minSearchChars} 115 | minColumnResizeWidth={minColumnResizeWidth} 116 | selectAllMode={selectAllMode} 117 | /> 118 |
119 |
120 | ); 121 | }; 122 | 123 | export default MyAwesomeTable; 124 | 125 | ReactDOM.render(, document.getElementById("root")); 126 | -------------------------------------------------------------------------------- /demo/src/views/async.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import GridTable from "../../../src"; 4 | import { ControllersDrawer } from "../components"; 5 | import getColumns from "../getColumns"; 6 | import MOCK_DATA from "../MOCK_DATA.json"; 7 | import "../index.css"; 8 | 9 | const MyAwesomeTable = () => { 10 | const tableManager = useRef(null); 11 | const setTableManager = (tm) => (tableManager.current = tm); 12 | const [editRowId, setEditRowId] = useState(null); 13 | let [searchText, setSearchText] = useState(""); 14 | let [selectedRowsIds, setSelectedRowsIds] = useState([]); 15 | let [sort, setSort] = useState({ colId: null, isAsc: true }); 16 | let [page, setPage] = useState(1); 17 | let [pageSize, setPageSize] = useState(20); 18 | let [pageSizes, setPageSizes] = useState([20, 50, 100]); 19 | let [enableColumnsReorder, setEnableColumnsReorder] = useState(true); 20 | let [highlightSearch, setHighlightSearch] = useState(true); 21 | let [showSearch, setShowSearch] = useState(true); 22 | let [showRowsInformation, setShowRowsInformation] = useState(true); 23 | let [showColumnVisibilityManager, setShowColumnVisibilityManager] = 24 | useState(true); 25 | let [isHeaderSticky, setIsHeaderSticky] = useState(true); 26 | let [isVirtualScroll, setIsVirtualScroll] = useState(true); 27 | let [isPaginated, setIsPaginated] = useState(true); 28 | let [minSearchChars, setMinSearchChars] = useState(2); 29 | let [minColumnResizeWidth, setMinColumnWidth] = useState(70); 30 | let [columns, setColumns] = useState( 31 | getColumns({ 32 | setRowsData: (newRows) => 33 | tableManager.current.rowsApi.setRows(newRows), 34 | }) 35 | ); 36 | let [isSettingsOpen, setIsSettingsOpen] = useState(false); 37 | let [selectAllMode, setSelectAllMode] = useState("page"); 38 | 39 | const controllers = { 40 | columns: [columns, setColumns], 41 | editRowId: [editRowId, setEditRowId], 42 | searchText: [searchText, setSearchText], 43 | selectedRowsIds: [selectedRowsIds, setSelectedRowsIds], 44 | sort: [sort, setSort], 45 | page: [page, setPage], 46 | pageSize: [pageSize, setPageSize], 47 | pageSizes: [pageSizes, setPageSizes], 48 | enableColumnsReorder: [enableColumnsReorder, setEnableColumnsReorder], 49 | highlightSearch: [highlightSearch, setHighlightSearch], 50 | showSearch: [showSearch, setShowSearch], 51 | showRowsInformation: [showRowsInformation, setShowRowsInformation], 52 | showColumnVisibilityManager: [ 53 | showColumnVisibilityManager, 54 | setShowColumnVisibilityManager, 55 | ], 56 | isHeaderSticky: [isHeaderSticky, setIsHeaderSticky], 57 | isVirtualScroll: [isVirtualScroll, setIsVirtualScroll], 58 | isPaginated: [isPaginated, setIsPaginated], 59 | minSearchChars: [minSearchChars, setMinSearchChars], 60 | minColumnResizeWidth: [minColumnResizeWidth, setMinColumnWidth], 61 | selectAllMode: [selectAllMode, setSelectAllMode], 62 | }; 63 | 64 | const onRowsRequest = async (requestData, tableManager) => { 65 | let { 66 | sortApi: { sortRows }, 67 | searchApi: { searchRows }, 68 | } = tableManager; 69 | 70 | let allRows = MOCK_DATA; 71 | allRows = searchRows(allRows); 72 | allRows = sortRows(allRows); 73 | 74 | await new Promise((r) => setTimeout(r, 1300)); 75 | 76 | return { 77 | rows: allRows.slice(requestData.from, requestData.to), 78 | totalRows: allRows.length, 79 | }; 80 | }; 81 | 82 | return ( 83 |
84 | 89 |
90 |
Async
91 | 125 |
126 |
127 | ); 128 | }; 129 | 130 | export default MyAwesomeTable; 131 | 132 | ReactDOM.render(, document.getElementById("root")); 133 | -------------------------------------------------------------------------------- /src/components/CellContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from "react"; 2 | import { getHighlightedText } from "../utils"; 3 | 4 | const CellContainer = ({ 5 | rowId, 6 | data, 7 | column, 8 | rowIndex, 9 | colIndex, 10 | isEdit, 11 | disableSelection, 12 | isSelected, 13 | tableManager, 14 | forwardRef, 15 | }) => { 16 | let { 17 | id, 18 | config: { 19 | highlightSearch, 20 | tableHasSelection, 21 | additionalProps: { cellContainer: additionalProps = {} }, 22 | }, 23 | rowsApi: { onRowClick }, 24 | rowEditApi: { editRow, setEditRow }, 25 | rowSelectionApi: { toggleRowSelection }, 26 | searchApi: { searchText, valuePassesSearch }, 27 | columnsApi: { visibleColumns }, 28 | } = tableManager; 29 | 30 | const getClassNames = () => { 31 | let classNames; 32 | const all = `rgt-cell rgt-row-${rowIndex} rgt-row-${ 33 | rowIndex % 2 === 0 ? "even" : "odd" 34 | }${isSelected ? " rgt-row-selected" : ""}${ 35 | isEdit ? " rgt-row-edit" : "" 36 | } ${additionalProps.className || ""}`.trim(); 37 | const virtualDefault = `${ 38 | !tableHasSelection 39 | ? "" 40 | : disableSelection 41 | ? " rgt-row-not-selectable" 42 | : " rgt-row-selectable" 43 | }`; 44 | const checkboxDefault = `${ 45 | column.pinned && colIndex === 0 46 | ? " rgt-cell-pinned rgt-cell-pinned-left" 47 | : "" 48 | }${ 49 | column.pinned && colIndex === visibleColumns.length - 1 50 | ? " rgt-cell-pinned rgt-cell-pinned-right" 51 | : "" 52 | } ${column.className}`.trim(); 53 | 54 | switch (column.id) { 55 | case "virtual": 56 | classNames = `${all} rgt-cell-virtual ${virtualDefault}`; 57 | break; 58 | case "checkbox": 59 | classNames = `${all} rgt-cell-checkbox ${checkboxDefault}`; 60 | break; 61 | default: 62 | classNames = `${all} rgt-cell-${column.field} ${virtualDefault} ${checkboxDefault}`; 63 | } 64 | 65 | return classNames; 66 | }; 67 | 68 | const textValue = useMemo( 69 | () => 70 | data && 71 | column 72 | .getValue?.({ 73 | tableManager, 74 | value: isEdit ? editRow[column.field] : data[column.field], 75 | column, 76 | rowData: data, 77 | }) 78 | ?.toString?.(), 79 | [column, data, editRow, isEdit, tableManager] 80 | ); 81 | 82 | const getValue = () => { 83 | let value; 84 | 85 | switch (column.id) { 86 | case "checkbox": 87 | value = isSelected; 88 | break; 89 | default: 90 | value = textValue; 91 | if ( 92 | !isEdit && 93 | highlightSearch && 94 | valuePassesSearch(value, column) 95 | ) 96 | return getHighlightedText(value, searchText); 97 | } 98 | 99 | return value; 100 | }; 101 | 102 | const onMouseOver = useCallback( 103 | (event) => { 104 | document 105 | .querySelectorAll(`#${id} .rgt-row-${rowIndex}`) 106 | .forEach((cell) => cell.classList.add("rgt-row-hover")); 107 | additionalProps.onMouseOver?.(event); 108 | }, 109 | [id, rowIndex, additionalProps] 110 | ); 111 | 112 | const onMouseOut = useCallback( 113 | (event) => { 114 | document 115 | .querySelectorAll(`#${id} .rgt-row-${rowIndex}`) 116 | .forEach((cell) => cell.classList.remove("rgt-row-hover")); 117 | additionalProps.onMouseOut?.(event); 118 | }, 119 | [id, rowIndex, additionalProps] 120 | ); 121 | 122 | if (data && onRowClick) { 123 | additionalProps = { 124 | onClick: (event) => 125 | onRowClick( 126 | { rowIndex, data, column, isEdit, event }, 127 | tableManager 128 | ), 129 | ...additionalProps, 130 | }; 131 | } 132 | 133 | let classNames = getClassNames(); 134 | let value = getValue(); 135 | 136 | let cellProps = { 137 | tableManager, 138 | value, 139 | textValue, 140 | data, 141 | column, 142 | colIndex, 143 | rowIndex, 144 | }; 145 | const isFirstEditableCell = useMemo( 146 | () => 147 | visibleColumns.findIndex( 148 | (visibleColumn) => 149 | visibleColumn.id !== "checkbox" && 150 | visibleColumn.editable !== false 151 | ) === colIndex, 152 | [visibleColumns, colIndex] 153 | ); 154 | 155 | return ( 156 |
166 | {column.id === "virtual" 167 | ? null 168 | : column.id === "checkbox" 169 | ? column.cellRenderer({ 170 | ...cellProps, 171 | onChange: () => toggleRowSelection(rowId), 172 | disabled: disableSelection, 173 | }) 174 | : !data 175 | ? column.placeHolderRenderer(cellProps) 176 | : column.editable && isEdit 177 | ? column.editorCellRenderer({ 178 | ...cellProps, 179 | onChange: setEditRow, 180 | isFirstEditableCell, 181 | }) 182 | : column.cellRenderer(cellProps)} 183 |
184 | ); 185 | }; 186 | 187 | export default CellContainer; 188 | -------------------------------------------------------------------------------- /demo/src/views/asyncControlled.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import GridTable from "../../../src"; 4 | import { ControllersDrawer } from "../components"; 5 | import getColumns from "../getColumns"; 6 | import MOCK_DATA from "../MOCK_DATA.json"; 7 | import "../index.css"; 8 | 9 | const MyAwesomeTable = () => { 10 | const tableManager = useRef(null); 11 | const setTableManager = (tm) => (tableManager.current = tm); 12 | const [rows, setRows] = useState([]); 13 | const [totalRows, setTotalRows] = useState(null); 14 | const [editRowId, setEditRowId] = useState(null); 15 | let [searchText, setSearchText] = useState(""); 16 | let [selectedRowsIds, setSelectedRowsIds] = useState([]); 17 | let [sort, setSort] = useState({ colId: null, isAsc: true }); 18 | let [page, setPage] = useState(1); 19 | let [pageSize, setPageSize] = useState(20); 20 | let [pageSizes, setPageSizes] = useState([20, 50, 100]); 21 | let [enableColumnsReorder, setEnableColumnsReorder] = useState(true); 22 | let [highlightSearch, setHighlightSearch] = useState(true); 23 | let [showSearch, setShowSearch] = useState(true); 24 | let [showRowsInformation, setShowRowsInformation] = useState(true); 25 | let [showColumnVisibilityManager, setShowColumnVisibilityManager] = 26 | useState(true); 27 | let [isHeaderSticky, setIsHeaderSticky] = useState(true); 28 | let [isVirtualScroll, setIsVirtualScroll] = useState(true); 29 | let [isPaginated, setIsPaginated] = useState(true); 30 | let [minSearchChars, setMinSearchChars] = useState(2); 31 | let [minColumnResizeWidth, setMinColumnWidth] = useState(70); 32 | let [columns, setColumns] = useState( 33 | getColumns({ 34 | setRowsData: (newRows) => 35 | tableManager.current.rowsApi.setRows(newRows), 36 | }) 37 | ); 38 | let [isSettingsOpen, setIsSettingsOpen] = useState(false); 39 | let [selectAllMode, setSelectAllMode] = useState("page"); 40 | 41 | const controllers = { 42 | columns: [columns, setColumns], 43 | editRowId: [editRowId, setEditRowId], 44 | searchText: [searchText, setSearchText], 45 | selectedRowsIds: [selectedRowsIds, setSelectedRowsIds], 46 | sort: [sort, setSort], 47 | page: [page, setPage], 48 | pageSize: [pageSize, setPageSize], 49 | pageSizes: [pageSizes, setPageSizes], 50 | enableColumnsReorder: [enableColumnsReorder, setEnableColumnsReorder], 51 | highlightSearch: [highlightSearch, setHighlightSearch], 52 | showSearch: [showSearch, setShowSearch], 53 | showRowsInformation: [showRowsInformation, setShowRowsInformation], 54 | showColumnVisibilityManager: [ 55 | showColumnVisibilityManager, 56 | setShowColumnVisibilityManager, 57 | ], 58 | isHeaderSticky: [isHeaderSticky, setIsHeaderSticky], 59 | isVirtualScroll: [isVirtualScroll, setIsVirtualScroll], 60 | isPaginated: [isPaginated, setIsPaginated], 61 | minSearchChars: [minSearchChars, setMinSearchChars], 62 | minColumnResizeWidth: [minColumnResizeWidth, setMinColumnWidth], 63 | selectAllMode: [selectAllMode, setSelectAllMode], 64 | }; 65 | 66 | const onRowsRequest = async (requestData, tableManager) => { 67 | let { 68 | sortApi: { sortRows }, 69 | searchApi: { searchRows }, 70 | } = tableManager; 71 | 72 | let allRows = MOCK_DATA; 73 | allRows = searchRows(allRows); 74 | allRows = sortRows(allRows); 75 | 76 | await new Promise((r) => setTimeout(r, 1300)); 77 | 78 | return { 79 | rows: allRows.slice(requestData.from, requestData.to), 80 | totalRows: allRows.length, 81 | }; 82 | }; 83 | 84 | return ( 85 |
86 | 91 |
92 |
93 | Async Controlled 94 |
95 | 133 |
134 |
135 | ); 136 | }; 137 | 138 | export default MyAwesomeTable; 139 | 140 | ReactDOM.render(, document.getElementById("root")); 141 | -------------------------------------------------------------------------------- /demo/src/views/asyncManaged.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import GridTable from "../../../src"; 4 | import { ControllersDrawer } from "../components"; 5 | import getColumns from "../getColumns"; 6 | import MOCK_DATA from "../MOCK_DATA.json"; 7 | import "../index.css"; 8 | 9 | const MyAwesomeTable = () => { 10 | const tableManager = useRef(null); 11 | const setTableManager = (tm) => (tableManager.current = tm); 12 | const [rows, setRows] = useState([]); 13 | const rowsRef = useRef([]); 14 | const [totalRows, setTotalRows] = useState(null); 15 | const [editRowId, setEditRowId] = useState(null); 16 | let [searchText, setSearchText] = useState(""); 17 | let [selectedRowsIds, setSelectedRowsIds] = useState([]); 18 | let [sort, setSort] = useState({ colId: null, isAsc: true }); 19 | let [page, setPage] = useState(1); 20 | let [pageSize, setPageSize] = useState(20); 21 | let [pageSizes, setPageSizes] = useState([20, 50, 100]); 22 | let [enableColumnsReorder, setEnableColumnsReorder] = useState(true); 23 | let [highlightSearch, setHighlightSearch] = useState(true); 24 | let [showSearch, setShowSearch] = useState(true); 25 | let [showRowsInformation, setShowRowsInformation] = useState(true); 26 | let [showColumnVisibilityManager, setShowColumnVisibilityManager] = 27 | useState(true); 28 | let [isHeaderSticky, setIsHeaderSticky] = useState(true); 29 | let [isVirtualScroll, setIsVirtualScroll] = useState(true); 30 | let [isPaginated, setIsPaginated] = useState(true); 31 | let [minSearchChars, setMinSearchChars] = useState(2); 32 | let [minColumnResizeWidth, setMinColumnWidth] = useState(70); 33 | let [columns, setColumns] = useState( 34 | getColumns({ 35 | setRowsData: (newRows) => { 36 | rowsRef.current = newRows; 37 | setRows(rowsRef.current); 38 | }, 39 | }) 40 | ); 41 | let [isSettingsOpen, setIsSettingsOpen] = useState(false); 42 | let [selectAllMode, setSelectAllMode] = useState("page"); 43 | 44 | const controllers = { 45 | columns: [columns, setColumns], 46 | editRowId: [editRowId, setEditRowId], 47 | searchText: [searchText, setSearchText], 48 | selectedRowsIds: [selectedRowsIds, setSelectedRowsIds], 49 | sort: [sort, setSort], 50 | page: [page, setPage], 51 | pageSize: [pageSize, setPageSize], 52 | pageSizes: [pageSizes, setPageSizes], 53 | enableColumnsReorder: [enableColumnsReorder, setEnableColumnsReorder], 54 | highlightSearch: [highlightSearch, setHighlightSearch], 55 | showSearch: [showSearch, setShowSearch], 56 | showRowsInformation: [showRowsInformation, setShowRowsInformation], 57 | showColumnVisibilityManager: [ 58 | showColumnVisibilityManager, 59 | setShowColumnVisibilityManager, 60 | ], 61 | isHeaderSticky: [isHeaderSticky, setIsHeaderSticky], 62 | isVirtualScroll: [isVirtualScroll, setIsVirtualScroll], 63 | isPaginated: [isPaginated, setIsPaginated], 64 | minSearchChars: [minSearchChars, setMinSearchChars], 65 | minColumnResizeWidth: [minColumnResizeWidth, setMinColumnWidth], 66 | selectAllMode: [selectAllMode, setSelectAllMode], 67 | }; 68 | 69 | const onRowsRequest = async (requestData, tableManager) => { 70 | const { 71 | sortApi: { sortRows }, 72 | searchApi: { searchRows }, 73 | asyncApi: { mergeRowsAt }, 74 | } = tableManager; 75 | 76 | let allRows = MOCK_DATA; 77 | allRows = searchRows(allRows); 78 | allRows = sortRows(allRows); 79 | 80 | await new Promise((r) => setTimeout(r, 1300)); 81 | 82 | rowsRef.current = [ 83 | ...mergeRowsAt( 84 | rowsRef.current, 85 | allRows.slice(requestData.from, requestData.to), 86 | requestData.from 87 | ), 88 | ]; 89 | setRows(rowsRef.current); 90 | setTotalRows(allRows.length); 91 | }; 92 | 93 | const onRowsReset = () => { 94 | rowsRef.current = []; 95 | setRows(rowsRef.current); 96 | setTotalRows(null); 97 | }; 98 | 99 | var a = ( 100 |
101 | 106 |
107 |
108 | Async Managed 109 |
110 | 147 |
148 |
149 | ); 150 | 151 | return a; 152 | }; 153 | 154 | export default MyAwesomeTable; 155 | 156 | ReactDOM.render(, document.getElementById("root")); 157 | -------------------------------------------------------------------------------- /src/components/HeaderCellContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SortableElement, SortableHandle } from "../drag-and-drop"; 3 | 4 | const SortableItem = ({ children, columnId, className }, ref) => ( 5 |
6 | {children} 7 |
8 | ); 9 | 10 | const SortableElementItem = SortableElement(React.forwardRef(SortableItem)); 11 | 12 | const DragHandleContainer = ({ children }, ref) => ( 13 | {children} 14 | ); 15 | 16 | const SortableDragHandle = SortableHandle( 17 | React.forwardRef(DragHandleContainer) 18 | ); 19 | 20 | const HeaderCellContainer = ({ index, column, tableManager }) => { 21 | let { 22 | config: { 23 | isHeaderSticky, 24 | components: { DragHandle }, 25 | additionalProps: { headerCellContainer: additionalProps = {} }, 26 | icons: { 27 | sortAscending: sortAscendingIcon, 28 | sortDescending: sortDescendingIcon, 29 | }, 30 | }, 31 | sortApi: { sort, toggleSort }, 32 | columnsApi: { visibleColumns }, 33 | config: { enableColumnsReorder }, 34 | columnsResizeApi: { useResizeRef }, 35 | rowSelectionApi: { selectAll: selectionProps }, 36 | } = tableManager; 37 | 38 | let resizeHandleRef = useResizeRef(column); 39 | 40 | const getClassNames = () => { 41 | let classNames; 42 | 43 | switch (column.id) { 44 | case "virtual": 45 | classNames = `rgt-cell-header rgt-cell-header-virtual-col${ 46 | isHeaderSticky ? " rgt-cell-header-sticky" : "" 47 | }`.trim(); 48 | break; 49 | default: 50 | classNames = `rgt-cell-header rgt-cell-header-${ 51 | column.id === "checkbox" ? "checkbox" : column.field 52 | }${ 53 | column.sortable !== false && 54 | column.id !== "checkbox" && 55 | column.id !== "virtual" 56 | ? " rgt-clickable" 57 | : "" 58 | }${ 59 | column.sortable !== false && column.id !== "checkbox" 60 | ? " rgt-cell-header-sortable" 61 | : " rgt-cell-header-not-sortable" 62 | }${ 63 | isHeaderSticky 64 | ? " rgt-cell-header-sticky" 65 | : " rgt-cell-header-not-sticky" 66 | }${ 67 | column.resizable !== false 68 | ? " rgt-cell-header-resizable" 69 | : " rgt-cell-header-not-resizable" 70 | }${ 71 | column.searchable !== false && column.id !== "checkbox" 72 | ? " rgt-cell-header-searchable" 73 | : " rgt-cell-header-not-searchable" 74 | }${ 75 | isPinnedLeft 76 | ? " rgt-cell-header-pinned rgt-cell-header-pinned-left" 77 | : "" 78 | }${ 79 | isPinnedRight 80 | ? " rgt-cell-header-pinned rgt-cell-header-pinned-right" 81 | : "" 82 | } ${column.className}`.trim(); 83 | } 84 | 85 | return ( 86 | classNames.trim() + 87 | " " + 88 | (additionalProps.className || "") 89 | ).trim(); 90 | }; 91 | 92 | const getAdditionalProps = () => { 93 | let mergedProps = { 94 | ...additionalProps, 95 | }; 96 | if (column.sortable) { 97 | let onClick = additionalProps.onClick; 98 | mergedProps.onClick = (e) => { 99 | toggleSort(column.id); 100 | onClick?.(e); 101 | }; 102 | } 103 | 104 | return mergedProps; 105 | }; 106 | 107 | let isPinnedRight = column.pinned && index === visibleColumns.length - 1; 108 | let isPinnedLeft = column.pinned && index === 0; 109 | let classNames = getClassNames(); 110 | let innerCellClassNames = `rgt-cell-header-inner${ 111 | column.id === "checkbox" ? " rgt-cell-header-inner-checkbox" : "" 112 | }${!isPinnedRight ? " rgt-cell-header-inner-not-pinned-right" : ""}`; 113 | additionalProps = getAdditionalProps(); 114 | 115 | let headerCellProps = { tableManager, column }; 116 | 117 | return ( 118 |
123 | {column.id === "virtual" ? null : ( 124 | 125 | 136 | {DragHandle && !isPinnedLeft && !isPinnedRight ? ( 137 | 138 | {} 139 | 140 | ) : null} 141 | {column.id === "checkbox" 142 | ? column.headerCellRenderer({ 143 | ...headerCellProps, 144 | ...selectionProps, 145 | }) 146 | : column.headerCellRenderer(headerCellProps)} 147 | {sort.colId !== column.id ? null : sort.isAsc ? ( 148 | 149 | {sortAscendingIcon} 150 | 151 | ) : ( 152 | 153 | {sortDescendingIcon} 154 | 155 | )} 156 | 157 | {column.resizable ? ( 158 | { 162 | event.preventDefault(); 163 | event.stopPropagation(); 164 | }} 165 | > 166 | ) : null} 167 | 168 | )} 169 |
170 | ); 171 | }; 172 | 173 | export default HeaderCellContainer; 174 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SortableContainer } from "./drag-and-drop"; 3 | import { Row, HeaderCellContainer } from "./components/"; 4 | import { useTableManager } from "./hooks/"; 5 | import PropTypes from "prop-types"; 6 | import "./index.css"; 7 | 8 | const SortableList = SortableContainer( 9 | ({ forwardRef, className, style, children }) => ( 10 |
11 | {children} 12 |
13 | ) 14 | ); 15 | 16 | const GridTable = (props) => { 17 | const tableManager = useTableManager(props); 18 | 19 | const { 20 | id, 21 | isLoading, 22 | config: { 23 | isVirtualScroll, 24 | rowIdField, 25 | components: { Header, Footer, Loader, NoResults, DragHandle }, 26 | }, 27 | refs: { rgtRef, tableRef }, 28 | columnsApi: { visibleColumns }, 29 | columnsReorderApi: { onColumnReorderStart, onColumnReorderEnd }, 30 | rowVirtualizer: { virtualItems }, 31 | paginationApi: { pageRows }, 32 | rowsApi: { totalRows }, 33 | } = tableManager; 34 | 35 | const rest = Object.keys(props).reduce((rest, key) => { 36 | if (GridTable.propTypes[key] === undefined) 37 | rest = { ...rest, [key]: props[key] }; 38 | return rest; 39 | }, {}); 40 | 41 | const classNames = ("rgt-wrapper " + (props.className || "")).trim(); 42 | 43 | return ( 44 |
45 |
46 | tableRef} 49 | className="rgt-container" 50 | axis="x" 51 | lockToContainerEdges 52 | distance={10} 53 | lockAxis="x" 54 | useDragHandle={!!DragHandle} 55 | onSortStart={onColumnReorderStart} 56 | onSortEnd={onColumnReorderEnd} 57 | style={{ 58 | display: "grid", 59 | overflow: "auto", 60 | flex: 1, 61 | gridTemplateColumns: visibleColumns 62 | .map((column) => column.width) 63 | .join(" "), 64 | gridTemplateRows: `repeat(${ 65 | pageRows.length + 1 + (isVirtualScroll ? 1 : 0) 66 | }, max-content)`, 67 | }} 68 | > 69 | {visibleColumns.map((visibleColumn, idx) => ( 70 | 76 | ))} 77 | {totalRows && visibleColumns.length > 1 78 | ? isVirtualScroll 79 | ? [ 80 | , 85 | ...virtualItems.map((virtualizedRow) => ( 86 | 93 | )), 94 | , 99 | ] 100 | : pageRows.map((rowData, index) => ( 101 | 107 | )) 108 | : null} 109 | 110 | {!totalRows || !visibleColumns.length ? ( 111 |
112 | {isLoading ? ( 113 | 114 | ) : ( 115 | 116 | )} 117 |
118 | ) : null} 119 |
120 |
121 | ); 122 | }; 123 | 124 | GridTable.defaultProps = { 125 | columns: [], 126 | rowIdField: "id", 127 | minColumnResizeWidth: 70, 128 | pageSizes: [20, 50, 100], 129 | isHeaderSticky: true, 130 | highlightSearch: true, 131 | minSearchChars: 2, 132 | isPaginated: true, 133 | isVirtualScroll: true, 134 | showSearch: true, 135 | showRowsInformation: true, 136 | showColumnVisibilityManager: true, 137 | enableColumnsReorder: true, 138 | requestDebounceTimeout: 300, 139 | getIsRowSelectable: () => true, 140 | getIsRowEditable: () => true, 141 | selectAllMode: "page", // ['page', 'all'] 142 | }; 143 | 144 | GridTable.propTypes = { 145 | // general 146 | columns: PropTypes.arrayOf(PropTypes.object).isRequired, 147 | rows: PropTypes.arrayOf(PropTypes.object), 148 | selectedRowsIds: PropTypes.array, 149 | searchText: PropTypes.string, 150 | getIsRowSelectable: PropTypes.func, 151 | getIsRowEditable: PropTypes.func, 152 | editRowId: PropTypes.any, 153 | // table config 154 | rowIdField: PropTypes.string, 155 | batchSize: PropTypes.number, 156 | isPaginated: PropTypes.bool, 157 | enableColumnsReorder: PropTypes.bool, 158 | pageSizes: PropTypes.arrayOf(PropTypes.number), 159 | pageSize: PropTypes.number, 160 | page: PropTypes.number, 161 | sort: PropTypes.object, 162 | minColumnResizeWidth: PropTypes.number, 163 | highlightSearch: PropTypes.bool, 164 | showSearch: PropTypes.bool, 165 | showRowsInformation: PropTypes.bool, 166 | showColumnVisibilityManager: PropTypes.bool, 167 | minSearchChars: PropTypes.number, 168 | isLoading: PropTypes.bool, 169 | isHeaderSticky: PropTypes.bool, 170 | isVirtualScroll: PropTypes.bool, 171 | icons: PropTypes.object, 172 | texts: PropTypes.object, 173 | additionalProps: PropTypes.object, 174 | components: PropTypes.object, 175 | totalRows: PropTypes.number, 176 | requestDebounceTimeout: PropTypes.number, 177 | selectAllMode: PropTypes.string, 178 | // events 179 | onColumnsChange: PropTypes.func, 180 | onSearchTextChange: PropTypes.func, 181 | onSelectedRowsChange: PropTypes.func, 182 | onSortChange: PropTypes.func, 183 | onRowClick: PropTypes.func, 184 | onEditRowIdChange: PropTypes.func, 185 | onPageChange: PropTypes.func, 186 | onPageSizeChange: PropTypes.func, 187 | onLoad: PropTypes.func, 188 | onColumnResizeStart: PropTypes.func, 189 | onColumnResize: PropTypes.func, 190 | onColumnResizeEnd: PropTypes.func, 191 | onColumnReorderStart: PropTypes.func, 192 | onColumnReorderEnd: PropTypes.func, 193 | onRowsRequest: PropTypes.func, 194 | onRowsReset: PropTypes.func, 195 | onRowsChange: PropTypes.func, 196 | onTotalRowsChange: PropTypes.func, 197 | }; 198 | 199 | export default GridTable; 200 | 201 | export * from "./components"; 202 | export * from "./hooks"; 203 | -------------------------------------------------------------------------------- /demo/src/components/TableControllers.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ControllerWrappper from "./ControllerWrappper"; 3 | 4 | const TableControllers = ({ controllers }) => { 5 | return ( 6 | 7 | 8 | controllers.page[1](~~e.target.value)} 13 | /> 14 | 15 | 16 | controllers.pageSize[1](~~e.target.value)} 21 | /> 22 | 23 | 24 | controllers.searchText[1](e.target.value)} 28 | /> 29 | 30 | 31 | 57 | 58 | 59 | 71 | 72 | 73 | 77 | controllers.enableColumnsReorder[1]( 78 | !controllers.enableColumnsReorder[0] 79 | ) 80 | } 81 | /> 82 | 83 | 84 | 88 | controllers.highlightSearch[1]( 89 | !controllers.highlightSearch[0] 90 | ) 91 | } 92 | /> 93 | 94 | 95 | 99 | controllers.showSearch[1](!controllers.showSearch[0]) 100 | } 101 | /> 102 | 103 | 104 | 108 | controllers.showColumnVisibilityManager[1]( 109 | !controllers.showColumnVisibilityManager[0] 110 | ) 111 | } 112 | /> 113 | 114 | 115 | 119 | controllers.showRowsInformation[1]( 120 | !controllers.showRowsInformation[0] 121 | ) 122 | } 123 | /> 124 | 125 | 126 | 130 | controllers.isHeaderSticky[1]( 131 | !controllers.isHeaderSticky[0] 132 | ) 133 | } 134 | /> 135 | 136 | 137 | 141 | controllers.isVirtualScroll[1]( 142 | !controllers.isVirtualScroll[0] 143 | ) 144 | } 145 | /> 146 | 147 | 148 | 152 | controllers.isPaginated[1](!controllers.isPaginated[0]) 153 | } 154 | /> 155 | 156 | 157 | 162 | controllers.minSearchChars[1](~~e.target.value) 163 | } 164 | /> 165 | 166 | 167 | 172 | controllers.minColumnResizeWidth[1](~~e.target.value) 173 | } 174 | /> 175 | 176 | 177 | 186 | 187 | 188 | ); 189 | }; 190 | 191 | export default TableControllers; 192 | -------------------------------------------------------------------------------- /src/drag-and-drop/utils.js: -------------------------------------------------------------------------------- 1 | /* global process */ 2 | export function arrayMove(array, from, to) { 3 | // Will be deprecated soon. Consumers should install 'array-move' instead 4 | // https://www.npmjs.com/package/array-move 5 | 6 | if (process.env.NODE_ENV !== "production") { 7 | if (typeof console !== "undefined") { 8 | // eslint-disable-next-line no-console 9 | console.warn( 10 | "Deprecation warning: arrayMove will no longer be exported by 'react-sortable-hoc' in the next major release. Please install the `array-move` package locally instead. https://www.npmjs.com/package/array-move" 11 | ); 12 | } 13 | } 14 | 15 | array = array.slice(); 16 | array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]); 17 | 18 | return array; 19 | } 20 | 21 | export function omit(obj, keysToOmit) { 22 | return Object.keys(obj).reduce((acc, key) => { 23 | if (keysToOmit.indexOf(key) === -1) { 24 | acc[key] = obj[key]; 25 | } 26 | 27 | return acc; 28 | }, {}); 29 | } 30 | 31 | export const events = { 32 | end: ["touchend", "touchcancel", "mouseup"], 33 | move: ["touchmove", "mousemove"], 34 | start: ["touchstart", "mousedown"], 35 | }; 36 | 37 | export const vendorPrefix = (function () { 38 | if (typeof window === "undefined" || typeof document === "undefined") { 39 | // Server environment 40 | return ""; 41 | } 42 | 43 | // fix for: https://bugzilla.mozilla.org/show_bug.cgi?id=548397 44 | // window.getComputedStyle() returns null inside an iframe with display: none 45 | // in this case return an array with a fake mozilla style in it. 46 | const styles = window.getComputedStyle(document.documentElement, "") || [ 47 | "-moz-hidden-iframe", 48 | ]; 49 | const pre = (Array.prototype.slice 50 | .call(styles) 51 | .join("") 52 | .match(/-(moz|webkit|ms)-/) || 53 | (styles.OLink === "" && ["", "o"]))[1]; 54 | 55 | switch (pre) { 56 | case "ms": 57 | return "ms"; 58 | default: 59 | return pre && pre.length 60 | ? pre[0].toUpperCase() + pre.substr(1) 61 | : ""; 62 | } 63 | })(); 64 | 65 | export function setInlineStyles(node, styles) { 66 | Object.keys(styles).forEach((key) => { 67 | node.style[key] = styles[key]; 68 | }); 69 | } 70 | 71 | export function setTranslate3d(node, translate) { 72 | node.style[`${vendorPrefix}Transform`] = 73 | translate == null 74 | ? "" 75 | : `translate3d(${translate.x}px,${translate.y}px,0)`; 76 | } 77 | 78 | export function setTransitionDuration(node, duration) { 79 | node.style[`${vendorPrefix}TransitionDuration`] = 80 | duration == null ? "" : `${duration}ms`; 81 | } 82 | 83 | export function closest(el, fn) { 84 | while (el) { 85 | if (fn(el)) { 86 | return el; 87 | } 88 | 89 | el = el.parentNode; 90 | } 91 | 92 | return null; 93 | } 94 | 95 | export function limit(min, max, value) { 96 | return Math.max(min, Math.min(value, max)); 97 | } 98 | 99 | function getPixelValue(stringValue) { 100 | if (stringValue.substr(-2) === "px") { 101 | return parseFloat(stringValue); 102 | } 103 | 104 | return 0; 105 | } 106 | 107 | export function getElementMargin(element) { 108 | const style = window.getComputedStyle(element); 109 | 110 | return { 111 | bottom: getPixelValue(style.marginBottom), 112 | left: getPixelValue(style.marginLeft), 113 | right: getPixelValue(style.marginRight), 114 | top: getPixelValue(style.marginTop), 115 | }; 116 | } 117 | 118 | export function provideDisplayName(prefix, Component) { 119 | const componentName = Component.displayName || Component.name; 120 | 121 | return componentName ? `${prefix}(${componentName})` : prefix; 122 | } 123 | 124 | export function getScrollAdjustedBoundingClientRect(node, scrollDelta) { 125 | const boundingClientRect = node.getBoundingClientRect(); 126 | 127 | return { 128 | top: boundingClientRect.top + scrollDelta.top, 129 | left: boundingClientRect.left + scrollDelta.left, 130 | }; 131 | } 132 | 133 | export function getPosition(event) { 134 | if (event.touches && event.touches.length) { 135 | return { 136 | x: event.touches[0].pageX, 137 | y: event.touches[0].pageY, 138 | }; 139 | } else if (event.changedTouches && event.changedTouches.length) { 140 | return { 141 | x: event.changedTouches[0].pageX, 142 | y: event.changedTouches[0].pageY, 143 | }; 144 | } else { 145 | return { 146 | x: event.pageX, 147 | y: event.pageY, 148 | }; 149 | } 150 | } 151 | 152 | export function isTouchEvent(event) { 153 | return ( 154 | (event.touches && event.touches.length) || 155 | (event.changedTouches && event.changedTouches.length) 156 | ); 157 | } 158 | 159 | export function getEdgeOffset(node, parent, offset = { left: 0, top: 0 }) { 160 | if (!node) { 161 | return undefined; 162 | } 163 | 164 | // Get the actual offsetTop / offsetLeft value, no matter how deep the node is nested 165 | const nodeOffset = { 166 | left: offset.left + node.offsetLeft, 167 | top: offset.top + node.offsetTop, 168 | }; 169 | 170 | if (node.parentNode === parent) { 171 | return nodeOffset; 172 | } 173 | 174 | return getEdgeOffset(node.parentNode, parent, nodeOffset); 175 | } 176 | 177 | export function getTargetIndex(newIndex, prevIndex, oldIndex) { 178 | if (newIndex < oldIndex && newIndex > prevIndex) { 179 | return newIndex - 1; 180 | } else if (newIndex > oldIndex && newIndex < prevIndex) { 181 | return newIndex + 1; 182 | } else { 183 | return newIndex; 184 | } 185 | } 186 | 187 | export function getLockPixelOffset({ lockOffset, width, height }) { 188 | let offsetX = lockOffset; 189 | let offsetY = lockOffset; 190 | let unit = "px"; 191 | 192 | if (typeof lockOffset === "string") { 193 | const match = /^[+-]?\d*(?:\.\d*)?(px|%)$/.exec(lockOffset); 194 | 195 | offsetX = parseFloat(lockOffset); 196 | offsetY = parseFloat(lockOffset); 197 | unit = match[1]; 198 | } 199 | 200 | if (unit === "%") { 201 | offsetX = (offsetX * width) / 100; 202 | offsetY = (offsetY * height) / 100; 203 | } 204 | 205 | return { 206 | x: offsetX, 207 | y: offsetY, 208 | }; 209 | } 210 | 211 | export function getLockPixelOffsets({ height, width, lockOffset }) { 212 | const offsets = Array.isArray(lockOffset) 213 | ? lockOffset 214 | : [lockOffset, lockOffset]; 215 | 216 | const [minLockOffset, maxLockOffset] = offsets; 217 | 218 | return [ 219 | getLockPixelOffset({ height, lockOffset: minLockOffset, width }), 220 | getLockPixelOffset({ height, lockOffset: maxLockOffset, width }), 221 | ]; 222 | } 223 | 224 | function isScrollable(el) { 225 | const computedStyle = window.getComputedStyle(el); 226 | const overflowRegex = /(auto|scroll)/; 227 | const properties = ["overflow", "overflowX", "overflowY"]; 228 | 229 | return properties.find((property) => 230 | overflowRegex.test(computedStyle[property]) 231 | ); 232 | } 233 | 234 | export function getScrollingParent(el) { 235 | if (!(el instanceof HTMLElement)) { 236 | return null; 237 | } else if (isScrollable(el)) { 238 | return el; 239 | } else { 240 | return getScrollingParent(el.parentNode); 241 | } 242 | } 243 | 244 | export function getContainerGridGap(element) { 245 | const style = window.getComputedStyle(element); 246 | 247 | if (style.display === "grid") { 248 | return { 249 | x: getPixelValue(style.gridColumnGap), 250 | y: getPixelValue(style.gridRowGap), 251 | }; 252 | } 253 | 254 | return { x: 0, y: 0 }; 255 | } 256 | 257 | export const KEYCODE = { 258 | TAB: 9, 259 | ESC: 27, 260 | SPACE: 32, 261 | LEFT: 37, 262 | UP: 38, 263 | RIGHT: 39, 264 | DOWN: 40, 265 | }; 266 | 267 | export const NodeType = { 268 | Anchor: "A", 269 | Button: "BUTTON", 270 | Canvas: "CANVAS", 271 | Input: "INPUT", 272 | Option: "OPTION", 273 | Textarea: "TEXTAREA", 274 | Select: "SELECT", 275 | }; 276 | 277 | export function cloneNode(node) { 278 | const selector = "input, textarea, select, canvas, [contenteditable]"; 279 | const fields = node.querySelectorAll(selector); 280 | const clonedNode = node.cloneNode(true); 281 | const clonedFields = [...clonedNode.querySelectorAll(selector)]; 282 | 283 | clonedFields.forEach((field, i) => { 284 | if (field.type !== "file") { 285 | field.value = fields[i].value; 286 | } 287 | 288 | // Fixes an issue with original radio buttons losing their value once the 289 | // clone is inserted in the DOM, as radio button `name` attributes must be unique 290 | if (field.type === "radio" && field.name) { 291 | field.name = `__sortableClone__${field.name}`; 292 | } 293 | 294 | if ( 295 | field.tagName === NodeType.Canvas && 296 | fields[i].width > 0 && 297 | fields[i].height > 0 298 | ) { 299 | const destCtx = field.getContext("2d"); 300 | destCtx.drawImage(fields[i], 0, 0); 301 | } 302 | }); 303 | 304 | return clonedNode; 305 | } 306 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --rgt-background-color: rgb(255, 255, 255); 3 | --rgt-shadow-color: rgb(0 0 0 / 0.25); 4 | --rgt-border-color: #eee; 5 | --rgt-button-color: #125082; 6 | --rgt-color1: #fff; 7 | --rgt-color2: #c5c5c5; 8 | --rgt-color3: #9e9e9e; 9 | --rgt-color4: yellow; 10 | --rgt-color5: #f5f5f5; 11 | 12 | --rgt-border: 1px solid var(--rgt-border-color); 13 | } 14 | 15 | /* general */ 16 | 17 | .rgt-text-truncate { 18 | white-space: nowrap; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | } 22 | 23 | .rgt-clickable { 24 | cursor: pointer; 25 | } 26 | 27 | .rgt-disabled { 28 | cursor: not-allowed; 29 | } 30 | 31 | .rgt-disabled-button { 32 | background: var(--rgt-color2) !important; 33 | cursor: not-allowed !important; 34 | } 35 | 36 | .rgt-flex-child { 37 | flex: 1; 38 | } 39 | 40 | .rgt-wrapper * { 41 | box-sizing: border-box; 42 | } 43 | 44 | .rgt-wrapper ::-webkit-scrollbar-track { 45 | background-color: #f5f5f5; 46 | } 47 | 48 | .rgt-wrapper ::-webkit-scrollbar { 49 | width: 8px; 50 | height: 8px; 51 | background-color: #f5f5f5; 52 | } 53 | 54 | .rgt-wrapper ::-webkit-scrollbar-thumb { 55 | background-color: #ddd; 56 | border: 2px solid #d8d8d8; 57 | } 58 | 59 | /* elements */ 60 | 61 | .rgt-wrapper { 62 | display: flex; 63 | flex-direction: column; 64 | position: relative; 65 | width: 100%; 66 | height: 100%; 67 | min-height: 388px; 68 | border: var(--rgt-border); 69 | } 70 | 71 | .rgt-container { 72 | background: var(--rgt-background-color); 73 | width: 100%; 74 | position: relative; 75 | /* height: 100%; */ 76 | } 77 | 78 | .rgt-cell { 79 | background: var(--rgt-background-color); 80 | display: flex; 81 | height: 100%; 82 | align-items: center; 83 | border-bottom: var(--rgt-border); 84 | min-height: 48px; 85 | } 86 | 87 | .rgt-cell-inner { 88 | margin: 0 20px; 89 | display: block; 90 | width: 100%; 91 | } 92 | 93 | .rgt-cell-header { 94 | display: flex; 95 | -webkit-user-select: none; 96 | -moz-user-select: none; 97 | -ms-user-select: none; 98 | user-select: none; 99 | width: 100%; 100 | z-index: 1; 101 | min-height: 48px; 102 | max-height: 48px; 103 | border-bottom: var(--rgt-border); 104 | } 105 | 106 | .rgt-cell-header-virtual-col { 107 | border-bottom: var(--rgt-border); 108 | background: var(--rgt-background-color); 109 | z-index: 2; 110 | } 111 | 112 | .rgt-cell-header-inner { 113 | padding: 0 20px; 114 | display: flex; 115 | flex: 1; 116 | align-items: center; 117 | position: relative; 118 | width: 100%; 119 | background: var(--rgt-background-color); 120 | overflow: hidden; 121 | } 122 | 123 | .rgt-cell-header-inner-not-pinned-right { 124 | border-right: var(--rgt-border); 125 | } 126 | 127 | .rgt-cell-header-inner-checkbox { 128 | padding: 0px; 129 | justify-content: center; 130 | } 131 | 132 | .rgt-placeholder-cell { 133 | position: relative; 134 | border-radius: 2px; 135 | height: 20px; 136 | width: 100%; 137 | display: inline-block; 138 | margin: 0 20px; 139 | overflow: hidden; 140 | background-color: #eee; 141 | } 142 | 143 | .rgt-placeholder-cell::after { 144 | content: ""; 145 | position: absolute; 146 | top: 0; 147 | right: 0; 148 | bottom: 0; 149 | left: 0; 150 | transform: translateX(-100%); 151 | background-image: linear-gradient( 152 | 90deg, 153 | rgba(255, 255, 255, 0) 0, 154 | rgba(255, 255, 255, 0.2) 20%, 155 | rgba(255, 255, 255, 0.5) 60%, 156 | rgba(255, 255, 255, 0) 157 | ); 158 | animation: loading 1.5s infinite; 159 | } 160 | 161 | @keyframes loading { 162 | 100% { 163 | transform: translateX(100%); 164 | } 165 | } 166 | 167 | .rgt-resize-handle { 168 | height: 100%; 169 | width: 10px; 170 | z-index: 1; 171 | cursor: w-resize; 172 | position: absolute; 173 | top: 0; 174 | right: 0; 175 | } 176 | 177 | .rgt-footer { 178 | display: flex; 179 | justify-content: space-between; 180 | align-items: center; 181 | box-sizing: border-box; 182 | font-weight: 500; 183 | background: var(--rgt-background-color); 184 | z-index: 1; 185 | border-top: var(--rgt-border); 186 | overflow-x: auto; 187 | overflow-y: hidden; 188 | } 189 | 190 | .rgt-footer-items-information { 191 | padding: 12px 20px; 192 | white-space: nowrap; 193 | display: inline-flex; 194 | align-items: center; 195 | } 196 | 197 | .rgt-footer-clear-selection-button { 198 | display: inline-flex; 199 | margin-left: 2px; 200 | margin-top: -8px; 201 | } 202 | 203 | .rgt-footer-page-size { 204 | display: flex; 205 | } 206 | 207 | .rgt-footer-page-size-select { 208 | cursor: pointer; 209 | margin-right: 20px; 210 | margin-left: 10px; 211 | border-radius: 4px; 212 | border-color: var(--rgt-border-color); 213 | } 214 | 215 | .rgt-footer-page-input { 216 | padding: 0px 0px 0px 5px; 217 | outline: none; 218 | flex: 1; 219 | max-width: 52px; 220 | line-height: 22px; 221 | margin: 0 10px -2px; 222 | border-radius: 4px; 223 | border: var(--rgt-border); 224 | } 225 | 226 | .rgt-footer-right-container { 227 | display: inline-flex; 228 | align-items: center; 229 | padding: 12px 20px; 230 | white-space: nowrap; 231 | } 232 | 233 | .rgt-footer-pagination { 234 | display: flex; 235 | } 236 | 237 | .rgt-footer-pagination-input-container { 238 | display: flex; 239 | align-items: center; 240 | justify-content: space-between; 241 | margin: 0px 10px 0 20px; 242 | } 243 | 244 | .rgt-footer-pagination-button { 245 | background: var(--rgt-button-color); 246 | color: var(--rgt-color1); 247 | margin-left: 10px; 248 | border: none; 249 | border-radius: 4px; 250 | padding: 0px 12px; 251 | cursor: pointer; 252 | display: block; 253 | min-height: 24px; 254 | max-height: 24px; 255 | min-width: 60px; 256 | outline: none; 257 | position: relative; 258 | box-shadow: 1px 1px 1px 0px var(--rgt-shadow-color); 259 | font-size: 12px; 260 | } 261 | 262 | .rgt-cell-checkbox { 263 | padding: 0 16px; 264 | box-sizing: border-box; 265 | justify-content: center; 266 | background: var(--rgt-background-color); 267 | } 268 | 269 | .rgt-sort-icon { 270 | font-size: 16px; 271 | margin-left: 5px; 272 | display: inline-flex; 273 | } 274 | 275 | .rgt-container-overlay { 276 | position: absolute; 277 | top: 99px; 278 | left: 0; 279 | right: 0; 280 | bottom: 57px; 281 | display: flex; 282 | align-items: center; 283 | justify-content: center; 284 | font-size: 36px; 285 | font-weight: 700; 286 | color: var(--rgt-color3); 287 | pointer-events: none; 288 | } 289 | 290 | .rgt-column-sort-ghost { 291 | border-left: var(--rgt-border); 292 | border-right: var(--rgt-border); 293 | z-index: 2; 294 | } 295 | 296 | .rgt-header-container { 297 | display: flex; 298 | width: 100%; 299 | background: var(--rgt-background-color); 300 | align-items: center; 301 | justify-content: space-between; 302 | border-bottom: var(--rgt-border); 303 | } 304 | 305 | .rgt-search-highlight { 306 | background: var(--rgt-color4); 307 | } 308 | 309 | .rgt-columns-manager-wrapper { 310 | position: relative; 311 | z-index: 3; 312 | display: inline-flex; 313 | padding: 10px; 314 | } 315 | 316 | .rgt-columns-manager-button { 317 | cursor: pointer; 318 | height: 26px; 319 | width: 26px; 320 | padding: 0; 321 | background: transparent; 322 | outline: none; 323 | border-radius: 50%; 324 | border: none; 325 | display: flex; 326 | align-items: center; 327 | justify-content: center; 328 | transition: background 0.2s ease; 329 | } 330 | 331 | .rgt-columns-manager-button:hover, 332 | .rgt-columns-manager-button-active { 333 | background: var(--rgt-color5); 334 | } 335 | 336 | .rgt-columns-manager-popover { 337 | display: inline-flex; 338 | flex-direction: column; 339 | transition: transform 0.1s ease-out; 340 | transform-origin: top right; 341 | transform: scale(0); 342 | padding: 10px 0px; 343 | position: absolute; 344 | right: 50%; 345 | top: 80%; 346 | background: var(--rgt-background-color); 347 | border-radius: 2px; 348 | box-shadow: 1px 1px 4px 0px var(--rgt-shadow-color); 349 | min-width: 200px; 350 | } 351 | 352 | .rgt-columns-manager-popover-open { 353 | transform: scale(1); 354 | } 355 | 356 | .rgt-columns-manager-popover-row { 357 | display: flex; 358 | flex: 1; 359 | justify-content: space-between; 360 | position: relative; 361 | font-size: 14px; 362 | align-items: center; 363 | } 364 | 365 | .rgt-columns-manager-popover-title { 366 | padding: 0 20px; 367 | font-weight: 500; 368 | margin-bottom: 10px; 369 | white-space: nowrap; 370 | font-size: 16px; 371 | } 372 | 373 | .rgt-columns-manager-popover-row > label { 374 | padding: 5px 40px 5px 20px; 375 | width: 100%; 376 | } 377 | 378 | .rgt-columns-manager-popover-row > input { 379 | margin: 0; 380 | position: absolute; 381 | right: 20px; 382 | pointer-events: none; 383 | } 384 | 385 | .rgt-columns-manager-popover-row:hover { 386 | background: var(--rgt-color5); 387 | } 388 | 389 | .rgt-columns-manager-popover-body { 390 | display: inline-flex; 391 | flex-direction: column; 392 | max-height: 290px; 393 | height: 100%; 394 | width: 100%; 395 | overflow: auto; 396 | max-width: 300px; 397 | } 398 | 399 | .rgt-search-container { 400 | width: 100%; 401 | z-index: 1; 402 | flex: 1; 403 | display: inline-flex; 404 | padding: 10px 10px 10px 20px; 405 | } 406 | 407 | .rgt-search-label { 408 | line-height: 30px; 409 | font-weight: 500; 410 | font-size: 16px; 411 | margin-right: 5px; 412 | display: inline-flex; 413 | align-items: center; 414 | } 415 | 416 | .rgt-search-icon { 417 | font-size: 22px; 418 | transform: rotate(-35deg); 419 | display: inline-block; 420 | margin-right: 5px; 421 | color: var(--rgt-color2); 422 | } 423 | 424 | .rgt-search-input { 425 | width: 100%; 426 | line-height: 30px; 427 | margin-right: 10px; 428 | flex: 1; 429 | border: none; 430 | outline: none; 431 | font-size: 16px; 432 | padding: 0; 433 | } 434 | 435 | .rgt-cell-editor-inner { 436 | position: relative; 437 | height: 30px; 438 | width: 100%; 439 | } 440 | 441 | .rgt-cell-editor-input { 442 | position: absolute; 443 | top: 0; 444 | left: 0; 445 | right: 0; 446 | bottom: 0; 447 | width: 100%; 448 | border: none; 449 | border-bottom: var(--rgt-border); 450 | outline: none; 451 | font-size: 16px; 452 | padding: 0; 453 | font-family: inherit; 454 | } 455 | 456 | .rgt-cell-header-sticky { 457 | position: -webkit-sticky; 458 | position: sticky; 459 | top: 0; 460 | } 461 | 462 | .rgt-cell-header-not-sticky { 463 | position: relative; 464 | } 465 | 466 | .rgt-cell-header-pinned { 467 | position: -webkit-sticky; 468 | position: sticky; 469 | z-index: 2; 470 | } 471 | 472 | .rgt-cell-header-pinned-left { 473 | left: 0; 474 | } 475 | 476 | .rgt-cell-header-pinned-right { 477 | right: 0; 478 | } 479 | 480 | .rgt-cell-pinned { 481 | position: -webkit-sticky; 482 | position: sticky; 483 | z-index: 1; 484 | } 485 | 486 | .rgt-cell-pinned-left { 487 | left: 0; 488 | } 489 | 490 | .rgt-cell-pinned-right { 491 | right: 0; 492 | } 493 | -------------------------------------------------------------------------------- /demo/src/components/ControllersDrawer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import TableControllers from "./TableControllers"; 3 | import ColumnsControllers from "./ColumnsControllers"; 4 | 5 | const styles = { 6 | wrapper: { 7 | display: "flex", 8 | flexDirection: "column", 9 | maxWidth: 300, 10 | width: "100%", 11 | fontSize: 14, 12 | boxShadow: 13 | "0px 8px 10px -5px rgba(0,0,0,0.2), 0px 16px 24px 2px rgba(0,0,0,0.14), 0px 6px 30px 5px rgba(0,0,0,0.12)", 14 | zIndex: 10, 15 | position: "fixed", 16 | top: 0, 17 | bottom: 0, 18 | left: 0, 19 | background: "#fff", 20 | transition: "transform 400ms cubic-bezier(0, 0, 0.2, 1) 0ms", 21 | }, 22 | title: { 23 | padding: "15px 20px", 24 | fontSize: 18, 25 | fontWeight: "bold", 26 | }, 27 | link: { 28 | marginRight: 8, 29 | borderRadius: "50%", 30 | boxShadow: "0px 2px 2px 0px rgb(0 0 0 / 30%)", 31 | fontSize: 0, 32 | border: "3px solid #fff", 33 | }, 34 | tabs: { 35 | width: "100%", 36 | }, 37 | tab: { 38 | width: "50%", 39 | border: "none", 40 | lineHeight: "34px", 41 | background: "#eef2f5", 42 | color: "#000", 43 | cursor: "pointer", 44 | fontSize: 14, 45 | outline: "none", 46 | }, 47 | activeTab: { 48 | background: "#0075ff", 49 | color: "#fff", 50 | }, 51 | drawerToggleButton: { 52 | cursor: "pointer", 53 | position: "absolute", 54 | width: "40px", 55 | height: "40px", 56 | right: "-40px", 57 | display: "flex", 58 | alignItems: "center", 59 | justifyContent: "center", 60 | background: "#fff", 61 | top: "14px", 62 | boxShadow: "5px 3px 6px 0px rgba(0,0,0,0.2)", 63 | }, 64 | drawerToggleIcon: { 65 | fontSize: "40px", 66 | margin: "-10px 3px 0 0", 67 | }, 68 | controllers: { 69 | overflow: "auto", 70 | flex: 1, 71 | padding: 20, 72 | }, 73 | }; 74 | 75 | const NPM_ICON = ( 76 | 85 | 86 | 90 | 94 | 98 | 102 | 103 | 104 | ); 105 | 106 | const GITHUB_ICON = ( 107 | 116 | 117 | 121 | 122 | 123 | ); 124 | 125 | const ControllersDrawer = ({ isOpen, onToggle, controllers }) => { 126 | const [tab, setTab] = useState("table"); 127 | const drawerStyles = { 128 | ...styles.wrapper, 129 | transform: isOpen 130 | ? "translate3d(0, 0, 0)" 131 | : "translate3d(-300px, 0, 0)", 132 | }; 133 | const tableTabStyles = { 134 | ...styles.tab, 135 | ...(tab === "table" ? styles.activeTab : {}), 136 | }; 137 | const columnsTabStyles = { 138 | ...styles.tab, 139 | ...(tab === "columns" ? styles.activeTab : {}), 140 | }; 141 | 142 | return ( 143 |
144 |
onToggle(!isOpen)} 147 | className="settingsDrawerButton" 148 | > 149 | 150 | {isOpen ? <>‹ : <>›} 151 | 152 |
153 |
154 | SETTINGS 155 | 177 |
178 |
179 | 182 | 188 |
189 |
190 | {tab === "table" ? ( 191 | 192 | ) : ( 193 | 194 | )} 195 |
196 |
197 | ); 198 | }; 199 | 200 | export default ControllersDrawer; 201 | --------------------------------------------------------------------------------