├── .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 |
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 |
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 |
47 |
53 | onChange({
54 | ...data,
55 | [column.field]: e.target.value,
56 | })
57 | }
58 | />
59 |
60 | ) : (
61 |
62 |
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 |
32 | );
33 |
34 | const CLEAR_ICON = (
35 |
56 | );
57 |
58 | const MENU_ICON = (
59 |
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 |
15 | );
16 | const SAVE_SVG = (
17 |
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 |
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 |
104 | );
105 |
106 | const GITHUB_ICON = (
107 |
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 |
--------------------------------------------------------------------------------