simple-table-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Outline.txt ADDED
@@ -0,0 +1,63 @@
1
+ Pros and cons of different react table libraries
2
+
3
+ ag-grid
4
+ pros:
5
+ - highly customizable
6
+ - performant
7
+ - feature rich
8
+ cons:
9
+ - crazy expensive ($1000 for a license)
10
+
11
+ react-table
12
+ pros:
13
+ - highly customizable
14
+ - performant
15
+ cons:
16
+ - not beginner friendly
17
+ - terrible syntax
18
+ - missing features
19
+
20
+
21
+ Handsontable
22
+ pros
23
+ - easy to set up
24
+ cons:
25
+ - terrible documentation
26
+ - not beginner friendly
27
+ - impossible to style
28
+
29
+
30
+
31
+
32
+ DevExtreme Data Grid
33
+ pros:
34
+ - cheap
35
+ cons:
36
+ - terrible UI
37
+ - slow
38
+
39
+ DataTables
40
+ cons:
41
+ - missing features
42
+ - not react specific
43
+
44
+
45
+ jqGrid
46
+ cons
47
+ - looks like the first website component ever made
48
+
49
+
50
+
51
+ Syncfusion Data Grid
52
+ pros:
53
+ - beautiful UI
54
+ cons:
55
+ - so slow
56
+ - few features
57
+
58
+ Tabulator
59
+ pros:
60
+ - feature rich
61
+ cons:
62
+ - ugly
63
+ - missing polish
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # Simple React Table Project
2
+
3
+ This project is a simple React table application created for fun
4
+
5
+ ## Available Scripts
6
+
7
+ In the project directory, you can run:
8
+
9
+ ### `npm start`
10
+
11
+ Runs the app in the development mode.\
12
+ Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "simple-table-core",
3
+ "version": "0.1.0",
4
+ "dependencies": {
5
+ "@types/node": "^16.18.111",
6
+ "@types/react": "^18.3.9",
7
+ "@types/react-dom": "^18.3.0",
8
+ "react": "^18.3.1",
9
+ "react-dom": "^18.3.1",
10
+ "react-scripts": "5.0.1",
11
+ "typescript": "^4.9.5",
12
+ "web-vitals": "^2.1.4"
13
+ },
14
+ "scripts": {
15
+ "start": "react-scripts start",
16
+ "build": "react-scripts build",
17
+ "test": "react-scripts test",
18
+ "eject": "react-scripts eject"
19
+ },
20
+ "eslintConfig": {
21
+ "extends": [
22
+ "react-app",
23
+ "react-app/jest"
24
+ ]
25
+ },
26
+ "browserslist": {
27
+ "production": [
28
+ ">0.2%",
29
+ "not dead",
30
+ "not op_mini all"
31
+ ],
32
+ "development": [
33
+ "last 1 chrome version",
34
+ "last 1 firefox version",
35
+ "last 1 safari version"
36
+ ]
37
+ }
38
+ }
Binary file
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="theme-color" content="#000000" />
7
+ <meta name="description" content="Simple React Table" />
8
+ <title>Simple React Table</title>
9
+ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
10
+ </head>
11
+ <body>
12
+ <noscript>You need to enable JavaScript to run this app.</noscript>
13
+ <div id="root"></div>
14
+ </body>
15
+ </html>
package/src/App.tsx ADDED
@@ -0,0 +1,20 @@
1
+ import SimpleTable from "./components/SimpleTable/SimpleTable";
2
+ import { sampleData } from "./consts/SampleData";
3
+
4
+ const HEADERS = [
5
+ { label: "id", accessor: "id" },
6
+ { label: "name", accessor: "name" },
7
+ { label: "age", accessor: "age" },
8
+ { label: "email", accessor: "email" },
9
+ { label: "address", accessor: "address" },
10
+ ];
11
+
12
+ const App = () => {
13
+ return (
14
+ <div className="app" style={{ padding: "2rem" }}>
15
+ <SimpleTable headers={HEADERS} rows={sampleData} />
16
+ </div>
17
+ );
18
+ };
19
+
20
+ export default App;
@@ -0,0 +1,60 @@
1
+ import React, { useState, useLayoutEffect, useEffect } from "react";
2
+ import usePrevious from "../hooks/usePrevious";
3
+ import calculateBoundingBoxes from "../helpers/calculateBoundingBoxes";
4
+
5
+ interface AnimateProps {
6
+ children: any;
7
+ animateRow?: boolean;
8
+ }
9
+
10
+ const Animate = ({ children, animateRow }: AnimateProps) => {
11
+ const [boundingBox, setBoundingBox] = useState<any>({});
12
+ const [prevBoundingBox, setPrevBoundingBox] = useState<any>({});
13
+ const prevChildren = usePrevious(children);
14
+
15
+ useLayoutEffect(() => {
16
+ const newBoundingBox = calculateBoundingBoxes(children);
17
+ setBoundingBox(newBoundingBox);
18
+ }, [children]);
19
+
20
+ useLayoutEffect(() => {
21
+ const prevBoundingBox = calculateBoundingBoxes(prevChildren);
22
+ setPrevBoundingBox(prevBoundingBox);
23
+ }, [prevChildren]);
24
+
25
+ useEffect(() => {
26
+ const hasPrevBoundingBox = Object.keys(prevBoundingBox).length;
27
+
28
+ if (hasPrevBoundingBox) {
29
+ React.Children.forEach(children, (child) => {
30
+ const domNode = child.ref.current;
31
+ const firstBox = prevBoundingBox[child.key];
32
+ const lastBox = boundingBox[child.key];
33
+ const changeInPosition = animateRow
34
+ ? firstBox.top - lastBox.top
35
+ : firstBox.left - lastBox.left;
36
+
37
+ if (changeInPosition) {
38
+ requestAnimationFrame(() => {
39
+ // Before the DOM paints, invert child to old position
40
+ domNode.style.transform = animateRow
41
+ ? `translateY(${changeInPosition}px)`
42
+ : `translateX(${changeInPosition}px)`;
43
+ domNode.style.transition = "transform 0s";
44
+
45
+ requestAnimationFrame(() => {
46
+ // After the previous frame, remove
47
+ // the transition to play the animation
48
+ domNode.style.transform = "";
49
+ domNode.style.transition = "transform 500ms";
50
+ });
51
+ });
52
+ }
53
+ });
54
+ }
55
+ }, [boundingBox, prevBoundingBox, children, animateRow]);
56
+
57
+ return children;
58
+ };
59
+
60
+ export default Animate;
@@ -0,0 +1,82 @@
1
+ import { useState, createRef, useRef, useReducer } from "react";
2
+ import useSelection from "../../hooks/useSelection";
3
+ import TableHeader from "./TableHeader";
4
+ import { onSort } from "../../utils/sortUtils";
5
+ import Animate from "../Animate";
6
+ import TableRow from "./TableRow";
7
+ import HeaderObject from "../../types/HeaderObject";
8
+
9
+ interface SpreadsheetProps {
10
+ headers: HeaderObject[];
11
+ rows: { [key: string]: any }[];
12
+ }
13
+
14
+ const SimpleTable = ({ headers, rows }: SpreadsheetProps) => {
15
+ const headersRef = useRef(headers);
16
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
17
+ const [sortedRows, setSortedRows] = useState(rows);
18
+ const [sortConfig, setSortConfig] = useState<{
19
+ key: HeaderObject;
20
+ direction: string;
21
+ } | null>(null);
22
+
23
+ const {
24
+ handleMouseDown,
25
+ handleMouseOver,
26
+ handleMouseUp,
27
+ isSelected,
28
+ getBorderClass,
29
+ isTopLeftCell,
30
+ } = useSelection(sortedRows, headers);
31
+
32
+ const handleSort = (columnIndex: number) => {
33
+ const { sortedData, newSortConfig } = onSort(
34
+ headers,
35
+ sortedRows,
36
+ sortConfig,
37
+ columnIndex
38
+ );
39
+ setSortedRows(sortedData);
40
+ setSortConfig(newSortConfig);
41
+ };
42
+ const onDragEnd = (newHeaders: HeaderObject[]) => {
43
+ headersRef.current = newHeaders;
44
+ forceUpdate();
45
+ };
46
+
47
+ return (
48
+ <div className="table-wrapper">
49
+ <table
50
+ className="simple-table"
51
+ onMouseUp={handleMouseUp}
52
+ onMouseLeave={handleMouseUp}
53
+ >
54
+ <TableHeader
55
+ headersRef={headersRef}
56
+ onSort={handleSort}
57
+ onDragEnd={onDragEnd}
58
+ />
59
+ <tbody>
60
+ <Animate animateRow={true}>
61
+ {sortedRows.map((row, rowIndex) => (
62
+ <TableRow
63
+ getBorderClass={getBorderClass}
64
+ handleMouseDown={handleMouseDown}
65
+ handleMouseOver={handleMouseOver}
66
+ headers={headersRef.current}
67
+ isSelected={isSelected}
68
+ isTopLeftCell={isTopLeftCell}
69
+ key={row.id}
70
+ ref={createRef()}
71
+ row={row}
72
+ rowIndex={rowIndex}
73
+ />
74
+ ))}
75
+ </Animate>
76
+ </tbody>
77
+ </table>
78
+ </div>
79
+ );
80
+ };
81
+
82
+ export default SimpleTable;
@@ -0,0 +1,50 @@
1
+ import { forwardRef, LegacyRef } from "react";
2
+
3
+ interface TableCellProps {
4
+ rowIndex: number;
5
+ colIndex: number;
6
+ content: any;
7
+ isSelected: boolean;
8
+ isTopLeftCell: boolean;
9
+ borderClass: string;
10
+ onMouseDown: (rowIndex: number, colIndex: number) => void;
11
+ onMouseOver: (rowIndex: number, colIndex: number) => void;
12
+ }
13
+
14
+ const TableCell = forwardRef(
15
+ (
16
+ {
17
+ rowIndex,
18
+ colIndex,
19
+ content,
20
+ isSelected,
21
+ isTopLeftCell,
22
+ borderClass,
23
+ onMouseDown,
24
+ onMouseOver,
25
+ }: TableCellProps,
26
+ ref: LegacyRef<HTMLTableCellElement>
27
+ ) => {
28
+ return (
29
+ <td
30
+ onMouseDown={() => onMouseDown(rowIndex, colIndex)}
31
+ onMouseOver={() => onMouseOver(rowIndex, colIndex)}
32
+ ref={ref}
33
+ >
34
+ <div
35
+ className={`table-cell ${
36
+ isSelected
37
+ ? isTopLeftCell
38
+ ? `selected-first-cell ${borderClass}`
39
+ : `selected ${borderClass}`
40
+ : ""
41
+ }`}
42
+ >
43
+ {content}
44
+ </div>
45
+ </td>
46
+ );
47
+ }
48
+ );
49
+
50
+ export default TableCell;
@@ -0,0 +1,38 @@
1
+ import { createRef, useRef } from "react";
2
+ import Animate from "../Animate";
3
+ import TableHeaderCell from "./TableHeaderCell";
4
+ import HeaderObject from "../../types/HeaderObject";
5
+
6
+ interface TableHeaderProps {
7
+ headersRef: React.RefObject<HeaderObject[]>;
8
+ onSort: (columnIndex: number) => void;
9
+ onDragEnd: (newHeaders: HeaderObject[]) => void;
10
+ }
11
+
12
+ const TableHeader = ({ headersRef, onSort, onDragEnd }: TableHeaderProps) => {
13
+ const draggedHeaderRef = useRef<HeaderObject | null>(null);
14
+ const hoveredHeaderRef = useRef<HeaderObject | null>(null);
15
+
16
+ return (
17
+ <thead className="table-header">
18
+ <tr>
19
+ <Animate>
20
+ {headersRef.current?.map((header, index) => (
21
+ <TableHeaderCell
22
+ draggedHeaderRef={draggedHeaderRef}
23
+ headersRef={headersRef}
24
+ hoveredHeaderRef={hoveredHeaderRef}
25
+ index={index}
26
+ key={header.accessor}
27
+ onDragEnd={onDragEnd}
28
+ onSort={onSort}
29
+ ref={createRef()}
30
+ />
31
+ ))}
32
+ </Animate>
33
+ </tr>
34
+ </thead>
35
+ );
36
+ };
37
+
38
+ export default TableHeader;
@@ -0,0 +1,77 @@
1
+ import { forwardRef, LegacyRef, useState, useRef } from "react";
2
+ import useTableHeaderCell from "../../hooks/useTableHeaderCell";
3
+ import { throttle } from "../../utils/performanceUtils";
4
+ import HeaderObject from "../../types/HeaderObject";
5
+
6
+ interface TableHeaderCellProps {
7
+ draggedHeaderRef: React.MutableRefObject<HeaderObject | null>;
8
+ headersRef: React.RefObject<HeaderObject[]>;
9
+ hoveredHeaderRef: React.MutableRefObject<HeaderObject | null>;
10
+ index: number;
11
+ onDragEnd: (newHeaders: HeaderObject[]) => void;
12
+ onSort: (columnIndex: number) => void;
13
+ }
14
+
15
+ const TableHeaderCell = forwardRef(
16
+ (
17
+ {
18
+ draggedHeaderRef,
19
+ headersRef,
20
+ hoveredHeaderRef,
21
+ index,
22
+ onDragEnd,
23
+ onSort,
24
+ }: TableHeaderCellProps,
25
+ ref: LegacyRef<HTMLTableCellElement>
26
+ ) => {
27
+ const [isDragging, setIsDragging] = useState(false);
28
+ const header = headersRef.current?.[index];
29
+ const { handleDragStart, handleDragOver, handleDragEnd } =
30
+ useTableHeaderCell({
31
+ draggedHeaderRef,
32
+ headersRef,
33
+ hoveredHeaderRef,
34
+ onDragEnd,
35
+ });
36
+
37
+ const handleDragStartWrapper = (header: HeaderObject) => {
38
+ setIsDragging(true);
39
+ handleDragStart(header);
40
+ };
41
+
42
+ const handleDragEndWrapper = () => {
43
+ setIsDragging(false);
44
+ handleDragEnd();
45
+ };
46
+
47
+ // Throttle the handleDragOver function
48
+ const throttledHandleDragOver = useRef(
49
+ throttle((header: HeaderObject) => {
50
+ handleDragOver(header);
51
+ }, 50) // Adjust the delay as needed
52
+ ).current;
53
+ if (!header) return null;
54
+
55
+ return (
56
+ <th
57
+ className={`table-header-cell ${
58
+ header === hoveredHeaderRef.current ? "hovered" : ""
59
+ } ${isDragging ? "dragging" : ""}`}
60
+ key={header?.accessor}
61
+ draggable
62
+ onDragStart={() => handleDragStartWrapper(header)}
63
+ onDragOver={(event) => {
64
+ event.preventDefault();
65
+ throttledHandleDragOver(header, event);
66
+ }}
67
+ onDragEnd={handleDragEndWrapper}
68
+ onClick={() => onSort(index)}
69
+ ref={ref}
70
+ >
71
+ {header?.label}
72
+ </th>
73
+ );
74
+ }
75
+ );
76
+
77
+ export default TableHeaderCell;
@@ -0,0 +1,54 @@
1
+ import { createRef, forwardRef, LegacyRef } from "react";
2
+ import TableCell from "./TableCell";
3
+ import Animate from "../Animate";
4
+ import HeaderObject from "../../types/HeaderObject";
5
+
6
+ interface TableRowProps {
7
+ rowIndex: number;
8
+ row: { [key: string]: any };
9
+ headers: HeaderObject[];
10
+ isSelected: (rowIndex: number, columnIndex: number) => boolean;
11
+ isTopLeftCell: (rowIndex: number, columnIndex: number) => boolean;
12
+ getBorderClass: (rowIndex: number, columnIndex: number) => string;
13
+ handleMouseDown: (rowIndex: number, columnIndex: number) => void;
14
+ handleMouseOver: (rowIndex: number, columnIndex: number) => void;
15
+ }
16
+
17
+ const TableRow = forwardRef(
18
+ (
19
+ {
20
+ rowIndex,
21
+ row,
22
+ headers,
23
+ isSelected,
24
+ isTopLeftCell,
25
+ getBorderClass,
26
+ handleMouseDown,
27
+ handleMouseOver,
28
+ }: TableRowProps,
29
+ ref: LegacyRef<HTMLTableRowElement>
30
+ ) => {
31
+ return (
32
+ <tr key={row.id} ref={ref}>
33
+ <Animate>
34
+ {headers.map((header, columnIndex) => (
35
+ <TableCell
36
+ key={header.accessor}
37
+ rowIndex={rowIndex}
38
+ colIndex={columnIndex}
39
+ content={row[header.accessor]}
40
+ isSelected={isSelected(rowIndex, columnIndex)}
41
+ isTopLeftCell={isTopLeftCell(rowIndex, columnIndex)}
42
+ borderClass={getBorderClass(rowIndex, columnIndex)}
43
+ onMouseDown={() => handleMouseDown(rowIndex, columnIndex)}
44
+ onMouseOver={() => handleMouseOver(rowIndex, columnIndex)}
45
+ ref={createRef()}
46
+ />
47
+ ))}
48
+ </Animate>
49
+ </tr>
50
+ );
51
+ }
52
+ );
53
+
54
+ export default TableRow;
@@ -0,0 +1,38 @@
1
+ export interface SpreadsheetRow {
2
+ id: number;
3
+ name: string;
4
+ age: number;
5
+ email: string;
6
+ address: string;
7
+ }
8
+
9
+ export const sampleData: SpreadsheetRow[] = [
10
+ {
11
+ id: 1,
12
+ name: "John Doe",
13
+ age: 28,
14
+ email: "john.doe@example.com",
15
+ address: "123 Main St, Anytown, USA",
16
+ },
17
+ {
18
+ id: 2,
19
+ name: "Jane Smith",
20
+ age: 34,
21
+ email: "jane.smith@example.com",
22
+ address: "456 Oak St, Sometown, USA",
23
+ },
24
+ {
25
+ id: 3,
26
+ name: "Alice Johnson",
27
+ age: 45,
28
+ email: "alice.johnson@example.com",
29
+ address: "789 Pine St, Yourtown, USA",
30
+ },
31
+ {
32
+ id: 4,
33
+ name: "Bob Brown",
34
+ age: 23,
35
+ email: "bob.brown@example.com",
36
+ address: "101 Maple St, Mytown, USA",
37
+ },
38
+ ];
@@ -0,0 +1,29 @@
1
+ import { Children, ReactNode } from "react";
2
+
3
+ interface BoundingBox {
4
+ bottom: number;
5
+ height: number;
6
+ left: number;
7
+ right: number;
8
+ top: number;
9
+ width: number;
10
+ }
11
+
12
+ const calculateBoundingBoxes = (
13
+ children: ReactNode
14
+ ): { [key: string]: BoundingBox } => {
15
+ const boundingBoxes: { [key: string]: BoundingBox } = {};
16
+
17
+ Children.forEach(children, (child: any) => {
18
+ if (child.ref && child.ref.current) {
19
+ const domNode = child.ref.current;
20
+ const nodeBoundingBox = domNode.getBoundingClientRect();
21
+
22
+ boundingBoxes[child.key] = nodeBoundingBox;
23
+ }
24
+ });
25
+
26
+ return boundingBoxes;
27
+ };
28
+
29
+ export default calculateBoundingBoxes;
@@ -0,0 +1,6 @@
1
+ export default function shuffleArray(array: any[]) {
2
+ return array
3
+ .map((a) => ({ sort: Math.random(), value: a }))
4
+ .sort((a, b) => a.sort - b.sort)
5
+ .map((a) => a.value);
6
+ }
@@ -0,0 +1,15 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ const usePrevious = (value: any) => {
4
+ const prevChildrenRef = useRef();
5
+
6
+ useEffect(() => {
7
+ prevChildrenRef.current = value;
8
+ }, [value]);
9
+
10
+ return prevChildrenRef.current;
11
+ };
12
+
13
+ export default usePrevious;
14
+
15
+ // https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
@@ -0,0 +1,105 @@
1
+ import { useState, useRef, useEffect, useCallback } from "react";
2
+ import HeaderObject from "../types/HeaderObject";
3
+
4
+ interface Cell {
5
+ row: number;
6
+ col: number;
7
+ }
8
+
9
+ const useSelection = (
10
+ rows: { [key: string]: any }[],
11
+ headers: HeaderObject[]
12
+ ) => {
13
+ const [selectedCells, setSelectedCells] = useState<Cell[]>([]);
14
+ const isSelecting = useRef(false);
15
+ const startCell = useRef<Cell | null>(null);
16
+
17
+ const copyToClipboard = useCallback(() => {
18
+ const rowsText = selectedCells.reduce((acc, { row, col }) => {
19
+ if (!acc[row]) acc[row] = [];
20
+ acc[row][col] = rows[row][headers[col].accessor];
21
+ return acc;
22
+ }, {} as { [key: number]: { [key: number]: any } });
23
+
24
+ const text = Object.values(rowsText)
25
+ .map((row) => Object.values(row).join("\t"))
26
+ .join("\n");
27
+
28
+ navigator.clipboard.writeText(text);
29
+ }, [selectedCells, rows, headers]);
30
+
31
+ useEffect(() => {
32
+ const handleKeyDown = (event: KeyboardEvent) => {
33
+ if ((event.ctrlKey || event.metaKey) && event.key === "c") {
34
+ copyToClipboard();
35
+ }
36
+ };
37
+
38
+ document.addEventListener("keydown", handleKeyDown);
39
+ return () => {
40
+ document.removeEventListener("keydown", handleKeyDown);
41
+ };
42
+ }, [copyToClipboard, selectedCells]);
43
+
44
+ const handleMouseDown = (rowIndex: number, colIndex: number) => {
45
+ isSelecting.current = true;
46
+ startCell.current = { row: rowIndex, col: colIndex };
47
+ setSelectedCells([{ row: rowIndex, col: colIndex }]);
48
+ };
49
+
50
+ const handleMouseOver = (rowIndex: number, colIndex: number) => {
51
+ if (isSelecting.current && startCell.current) {
52
+ const newSelectedCells = [];
53
+ const startRow = Math.min(startCell.current.row, rowIndex);
54
+ const endRow = Math.max(startCell.current.row, rowIndex);
55
+ const startCol = Math.min(startCell.current.col, colIndex);
56
+ const endCol = Math.max(startCell.current.col, colIndex);
57
+
58
+ for (let row = startRow; row <= endRow; row++) {
59
+ for (let col = startCol; col <= endCol; col++) {
60
+ newSelectedCells.push({ row, col });
61
+ }
62
+ }
63
+ setSelectedCells(newSelectedCells);
64
+ }
65
+ };
66
+
67
+ const handleMouseUp = () => {
68
+ isSelecting.current = false;
69
+ startCell.current = null;
70
+ };
71
+
72
+ const isSelected = (rowIndex: number, colIndex: number) => {
73
+ return selectedCells.some(
74
+ (cell) => cell.row === rowIndex && cell.col === colIndex
75
+ );
76
+ };
77
+
78
+ const getBorderClass = (rowIndex: number, colIndex: number) => {
79
+ const classes = [];
80
+ if (!isSelected(rowIndex - 1, colIndex)) classes.push("border-top-blue");
81
+ if (!isSelected(rowIndex + 1, colIndex)) classes.push("border-bottom-blue");
82
+ if (!isSelected(rowIndex, colIndex - 1)) classes.push("border-left-blue");
83
+ if (!isSelected(rowIndex, colIndex + 1)) classes.push("border-right-blue");
84
+ return classes.join(" ");
85
+ };
86
+
87
+ const isTopLeftCell = (rowIndex: number, colIndex: number) => {
88
+ return (
89
+ rowIndex === Math.min(...selectedCells.map((cell) => cell.row)) &&
90
+ colIndex === Math.min(...selectedCells.map((cell) => cell.col))
91
+ );
92
+ };
93
+
94
+ return {
95
+ selectedCells,
96
+ handleMouseDown,
97
+ handleMouseOver,
98
+ handleMouseUp,
99
+ isSelected,
100
+ getBorderClass,
101
+ isTopLeftCell,
102
+ };
103
+ };
104
+
105
+ export default useSelection;
@@ -0,0 +1,72 @@
1
+ import HeaderObject from "../types/HeaderObject";
2
+
3
+ interface UseTableHeaderCellProps {
4
+ draggedHeaderRef: React.MutableRefObject<HeaderObject | null>;
5
+ headersRef: React.RefObject<HeaderObject[]>;
6
+ hoveredHeaderRef: React.MutableRefObject<HeaderObject | null>;
7
+ onDragEnd: (newHeaders: HeaderObject[]) => void;
8
+ }
9
+ var isUpdating = false;
10
+
11
+ const useTableHeaderCell = ({
12
+ draggedHeaderRef,
13
+ headersRef,
14
+ hoveredHeaderRef,
15
+ onDragEnd,
16
+ }: UseTableHeaderCellProps) => {
17
+ const handleDragStart = (header: HeaderObject) => {
18
+ draggedHeaderRef.current = header;
19
+ };
20
+
21
+ const updateHeaders = (hoveredHeader: HeaderObject) => {
22
+ if (isUpdating) return;
23
+ hoveredHeaderRef.current = hoveredHeader;
24
+
25
+ if (
26
+ hoveredHeader.accessor !== draggedHeaderRef.current?.accessor &&
27
+ draggedHeaderRef.current !== null &&
28
+ !isUpdating
29
+ ) {
30
+ isUpdating = true;
31
+ const newHeaders = [...(headersRef.current || [])];
32
+ const draggedHeaderIndex = headersRef.current?.findIndex(
33
+ (header) => header.accessor === draggedHeaderRef.current?.accessor
34
+ );
35
+ const hoveredHeaderIndex = headersRef.current?.findIndex(
36
+ (header) => header.accessor === hoveredHeader.accessor
37
+ );
38
+ if (draggedHeaderIndex === undefined || hoveredHeaderIndex === undefined)
39
+ return;
40
+
41
+ const [draggedHeader] = newHeaders.splice(draggedHeaderIndex, 1);
42
+ newHeaders.splice(hoveredHeaderIndex, 0, draggedHeader);
43
+
44
+ // Check if the newHeaders array is different from the original headers array
45
+ if (JSON.stringify(newHeaders) !== JSON.stringify(headersRef.current))
46
+ setTimeout(() => {
47
+ onDragEnd(newHeaders);
48
+
49
+ setTimeout(() => {
50
+ isUpdating = false;
51
+ }, 500);
52
+ }, 50);
53
+ }
54
+ };
55
+
56
+ const handleDragOver = (hoveredHeader: HeaderObject) => {
57
+ updateHeaders(hoveredHeader);
58
+ };
59
+
60
+ const handleDragEnd = () => {
61
+ draggedHeaderRef.current = null;
62
+ hoveredHeaderRef.current = null;
63
+ };
64
+
65
+ return {
66
+ handleDragStart,
67
+ handleDragOver,
68
+ handleDragEnd,
69
+ };
70
+ };
71
+
72
+ export default useTableHeaderCell;
package/src/index.tsx ADDED
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App";
4
+
5
+ import "./styles/index.css";
6
+ import "./styles/table.css";
7
+
8
+ const root = ReactDOM.createRoot(
9
+ document.getElementById("root") as HTMLElement
10
+ );
11
+
12
+ root.render(
13
+ <React.StrictMode>
14
+ <App />
15
+ </React.StrictMode>
16
+ );
@@ -0,0 +1 @@
1
+ /// <reference types="react-scripts" />
@@ -0,0 +1,7 @@
1
+ /* Import Nunito font from Google Fonts */
2
+ @import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;700&display=swap");
3
+
4
+ /* Apply Nunito as the default font */
5
+ body {
6
+ font-family: "Nunito", sans-serif;
7
+ }
@@ -0,0 +1,93 @@
1
+ :root {
2
+ --blue-50: #ebf8ff;
3
+ --blue-100: #bee3f8;
4
+ --blue-200: #90cdf4;
5
+ --blue-300: #63b3ed;
6
+ --blue-400: #4299e1;
7
+ --blue-500: #3182ce;
8
+ --blue-600: #2b6cb0;
9
+ --blue-700: #2c5282;
10
+ --blue-800: #2a4365;
11
+ --blue-900: #1a365d;
12
+ --selected-first-cell: white;
13
+ --light-grey: #d3d3d3;
14
+ --border-radius: 4px;
15
+ --border-width: 1px;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ margin: 0;
21
+ padding: 0;
22
+ }
23
+
24
+ /* Add a new class for the table wrapper */
25
+ .table-wrapper {
26
+ border: 1px solid var(--light-grey);
27
+ border-radius: var(--border-radius);
28
+ overflow: hidden; /* Ensures the table stays within the rounded corners */
29
+ }
30
+
31
+ table {
32
+ border-collapse: collapse;
33
+ width: 100%;
34
+ table-layout: fixed; /* Ensures even width for all th elements */
35
+ }
36
+
37
+ thead tr,
38
+ tr:not(:last-child) {
39
+ border-bottom: 1px solid var(--light-grey);
40
+ }
41
+
42
+ tr {
43
+ transition: transform 0.4s, top 0.4s, opacity 0.2s;
44
+ transform: translateY(0);
45
+ }
46
+
47
+ .dragging {
48
+ background-color: var(--blue-100);
49
+ }
50
+
51
+ th {
52
+ width: 100px; /* Set a specific width for all th elements */
53
+ }
54
+
55
+ th,
56
+ .table-cell {
57
+ user-select: none;
58
+ cursor: pointer;
59
+ padding: 8px;
60
+ text-align: left;
61
+ }
62
+
63
+ .table-cell {
64
+ border: var(--border-width) solid transparent;
65
+ }
66
+
67
+ .selected {
68
+ background-color: var(--blue-200);
69
+ }
70
+
71
+ .selected-first-cell {
72
+ background-color: var(--selected-first-cell);
73
+ }
74
+
75
+ .border-top-blue {
76
+ border-top: var(--border-width) solid var(--blue-500);
77
+ }
78
+
79
+ .border-bottom-blue {
80
+ border-bottom: var(--border-width) solid var(--blue-500);
81
+ }
82
+
83
+ .border-left-blue {
84
+ border-left: var(--border-width) solid var(--blue-500);
85
+ }
86
+
87
+ .border-right-blue {
88
+ border-right: var(--border-width) solid var(--blue-500);
89
+ }
90
+
91
+ .border-top-white {
92
+ border-top: var(--border-width) solid white;
93
+ }
@@ -0,0 +1,6 @@
1
+ type HeaderObject = {
2
+ label: string;
3
+ accessor: string;
4
+ };
5
+
6
+ export default HeaderObject;
@@ -0,0 +1,17 @@
1
+ export const throttle = (func: (...args: any[]) => void, limit: number) => {
2
+ let isFirstCall = true;
3
+ let inThrottle = true;
4
+
5
+ return function (this: any, ...args: any[]) {
6
+ if (isFirstCall) {
7
+ isFirstCall = false;
8
+ setTimeout(() => (inThrottle = false), limit);
9
+ return;
10
+ }
11
+ if (!inThrottle) {
12
+ func.apply(this, args);
13
+ inThrottle = true;
14
+ setTimeout(() => (inThrottle = false), limit);
15
+ }
16
+ };
17
+ };
@@ -0,0 +1,30 @@
1
+ import HeaderObject from "../types/HeaderObject";
2
+
3
+ export const onSort = (
4
+ headers: HeaderObject[],
5
+ rows: { [key: string]: any }[],
6
+ sortConfig: { key: HeaderObject; direction: string } | null,
7
+ columnIndex: number
8
+ ) => {
9
+ const key = headers[columnIndex];
10
+ let direction = "ascending";
11
+ if (
12
+ sortConfig &&
13
+ sortConfig.key.accessor === key.accessor &&
14
+ sortConfig.direction === "ascending"
15
+ ) {
16
+ direction = "descending";
17
+ }
18
+
19
+ const sortedData = [...rows].sort((a, b) => {
20
+ if (a[key.accessor] < b[key.accessor]) {
21
+ return direction === "ascending" ? -1 : 1;
22
+ }
23
+ if (a[key.accessor] > b[key.accessor]) {
24
+ return direction === "ascending" ? 1 : -1;
25
+ }
26
+ return 0;
27
+ });
28
+
29
+ return { sortedData, newSortConfig: { key, direction } };
30
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "strict": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "module": "esnext",
17
+ "moduleResolution": "node",
18
+ "resolveJsonModule": true,
19
+ "isolatedModules": true,
20
+ "noEmit": true,
21
+ "jsx": "react-jsx"
22
+ },
23
+ "include": [
24
+ "src"
25
+ ]
26
+ }