setShowSelect(false)}
+ onKeyPress={() => setShowSelect(false)}
+ />
+ )}
+ {showSelect && (
+
+ )}
+ >
+ );
+ break;
+ default:
+ element =
;
+ break;
+ }
+
+ return element;
+}
diff --git a/src/components/Table/Header.jsx b/src/components/Table/Header.jsx
new file mode 100644
index 0000000..19f4932
--- /dev/null
+++ b/src/components/Table/Header.jsx
@@ -0,0 +1,372 @@
+import React, { useState, useEffect } from 'react';
+import { usePopper } from 'react-popper';
+import getIcon from '../Icons/Icons';
+import { shortId } from '../utils/ui_utils';
+
+export default function Header({
+ column: { id, created, label, dataType, getResizerProps, getHeaderProps },
+ setSortBy,
+ dataDispatch,
+}) {
+ const [expanded, setExpanded] = useState(created || false);
+ const [referenceElement, setReferenceElement] = useState(null);
+ const [popperElement, setPopperElement] = useState(null);
+ const [inputRef, setInputRef] = useState(null);
+ const { styles, attributes } = usePopper(referenceElement, popperElement, {
+ placement: 'bottom',
+ strategy: 'absolute',
+ });
+ const [header, setHeader] = useState(label);
+ const [typeReferenceElement, setTypeReferenceElement] = useState(null);
+ const [typePopperElement, setTypePopperElement] = useState(null);
+ const [showType, setShowType] = useState(false);
+ const buttons = [
+ {
+ onClick: () => {
+ dataDispatch({
+ type: 'update_column_header',
+ columnId: id,
+ label: header,
+ });
+ setSortBy([{ id, desc: false }]);
+ setExpanded(false);
+ },
+ icon: getIcon('arrowup'),
+ label: 'Sort ascending',
+ },
+ {
+ onClick: () => {
+ dataDispatch({
+ type: 'update_column_header',
+ columnId: id,
+ label: header,
+ });
+ setSortBy([{ id, desc: true }]);
+ setExpanded(false);
+ },
+ icon: getIcon('arrowdown'),
+ label: 'Sort descending',
+ },
+ {
+ onClick: () => {
+ dataDispatch({
+ type: 'update_column_header',
+ columnId: id,
+ label: header,
+ });
+ dataDispatch({
+ type: 'add_column_to_left',
+ columnId: id,
+ focus: false,
+ });
+ setExpanded(false);
+ },
+ icon: getIcon('arrowleft'),
+ label: 'Insert left',
+ },
+ {
+ onClick: () => {
+ dataDispatch({
+ type: 'update_column_header',
+ columnId: id,
+ label: header,
+ });
+ dataDispatch({
+ type: 'add_column_to_right',
+ columnId: id,
+ focus: false,
+ });
+ setExpanded(false);
+ },
+ icon: getIcon('arrowright'),
+ label: 'Insert right',
+ },
+ {
+ onClick: () => {
+ dataDispatch({
+ type: 'update_column_header',
+ columnId: id,
+ label: header,
+ });
+ dataDispatch({ type: 'delete_column', columnId: id });
+ setExpanded(false);
+ },
+ icon: getIcon('trash'),
+ label: 'Delete',
+ },
+ ];
+
+ const types = [
+ {
+ onClick: () => {
+ dataDispatch({
+ type: 'update_column_type',
+ columnId: id,
+ dataType: 'select',
+ });
+ setShowType(false);
+ setExpanded(false);
+ },
+ icon: getIcon('multi'),
+ label: 'Select',
+ },
+ {
+ onClick: () => {
+ dataDispatch({
+ type: 'update_column_type',
+ columnId: id,
+ dataType: 'text',
+ });
+ setShowType(false);
+ setExpanded(false);
+ },
+ icon: getIcon('text'),
+ label: 'Text',
+ },
+ {
+ onClick: () => {
+ dataDispatch({
+ type: 'update_column_type',
+ columnId: id,
+ dataType: 'number',
+ });
+ setShowType(false);
+ setExpanded(false);
+ },
+ icon: getIcon('hash'),
+ label: 'Number',
+ },
+ ];
+
+ let propertyIcon;
+ switch (dataType) {
+ case 'number':
+ propertyIcon = getIcon('hash');
+ break;
+ case 'text':
+ propertyIcon = getIcon('text');
+ break;
+ case 'select':
+ propertyIcon = getIcon('multi');
+ break;
+ default:
+ break;
+ }
+
+ useEffect(() => {
+ if (created) {
+ setExpanded(true);
+ }
+ }, [created]);
+
+ useEffect(() => {
+ setHeader(label);
+ }, [label]);
+
+ useEffect(() => {
+ if (inputRef) {
+ inputRef.focus();
+ inputRef.select();
+ }
+ }, [inputRef]);
+
+ const typePopper = usePopper(typeReferenceElement, typePopperElement, {
+ placement: 'right',
+ strategy: 'fixed',
+ });
+
+ function handleKeyDown(e) {
+ if (e.key === 'Enter') {
+ dataDispatch({
+ type: 'update_column_header',
+ columnId: id,
+ label: header,
+ });
+ setExpanded(false);
+ }
+ }
+
+ function handleChange(e) {
+ setHeader(e.target.value);
+ }
+
+ function handleBlur(e) {
+ e.preventDefault();
+ dataDispatch({ type: 'update_column_header', columnId: id, label: header });
+ }
+
+ return id !== 999999 ? (
+ <>
+
setExpanded(false)}
+ onKeyPress={() => setExpanded(false)}
+ />
+ )}
+ {expanded && (
+ // Menu
+
+ {/* Menu Background */}
+
+ {/* Column Name Input */}
+
+
+
+
+ {/* Property Type - Icon and Label */}
+
+ Data Type
+
+
+ {/* Property Type Select Menu */}
+
+
+ {showType && (
+
setShowType(true)}
+ onMouseLeave={() => setShowType(false)}
+ {...typePopper.attributes.popper}
+ style={{
+ ...typePopper.styles.popper,
+ width: 200,
+ zIndex: 4,
+ padding: '4px 0px',
+ }}
+ >
+ {types.map((type) => (
+
+ ))}
+
+ )}
+
+ {/* Sort/Add/Delete */}
+
+
+
+ Functions
+
+
+
+ {buttons.map((button) => (
+
+ ))}
+
+
+
+ )}
+ >
+ ) : (
+ // Add Column
+
+
+ dataDispatch({
+ type: 'add_column_to_left',
+ columnId: 999999,
+ focus: true,
+ })
+ }
+ onKeyPress={() =>
+ dataDispatch({
+ type: 'add_column_to_left',
+ columnId: 999999,
+ focus: true,
+ })
+ }
+ >
+ {/* Column Plus Icon */}
+ {getIcon('plus')}
+
+
+ );
+}
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
new file mode 100644
index 0000000..8b748ad
--- /dev/null
+++ b/src/components/Table/Table.tsx
@@ -0,0 +1,133 @@
+import React, { useMemo } from 'react';
+import {
+ useTable,
+ useFlexLayout,
+ useResizeColumns,
+ useSortBy,
+} from 'react-table';
+import Cell from './Cell';
+import Header from './Header';
+import getIcon from '../Icons/Icons';
+import { shortId } from '../utils/ui_utils';
+
+const defaultColumn = {
+ minWidth: 50,
+ width: 150,
+ maxWidth: 400,
+ Cell,
+ Header,
+ sortType: 'alphanumericFalsyLast',
+};
+
+export default function Table({
+ columns,
+ data,
+ dispatch: dataDispatch,
+ skipReset,
+}) {
+ const sortTypes = useMemo(
+ () => ({
+ alphanumericFalsyLast(rowA, rowB, columnId, desc) {
+ if (!rowA.values[columnId] && !rowB.values[columnId]) {
+ return 0;
+ }
+
+ if (!rowA.values[columnId]) {
+ return desc ? -1 : 1;
+ }
+
+ if (!rowB.values[columnId]) {
+ return desc ? 1 : -1;
+ }
+
+ return Number.isNaN(rowA.values[columnId])
+ ? rowA.values[columnId].localeCompare(rowB.values[columnId])
+ : rowA.values[columnId] - rowB.values[columnId];
+ },
+ }),
+ []
+ );
+
+ const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
+ useTable(
+ {
+ columns,
+ data,
+ defaultColumn,
+ dataDispatch,
+ autoResetSortBy: !skipReset,
+ autoResetFilters: !skipReset,
+ autoResetRowState: !skipReset,
+ sortTypes,
+ },
+ useFlexLayout,
+ useResizeColumns,
+ useSortBy
+ );
+
+ // function isTableResizing() {
+ // for (const headerGroup of headerGroups) {
+ // for (const column of headerGroup.headers) {
+ // if (column.isResizing) {
+ // return true;
+ // }
+ // }
+ // }
+
+ // return false;
+ // }
+
+ return (
+ <>
+
+
+ {headerGroups.map((headerGroup) => (
+
+ {headerGroup.headers.map((column) => column.render('Header'))}
+
+ ))}
+
+
+ {rows.map((row) => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map((cell) => (
+
+ {cell.render('Cell')}
+
+ ))}
+
+ );
+ })}
+
dataDispatch({ type: 'add_row' })}
+ onKeyPress={() => dataDispatch({ type: 'add_row' })}
+ >
+
+ {getIcon('plus')}
+
+ New
+
+
+
+ >
+ );
+}
diff --git a/src/components/TableScreen/TableScreen.tsx b/src/components/TableScreen/TableScreen.tsx
new file mode 100644
index 0000000..22f8287
--- /dev/null
+++ b/src/components/TableScreen/TableScreen.tsx
@@ -0,0 +1,326 @@
+import React, { useEffect, useReducer } from 'react';
+import Table from '../Table/Table';
+import { shortId, randomColor } from '../utils/ui_utils';
+
+function fakeData() {
+ const data = [];
+ const options = [];
+ for (let i = 0; i < 10; i += 1) {
+ const row = {
+ ID: 1,
+ firstName: 'temp_firstname',
+ lastName: 'temp_lastname',
+ email: 'temp_email@example.com',
+ age: 1 + i * i + i,
+ district: 'example_address',
+ };
+ options.push({ label: row.district, backgroundColor: randomColor() });
+
+ data.push(row);
+ }
+
+ const columns = [
+ {
+ id: 'firstName',
+ label: 'First Name',
+ accessor: 'firstName',
+ minWidth: 100,
+ dataType: 'text',
+ options: [],
+ },
+ {
+ id: 'lastName',
+ label: 'Last Name',
+ accessor: 'lastName',
+ minWidth: 100,
+ dataType: 'text',
+ options: [],
+ },
+ {
+ id: 'age',
+ label: 'Age',
+ accessor: 'age',
+ width: 80,
+ dataType: 'number',
+ options: [],
+ },
+ {
+ id: 'email',
+ label: 'E-Mail',
+ accessor: 'email',
+ width: 300,
+ dataType: 'text',
+ options: [],
+ },
+ {
+ id: 'district',
+ label: 'District',
+ accessor: 'district',
+ dataType: 'select',
+ width: 200,
+ options,
+ },
+ {
+ id: 999999,
+ width: 20,
+ label: '+',
+ disableResizing: true,
+ dataType: 'null',
+ },
+ ];
+
+ return { columns, data, skipReset: false };
+}
+
+function reducer(state, action) {
+ switch (action.type) {
+ case 'add_option_to_column': {
+ const optionIndex = state.columns.findIndex(
+ (column) => column.id === action.columnId
+ );
+ return {
+ ...state,
+ skipReset: true,
+ columns: [
+ ...state.columns.slice(0, optionIndex),
+ {
+ ...state.columns[optionIndex],
+ options: [
+ ...state.columns[optionIndex].options,
+ { label: action.option, backgroundColor: action.backgroundColor },
+ ],
+ },
+ ...state.columns.slice(optionIndex + 1, state.columns.length),
+ ],
+ };
+ }
+ case 'add_row':
+ return {
+ ...state,
+ skipReset: true,
+ data: [...state.data, {}],
+ };
+ case 'update_column_type': {
+ const typeIndex = state.columns.findIndex(
+ (column) => column.id === action.columnId
+ );
+ switch (action.dataType) {
+ case 'number':
+ if (state.columns[typeIndex].dataType === 'number') {
+ return state;
+ }
+ return {
+ ...state,
+ columns: [
+ ...state.columns.slice(0, typeIndex),
+ { ...state.columns[typeIndex], dataType: action.dataType },
+ ...state.columns.slice(typeIndex + 1, state.columns.length),
+ ],
+ data: state.data.map((row) => ({
+ ...row,
+ [action.columnId]: Number.isNaN(row[action.columnId])
+ ? ''
+ : Number.parseInt(row[action.columnId], 10),
+ })),
+ };
+
+ case 'select': {
+ if (state.columns[typeIndex].dataType === 'select') {
+ return {
+ ...state,
+ columns: [
+ ...state.columns.slice(0, typeIndex),
+ { ...state.columns[typeIndex], dataType: action.dataType },
+ ...state.columns.slice(typeIndex + 1, state.columns.length),
+ ],
+ skipReset: true,
+ };
+ }
+ const options = [];
+ state.data.forEach((row) => {
+ if (row[action.columnId]) {
+ options.push({
+ label: row[action.columnId],
+ backgroundColor: randomColor(),
+ });
+ }
+ });
+ return {
+ ...state,
+ columns: [
+ ...state.columns.slice(0, typeIndex),
+ {
+ ...state.columns[typeIndex],
+ dataType: action.dataType,
+ options: [...state.columns[typeIndex].options, ...options],
+ },
+ ...state.columns.slice(typeIndex + 1, state.columns.length),
+ ],
+ skipReset: true,
+ };
+ }
+ case 'text':
+ if (state.columns[typeIndex].dataType === 'text') {
+ return state;
+ }
+ if (state.columns[typeIndex].dataType === 'select') {
+ return {
+ ...state,
+ skipReset: true,
+ columns: [
+ ...state.columns.slice(0, typeIndex),
+ { ...state.columns[typeIndex], dataType: action.dataType },
+ ...state.columns.slice(typeIndex + 1, state.columns.length),
+ ],
+ };
+ }
+ return {
+ ...state,
+ skipReset: true,
+ columns: [
+ ...state.columns.slice(0, typeIndex),
+ { ...state.columns[typeIndex], dataType: action.dataType },
+ ...state.columns.slice(typeIndex + 1, state.columns.length),
+ ],
+ data: state.data.map((row) => ({
+ ...row,
+ [action.columnId]: `${row[action.columnId]}`,
+ })),
+ };
+
+ default:
+ return state;
+ }
+ }
+ case 'update_column_header': {
+ const index = state.columns.findIndex(
+ (column) => column.id === action.columnId
+ );
+ return {
+ ...state,
+ skipReset: true,
+ columns: [
+ ...state.columns.slice(0, index),
+ { ...state.columns[index], label: action.label },
+ ...state.columns.slice(index + 1, state.columns.length),
+ ],
+ };
+ }
+ case 'update_cell':
+ return {
+ ...state,
+ skipReset: true,
+ data: state.data.map((row, ridx) => {
+ if (ridx === action.rowIndex) {
+ return {
+ ...state.data[action.rowIndex],
+ [action.columnId]: action.value,
+ };
+ }
+ return row;
+ }),
+ };
+ case 'add_column_to_left': {
+ const leftIndex = state.columns.findIndex(
+ (column) => column.id === action.columnId
+ );
+ const leftId = shortId();
+ return {
+ ...state,
+ skipReset: true,
+ columns: [
+ ...state.columns.slice(0, leftIndex),
+ {
+ id: leftId,
+ label: 'Column',
+ accessor: leftId,
+ dataType: 'text',
+ created: action.focus && true,
+ options: [],
+ },
+ ...state.columns.slice(leftIndex, state.columns.length),
+ ],
+ };
+ }
+ case 'add_column_to_right': {
+ const rightIndex = state.columns.findIndex(
+ (column) => column.id === action.columnId
+ );
+ const rightId = shortId();
+ return {
+ ...state,
+ skipReset: true,
+ columns: [
+ ...state.columns.slice(0, rightIndex + 1),
+ {
+ id: rightId,
+ label: 'Column',
+ accessor: rightId,
+ dataType: 'text',
+ created: action.focus && true,
+ options: [],
+ },
+ ...state.columns.slice(rightIndex + 1, state.columns.length),
+ ],
+ };
+ }
+ case 'delete_column': {
+ const deleteIndex = state.columns.findIndex(
+ (column) => column.id === action.columnId
+ );
+ return {
+ ...state,
+ skipReset: true,
+ columns: [
+ ...state.columns.slice(0, deleteIndex),
+ ...state.columns.slice(deleteIndex + 1, state.columns.length),
+ ],
+ };
+ }
+ case 'enable_reset':
+ return {
+ ...state,
+ skipReset: false,
+ };
+ default:
+ return state;
+ }
+}
+
+function TableScreen() {
+ const [state, dispatch] = useReducer(reducer, fakeData());
+
+ useEffect(() => {
+ dispatch({ type: 'enable_reset' });
+ }, [state.data, state.columns]);
+
+ return (
+
+ );
+}
+
+export default TableScreen;
diff --git a/src/components/utils/ui_utils.js b/src/components/utils/ui_utils.js
new file mode 100644
index 0000000..0777d3c
--- /dev/null
+++ b/src/components/utils/ui_utils.js
@@ -0,0 +1,7 @@
+export function randomColor() {
+ return `hsl(${Math.floor(Math.random() * 360)}, 95%, 90%)`;
+}
+
+export function shortId() {
+ return `_ ${Math.random().toString(36).substr(2, 9)}`;
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 8331ec1..239d2ee 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,5 +1,9 @@
module.exports = {
theme: {},
- variants: {},
+ variants: {
+ extend: {
+ backgroundColor: ['even'],
+ },
+ },
plugins: [],
};