├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENCE.md ├── README.md ├── config-overrides.js ├── package.json ├── public ├── favicon.ico ├── index.html ├── lemonunit-logo.png ├── logo1024.png ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.tsx ├── components │ ├── Footer.tsx │ └── Navigation.tsx ├── editors │ ├── CSVEditor.tsx │ ├── DataGridEditor.tsx │ ├── JSONEditor.tsx │ └── types.d.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts └── serviceWorker.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [[0.3.0](https://github.com/LemonUnit/csv-editor-online/releases/tag/0.3.0)] - 2019-09-23 8 | ### Added 9 | - Added Twitter card config 10 | 11 | ## [[0.2.0](https://github.com/LemonUnit/csv-editor-online/releases/tag/0.2.0)] - 2019-09-12 12 | ### Fixed 13 | - Fixed error with empty rows in Data Grid [#1](https://github.com/LemonUnit/csv-editor-online/issues/1) 14 | ### Added 15 | - Enabled PWA support [#2](https://github.com/LemonUnit/csv-editor-online/issues/3) 16 | ### Changed 17 | - Improved CSV delimiter logic [#3](https://github.com/LemonUnit/csv-editor-online/issues/3) 18 | 19 | ## [[0.1.0](https://github.com/LemonUnit/csv-editor-online/releases/tag/0.1.0)] - 2019-09-05 20 | ### Added 21 | - Initial application configuration with Create React App, TypeScript and Ant Design 22 | - Added CSV, GRID and JSON modes 23 | - Added deploy to GitHub Pages 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions to the `csv-editor-online` are very welcome! We can't do this 4 | alone. You can contribute in different ways: spread the word, report bugs, come 5 | up with ideas and suggestions, and contribute to the code. 6 | 7 | Thanks! -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 LemonUnit 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSV Editor Online 2 | 3 | Editor which allows to change CSV in spreadsheet view in simple way. 4 | 5 | https://lemonunit.github.io/csv-editor-online/ 6 | 7 | ![CSV View](https://i.ibb.co/SfhJHvV/csvview.png) 8 | ![Grid Racing](https://i.ibb.co/3ScZXtp/gridview.png) 9 | ![JSON Racing](https://i.ibb.co/Rb2pHkH/jsonview.png) 10 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const { override, fixBabelImports, addLessLoader } = require('customize-cra'); 2 | 3 | module.exports = override( 4 | fixBabelImports('import', { 5 | libraryName: 'antd', 6 | libraryDirectory: 'es', 7 | style: true, 8 | }), 9 | addLessLoader({ 10 | javascriptEnabled: true, 11 | modifyVars: { 12 | '@primary-color': '#1890ff', // primary color for all components 13 | '@link-color': '#1890ff', // link color 14 | '@success-color': '#52c41a', // success state color 15 | '@warning-color': '#faad14', // warning state color 16 | '@error-color': '#f5222d', // error state color 17 | '@font-size-base': '18px', // major text font size 18 | '@heading-color': 'rgba(0, 0, 0, 0.85)', // heading text color 19 | '@text-color': 'rgba(0, 0, 0, 0.65)', // major text color 20 | '@text-color-secondary': 'rgba(0, 0, 0, .45)', // secondary text color 21 | '@disabled-color': 'rgba(0, 0, 0, .25)', // disable state color 22 | '@border-radius-base': '3px', // major border radius 23 | '@border-color-base': '#d9d9d9', // major border color 24 | '@box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)' // major shadow for layers 25 | }, 26 | }), 27 | ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-editor-online", 3 | "version": "0.1.0", 4 | "homepage": "http://lemonunit.github.io/csv-editor-online", 5 | "dependencies": { 6 | "@types/jest": "24.0.18", 7 | "@types/node": "12.7.2", 8 | "@types/react": "16.9.2", 9 | "@types/react-dom": "16.9.0", 10 | "antd": "^3.22.2", 11 | "brace": "^0.11.1", 12 | "csv-string": "^3.1.6", 13 | "customize-cra": "^0.5.0", 14 | "react": "^16.9.0", 15 | "react-ace": "^7.0.4", 16 | "react-app-rewired": "^2.1.3", 17 | "react-dom": "^16.9.0", 18 | "react-scripts": "3.1.1", 19 | "react-spreadsheet": "^0.4.34", 20 | "throttle-debounce": "^2.1.0", 21 | "typescript": "3.5.3" 22 | }, 23 | "scripts": { 24 | "start": "react-app-rewired start", 25 | "build": "react-app-rewired build", 26 | "test": "react-app-rewired test", 27 | "eject": "react-scripts eject", 28 | "predeploy": "npm run build", 29 | "deploy": "gh-pages -d build" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@types/throttle-debounce": "^2.1.0", 48 | "babel-plugin-import": "^1.12.1", 49 | "eslint-utils": "1.4.1", 50 | "gh-pages": "^2.1.1", 51 | "less": "^3.10.3", 52 | "less-loader": "^5.0.0" 53 | } 54 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonUnit/csv-editor-online/b13203116e5bfa51fe2cfcef462b09d8bbc59586/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 38 | CSV Editor Online 39 | 40 | 41 | 42 | 43 |
44 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/lemonunit-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonUnit/csv-editor-online/b13203116e5bfa51fe2cfcef462b09d8bbc59586/public/lemonunit-logo.png -------------------------------------------------------------------------------- /public/logo1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonUnit/csv-editor-online/b13203116e5bfa51fe2cfcef462b09d8bbc59586/public/logo1024.png -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonUnit/csv-editor-online/b13203116e5bfa51fe2cfcef462b09d8bbc59586/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonUnit/csv-editor-online/b13203116e5bfa51fe2cfcef462b09d8bbc59586/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CSV Editor", 3 | "name": "CSV Editor", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#001529", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | color: #fff; 3 | float: left; 4 | margin-right: 30px; 5 | } 6 | 7 | .Editor { 8 | width: 100vw; 9 | height: calc(100vh - 64px - 30px); 10 | overflow: auto; 11 | } 12 | 13 | .ant-layout-header { 14 | padding: 0 30px !important; 15 | } 16 | 17 | .Spreadsheet { 18 | font-size: 12px !important; 19 | } 20 | 21 | .Spreadsheet th { 22 | min-width: 30px !important; 23 | min-height: 20px !important; 24 | } 25 | 26 | .Spreadsheet td { 27 | min-width: 100px !important; 28 | min-height: 20px !important; 29 | } 30 | 31 | .SpreadsheetTable tr:nth-child(2) td { 32 | font-weight: bold; 33 | } 34 | 35 | .ConfigBar { 36 | padding: 5px 30px; 37 | background: #fff; 38 | height: 40px; 39 | font-size: 12px; 40 | border-bottom: 1px solid #e8e8e8; 41 | } 42 | 43 | .ConfigBar .ConfigBarWidgetLabel { 44 | padding-right: 10px; 45 | color: #777; 46 | } 47 | 48 | .Footer { 49 | height: 30px; 50 | line-height: 30px; 51 | padding: 0 10px; 52 | background: #fff; 53 | font-size: 13px; 54 | border-top: 1px solid #e8e8e8; 55 | } 56 | 57 | .GitHubIcon { 58 | color: #ccc; 59 | font-size: 20px; 60 | } 61 | 62 | .GitHubIcon:hover { 63 | color: #333; 64 | } 65 | 66 | .LemonUnitIcon { 67 | position: relative; 68 | } 69 | 70 | .LemonUnitIcon img { 71 | top: -1px; 72 | position: relative; 73 | height: 24px; 74 | opacity: 0.4; 75 | transition: opacity 0.4s ease-out; 76 | } 77 | 78 | .LemonUnitIcon:hover img { 79 | opacity: 1; 80 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import { Navigation } from "./components/Navigation"; 4 | import { Footer } from "./components/Footer"; 5 | 6 | import { SourceItems } from "./editors/types"; 7 | import { CSVEditor } from "./editors/CSVEditor"; 8 | import { JSONEditor } from "./editors/JSONEditor"; 9 | import { DataGridEditor } from "./editors/DataGridEditor"; 10 | 11 | import "./App.css"; 12 | 13 | export enum EditorMode { 14 | CSV = "CSV", 15 | GRID = "GRID", 16 | JSON = "JSON" 17 | } 18 | 19 | interface AppProps { } 20 | 21 | interface AppState { 22 | mode: EditorMode; 23 | source: SourceItems; 24 | } 25 | 26 | const modeToComponentMap = Object.freeze({ 27 | [EditorMode.CSV]: CSVEditor, 28 | [EditorMode.JSON]: JSONEditor, 29 | [EditorMode.GRID]: DataGridEditor, 30 | }) 31 | 32 | export class App extends Component { 33 | constructor(props: AppProps) { 34 | super(props); 35 | this.state = { 36 | mode: EditorMode.CSV, 37 | source: [] 38 | } 39 | } 40 | 41 | handleModeChange = (mode: EditorMode) => this.setState({ mode }) 42 | 43 | handleSourceChange = (source: SourceItems) => this.setState({ source }) 44 | 45 | render() { 46 | const EditorComponent = modeToComponentMap[this.state.mode] ||
Error
; 47 | 48 | return ( 49 |
50 | 54 |
55 | 59 |
60 |
61 |
62 | ) 63 | } 64 | } -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Icon, Row, Col } from "antd"; 3 | 4 | interface Props { } 5 | interface State { } 6 | 7 | export const FOOTER_HEIGHT = 30; 8 | 9 | export class Footer extends Component { 10 | renderGithubLink = () => { 11 | return ( 12 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | renderLemonUnitLink = () => { 25 | return ( 26 | 32 | LemonUnit logo 33 | 34 | ); 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 | 41 | 42 | {this.renderLemonUnitLink()} 43 | 44 | 45 | {this.renderGithubLink()} 46 | 47 | 48 |
49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Layout, Menu, Icon } from "antd"; 3 | 4 | import { EditorMode } from "../App"; 5 | 6 | interface Props { 7 | onModeChange: (mode: EditorMode) => void; 8 | mode: EditorMode; 9 | } 10 | 11 | interface State { } 12 | 13 | export const NAVIGATION_HEIGHT = 64; 14 | export const CONFIG_BAR_HEIGHT = 40; 15 | 16 | export class Navigation extends Component { 17 | renderLogo = () => { 18 | return ( 19 |
20 | CSV Editor 21 |
22 | ); 23 | } 24 | 25 | renderButtons = () => { 26 | const buttons = [ 27 | EditorMode.CSV, 28 | EditorMode.GRID, 29 | EditorMode.JSON 30 | ].map(mode => ( 31 | this.props.onModeChange(mode)} 34 | > 35 | {mode} 36 | 37 | )); 38 | 39 | return ( 40 | 46 | {buttons} 47 | 48 | ); 49 | } 50 | 51 | render() { 52 | return ( 53 | 54 | {this.renderLogo()} 55 | {this.renderButtons()} 56 | 57 | ) 58 | } 59 | } -------------------------------------------------------------------------------- /src/editors/CSVEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import AceEditor from "react-ace"; 3 | import { Radio, Modal } from "antd"; 4 | import { RadioChangeEvent } from "antd/lib/radio"; 5 | // @ts-ignore 6 | import * as csvString from "csv-string"; 7 | import brace from "brace"; // eslint-disable-line @typescript-eslint/no-unused-vars 8 | 9 | import "brace/theme/github"; 10 | import "brace/mode/json"; 11 | 12 | import { EditorProps } from "./types"; 13 | import { NAVIGATION_HEIGHT, CONFIG_BAR_HEIGHT } from "../components/Navigation"; 14 | import { FOOTER_HEIGHT } from "../components/Footer"; 15 | 16 | enum CSVDelimiter { 17 | SEMICOLON = ";", 18 | COMMA = ",", 19 | VARTICAL_BAR = "|" 20 | } 21 | 22 | interface Props extends EditorProps { } 23 | 24 | interface State { 25 | csv: string; 26 | delimiter: CSVDelimiter; 27 | editorHeight: number; 28 | } 29 | 30 | const defaultState: State = { 31 | csv: "", 32 | editorHeight: 0, 33 | delimiter: CSVDelimiter.SEMICOLON 34 | } 35 | 36 | export class CSVEditor extends Component { 37 | state = { 38 | ...defaultState, 39 | delimiter: 'localStorage' in window 40 | ? window.localStorage.getItem('delimiter') as CSVDelimiter || CSVDelimiter.SEMICOLON 41 | : CSVDelimiter.SEMICOLON 42 | }; 43 | 44 | componentDidMount() { 45 | window.addEventListener('resize', () => this.updateEditorHeight()) 46 | 47 | this.updateEditorHeight() 48 | this.parseSourceItemsToCsv(); 49 | } 50 | 51 | componentWillUnmount() { 52 | window.removeEventListener('resize', () => this.updateEditorHeight()) 53 | 54 | this.setState(defaultState); 55 | } 56 | 57 | updateEditorHeight = () => this.setState({ 58 | editorHeight: window.innerHeight - NAVIGATION_HEIGHT - FOOTER_HEIGHT - CONFIG_BAR_HEIGHT 59 | }) 60 | 61 | parseSourceItemsToCsv = () => this.setState({ 62 | csv: csvString.stringify(this.props.source, this.state.delimiter) 63 | }); 64 | 65 | updateSourceItemsByCsvString = () => { 66 | const sourceItems = csvString.parse(this.state.csv, this.state.delimiter); 67 | 68 | this.props.onSourceChange(sourceItems); 69 | } 70 | 71 | handleChange = (csv: string) => this.setState({ 72 | csv 73 | }, () => this.updateSourceItemsByCsvString()); 74 | 75 | handleDelimiterChange = (e: RadioChangeEvent) => { 76 | const { value } = e.target; 77 | 78 | if (!this.state.csv.trim()) { 79 | return this.updateDelimiter(value); 80 | } 81 | 82 | Modal.confirm({ 83 | title: 'Do you want to update CSV string to new delimiter?', 84 | content: '', 85 | onOk: () => this.updateCSVStringDelimiter(value), 86 | onCancel: () => this.updateDelimiter(value) 87 | }); 88 | } 89 | 90 | updateCSVStringDelimiter = (delimiter: CSVDelimiter) => this.setState({ 91 | csv: csvString.stringify(csvString.parse(this.state.csv, this.state.delimiter), delimiter) 92 | }, () => this.updateDelimiter(delimiter)); 93 | 94 | updateDelimiter = (delimiter: CSVDelimiter) => this.setState({ 95 | delimiter 96 | }, () => { 97 | if ('localStorage' in window) { 98 | window.localStorage.setItem('delimiter', delimiter); 99 | } 100 | }); 101 | 102 | renderConfigBar = () => ( 103 |
104 |
105 | CSV Delimeter: 106 | 107 | 112 | {[ 113 | CSVDelimiter.SEMICOLON, 114 | CSVDelimiter.COMMA, 115 | CSVDelimiter.VARTICAL_BAR 116 | ].map((item, index) => ( 117 | 121 | {item} 122 | 123 | ))} 124 | 125 | 126 |
127 |
128 | ) 129 | 130 | render() { 131 | return ( 132 | <> 133 | {this.renderConfigBar()} 134 | 144 | 145 | ) 146 | } 147 | } -------------------------------------------------------------------------------- /src/editors/DataGridEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | // @ts-ignore 3 | import Spreadsheet from "react-spreadsheet"; 4 | import { debounce } from "throttle-debounce"; 5 | 6 | import { EditorProps, CellItem } from "./types"; 7 | 8 | interface Props extends EditorProps { } 9 | 10 | interface GridCellItem { 11 | value: string | number | null; 12 | } 13 | 14 | interface State { 15 | grid: GridCellItem[][], 16 | activeRow: number; 17 | activeCol: number; 18 | } 19 | 20 | interface OnActivateOptons { 21 | column: number; 22 | row: number; 23 | } 24 | 25 | const DEFAULT_CELL_ACTIVE_INDEX = -1; 26 | const MIN_GRID_ROWS = 50; 27 | const MIN_GRID_COLUMNS = 26; 28 | 29 | function getEmptyRow(columnsCount: number): GridCellItem[] { 30 | const row = []; 31 | 32 | for (let i = 0; i < columnsCount; i++) { 33 | row[i] = { value: '' } 34 | } 35 | 36 | return row; 37 | } 38 | 39 | function getMultipleEmptyRows(rowsCount: number, columnsCount: number): GridCellItem[][] { 40 | const emptyRow = getEmptyRow(columnsCount); 41 | 42 | const rows = []; 43 | 44 | for (let i = 0; i < rowsCount; i++) { 45 | rows[i] = emptyRow 46 | } 47 | 48 | return rows; 49 | } 50 | 51 | const defaultState: State = { 52 | activeRow: DEFAULT_CELL_ACTIVE_INDEX, 53 | activeCol: DEFAULT_CELL_ACTIVE_INDEX, 54 | grid: [] 55 | } 56 | 57 | export class DataGridEditor extends Component { 58 | state = defaultState; 59 | 60 | componentDidMount() { 61 | this.parseSourceItemsToGrid(); 62 | } 63 | 64 | componentWillUnmount() { 65 | this.addNewRowIfLastIsActive(); 66 | this.setState(defaultState); 67 | } 68 | 69 | parseSourceItemsToGrid = () => { 70 | const { source } = this.props; 71 | 72 | const sourceRowsLength = source.length; 73 | 74 | const grid: GridCellItem[][] = []; 75 | 76 | for (let rowIndex = 0; rowIndex < sourceRowsLength; rowIndex++) { 77 | const row = source[rowIndex]; 78 | 79 | if (!row) { 80 | continue; 81 | } 82 | 83 | const rowColumnsLength = row.length; 84 | 85 | if (!grid[rowIndex]) { 86 | grid[rowIndex] = []; 87 | } 88 | 89 | for (let rowCellIndex = 0; rowCellIndex < rowColumnsLength; rowCellIndex++) { 90 | const _cell = row[rowCellIndex]; 91 | 92 | grid[rowIndex][rowCellIndex] = { 93 | value: _cell 94 | }; 95 | } 96 | 97 | const columnsToAdd = MIN_GRID_COLUMNS - rowColumnsLength; 98 | 99 | for (let addColumnIndex = 0; addColumnIndex < columnsToAdd; addColumnIndex++) { 100 | grid[rowIndex][rowColumnsLength + addColumnIndex] = { 101 | value: "" 102 | }; 103 | } 104 | } 105 | 106 | const rowsToAdd = MIN_GRID_ROWS - sourceRowsLength; 107 | const emptyRow = getEmptyRow(grid[0] ? grid[0].length : MIN_GRID_COLUMNS); 108 | 109 | for (let addRowIndex = 0; addRowIndex < rowsToAdd; addRowIndex++) { 110 | grid[sourceRowsLength + addRowIndex] = emptyRow; 111 | } 112 | 113 | return this.setState({ 114 | grid 115 | }); 116 | } 117 | 118 | updateSourceItemsByDataGrid = () => { 119 | const { grid } = this.state; 120 | 121 | const sourceItems: CellItem[][] = []; 122 | 123 | const reverseGridRows = [...grid].reverse(); 124 | const notEmptyGridRowIndex = reverseGridRows.findIndex(rowCells => rowCells.some(cell => Boolean(cell.value))); 125 | const gridRows = reverseGridRows.slice(notEmptyGridRowIndex).reverse(); 126 | 127 | for (let gridRowIndex = 0, gridRowsLength = gridRows.length; gridRowIndex < gridRowsLength; gridRowIndex++) { 128 | if (!sourceItems[gridRowIndex]) { 129 | sourceItems[gridRowIndex] = [] 130 | } 131 | 132 | const reverseGridRows = [...gridRows[gridRowIndex]].reverse(); 133 | 134 | const notEmptyCellIndex = reverseGridRows.findIndex(item => Boolean(item.value)); 135 | const nonEmptyCells = reverseGridRows.slice(notEmptyCellIndex).reverse(); 136 | 137 | if (notEmptyCellIndex < 0) { 138 | continue; 139 | } 140 | 141 | for (let nonEmptyCellIndex = 0; nonEmptyCellIndex < nonEmptyCells.length; nonEmptyCellIndex++) { 142 | const _cell = nonEmptyCells[nonEmptyCellIndex]; 143 | 144 | sourceItems[gridRowIndex][nonEmptyCellIndex] = _cell.value; 145 | } 146 | } 147 | 148 | this.props.onSourceChange(sourceItems); 149 | } 150 | 151 | addNewRowIfLastIsActive = () => { 152 | const { activeRow, grid } = this.state; 153 | 154 | if (activeRow === (grid.length - 1)) { 155 | this.setState({ 156 | grid: [ 157 | ...grid, 158 | ...getMultipleEmptyRows(10, grid[0] ? grid[0].length : MIN_GRID_COLUMNS) 159 | ] 160 | }); 161 | } 162 | } 163 | 164 | onActivate = ({ column, row }: OnActivateOptons) => this.setState({ 165 | activeCol: column, 166 | activeRow: row 167 | }); 168 | 169 | onChange = debounce(100, (grid: GridCellItem[][]) => this.setState({ 170 | grid 171 | }, () => this.updateSourceItemsByDataGrid())); 172 | 173 | render() { 174 | return ( 175 | 180 | ) 181 | } 182 | } -------------------------------------------------------------------------------- /src/editors/JSONEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import AceEditor from "react-ace"; 3 | import brace from "brace"; // eslint-disable-line @typescript-eslint/no-unused-vars 4 | 5 | import "brace/theme/github"; 6 | import "brace/mode/json"; 7 | 8 | import { EditorProps } from "./types"; 9 | import { NAVIGATION_HEIGHT } from "../components/Navigation"; 10 | import { FOOTER_HEIGHT } from "../components/Footer"; 11 | 12 | interface Props extends EditorProps { } 13 | 14 | interface State { 15 | json: string; 16 | editorHeight: number; 17 | } 18 | 19 | const defaultState: State = { 20 | json: "[]", 21 | editorHeight: 0 22 | } 23 | 24 | export class JSONEditor extends Component { 25 | state = defaultState; 26 | 27 | componentDidMount() { 28 | this.updateEditorHeight(); 29 | this.parseSourceItemsToJson(); 30 | 31 | window.addEventListener('resize', () => this.updateEditorHeight()); 32 | } 33 | 34 | componentWillUnmount() { 35 | this.setState(defaultState); 36 | 37 | window.removeEventListener('resize', () => this.updateEditorHeight()); 38 | } 39 | 40 | updateEditorHeight = () => this.setState({ 41 | editorHeight: window.innerHeight - FOOTER_HEIGHT - NAVIGATION_HEIGHT 42 | }) 43 | 44 | parseSourceItemsToJson = () => this.setState({ 45 | json: JSON.stringify(this.props.source, null, 2) 46 | }); 47 | 48 | handleChange = (json: string) => this.setState({ 49 | json 50 | }, () => this.updateSourceItemsByCsvString()); 51 | 52 | updateSourceItemsByCsvString = () => { 53 | try { 54 | const sourceItems = JSON.parse(this.state.json); 55 | this.props.onSourceChange(sourceItems); 56 | } catch (e) { } 57 | } 58 | 59 | render() { 60 | return ( 61 | 71 | ) 72 | } 73 | } -------------------------------------------------------------------------------- /src/editors/types.d.ts: -------------------------------------------------------------------------------- 1 | export type CellItem = string | number | null; 2 | 3 | export type CellItemRow = CellItem[]; 4 | 5 | export type SourceItems = CellItemRow[]; 6 | 7 | export interface EditorProps { 8 | onSourceChange: (source: SourceItems) => void; 9 | source: SourceItems; 10 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { App } from './App'; 5 | 6 | import * as serviceWorker from './serviceWorker'; 7 | import './index.css'; 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | 11 | // If you want your app to work offline and load faster, you can change 12 | // unregister() to register() below. Note this comes with some pitfalls. 13 | // Learn more about service workers: https://bit.ly/CRA-PWA 14 | serviceWorker.register(); 15 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 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 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------