diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..98471ea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,42 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +indent_brace_style = K&R +curly_bracket_next_line = false +spaces_around_operators = true +max_line_length = 120 +tab_width = 2 +indent_size = 2 +continuation_indent_size = 4 +indent_style = tab +insert_final_newline = true + +[*.py] +indent_style = space +indent_size = 4 +continuation_indent_size = ignore +max_line_length = off + +[*.{vue,html,md,mustache,jsp,tag}] +tab_width=4 +indent_size=4 +max_line_length = off + +[*.{vue,ts,js}] +quote_type = single + +[*.{vue,ts,js,php,md,less,scss,saas,css,tsx,jsx}] +indent_style = space + + +[*.min.*] +indent_style = ignore +trim_trailing_whitespace = false +insert_final_newline = ignore +max_line_length = 6000 + +[*.{php,md,vue,html,mustache,jsp,tag,tsx,jsx}] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..224ba5a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,46 @@ +# Auto detect text files - normalize line endings to LF on checkin +* text=auto + +# Text files - normalize line endings to LF on checkout +*.ts text eol=lf +*.js text eol=lf +*.ejs text eol=lf +*.jsx text eol=lf +*.vue text eol=lf +*.html text eol=lf +*.css text eol=lf +*.less text eol=lf +*.scss eol=lf +*.yml text eol=lf +*.json text eol=lf +*.md text eol=lf +*.tag eol=lf +*.jsp eol=lf +*.json eol=lf + +.babelrc text eol=lf +.editorconfig text eol=lf +.eslintignore text eol=lf +.eslintrc text eol=lf +.flowconfig text eol=lf +.gitattributes text eol=lf +.gitignore text eol=lf +.gitkeep text eol=lf +.htmlhintrc text eol=lf +.modernizrrc text eol=lf + +# Binary files +*.ico binary +*.jpg binary +*.gif binary +*.png binary + +*.eot binary +*.ttf binary +*.woff binary +*.woff2 binary + +*.mp3 binary +*.mp4 binary +*.webm binary +*.ogg binary diff --git a/.gitignore b/.gitignore index 4d29575..19cd9b7 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,47 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +*.log +/typings + +# System files +.DS_Store +Thumbs.db +desktop.ini diff --git a/TODO b/TODO new file mode 100644 index 0000000..ddea301 --- /dev/null +++ b/TODO @@ -0,0 +1,14 @@ +[ ] find Grid components and to get choice + [ ] let's make some choice to use +[ ] find Math components +[ ] integrate Grid into this project +[ ] let's start 1st view with the Grid +[ ] studying events in cells and try to use it to integrate Math component to calculate +[ ] create in GitHub this proj and pub all files as a 1st branch + +[ ] try to use console Git commands +[ ] find some chart components +[ ] make choice and integrate some diagram into our Excel app + +[ ] npm i immutability-helper react-contenteditable +[ ] npm i clsx diff --git a/package-lock.json b/package-lock.json index 669f50c..35819df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,15 @@ "@types/node": "^16.11.65", "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", + "clsx": "^1.2.1", + "immutability-helper": "^3.1.1", "react": "^18.2.0", + "react-contenteditable": "^3.3.6", "react-dom": "^18.2.0", + "react-popper": "^2.3.0", "react-scripts": "5.0.1", + "react-table": "^7.8.0", + "react-window": "^1.8.7", "typescript": "^4.8.4", "web-vitals": "^2.1.4" } @@ -3074,6 +3080,16 @@ } } }, + "node_modules/@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -5397,6 +5413,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8500,6 +8524,11 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutability-helper": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", + "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -11333,6 +11362,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -13590,6 +13624,18 @@ "node": ">=14" } }, + "node_modules/react-contenteditable": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.6.tgz", + "integrity": "sha512-61+Anbmzggel1sP7nwvxq3d2woD3duR5R89RoLGqKan1A+nruFIcmLjw2F+qqk70AyABls0BDKzE1vqS1UIF1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -13724,11 +13770,30 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -13809,6 +13874,34 @@ } } }, + "node_modules/react-table": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", + "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" + } + }, + "node_modules/react-window": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz", + "integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15647,6 +15740,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -18584,6 +18685,12 @@ "source-map": "^0.7.3" } }, + "@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "peer": true + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -20320,6 +20427,11 @@ "wrap-ansi": "^7.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -22568,6 +22680,11 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==" }, + "immutability-helper": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", + "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -24624,6 +24741,11 @@ "fs-monkey": "^1.0.3" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -26057,6 +26179,15 @@ "whatwg-fetch": "^3.6.2" } }, + "react-contenteditable": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.6.tgz", + "integrity": "sha512-61+Anbmzggel1sP7nwvxq3d2woD3duR5R89RoLGqKan1A+nruFIcmLjw2F+qqk70AyABls0BDKzE1vqS1UIF1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + } + }, "react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -26157,11 +26288,25 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "requires": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -26222,6 +26367,21 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-table": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", + "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", + "requires": {} + }, + "react-window": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz", + "integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==", + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -27576,6 +27736,14 @@ "makeerror": "1.0.12" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 90fca80..3b393b4 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,15 @@ "@types/node": "^16.11.65", "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", + "clsx": "^1.2.1", + "immutability-helper": "^3.1.1", "react": "^18.2.0", + "react-contenteditable": "^3.3.6", "react-dom": "^18.2.0", + "react-popper": "^2.3.0", "react-scripts": "5.0.1", + "react-table": "^7.8.0", + "react-window": "^1.8.7", "typescript": "^4.8.4", "web-vitals": "^2.1.4" }, diff --git a/src/App.tsx b/src/App.tsx index a53698a..8d1c80b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,27 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; +import React from "react"; +// import logo from "./logo.svg"; +// import "./App.css"; -function App() { - return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
- ); -} +type GreetProps = typeof App.defaultProps & { + age: number; +}; + +type GreetState = { + name: string; +}; +class App extends React.Component { + static defaultProps = { + age: 21, + }; + state = { name: "Mike" }; + + render() { + return ( +
+ {this.props.age} {this.state.name} +
+ ); + } +} export default App; diff --git a/src/index.tsx b/src/index.tsx index 032464f..d8d43fa 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,12 +1,13 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); + root.render( diff --git a/src/lib/editableGrid/Badge.jsx b/src/lib/editableGrid/Badge.jsx new file mode 100644 index 0000000..fbe6ab0 --- /dev/null +++ b/src/lib/editableGrid/Badge.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +export default function Badge({ value, backgroundColor }) { + return ( + + {value} + + ); +} diff --git a/src/lib/editableGrid/Cell.jsx b/src/lib/editableGrid/Cell.jsx new file mode 100644 index 0000000..c2204ab --- /dev/null +++ b/src/lib/editableGrid/Cell.jsx @@ -0,0 +1,200 @@ +import React, { useEffect, useState } from 'react'; +import ContentEditable from 'react-contenteditable'; +import Badge from './Badge'; +import { usePopper } from 'react-popper'; +import { grey } from './colors'; +import PlusIcon from './img/Plus'; +import { ActionTypes, DataTypes, randomColor } from './utils'; +import { createPortal } from 'react-dom'; + +export default function Cell({ + value: initialValue, + row: { index }, + column: { id, dataType, options }, + dataDispatch, +}) { + const [value, setValue] = useState({ value: initialValue, update: false }); + const [selectRef, setSelectRef] = useState(null); + const [selectPop, setSelectPop] = useState(null); + const [showSelect, setShowSelect] = useState(false); + const [showAdd, setShowAdd] = useState(false); + const [addSelectRef, setAddSelectRef] = useState(null); + const { styles, attributes } = usePopper(selectRef, selectPop, { + placement: 'bottom-start', + strategy: 'fixed', + }); + + function handleOptionKeyDown(e) { + if (e.key === 'Enter') { + if (e.target.value !== '') { + dataDispatch({ + type: ActionTypes.ADD_OPTION_TO_COLUMN, + option: e.target.value, + backgroundColor: randomColor(), + columnId: id, + }); + } + setShowAdd(false); + } + } + + function handleAddOption(e) { + setShowAdd(true); + } + + function handleOptionBlur(e) { + if (e.target.value !== '') { + dataDispatch({ + type: ActionTypes.ADD_OPTION_TO_COLUMN, + option: e.target.value, + backgroundColor: randomColor(), + columnId: id, + }); + } + setShowAdd(false); + } + + function getColor() { + let match = options.find(option => option.label === value.value); + return (match && match.backgroundColor) || grey(200); + } + + function onChange(e) { + setValue({ value: e.target.value, update: false }); + } + + function handleOptionClick(option) { + setValue({ value: option.label, update: true }); + setShowSelect(false); + } + + function getCellElement() { + switch (dataType) { + case DataTypes.TEXT: + return ( + setValue(old => ({ value: old.value, update: true }))} + className="data-input" + /> + ); + case DataTypes.NUMBER: + return ( + setValue(old => ({ value: old.value, update: true }))} + className="data-input text-align-right" + /> + ); + case DataTypes.SELECT: + return ( + <> +
setShowSelect(true)} + > + {value.value && ( + + )} +
+ {showSelect && ( +
setShowSelect(false)} /> + )} + {showSelect && + createPortal( +
+
+ {options.map(option => ( +
handleOptionClick(option)} + > + +
+ ))} + {showAdd && ( +
+ +
+ )} +
+ + + + } + backgroundColor={grey(200)} + /> +
+
+
, + document.querySelector('#popper-portal') + )} + + ); + default: + return ; + } + } + + useEffect(() => { + if (addSelectRef && showAdd) { + addSelectRef.focus(); + } + }, [addSelectRef, showAdd]); + + useEffect(() => { + setValue({ value: initialValue, update: false }); + }, [initialValue]); + + useEffect(() => { + if (value.update) { + dataDispatch({ + type: ActionTypes.UPDATE_CELL, + columnId: id, + rowIndex: index, + value: value.value, + }); + } + }, [value, dataDispatch, id, index]); + + return getCellElement(); +} diff --git a/src/lib/editableGrid/EditableGrid.tsx b/src/lib/editableGrid/EditableGrid.tsx new file mode 100644 index 0000000..7c83693 --- /dev/null +++ b/src/lib/editableGrid/EditableGrid.tsx @@ -0,0 +1,213 @@ +import React, { useEffect, useReducer } from 'react'; +import './style.css'; +import Table from './Table'; +import { + randomColor, + shortId, + makeData, + ActionTypes, + DataTypes, +} from './utils'; +import update from 'immutability-helper'; + +function reducer(state, action) { + switch (action.type) { + case ActionTypes.ADD_OPTION_TO_COLUMN: + const optionIndex = state.columns.findIndex( + column => column.id === action.columnId + ); + return update(state, { + skipReset: { $set: true }, + columns: { + [optionIndex]: { + options: { + $push: [ + { + label: action.option, + backgroundColor: action.backgroundColor, + }, + ], + }, + }, + }, + }); + case ActionTypes.ADD_ROW: + return update(state, { + skipReset: { $set: true }, + data: { $push: [{}] }, + }); + case ActionTypes.UPDATE_COLUMN_TYPE: + const typeIndex = state.columns.findIndex( + column => column.id === action.columnId + ); + switch (action.dataType) { + case DataTypes.NUMBER: + if (state.columns[typeIndex].dataType === DataTypes.NUMBER) { + return state; + } else { + return update(state, { + skipReset: { $set: true }, + columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, + data: { + $apply: data => + data.map(row => ({ + ...row, + [action.columnId]: isNaN(row[action.columnId]) + ? '' + : Number.parseInt(row[action.columnId]), + })), + }, + }); + } + case DataTypes.SELECT: + if (state.columns[typeIndex].dataType === DataTypes.SELECT) { + return state; + } else { + let options = []; + state.data.forEach(row => { + if (row[action.columnId]) { + options.push({ + label: row[action.columnId], + backgroundColor: randomColor(), + }); + } + }); + return update(state, { + skipReset: { $set: true }, + columns: { + [typeIndex]: { + dataType: { $set: action.dataType }, + options: { $push: options }, + }, + }, + }); + } + case DataTypes.TEXT: + if (state.columns[typeIndex].dataType === DataTypes.TEXT) { + return state; + } else if (state.columns[typeIndex].dataType === DataTypes.SELECT) { + return update(state, { + skipReset: { $set: true }, + columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, + }); + } else { + return update(state, { + skipReset: { $set: true }, + columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, + data: { + $apply: data => + data.map(row => ({ + ...row, + [action.columnId]: row[action.columnId] + '', + })), + }, + }); + } + default: + return state; + } + case ActionTypes.UPDATE_COLUMN_HEADER: + const index = state.columns.findIndex( + column => column.id === action.columnId + ); + return update(state, { + skipReset: { $set: true }, + columns: { [index]: { label: { $set: action.label } } }, + }); + case ActionTypes.UPDATE_CELL: + return update(state, { + skipReset: { $set: true }, + data: { + [action.rowIndex]: { [action.columnId]: { $set: action.value } }, + }, + }); + case ActionTypes.ADD_COLUMN_TO_LEFT: + const leftIndex = state.columns.findIndex( + column => column.id === action.columnId + ); + let leftId = shortId(); + return update(state, { + skipReset: { $set: true }, + columns: { + $splice: [ + [ + leftIndex, + 0, + { + id: leftId, + label: 'Column', + accessor: leftId, + dataType: DataTypes.TEXT, + created: action.focus && true, + options: [], + }, + ], + ], + }, + }); + case ActionTypes.ADD_COLUMN_TO_RIGHT: + const rightIndex = state.columns.findIndex( + column => column.id === action.columnId + ); + const rightId = shortId(); + return update(state, { + skipReset: { $set: true }, + columns: { + $splice: [ + [ + rightIndex + 1, + 0, + { + id: rightId, + label: 'Column', + accessor: rightId, + dataType: DataTypes.TEXT, + created: action.focus && true, + options: [], + }, + ], + ], + }, + }); + case ActionTypes.DELETE_COLUMN: + const deleteIndex = state.columns.findIndex( + column => column.id === action.columnId + ); + return update(state, { + skipReset: { $set: true }, + columns: { $splice: [[deleteIndex, 1]] }, + }); + case ActionTypes.ENABLE_RESET: + return update(state, { skipReset: { $set: true } }); + default: + return state; + } +} + +function App() { + const [state, dispatch] = useReducer(reducer, makeData(1000)); + + useEffect(() => { + dispatch({ type: ActionTypes.ENABLE_RESET }); + }, [state.data, state.columns]); + + return ( +
+ +
+ + ); +} + +export default App; diff --git a/src/lib/editableGrid/Header.jsx b/src/lib/editableGrid/Header.jsx new file mode 100644 index 0000000..6d36528 --- /dev/null +++ b/src/lib/editableGrid/Header.jsx @@ -0,0 +1,347 @@ +import React, { useState, useEffect } from 'react'; +import { usePopper } from 'react-popper'; +import { grey } from './colors'; +import ArrowUpIcon from './img/ArrowUp'; +import ArrowDownIcon from './img/ArrowDown'; +import ArrowLeftIcon from './img/ArrowLeft'; +import ArrowRightIcon from './img/ArrowRight'; +import TrashIcon from './img/Trash'; +import TextIcon from './img/Text'; +import MultiIcon from './img/Multi'; +import HashIcon from './img/Hash'; +import PlusIcon from './img/Plus'; +import { ActionTypes, DataTypes, shortId } from './utils'; + +function getPropertyIcon(dataType) { + switch (dataType) { + case DataTypes.NUMBER: + return ; + case DataTypes.TEXT: + return ; + case DataTypes.SELECT: + return ; + default: + return null; + } +} + +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 typePopper = usePopper(typeReferenceElement, typePopperElement, { + placement: 'right', + strategy: 'fixed', + }); + const [showType, setShowType] = useState(false); + const buttons = [ + { + onClick: e => { + dataDispatch({ + type: ActionTypes.UPDATE_COLUMN_HEADER, + columnId: id, + label: header, + }); + setSortBy([{ id: id, desc: false }]); + setExpanded(false); + }, + icon: , + label: 'Sort ascending', + }, + { + onClick: e => { + dataDispatch({ + type: ActionTypes.UPDATE_COLUMN_HEADER, + columnId: id, + label: header, + }); + setSortBy([{ id: id, desc: true }]); + setExpanded(false); + }, + icon: , + label: 'Sort descending', + }, + { + onClick: e => { + dataDispatch({ + type: ActionTypes.UPDATE_COLUMN_HEADER, + columnId: id, + label: header, + }); + dataDispatch({ + type: ActionTypes.ADD_COLUMN_TO_LEFT, + columnId: id, + focus: false, + }); + setExpanded(false); + }, + icon: , + label: 'Insert left', + }, + { + onClick: e => { + dataDispatch({ + type: ActionTypes.UPDATE_COLUMN_HEADER, + columnId: id, + label: header, + }); + dataDispatch({ + type: ActionTypes.ADD_COLUMN_TO_RIGHT, + columnId: id, + focus: false, + }); + setExpanded(false); + }, + icon: , + label: 'Insert right', + }, + { + onClick: e => { + dataDispatch({ + type: ActionTypes.UPDATE_COLUMN_HEADER, + columnId: id, + label: header, + }); + dataDispatch({ type: ActionTypes.DELETE_COLUMN, columnId: id }); + setExpanded(false); + }, + icon: , + label: 'Delete', + }, + ]; + const propertyIcon = getPropertyIcon(dataType); + + const types = [ + { + onClick: e => { + dataDispatch({ + type: 'update_column_type', + columnId: id, + dataType: DataTypes.SELECT, + }); + setShowType(false); + setExpanded(false); + }, + icon: , + label: 'Select', + }, + { + onClick: e => { + dataDispatch({ + type: 'update_column_type', + columnId: id, + dataType: DataTypes.TEXT, + }); + setShowType(false); + setExpanded(false); + }, + icon: , + label: 'Text', + }, + { + onClick: e => { + dataDispatch({ + type: 'update_column_type', + columnId: id, + dataType: DataTypes.NUMBER, + }); + setShowType(false); + setExpanded(false); + }, + icon: , + label: 'Number', + }, + ]; + + 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 }); + } + + function getHeader() { + if (id !== 999999) { + return ( + <> +
+
setExpanded(true)} + ref={setReferenceElement} + > + + {propertyIcon} + + {label} +
+
+
+ {expanded && ( +
setExpanded(false)} /> + )} + {expanded && ( +
+
+
+
+ +
+ + Property Type + +
+
+ + {showType && ( +
setShowType(true)} + onMouseLeave={() => setShowType(false)} + {...typePopper.attributes.popper} + style={{ + ...typePopper.styles.popper, + width: 200, + backgroundColor: 'white', + zIndex: 4, + }} + > + {types.map(type => ( + + ))} +
+ )} +
+
+ {buttons.map(button => ( + + ))} +
+
+
+ )} + + ); + } + return ( +
+
+ dataDispatch({ + type: 'add_column_to_left', + columnId: 999999, + focus: true, + }) + } + > + + + +
+
+ ); + } + + useEffect(() => { + if (created) { + setExpanded(true); + } + }, [created]); + + useEffect(() => { + setHeader(label); + }, [label]); + + useEffect(() => { + if (inputRef) { + inputRef.focus(); + inputRef.select(); + } + }, [inputRef]); + + return getHeader(); +} diff --git a/src/lib/editableGrid/Table.jsx b/src/lib/editableGrid/Table.jsx new file mode 100644 index 0000000..d43bc56 --- /dev/null +++ b/src/lib/editableGrid/Table.jsx @@ -0,0 +1,141 @@ +import React, { useMemo } from 'react'; +import clsx from 'clsx'; +import { + useTable, + useBlockLayout, + useResizeColumns, + useSortBy, +} from 'react-table'; +import Cell from './Cell'; +import Header from './Header'; +import PlusIcon from './img/Plus'; +import { ActionTypes } from './utils'; +import { FixedSizeList } from 'react-window'; +import scrollbarWidth from './scrollbarWidth'; + +const defaultColumn = { + minWidth: 50, + width: 150, + maxWidth: 400, + Cell: Cell, + Header: 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 isNaN(rowA.values[columnId]) + ? rowA.values[columnId].localeCompare(rowB.values[columnId]) + : rowA.values[columnId] - rowB.values[columnId]; + }, + }), + [] + ); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + totalColumnsWidth, + } = useTable( + { + columns, + data, + defaultColumn, + dataDispatch, + autoResetSortBy: !skipReset, + autoResetFilters: !skipReset, + autoResetRowState: !skipReset, + sortTypes, + }, + useBlockLayout, + useResizeColumns, + useSortBy + ); + + const RenderRow = React.useCallback( + ({ index, style }) => { + const row = rows[index]; + prepareRow(row); + return ( +
+ {row.cells.map(cell => ( +
+ {cell.render('Cell')} +
+ ))} +
+ ); + }, + [prepareRow, rows] + ); + + function isTableResizing() { + for (let headerGroup of headerGroups) { + for (let column of headerGroup.headers) { + if (column.isResizing) { + return true; + } + } + } + + return false; + } + + return ( + <> +
+
+ {headerGroups.map(headerGroup => ( +
+ {headerGroup.headers.map(column => column.render('Header'))} +
+ ))} +
+
+ + {RenderRow} + +
dataDispatch({ type: ActionTypes.ADD_ROW })} + > + + + + New +
+
+
+ + ); +} diff --git a/src/lib/editableGrid/colors.js b/src/lib/editableGrid/colors.js new file mode 100644 index 0000000..b7d2006 --- /dev/null +++ b/src/lib/editableGrid/colors.js @@ -0,0 +1,16 @@ +export function grey(value) { + let reference = { + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#eeeeee', + 300: '#e0e0e0', + 400: '#bdbdbd', + 500: '#9e9e9e', + 600: '#757575', + 700: '#616161', + 800: '#424242', + 900: '#212121', + }; + + return reference[value]; +} diff --git a/src/lib/editableGrid/scrollbarWidth.js b/src/lib/editableGrid/scrollbarWidth.js new file mode 100644 index 0000000..6d43662 --- /dev/null +++ b/src/lib/editableGrid/scrollbarWidth.js @@ -0,0 +1,13 @@ +const scrollbarWidth = () => { + const scrollDiv = document.createElement('div'); + scrollDiv.setAttribute( + 'style', + 'width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;' + ); + document.body.appendChild(scrollDiv); + const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + return scrollbarWidth; +}; + +export default scrollbarWidth; diff --git a/src/lib/editableGrid/utils.js b/src/lib/editableGrid/utils.js new file mode 100644 index 0000000..c7f6c5f --- /dev/null +++ b/src/lib/editableGrid/utils.js @@ -0,0 +1,96 @@ +import faker from 'faker'; + +export function shortId() { + return '_' + Math.random().toString(36).substr(2, 9); +} + +export function randomColor() { + return `hsl(${Math.floor(Math.random() * 360)}, 95%, 90%)`; +} + +export function makeData(count) { + let data = []; + let options = []; + for (let i = 0; i < count; i++) { + let row = { + ID: faker.mersenne.rand(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + email: faker.internet.email(), + age: Math.floor(20 + Math.random() * 20), + music: faker.music.genre(), + }; + options.push({ label: row.music, backgroundColor: randomColor() }); + + data.push(row); + } + + let columns = [ + { + id: 'firstName', + label: 'First Name', + accessor: 'firstName', + minWidth: 100, + dataType: DataTypes.TEXT, + options: [], + }, + { + id: 'lastName', + label: 'Last Name', + accessor: 'lastName', + minWidth: 100, + dataType: DataTypes.TEXT, + options: [], + }, + { + id: 'age', + label: 'Age', + accessor: 'age', + width: 80, + dataType: DataTypes.NUMBER, + options: [], + }, + { + id: 'email', + label: 'E-Mail', + accessor: 'email', + width: 300, + dataType: DataTypes.TEXT, + options: [], + }, + { + id: 'music', + label: 'Music Preference', + accessor: 'music', + dataType: DataTypes.SELECT, + width: 200, + options: options, + }, + { + id: 999999, + width: 20, + label: '+', + disableResizing: true, + dataType: 'null', + }, + ]; + return { columns: columns, data: data, skipReset: false }; +} + +export const ActionTypes = Object.freeze({ + ADD_OPTION_TO_COLUMN: 'add_option_to_column', + ADD_ROW: 'add_row', + UPDATE_COLUMN_TYPE: 'update_column_type', + UPDATE_COLUMN_HEADER: 'update_column_header', + UPDATE_CELL: 'update_cell', + ADD_COLUMN_TO_LEFT: 'add_column_to_left', + ADD_COLUMN_TO_RIGHT: 'add_column_to_right', + DELETE_COLUMN: 'delete_column', + ENABLE_RESET: 'enable_reset', +}); + +export const DataTypes = Object.freeze({ + NUMBER: 'number', + TEXT: 'text', + SELECT: 'select', +}); diff --git a/src/plugins/grid/Grid.tsx b/src/plugins/grid/Grid.tsx new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json index a273b0c..a0b3159 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,31 @@ { + "compileOnSave": false, "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, + "allowSyntheticDefaultImports": true, + "baseUrl": "./", + "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, + "strict": false, + "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "module": "esnext", + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" + "importHelpers": true, + "target": "es2020", + "module": "es2020", + "lib": [ + "es2018", + "dom" + ], + "skipLibCheck": true }, - "include": [ - "src" - ] + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + } }