├── .babelrc ├── .gitignore ├── .npmignore ├── .storybook ├── addons.js ├── config.js └── preview-head.html ├── changelog.md ├── package-lock.json ├── package.json ├── readme.md ├── src ├── Grid.jsx ├── GridBody.jsx ├── GridCell.jsx ├── GridHeader.jsx ├── GridMath.jsx ├── GridRow.jsx ├── GridStore.jsx ├── GridTextBox.jsx ├── GridTools.jsx ├── animTest │ └── AnimTest.jsx ├── docTool │ ├── ColumnUI.jsx │ ├── CompactObjView.jsx │ ├── DataMaker.jsx │ ├── DocStore.jsx │ ├── DocUI.jsx │ ├── NumWheel.jsx │ ├── TextParam.jsx │ ├── Toggle.jsx │ └── ToggleFolder.jsx ├── easyTools │ ├── AltTextOverlay.jsx │ ├── DatePickerOverlay.jsx │ ├── EasyBool.jsx │ └── MenuPickerOverlay.jsx ├── rrj-compile │ ├── index.js │ └── output-min.js ├── test │ ├── EasyBool.test.jsx │ └── GridMath.test.jsx └── util │ └── isEmpty.jsx ├── static └── TestCSS.css ├── stories ├── WrapperEasyBool.jsx ├── dataNoiseGiant.js ├── dataNoiseMedium.js ├── dataNoiseSmall.js ├── easyTools.js ├── index.js └── testParameters.jsx └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | 4 | "plugins": [ 5 | "add-module-exports", 6 | "transform-decorators-legacy", 7 | "transform-react-display-name" 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.iml 5 | webpack-stats.json 6 | npm-debug.log 7 | *.orig 8 | coverage/ 9 | *.DS_Store 10 | *.py 11 | .cache/ 12 | phantomjsdriver.log 13 | lib/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | stories/ 2 | .storybook/ 3 | src/ -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import 'storybook-addon-jsx/register'; 2 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | function loadStories() { 4 | require('../stories/index.js'); 5 | // You can require as many stories as you need. 6 | } 7 | 8 | configure(loadStories, module); -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 2 | # mobx-form - Changelog 3 | ## v1.0.1 4 | - **Build Scripts Changes** 5 | - Add build tasks - [2b36cf5]( https://github.com/royriojas/mobx-form/commit/2b36cf5 ), [Roy Riojas](https://github.com/Roy Riojas), 23/06/2016 20:26:51 6 | 7 | 8 | - **Refactoring** 9 | - Add a helper to improve how fields are set - [c208bc7]( https://github.com/royriojas/mobx-form/commit/c208bc7 ), [Roy Riojas](https://github.com/Roy Riojas), 23/06/2016 20:22:35 10 | 11 | 12 | - **Documentation** 13 | - add more documentation - [443d90d]( https://github.com/royriojas/mobx-form/commit/443d90d ), [Roy Riojas](https://github.com/Roy Riojas), 15/06/2016 13:02:55 14 | 15 | 16 | - add missed import - [3da648e]( https://github.com/royriojas/mobx-form/commit/3da648e ), [Roy Riojas](https://github.com/Roy Riojas), 15/06/2016 12:53:32 17 | 18 | 19 | - **Features** 20 | - initial version - [02003a4]( https://github.com/royriojas/mobx-form/commit/02003a4 ), [Roy Riojas](https://github.com/Roy Riojas), 15/06/2016 12:49:00 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-json-grid", 3 | "version": "1.0.31", 4 | "description": "A grid UI component for any valid form of JSON", 5 | "scripts": { 6 | "test": "jest", 7 | "check": "echo \"TODO: add tests\" && exit 0", 8 | "changelog": "changelogx -f markdown -o ./changelog.md", 9 | "do-changelog": "npm run changelog && git add ./changelog.md && git commit -m 'DOC: Generate changelog' --no-verify", 10 | "install-hooks": "changelogx install-hook", 11 | "pre-v": "npm run check", 12 | "post-v": "npm run do-changelog && git push --no-verify && git push --tags --no-verify", 13 | "bump-major": "npm run pre-v && npm version major -m 'BLD: Release v%s' && npm run post-v", 14 | "bump-minor": "npm run pre-v && npm version minor -m 'BLD: Release v%s' && npm run post-v", 15 | "bump-patch": "npm run pre-v && npm version patch -m 'BLD: Release v%s' && npm run post-v", 16 | "build": "babel src/ -d lib/", 17 | "storybook": "start-storybook -p 6006 -c .storybook -s ./static" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/jason-henriksen/react-json-grid.git" 22 | }, 23 | "keywords": [ 24 | "grid", 25 | "json", 26 | "mobx" 27 | ], 28 | "author": "jason henriksen", 29 | "license": "MIT", 30 | "main": "lib/Grid.js", 31 | "dependencies": { 32 | "accounting": "^0.4.1", 33 | "coalescy": "^1.0.0", 34 | "debouncy": "^1.0.7", 35 | "json-stable-stringify": "^1.0.1", 36 | "kotlin": "^1.2.21", 37 | "mdi-react": "^2.1.19", 38 | "mobx": "^3.6.2", 39 | "mobx-react": "^4.4.2", 40 | "moment": "^2.21.0", 41 | "normalize.css": "^8.0.0", 42 | "npm": "^5.7.1", 43 | "prop-types": "^15.6.1", 44 | "react": "^16.2.0", 45 | "react-autobind": "^1.0.6", 46 | "react-container-dimensions": "^1.3.3", 47 | "react-datepicker": "^1.2.2", 48 | "react-dom": "^16.2.0", 49 | "react-scrollbar-size": "^2.1.0", 50 | "react-tiny-virtual-list": "^2.1.4", 51 | "react-tooltip": "^3.4.0", 52 | "react-factorial-test": "0.0.20" 53 | }, 54 | "changelogx": { 55 | "ignoreRegExp": [ 56 | "BLD: Release", 57 | "DOC: Generate Changelog", 58 | "Generated Changelog" 59 | ], 60 | "issueIDRegExp": "#(\\d+)", 61 | "commitURL": "https://github.com/royriojas/mobx-form/commit/{0}", 62 | "authorURL": "https://github.com/{0}", 63 | "issueIDURL": "https://github.com/royriojas/mobx-form/issues/{0}", 64 | "projectName": "mobx-form" 65 | }, 66 | "devDependencies": { 67 | "@storybook/react": "^3.3.15", 68 | "babel-cli": "^6.10.1", 69 | "babel-plugin-add-module-exports": "^0.2.1", 70 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 71 | "babel-polyfill": "^6.9.1", 72 | "babel-preset-es2015": "^6.9.0", 73 | "babel-preset-react": "^6.5.0", 74 | "babel-preset-stage-0": "^6.5.0", 75 | "changelogx": "^1.0.19", 76 | "enzyme": "^3.3.0", 77 | "enzyme-adapter-react-16": "^1.1.1", 78 | "jest-cli": "^22.4.2", 79 | "storybook-addon-jsx": "^5.3.0", 80 | "storybook-addon-props": "^3.0.4" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple React grid UI to display and edit any array of JSON data. 4 | 5 | Explore the API with a live-example, API Playground here: 6 | ## [https://react-json-grid.azurewebsites.net/](https://react-json-grid.azurewebsites.net/) 7 | 8 | Most grid UI tools are either lacking in features, or require you to format your data in order to make it display in the grid. react-json-grid is designed to work with whatever data you already have handy, whether it's an array of JSON objects or simply a two-dimensional array of values. 9 | 10 | ## Easiest API Possible 11 | This component is built for line-of-business workhorse apps. Primarily, this was created to assist with banking applications that require large amounts of configuration and editing long lists of complex data. 12 | 13 | 14 | Because it has to have near-XLS levels of user-friendliness coupled with repeated use throughout the application, it is designed to be as simple to use and configure as possible. For example, no matter what the format of data you've received, react-jason-grid can usually handle parsing and editing it. Just go to the live [playground](https://react-json-grid.azurewebsites.net/) and give it a try. 15 | 16 | ## April 2018 - Still in Development 17 | While this code is being written with the intention of production use with many thousands of rows, it is still under active development. The code should be ready for production use soon. Your ideas and contributions are welcome! 18 | 19 | 20 | ## Features 21 | - Accepts input data formatted as: 22 | - Array of JavaScript objects 23 | - Array of arrays 24 | - Array of primitives 25 | - Comma-Separated Values string ( next month ) 26 | - Pipe-Seperated Values string ( next month ) 27 | - Name-Value Pairs ( next month ) 28 | 29 | - Very Fast manipulation of 10s of Thousands of Items 30 | - Built-in Test UI allows test of 50K items at a button press 31 | - Editing of items is still quite smooth 32 | - Intended for large-scale, prodution use in line-of-business applications 33 | 34 | - Modern ES6 Component Code 35 | - Built to React 16.2 and will be kept up to date as React grows 36 | - No runtime warnings 37 | - Includes comprehensive storybook testing 38 | - Built for extensibility 39 | - Built for easy maintenance 40 | - Written with the observer/observable pattern of data management 41 | - HEAVILY commented for you to edit and contribute 42 | 43 | - Built in editor for editing the grid 44 | - Cells can be edited by default 45 | - Various editors may be used by table or by column 46 | 47 | - Built in formatters & validations for: 48 | - Ints / Floats 49 | - Dollars / Euros / Pounds 50 | - Checkboxes 51 | - Menus 52 | - Date Picker / DateTime Picker 53 | - Email addresses (next month) 54 | - Snail mail addresses (next month) 55 | - IP addresses (next month) 56 | 57 | - Pivoted Tables 58 | 59 | - Ability to edit the data in "textarea mode" 60 | 61 | - Mouseover Help Text on Headers 62 | 63 | - Style object control over cells, headers and inputs 64 | 65 | - ClassName control over cells, headers and inputs 66 | 67 | - Component replacements for cells, headers and inputs 68 | 69 | - Built-in Tool Hooks 70 | - Add/Remove Rows 71 | - Import/Export Data 72 | - Page Controls 73 | - Custom Component Controls 74 | 75 | - Multiple data manipulation styles 76 | - onChange / onRowAdd / onRowCut 77 | - onDataReplacement 78 | 79 | - Multiple data input styles styles 80 | - Supply entire data set a property 81 | - Supply a row-level get method (next month) 82 | - Supply a MobX observable object and let the grid manipulate the data for you (next month) 83 | 84 | 85 | 86 | ## Install for Use 87 | ```bash 88 | npm i --save react-json-grid 89 | ``` 90 | 91 | ## Install for testing 92 | ```bash 93 | git clone https://github.com/jason-henriksen/react-json-grid.git 94 | cd react-json-grid 95 | npm install 96 | npm run storybook 97 | ``` 98 | 99 | ## Simple Usage 100 | The API playground shows you the JSX you need to write in order to get the effect shown in the playground. In a nutshell, the API looks like this: 101 | 102 | ```javascript 103 | this.data = 104 | [ {r:5,a:5,b:6,c:8,d:90}, 105 | {r:4,a:5,b:6,c:8,d:90}, 106 | {r:3,a:5,b:6,c:8,d:90}, 107 | {r:2,a:5,b:6,c:8,d:90}, 108 | {r:1,a:5,b:6,c:8,d:90}]; 109 | 110 | return( 111 | { data[y][objKey]=value; }} 114 | /> 115 | ); 116 | ``` 117 | 118 | This project is inspired by react-data-grid. 119 | 120 | -------------------------------------------------------------------------------- /src/Grid.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import GridBody from './GridBody'; 4 | import GridStore from './GridStore'; 5 | import Provider from 'mobx-react'; 6 | import autoBind from 'react-autobind'; 7 | import ScrollbarSize from 'react-scrollbar-size'; 8 | 9 | class Grid extends React.Component { 10 | 11 | 12 | constructor(props) { 13 | super(props); 14 | autoBind(this); 15 | this.scrollBarWide = 20; 16 | this.GridStore = new GridStore(); 17 | } 18 | 19 | setScrollBarWide(exp) { this.scrollBarWide = exp.scrollbarWidth+2; } // account for rounding error 20 | 21 | render(){ 22 | return( 23 |
24 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | // Proptypes 31 | Grid.propTypes = { 32 | rowCount: PropTypes.number, 33 | rowHigh: PropTypes.number, 34 | colHeaderHigh: PropTypes.number, 35 | colHeaderHide: PropTypes.bool, 36 | gridHigh: PropTypes.number, 37 | }; 38 | 39 | // Default proptypes 40 | Grid.defaultProps = { 41 | }; 42 | 43 | 44 | export default Grid; 45 | 46 | -------------------------------------------------------------------------------- /src/GridBody.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import VirtualList from 'react-tiny-virtual-list'; 4 | import { observable,action,trace } from 'mobx'; 5 | import { observer } from 'mobx-react'; 6 | import ScrollbarSize from 'react-scrollbar-size'; 7 | import autoBind from 'react-autobind'; 8 | import GridRow from './GridRow'; 9 | import GridTextBox from './GridTextBox'; 10 | import GridHeader from './GridHeader'; 11 | import GridTools from './GridTools'; 12 | import ReactTooltip from 'react-tooltip'; 13 | 14 | import DatePickerOverlay from './easyTools/DatePickerOverlay'; 15 | import MenuPickerOverlay from './easyTools/MenuPickerOverlay'; 16 | import AltTextOverlay from './easyTools/AltTextOverlay'; 17 | import EasyBool from './easyTools/EasyBool'; 18 | 19 | // Wrapper for the grid 20 | // This grid allows deep styling via style objects, 21 | // but retains control of border and padding to ensure that the grid lines up. 22 | const GridBody = observer( class GridBody extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | autoBind(this); 26 | this.componentWillReceiveProps(props); // first call needs to set up the data store 27 | } 28 | 29 | @observable scrollBarWide = 0; // keep track of how wide the scroll bar will be 30 | @action setScrollBarWide(exp) { 31 | // account for rounding error 32 | this.props.GridStore.scrollBarWide = exp.scrollbarWidth+2; 33 | } 34 | 35 | // prep the data store 36 | componentWillReceiveProps(nextProps) 37 | { 38 | if(nextProps.debugGridMath){ 39 | console.log('componentWillReceiveProps'); 40 | } 41 | 42 | if (this.props.GridStore) { 43 | this.props.GridStore.prepSelectionField(nextProps); 44 | } 45 | else{ 46 | console.log('missing grid store'); 47 | } 48 | } 49 | 50 | 51 | @action blurControl(evt){ 52 | // brain dead LUCK is the only thing making this work. 53 | // when moving focus in the cell, it sets autoFocus true. 54 | // then it renders all the cells and one of them takes focus, focusing the new cell and bluring the old one. 55 | // the blur event comes here and tells us to not steal focus any more. 56 | // Now we can type into other things all day. But only if we click into a cell will the focus move with us. Lucky! 57 | this.props.GridStore.autoFocus=false; 58 | } 59 | 60 | render() 61 | { 62 | // do a bunch of math to figure out where things should go. 63 | var ui = this.props.GridStore.uiMath; 64 | 65 | 66 | ///////// Add container dimension information from https://github.com/maslianok/react-resize-detector 67 | 68 | 69 | // log what we're doing if asked to 70 | if(this.props.debugGridMath){ 71 | console.log('render grid'); 72 | console.log(ui); 73 | } 74 | 75 | // text view 76 | if(this.props.editAsText){ 77 | return(); // forget this complex grid nonsense. Just make a big text area and edit the stuff that way. 78 | } 79 | 80 | // error view 81 | if(ui.notReady){ 82 | return( // something isn't ready. Just drop an error and move on. 83 |
84 | 88 | Sorry, this grid is not ready to display.

89 | {ui.notReady} 90 |
); 91 | } 92 | 93 | // the real stuff. Release the Kraken... I mean the Grid. 94 | return( 95 |
102 | {/* ScrollbarSize gives the code information about how wide the scroll bar is */ } 103 | 104 | 105 | {/* put the header in place */} 106 | 107 | 108 | {/* put the header in place */} 109 | 110 | 111 | {/* Overlay Data Components: if needed, put the date picker / menu picker overlay in place */} 112 | {(this.props.GridStore.showDatePicker||this.props.GridStore.showDateTimePicker) && } 113 | {(this.props.GridStore.showMenuPicker) && } 114 | 115 | {/* Column Header help text on mouse over */} 116 | 117 | 118 | {/* VirtualList renders only the rows that are visible */ } 119 | 125 |
126 | 137 |
} 138 | /> 139 | 140 | {/* Either keeps size and show a box to fill unused space, or just shrink the grid vertical space used. */ } 141 | {this.props.gridHighCollapse === false && ui.collapseAvailable>0 && 142 |
157 | } 158 | 159 | {/* Draw a bottom line if it is needed. */ } 160 | {ui.showBottomGridLine && // needs the 2x border wide added because this is only a top border 161 |
168 | } 169 | 170 | {/* Render any tools that are enabled. */ } 171 | 172 | 173 |
174 | ); 175 | } 176 | }) 177 | 178 | 179 | 180 | export default GridBody; -------------------------------------------------------------------------------- /src/GridCell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import VirtualList from 'react-tiny-virtual-list'; 4 | import { observable, action } from 'mobx'; 5 | import { observer } from 'mobx-react'; 6 | import ScrollbarSize from 'react-scrollbar-size'; 7 | import autoBind from 'react-autobind'; 8 | import ReactTooltip from 'react-tooltip'; 9 | 10 | import moment from 'moment'; 11 | import accounting from 'accounting'; 12 | 13 | import DatePicker from 'react-datepicker'; 14 | import 'react-datepicker/dist/react-datepicker.css'; 15 | import isEmpty from './util/isEmpty'; 16 | 17 | import EasyBool from './easyTools/EasyBool'; 18 | 19 | window.reactJsonGridFocusInput = function(elem){ 20 | elem.focus(); 21 | elem.setSelectionRange(elem.value.length,elem.value.length); 22 | } 23 | 24 | 25 | @observer class GridCell extends React.Component { 26 | constructor(props) { super(props); autoBind(this); } 27 | 28 | @action isEditDisabled(){ 29 | var ed=(this.props.uiMath.editDisabled === true); 30 | if( (this.props.GridStore.colDefListByKey && 31 | this.props.GridStore.colDefListByKey[this.props.objKey] && // edit disabled by column 32 | this.props.GridStore.colDefListByKey[this.props.objKey].editDisabled === true)){ 33 | ed=true; 34 | } 35 | return ed; 36 | } 37 | 38 | @action onClick(evt) 39 | { 40 | this.props.GridStore.autoFocus=true; // if we edit, auto focus. 41 | 42 | if (this.props.x === this.props.GridStore.cursor.x && // clicked the currently highlighted cell 43 | this.props.y === this.props.GridStore.cursor.y && // clicked the currently highlighted cell 44 | this.isEditDisabled() === false // edit is allowable 45 | ) { 46 | this.props.GridStore.cursor.editX = this.props.x; 47 | this.props.GridStore.cursor.editY = this.props.y; 48 | this.props.GridStore.cursor.editObjKey = this.props.objKey; 49 | this.props.GridStore.curEditingValue = this.props.cellData; 50 | 51 | // check for dates and menus 52 | if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyDate){ 53 | this.props.GridStore.showDatePicker=true; 54 | } 55 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyDateTime){ 56 | this.props.GridStore.showDateTimePicker=true; 57 | } 58 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyMenu) { 59 | this.props.GridStore.showMenuPicker = true; 60 | } 61 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].overlayComp) { 62 | this.props.GridStore.showOverlayComp = true; 63 | } 64 | } 65 | else{ 66 | this.props.GridStore.cursor.x = this.props.x; 67 | this.props.GridStore.cursor.y = this.props.y; 68 | if (this.props.GridStore.cursor.x < 0) { this.props.GridStore.cursor.x=0; } // can't edit row headers 69 | } 70 | } 71 | 72 | @action onKeyDownWhenViewing(e) 73 | { 74 | this.props.GridStore.autoFocus=true; // if we edit, auto focus it. 75 | 76 | var editDisabled = this.isEditDisabled(); 77 | 78 | if (this.props.x !== this.props.GridStore.cursor.editX || 79 | this.props.y !== this.props.GridStore.cursor.editY) { 80 | if (e.keyCode == '37' || e.keyCode == '38' || e.keyCode == '39' || e.keyCode == '40' ) { // arrows 81 | this.props.GridStore.cellMoveKey(e); 82 | } 83 | else if (e.keyCode == '13') { // enter 84 | if(e.shiftKey) { 85 | // back-enter 86 | this.props.GridStore.cursor.y--; 87 | if (this.props.GridStore.cursor.y < 0 ) { 88 | this.props.GridStore.cursor.y = 0; 89 | } 90 | } 91 | else{ 92 | // forward-enter 93 | this.props.GridStore.cursor.y++; 94 | if (this.props.GridStore.cursor.y > this.props.GridStore.cursor.maxY) { 95 | this.props.GridStore.cursor.y = 0; 96 | } 97 | } 98 | } 99 | else if (e.keyCode == '9') { // tab 100 | if(e.shiftKey) { 101 | // back tab 102 | this.props.GridStore.cursor.x--; 103 | if (this.props.GridStore.cursor.x < 0) { 104 | this.props.GridStore.cursor.x = this.props.GridStore.cursor.maxX; 105 | this.props.GridStore.cursor.y--; 106 | } 107 | if (this.props.GridStore.cursor.y < 0) { 108 | this.props.GridStore.cursor.y = 0; 109 | } 110 | } 111 | else{ 112 | // forward tab 113 | this.props.GridStore.cursor.x++; 114 | if (this.props.GridStore.cursor.x > this.props.GridStore.cursor.maxX) { 115 | this.props.GridStore.cursor.x = 0; 116 | this.props.GridStore.cursor.y++; 117 | } 118 | if (this.props.GridStore.cursor.y > this.props.GridStore.cursor.maxY) { 119 | this.props.GridStore.cursor.y = 0; 120 | } 121 | } 122 | } 123 | else if (e.keyCode == '32' && editDisabled === false) { // space 124 | // cell edit 125 | this.props.GridStore.cursor.editX = this.props.x; 126 | this.props.GridStore.cursor.editY = this.props.y; 127 | this.props.GridStore.cursor.editObjKey = this.props.objKey; 128 | this.props.GridStore.curEditingValue = this.props.GridStore.getDataRespectingPivotAtEditCursor(this.props.data); 129 | // pop overlay editors if needed 130 | if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyDate){ 131 | this.props.GridStore.showDatePicker=true; 132 | } 133 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyDateTime){ 134 | this.props.GridStore.showDateTimePicker=true; 135 | } 136 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyMenu) { 137 | this.props.GridStore.showMenuPicker = true; 138 | } 139 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].overlayComp) { 140 | this.props.GridStore.showOverlayComp = true; 141 | } 142 | } 143 | else if (e.keyCode == '46' && editDisabled === false) { // delete: instant kill! 144 | this.props.GridStore.onChangePivotWrapper(this.props.x, this.props.y, this.props.objKey, ''); 145 | } 146 | else{ 147 | if (e.key.length === 1 && editDisabled === false){ // must be a char 148 | // not that the react code calls a javascript function to make sure the cursor goes to the end of the input so you can keep typing naturally. 149 | this.props.GridStore.cursor.editX = this.props.x; 150 | this.props.GridStore.cursor.editY = this.props.y; 151 | this.props.GridStore.cursor.editObjKey = this.props.objKey; 152 | if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyDate){ 153 | this.props.GridStore.showDatePicker=true; 154 | } 155 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyDateTime){ 156 | this.props.GridStore.showDateTimePicker=true; 157 | } 158 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].easyMenu) { 159 | this.props.GridStore.showMenuPicker = true; 160 | } 161 | else if (this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[this.props.objKey] && this.props.GridStore.colDefListByKey[this.props.objKey].overlayComp) { 162 | this.props.GridStore.showOverlayComp = true; 163 | } 164 | else{ 165 | this.props.GridStore.curEditingValue = ''+e.key; 166 | } 167 | } 168 | } 169 | } 170 | e.stopPropagation(); 171 | e.preventDefault(); 172 | } 173 | 174 | 175 | @action valChange(evt){ 176 | this.props.GridStore.curEditingValue = evt.target.value; 177 | } 178 | 179 | @action valChangeDate(value) { 180 | this.props.GridStore.curEditingValue = value; 181 | } 182 | 183 | 184 | @action endEdit() 185 | { 186 | if (this.props.GridStore.colDefListByKey && 187 | this.props.GridStore.colDefListByKey[this.props.objKey]) { 188 | if ( 189 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyInt && !this.props.GridStore.curEditIsValidFor.isValidInt) || 190 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyFloat && !this.props.GridStore.curEditIsValidFor.isValidFloat) || 191 | ( (this.props.GridStore.colDefListByKey[this.props.objKey].easyDollar || 192 | this.props.GridStore.colDefListByKey[this.props.objKey].easyEuro || 193 | this.props.GridStore.colDefListByKey[this.props.objKey].easyPound) 194 | && !this.props.GridStore.curEditIsValidFor.isValidFloat) 195 | ) { 196 | // value is not valid for the field definition. Do not make the change. 197 | this.props.GridStore.cursor.editX = -1; 198 | this.props.GridStore.cursor.editY = -1; 199 | return; 200 | } 201 | } 202 | 203 | this.props.GridStore.onChangePivotWrapper(this.props.x, this.props.y, this.props.objKey, this.props.GridStore.curEditingValue); 204 | this.props.GridStore.cursor.editX = -1; 205 | this.props.GridStore.cursor.editY = -1; 206 | } 207 | 208 | @action onKeyDownWhenEditing(e) { 209 | 210 | if (e.keyCode == '13') { 211 | // commit the value 212 | this.endEdit(); 213 | // move the cursor 214 | if(e.shiftKey) { 215 | // back-enter 216 | this.props.GridStore.cursor.y--; 217 | if (this.props.GridStore.cursor.y < 0 ) { 218 | this.props.GridStore.cursor.y = 0; 219 | } 220 | } 221 | else{ 222 | // forward-enter 223 | this.props.GridStore.cursor.y++; 224 | if (this.props.GridStore.cursor.y > this.props.GridStore.cursor.maxY) { 225 | this.props.GridStore.cursor.y = 0; 226 | } 227 | } 228 | 229 | // start editing new location 230 | this.props.GridStore.cursor.editX = this.props.GridStore.cursor.x; 231 | this.props.GridStore.cursor.editY = this.props.GridStore.cursor.y; 232 | this.props.GridStore.curEditingValue = this.props.GridStore.getDataRespectingPivotAtEditCursor(this.props.data); 233 | } 234 | else if (e.keyCode == '9') { // tab 235 | // commit the value 236 | this.endEdit(); 237 | 238 | // move the cursor 239 | if(e.shiftKey) { 240 | // back tab 241 | this.props.GridStore.cursor.x--; 242 | if (this.props.GridStore.cursor.x < 0) { 243 | this.props.GridStore.cursor.x = this.props.GridStore.cursor.maxX; 244 | this.props.GridStore.cursor.y--; 245 | } 246 | if (this.props.GridStore.cursor.y < 0) { 247 | this.props.GridStore.cursor.y = 0; 248 | } 249 | } 250 | else{ 251 | // forward tab 252 | this.props.GridStore.cursor.x++; 253 | if (this.props.GridStore.cursor.x > this.props.GridStore.cursor.maxX) { 254 | this.props.GridStore.cursor.x = 0; 255 | this.props.GridStore.cursor.y++; 256 | } 257 | if (this.props.GridStore.cursor.y > this.props.GridStore.cursor.maxY) { 258 | this.props.GridStore.cursor.y = 0; 259 | } 260 | } 261 | 262 | // start editing the next one 263 | this.props.GridStore.cursor.editX = this.props.GridStore.cursor.x; 264 | this.props.GridStore.cursor.editY = this.props.GridStore.cursor.y; 265 | this.props.GridStore.curEditingValue = this.props.GridStore.getDataRespectingPivotAtEditCursor(this.props.data); 266 | this.props.GridStore.autoFocus=true; 267 | e.stopPropagation(); 268 | e.preventDefault(); 269 | } 270 | else if (e.keyCode == '27') { 271 | // cell edit abort 272 | this.props.GridStore.cursor.editX = -1; 273 | this.props.GridStore.cursor.editY = -1; 274 | } 275 | 276 | } 277 | 278 | 279 | renderZero(tval) { 280 | if (0 === tval) { return '0'; } 281 | else if ('false' === ''+tval) { return 'false'; } 282 | else if ('true' === ''+tval) { return 'true'; } 283 | else { return tval } 284 | } 285 | 286 | 287 | 288 | render() { 289 | 290 | // Build Render Style object 291 | var style={...this.props.styleCell,boxSizing: 'content-box'}; 292 | var styleIn={...this.props.styleInput,boxSizing: 'content-box'}; 293 | var isSelected = false; 294 | 295 | if (this.props.GridStore.selectionBounds.l <= this.props.x && 296 | this.props.GridStore.selectionBounds.r >= this.props.x && 297 | this.props.GridStore.selectionBounds.t <= this.props.y && 298 | this.props.GridStore.selectionBounds.b >= this.props.y 299 | ) { 300 | // CSSNOTE! 301 | style.zIndex = 5; 302 | isSelected = true; 303 | } 304 | 305 | if(!this.props.isFirstColumn){ 306 | style.marginLeft=-1*this.props.uiMath.borderWide; 307 | styleIn.marginLeft=-1*this.props.uiMath.borderWide; 308 | } 309 | else{ 310 | style.marginLeft=0; 311 | styleIn.marginLeft=0; 312 | } 313 | 314 | // over ride width if needed 315 | if (this.props.GridStore.colDefListByKey[this.props.objKey]) { // is there a colDef that uses this key? 316 | var curColWide = style.width; 317 | if(this.props.GridStore.colDefListByKey[this.props.objKey].forceColWide){ 318 | curColWide = this.props.GridStore.colDefListByKey[this.props.objKey].forceColWide; 319 | } 320 | style.width = curColWide; 321 | } 322 | 323 | // render data standard 324 | var renderPlan = ''; 325 | var isFocusNeeded = this.props.GridStore.autoFocus && this.props.x === this.props.GridStore.cursor.x && this.props.y === this.props.GridStore.cursor.y; 326 | 327 | 328 | var assumeEditOk=true; 329 | if (this.props.GridStore.colDefListByKey && 330 | this.props.GridStore.colDefListByKey[this.props.objKey] && 331 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyBool===true || // boolean doesn't need the editor 332 | this.props.GridStore.colDefListByKey[this.props.objKey].easyMenu===true ) // menu doesn't need the editor 333 | ){ 334 | assumeEditOk = false; 335 | } 336 | if(this.isEditDisabled()){ assumeEditOk=false; } 337 | 338 | //=== Editor Handling 339 | if (assumeEditOk && 340 | this.props.x === this.props.GridStore.cursor.x && // cur render cell is cur cell 341 | this.props.y === this.props.GridStore.cursor.y && // cur render cell is cur cell 342 | this.props.x === this.props.GridStore.cursor.editX && // cur cell is edit cell 343 | this.props.y === this.props.GridStore.cursor.editY // cur cell is edit cell 344 | ) 345 | { 346 | styleIn.verticalAlign='top'; 347 | styleIn.width = style.width; // use the column defined width override if needed. 348 | 349 | // check for easy column tools 350 | if(this.props.GridStore.colDefListByKey && 351 | this.props.GridStore.colDefListByKey[this.props.objKey]){ 352 | 353 | // check validation 354 | if ( 355 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyInt && !this.props.GridStore.curEditIsValidFor.isValidInt) || 356 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyFloat && !this.props.GridStore.curEditIsValidFor.isValidFloat) || 357 | ((this.props.GridStore.colDefListByKey[this.props.objKey].easyDollar || 358 | this.props.GridStore.colDefListByKey[this.props.objKey].easyEuro || 359 | this.props.GridStore.colDefListByKey[this.props.objKey].easyPound) && !this.props.GridStore.curEditIsValidFor.isValidFloat) 360 | ){ 361 | styleIn.outline="5px red dashed"; 362 | } 363 | 364 | // check right alignment 365 | if ( 366 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyInt) || 367 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyFloat) || 368 | ((this.props.GridStore.colDefListByKey[this.props.objKey].easyDollar || 369 | this.props.GridStore.colDefListByKey[this.props.objKey].easyEuro || 370 | this.props.GridStore.colDefListByKey[this.props.objKey].easyPound)) 371 | ) { 372 | styleIn.textAlign = "right"; 373 | } 374 | } 375 | 376 | var curDisplayVal = this.props.GridStore.curEditingValue; 377 | if(null === curDisplayVal){ curDisplayVal = this.props.cellData;} 378 | 379 | // check for easy Date 380 | if (this.props.GridStore.colDefListByKey && 381 | this.props.GridStore.colDefListByKey[this.props.objKey] && 382 | this.props.GridStore.colDefListByKey[this.props.objKey].easyDate){ 383 | renderPlan=
{curDisplayVal}
384 | } 385 | else if (this.props.GridStore.colDefListByKey && 386 | this.props.GridStore.colDefListByKey[this.props.objKey] && 387 | this.props.GridStore.colDefListByKey[this.props.objKey].easyDateTime) { 388 | renderPlan=
{curDisplayVal}
389 | } 390 | else if (this.props.GridStore.colDefListByKey && 391 | this.props.GridStore.colDefListByKey[this.props.objKey] && 392 | this.props.GridStore.colDefListByKey[this.props.objKey].easyMenu) { 393 | renderPlan=
{curDisplayVal}
394 | } 395 | else{ 396 | // use the normal text input editor 397 | var cellClassName = this.props.GridStore.classInput; 398 | 399 | renderPlan = 400 | input && window.reactJsonGridFocusInput(input)} 406 | onBlur={this.endEdit} 407 | /> 408 | } 409 | } 410 | else{ 411 | //===== VIEW SIDE 412 | var altText=''; 413 | 414 | var renderVal = '' + (this.renderZero(this.props.cellData)||''); 415 | if (this.props.GridStore.colDefListByKey && 416 | this.props.GridStore.colDefListByKey[this.props.objKey] 417 | ){ 418 | // we have a custom view component. Render it. 419 | // it may want to change values directly, so give it everything it needs 420 | 421 | var styleByCol={}; 422 | if(this.props.x>=0){ 423 | styleByCol = (this.props.GridStore.colDefListByKey[this.props.objKey].styleCell||{}) 424 | } 425 | 426 | // check right alignment 427 | if ( 428 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyInt) || 429 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyFloat) || 430 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyDate) || 431 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyDateTime) || 432 | ((this.props.GridStore.colDefListByKey[this.props.objKey].easyDollar || 433 | this.props.GridStore.colDefListByKey[this.props.objKey].easyEuro || 434 | this.props.GridStore.colDefListByKey[this.props.objKey].easyPound)) 435 | ) { 436 | style.textAlign = "right"; 437 | // check validation 438 | if ( 439 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyInt && !this.props.GridStore.checkValidInt(this.props.cellData) ) || 440 | (this.props.GridStore.colDefListByKey[this.props.objKey].easyFloat && !this.props.GridStore.checkValidFloat(this.props.cellData) ) || 441 | ((this.props.GridStore.colDefListByKey[this.props.objKey].easyDollar || 442 | this.props.GridStore.colDefListByKey[this.props.objKey].easyEuro || 443 | this.props.GridStore.colDefListByKey[this.props.objKey].easyPound) && !this.props.GridStore.checkValidFloat(this.props.cellData) ) 444 | ) { 445 | style.outline = "3px orange dashed"; 446 | } 447 | 448 | // since we're here: format the money: 449 | if (this.props.GridStore.colDefListByKey[this.props.objKey].easyDollar) { 450 | renderVal = accounting.formatMoney(this.props.cellData, "$", 2, ",", "."); 451 | } 452 | else if (this.props.GridStore.colDefListByKey[this.props.objKey].easyEuro) { 453 | renderVal = accounting.formatMoney(this.props.cellData, "€", 2, ".", ","); 454 | } 455 | else if (this.props.GridStore.colDefListByKey[this.props.objKey].easyPound) { 456 | renderVal = accounting.formatMoney(this.props.cellData, "£", 2, ".", ","); 457 | } 458 | // since we're here: highlight invalid dates & times 459 | if (this.props.GridStore.colDefListByKey[this.props.objKey].easyDate) { 460 | var parsed = moment(this.props.cellData, this.props.uiMath.formatDate); 461 | if(!parsed.isValid){ 462 | style.outline = "3px orange dashed"; 463 | } 464 | else{ 465 | renderVal = parsed.format(this.props.uiMath.formatDate); 466 | } 467 | } 468 | else if (this.props.GridStore.colDefListByKey[this.props.objKey].easyDateTime) { 469 | var parsed = moment(this.props.cellData, this.props.uiMath.formatDate+' '+this.props.uiMath.formatTime); 470 | if(!parsed.isValid){ 471 | style.outline = "3px orange dashed"; 472 | } 473 | else{ 474 | renderVal = parsed.format(this.props.uiMath.formatDate+' '+this.props.uiMath.formatTime); 475 | } 476 | } 477 | 478 | } 479 | 480 | // check for custom renders 481 | if (this.props.GridStore.colDefListByKey[this.props.objKey].compCell){ 482 | renderVal = { 483 | React.cloneElement( 484 | this.props.GridStore.colDefListByKey[this.props.objKey].compCell, 485 | { 486 | x: (this.props.GridStore.pivotOn || 0 === this.props.GridStore.pivotOn) ? this.props.y : this.props.x, 487 | y: (this.props.GridStore.pivotOn || 0 === this.props.GridStore.pivotOn) ? this.props.x : this.props.x, 488 | objKey: this.props.objKey, 489 | cellData: this.props.cellData, 490 | id: this.props.id+'-comp', 491 | onChange: this.props.onChange , 492 | } 493 | ) 494 | }; 495 | } 496 | 497 | // handle easyBool 498 | if (this.props.GridStore.colDefListByKey[this.props.objKey].easyBool) { 499 | 500 | var disableEdit = this.isEditDisabled(); 501 | 502 | // note that the false || 0 check is required to pivot on the 0th column. 503 | renderVal = 511 | } 512 | 513 | } 514 | 515 | 516 | 517 | // build the className and style strings 518 | var cellClassName=''; 519 | var dataClassName=''; 520 | var finalStyleCell={}; 521 | var finalStyleData={}; 522 | 523 | if(this.props.x===-1){ 524 | // this is a row header. Precedence is classHeaderCell, classRowHeaderCell 525 | cellClassName = (this.props.GridStore.classHeaderCell||'')+' '+(this.props.GridStore.classRowHeaderCell||''); // header + rowHeader cell 526 | dataClassName = (this.props.GridStore.classHeaderData||'')+' '+(this.props.GridStore.classRowHeaderData||''); // header + rowHeader data 527 | 528 | var rowKey = this.props.uiMath.rowHeaderList[this.props.y]; // add column specifications 529 | if(this.props.GridStore.colDefListByKey && this.props.GridStore.colDefListByKey[rowKey]){ 530 | cellClassName = cellClassName +' '+ (this.props.GridStore.colDefListByKey[rowKey].classHeaderCell||''); 531 | dataClassName = dataClassName +' '+ (this.props.GridStore.colDefListByKey[rowKey].classHeaderData||''); 532 | } 533 | 534 | var defaultHeaderStyle ={}; 535 | if(isEmpty(this.props.GridStore.styleHeaderCell) && isEmpty(this.props.GridStore.classHeaderCell) && isEmpty(this.props.GridStore.styleRowHeaderCell)){ 536 | defaultHeaderStyle = {backgroundColor: '#F3F3F3',textAlign:'center'}; 537 | } 538 | 539 | finalStyleCell = {...defaultHeaderStyle,...this.props.GridStore.styleHeaderCell,...this.props.GridStore.styleRowHeaderCell,...style}; // make the grid styling override the user styling 540 | finalStyleData = {...this.props.GridStore.styleHeaderData,...this.props.GridStore.styleRowHeaderData}; // just user stying on data 541 | 542 | if (this.props.GridStore.colDefListByKey[rowKey] && this.props.GridStore.colDefListByKey[rowKey].altText) { 543 | altText = this.props.GridStore.colDefListByKey[rowKey ].altText; 544 | } 545 | 546 | } 547 | else{ 548 | // this is a normal row 549 | cellClassName = cellClassName+' '+this.props.GridStore.classCell; // cell 550 | dataClassName = dataClassName+' '+this.props.GridStore.classData; // data 551 | finalStyleCell = {...this.props.GridStore.styleCell,...style}; // make the grid styling override the user styling 552 | finalStyleData = {...this.props.GridStore.styleData}; // just user stying on data 553 | 554 | if(this.props.y % 2 === 1){ 555 | cellClassName = cellClassName+' '+this.props.GridStore.classCellOddRow; // cell 556 | dataClassName = dataClassName+' '+this.props.GridStore.classDataOddRow; // data 557 | finalStyleCell = {...finalStyleCell,...this.props.GridStore.styleCellOddRow}; // make the grid styling override the user styling 558 | finalStyleData = {...finalStyleData,...this.props.GridStore.styleDataOddRow}; // make the grid styling override the user styling 559 | } 560 | } 561 | 562 | if(isSelected){ 563 | var defaultSelection = {}; 564 | if(isEmpty(this.props.GridStore.styleSelected) && isEmpty(this.props.GridStore.classSelected)){ 565 | defaultSelection = {backgroundColor: 'lightblue'}; 566 | } 567 | finalStyleCell = { ...finalStyleCell, ...defaultSelection, ...this.props.GridStore.styleSelected}; 568 | cellClassName = cellClassName+' '+this.props.GridStore.classSelected; 569 | } 570 | 571 | //--- handle row header help text - currently broken! 572 | var helpComp; 573 | if (this.props.x === -1 && this.props.GridStore.colDefListByKey && 574 | this.props.GridStore.colDefListByKey[rowKey] && 575 | this.props.GridStore.colDefListByKey[rowKey].altText 576 | ) { 577 | // handle alt text. Note that the 'text' could be a component. regular header 578 | renderPlan = 579 |
div && isFocusNeeded && div.focus()} 585 | onKeyDown={this.onKeyDownWhenViewing}> 586 | 588 |
{renderVal}
589 |
590 |
591 | } 592 | else{ 593 | renderPlan =
div && isFocusNeeded && div.focus()} 599 | onKeyDown={this.onKeyDownWhenViewing}> 600 |
{renderVal}
601 |
; 602 | } 603 | } 604 | 605 | return(renderPlan); 606 | 607 | } 608 | } 609 | /*** 610 | * 611 | ASDF{altText} 612 | 613 | 614 | */ 615 | 616 | 617 | export default GridCell; 618 | -------------------------------------------------------------------------------- /src/GridHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observable, action } from 'mobx'; 4 | import { observer } from 'mobx-react'; 5 | import autoBind from 'react-autobind'; 6 | import ReactTooltip from 'react-tooltip'; 7 | import isEmpty from './util/isEmpty'; 8 | 9 | 10 | 11 | @observer class GridHeader extends React.Component { 12 | constructor(props) { super(props); autoBind(this); } 13 | 14 | render(){ 15 | 16 | var header=[]; 17 | var marginOffset=0; 18 | var ui = this.props.uiMath; 19 | 20 | 21 | 22 | if (!this.props.colHeaderHide && !ui.forceColHeaderHide) { // provide a header row. 23 | for(var ctr=0;ctr0){ // this code should be removed when I'm confident it's not needed. Note left 5/2018 35 | if (this.props.GridStore.colDefListByKey[ this.props.GridStore.keyList[ctr-1] ].altText) { 36 | helpComp = this.props.GridStore.colDefListByKey[ this.props.GridStore.keyList[ctr-1] ].altText; 37 | } 38 | } 39 | */ 40 | var targetCol = 0; 41 | for (var tctr = 0; tctr < ui.colHeaderKeyList.length; tctr++) { 42 | if (''+ui.colHeaderKeyList[tctr] === ''+this.props.pivotOn) { targetCol = tctr; }// this is the header index matching the pivotOn key, offset by 1 due to leading '/' 43 | } 44 | colTitle = ''+this.props.GridStore.getDataRespectingPivotAtLocation(this.props.data, ctr - 1, targetCol - 1);// both -1 are to account for the '/' in the header row. 45 | } 46 | else{ 47 | //== NonPivot, With ColDefs 48 | gridColLocalStyle = (this.props.GridStore.colDefListByKey[keyName].styleHeader || {}); 49 | if (this.props.GridStore.colDefListByKey[keyName].widePx) { // width by px 50 | curColWide = this.props.GridStore.colDefListByKey[keyName].widePx; 51 | } 52 | else if (this.props.GridStore.colDefListByKey[keyName].widePct) { // width by pct 53 | curColWide = ui.rowWide * (this.props.GridStore.colDefListByKey[keyName].widePct / 100); 54 | } 55 | 56 | colTitle = ''+(this.props.GridStore.colDefListByKey[keyName].title || keyName); // if there is a title for the colDef use it, or just stick with thekey 57 | if (this.props.GridStore.colDefListByKey[keyName].altText) { 58 | // handle alt text. Note that the 'text' could be a component. regular header 59 | helpComp = this.props.GridStore.colDefListByKey[keyName].altText; 60 | } 61 | } 62 | } 63 | else{ 64 | if (this.props.pivotOn || this.props.pivotOn === 0) { 65 | // no column defs, Pivot On 66 | if(ctr>=0){ 67 | // find the index of the colDefListByKey item for the pivotOn target: 68 | var targetCol=0; 69 | for(var tctr=0;tctr 110 |
126 |
127 | {colTitle} 128 |
129 |
130 | ); 131 | marginOffset=-1*ui.borderWide; 132 | } 133 | } 134 | else{ // header is hidden so provide a top border line. The 2x borderWide extra width is needed because this is a top only border 135 | if(!ui.forceColHeaderHide){ 136 | header.push(
); 143 | } 144 | } 145 | 146 | return( 147 |
148 | {header} 149 |
150 | ); 151 | } 152 | } 153 | 154 | export default GridHeader; -------------------------------------------------------------------------------- /src/GridMath.jsx: -------------------------------------------------------------------------------- 1 | import autoBind from 'react-autobind'; 2 | 3 | 4 | 5 | class GridMath 6 | { // Just a class. Used to calculate valudes needed by the UI. 7 | constructor() { 8 | autoBind(this); 9 | } 10 | 11 | // javascript sucks at math 12 | makeValidInt(inputVal,defaultVal){ 13 | var res = (inputVal||defaultVal); // use a default instead of null. 14 | if(inputVal===0){res=inputVal;} // zero is clearly indistinguishabel from null, so if you meant zero, use zero not the default. 15 | if(res<0){ res = defaultVal; } // but negative numbers are just daft. Us the default after all. 16 | if( (''+res).includes('px') ){ console.log('Please pass numbers to Grid. You do not need to add "px" to anything. This number will be ignored. Using '+defaultVal+' instead.'); return defaultVal;} 17 | return Number(res); // oh, and make sure it's actully a number 18 | } 19 | 20 | // this used to all be in the render method of GridBody. 21 | // It was moved here so that it could be unit tested without having to render anything. 22 | calcGridBody(props,scrollBarWide) 23 | { 24 | if(props.debugGridMath){ 25 | console.log('do the math'); 26 | } 27 | 28 | // object to return 29 | var result={}; 30 | result.keyNames=[]; // always the keys present either from data inspection, or column definition 31 | result.showBottomGridLine=false; 32 | result.rowHeaderList=[]; 33 | result.colHeaderKeyList=[]; // when pivoted, this will NOT match the keyNames list. 34 | result.saveColumnForRowHeader=0; 35 | result.debugGridMath = props.debugGridMath; 36 | result.id = props.id || 'reactJsonGridId'; 37 | 38 | 39 | // math needs access to columns by key name. 40 | if (props.columnList && props.columnList.length>0) { 41 | result.colDefListByKey = {}; 42 | // make a map of keys to objects for easy access later. 43 | for (var clctr = 0; clctr < props.columnList.length; clctr++) { 44 | // THIS IS VITAL! Otherwise you risk one grid bleeding into another grid during test or object re-use. 45 | result.colDefListByKey[props.columnList[clctr].key] = Object.assign({},props.columnList[clctr]); 46 | } 47 | } 48 | else { 49 | result.colDefListByKey = {}; 50 | } 51 | 52 | // empty object checking & data cleanup 53 | if(!scrollBarWide){ result.notReady='missing scrollbar size'; return result } 54 | if(!props){ result.notReady='missing grid props'; return result } 55 | if(!props.data && !props.columnList){ result.notReady = "No Data Provided"; return result } 56 | 57 | // try to handle bad input data 58 | var data = props.data; 59 | if(!props.data && props.columnList){ data=[]; } 60 | 61 | if( (typeof data === 'string' || data instanceof String) || // array of strings 62 | (typeof data === 'number' && isFinite(data) ) || // array of numbers 63 | (typeof data === 'boolean') ){ // array of booleans 64 | result.notReady='Grid data must be an array'; return result; 65 | } 66 | 67 | // don't us foo.isArray because MobX and other arrays don't look like arrays, but are. 68 | if(!data.length && 0!==data.length){ result.notReady = "Input Data is not an array"; return result } 69 | if(data.length===0 && !props.columnList){ result.notReady = "No sample data supplied and no column definition list supplied. To start with an empty array, please define the columns."; return result; } 70 | if(data.length>0 && !props.columnList && 71 | (data[0]===null || typeof data[0] === 'undefined')){ 72 | result.notReady = "Falsey sample data supplied with no column definition list supplied."; return result; 73 | } 74 | 75 | // add validation that "px" should not be used, and only numbers should be passed as parameters. 76 | 77 | if( data[0] && typeof data[0] === 'object' && data[0].constructor === RegExp){ result.notReady = "Grids of RegExp objects are not supported."; return result; } 78 | if(typeof data[0] === 'symbol'){ result.notReady = "Grids of Symbol objects are not supported."; return result; } 79 | if(typeof data[0] === 'function'){ result.notReady = "Grids of Function objects are not supported."; return result; } 80 | 81 | // if it's an array of primitivs, make sure to treat it as if it were an array of objects 82 | if( (typeof data[0] === 'string' || data[0] instanceof String) || // array of strings 83 | (typeof data[0] === 'number' && isFinite(data[0]) ) || // array of numbers 84 | (typeof data[0] === 'boolean') ){ // array of booleans 85 | result.isPrimitiveData=true; 86 | } 87 | 88 | if(props.columnList && !props.columnList.length){ 89 | result.notReady = "If the columnList property is set, it must be an array with more that one object in it."; return result; 90 | } 91 | 92 | // validate any column data 93 | if (props.columnList) { 94 | if(result.isPrimitiveData && props.columnList ){ 95 | // make sure at least one column is 'data' for primitives 96 | var foundPrimCol = false; 97 | for (var cctr = 0; cctr < props.columnList.length;cctr++){ 98 | if(props.columnList[cctr].key==='data'){foundPrimCol=true;} 99 | } 100 | if(!foundPrimCol){ 101 | result.notReady = "When using a list of primitives, and a columnList, one of the column keys must be named 'data'. (Prim Lists use a hard coded key)"; return result; 102 | } 103 | } 104 | else{ 105 | // make sure at least one column key matches a data key. 106 | var foundPrimCol = false; 107 | for (var cctr = 0; cctr < props.columnList.length; cctr++) { 108 | if (data[0][ props.columnList[cctr].key ]) { foundPrimCol = true; } 109 | } 110 | if (!foundPrimCol) { 111 | result.notReady = "None of the key properties of your columnList match a key in your data. For arrray data, use integer keys. For object data you field names."; return result; 112 | } 113 | } 114 | } 115 | 116 | //TODO: make the data conversion and hold onto the converted data. 117 | // just give data available calls? onChange pass an object that can get the translation? 118 | // give an object that can be used to ask for translation at any time. 119 | // data.asJSON() 120 | // data.asCSV() 121 | // data.asPSV() 122 | 123 | // general info 124 | result.borderWide = this.makeValidInt(props.borderWide,1); 125 | result.padWide = this.makeValidInt(props.padWide, 3); 126 | 127 | result.gridWide = this.makeValidInt(props.gridWide,800); 128 | result.autoColWide = 0; // width of the default filled column, before weights 129 | result.fixedRowCount = props.rowCount; // what is the rowCount limit 130 | 131 | result.editDisabled = props.editDisabled || false; 132 | 133 | // how high is each row: user requested height does NOT include padding. 134 | result.rowHighNoPad = this.makeValidInt(props.rowHigh, 18); 135 | if (-1 === result.rowHighNoPad) { result.rowHighNoPad=23;} 136 | result.rowHighWithPad = this.makeValidInt(result.rowHighNoPad, 18); 137 | result.rowHighWithPad += result.padWide; 138 | result.rowHighWithPad += result.padWide; 139 | 140 | // column header height 141 | result.colHeaderHigh = (props.colHeaderHigh||-1); 142 | if (-1 === result.colHeaderHigh) { result.colHeaderHigh = 18; } 143 | if (props.colHeaderHide) { result.colHeaderHigh = 0; } // hide not wide or high 144 | 145 | // header height 146 | if (props.colHeaderHide || result.forceColHeaderHide) { // provide a header row. 147 | result.headerUsage=1; 148 | } 149 | else{ 150 | result.headerUsage=(result.colHeaderHigh+(2*result.padWide)+(2*result.borderWide)); 151 | } 152 | 153 | // tools height 154 | result.toolUsage = 0; 155 | if(props.showToolsAddCut || props.showToolsImpExp || props.showToolsPage || props.showToolsCustom){ 156 | result.toolUsage = 35; 157 | } 158 | 159 | // collapsed grid height 160 | result.collapseAmount=0; 161 | 162 | 163 | result.formatDate = props.formatDate||'YYYY-MM-DD'; 164 | result.formatTime = props.formatTime||'HH:mm'; 165 | var autoColCount=0; 166 | // look at the data to display and figure out what we need to do. 167 | if( (data && data.length>0) || (props.columnList && props.columnList.length>0) ){ // col def from colList or from data 168 | if(result.isPrimitiveData){ 169 | // ==== PRIMITIVES we have only one line of data to display 170 | result.keyNames.push('data'); 171 | if(props.pivotOn || props.pivotOn===0){ 172 | result.rowHeaderList=['data']; // one row header 173 | result.fixedRowCount = 1; // one row 174 | result.saveColumnForRowHeader=1; // will have a row header. 175 | result.forceColHeaderHide=true; // no column headers allowed on pivoted primitives. 176 | result.headerUsage=1; 177 | result.colHeaderKeyList=[]; // I mean it: no column headers allowed! 178 | result.colHeaderKeyList.push('\\'); // extra column on header for row headers. 179 | for(var pctr=0;pctr width 183 | result.dataHigh = 1; // 1 row hight 184 | } 185 | else{ 186 | result.colHeaderKeyList=result.keyNames; // just one header 187 | result.fixedRowCount = data.length; // amount of data is the rows 188 | result.dataWide = 1; // 1 item wide. 189 | result.dataHigh = data.length; // length items tall. 190 | } 191 | } 192 | else{ 193 | // ==== OBJECTS we have rows of objects to display 194 | if(props.pivotOn || props.pivotOn === 0){ // pivot the data using this key as the col header 195 | //---- PIVOTED FLOW 196 | result.colHeaderKeyList.push('\\'); // extra column on header for row headers. 197 | var temp = Object.keys(data[0]); 198 | for(var pctr=0;pctr data width. (data high handled later) 222 | } 223 | else{ // no column defs, inspect the first object. 224 | if(data[0].length){ // probably an array-look-alike. Use indexes 225 | for (var ictr = 0; ictr < data[0].length;ictr++){ 226 | result.keyNames.push(ictr); 227 | } 228 | result.colHeaderKeyList = result.keyNames; // keys => columns here 229 | result.dataWide = data[0].length; // keynames is width (data high handled later) 230 | } 231 | else{ // likely an object. Treat it normally. 232 | result.keyNames = Object.keys(data[0]); // pull the key names 233 | result.colHeaderKeyList = result.keyNames; // keys => columns here 234 | result.dataWide = result.keyNames.length; // keynames is width (data high handled later) 235 | } 236 | } 237 | 238 | result.fixedRowCount = data.length; // allow over-ride item count. 239 | result.dataHigh = result.fixedRowCount; 240 | } 241 | } 242 | 243 | // fix degenerate sizing 244 | if(result.gridWide < scrollBarWide + 10*result.keyNames.length){ 245 | result.gridWide = scrollBarWide + 10*result.keyNames.length; 246 | } 247 | result.rowWide = result.gridWide - (scrollBarWide||16); // how wide is each row. 248 | var availableWide = result.rowWide; // amount of space to allocate evenly 249 | 250 | autoColCount = result.colHeaderKeyList.length; 251 | if ((props.pivotOn || props.pivotOn === 0) && props.pivotRowHeaderWide && props.pivotRowHeaderWide!==-1){ 252 | // don't autoColCount the rowHeader 253 | autoColCount--; 254 | } 255 | 256 | // calculate wide row headers, respecting pivots 257 | if ((props.pivotOn || props.pivotOn === 0) && props.pivotRowHeaderWide && props.pivotRowHeaderWide !== -1) { 258 | result.pivotRowHeaderWide = Number(props.pivotRowHeaderWide); 259 | result.pivotRowHeaderWideTotal = result.pivotRowHeaderWide; 260 | result.pivotRowHeaderWideTotal += (result.borderWide); // each column minus right border amount 261 | result.pivotRowHeaderWideTotal += (result.padWide); // each column minus left pad amount 262 | result.pivotRowHeaderWideTotal += (result.padWide); // each column minus right pad amount 263 | availableWide -= result.pivotRowHeaderWideTotal; // allow a set width pivot header, but still only autocol for pivoted data 264 | } 265 | else { 266 | result.pivotRowHeaderWide = 0; 267 | result.pivotRowHeaderWideTotal = 0; 268 | } 269 | 270 | //==== now calculate column actual sizes and autocol size 271 | 272 | 273 | var fixedWide = 0; // becomes the new rowWide is all columns are specified 274 | var pctColCount = 0; // how many pct columns do we have. 275 | var cellBuffer = Number(result.borderWide) + Number(result.padWide) + Number(result.padWide); 276 | var change = 0; 277 | if (props.columnList && result.colHeaderKeyList.length && !props.pivotOn && props.pivotOn!==0){ // only autosize allowed on pivoted data 278 | autoColCount = 0; // number of columns that need auto width, i.e. column defs without a pct or px 279 | 280 | //--- round 1: handle the fixed px width columns 281 | for (var cctr = 0; cctr < result.colHeaderKeyList.length;cctr++){ 282 | change=0; 283 | 284 | if (result.colDefListByKey[result.colHeaderKeyList[cctr]]) { // is there a colDef that uses this key? 285 | if (result.colDefListByKey[result.colHeaderKeyList[cctr]].widePx) { 286 | change = Number(result.colDefListByKey[result.colHeaderKeyList[cctr]].widePx); // user requested size 287 | result.colDefListByKey[result.colHeaderKeyList[cctr]].forceColWide = change; // goal user area, without buffer 288 | change += cellBuffer; // actual cell size = user requested + cell buffer (padding and border) 289 | fixedWide+=change; 290 | availableWide -= change; 291 | } 292 | else if (result.colDefListByKey[result.colHeaderKeyList[cctr]].widePct) { 293 | // wait for later until we know all the fixed widths. 294 | pctColCount++; 295 | } 296 | else{ 297 | autoColCount++; // take away a default amount for each autocol 298 | } 299 | } 300 | } 301 | //-- now hold space for autocol columns and cell buffers 302 | availableWide -= (autoColCount * (5 + cellBuffer)) + (pctColCount*cellBuffer); 303 | //--- round 2: handle the pct width columns 304 | for (var cctr = 0; cctr < result.colHeaderKeyList.length; cctr++) { 305 | //change = 0; 306 | 307 | if (result.colDefListByKey[result.colHeaderKeyList[cctr]]) { // is there a colDef that uses this key? 308 | if (result.colDefListByKey[result.colHeaderKeyList[cctr]].widePx) { 309 | // already done 310 | } 311 | else if (result.colDefListByKey[result.colHeaderKeyList[cctr]].widePct) { 312 | change = (availableWide 313 | * (Number(result.colDefListByKey[result.colHeaderKeyList[cctr]].widePct) / 100)); 314 | // user area as pct of availble space. cell buffer was already removed, and so should not be in the user area number 315 | result.colDefListByKey[result.colHeaderKeyList[cctr]].forceColWide = change - cellBuffer; 316 | } 317 | else { 318 | // already done 319 | } 320 | } 321 | } 322 | 323 | } 324 | 325 | 326 | //--- no column width data 327 | if(autoColCount>0){ 328 | result.autoColWide = 329 | ( availableWide - // total width 330 | result.borderWide // minus left most border bar 331 | // scrollbar already handled by basin on rowWide. 332 | ) / (autoColCount) 333 | } 334 | else{ result.autoColWide=0; } 335 | result.autoColWideWithBorderAndPad = result.autoColWide; 336 | result.autoColWide -= (result.borderWide); // each column minus right border amount 337 | result.autoColWide -= (result.padWide); // each column minus left pad amount 338 | result.autoColWide -= (result.padWide); // each column minus right pad amount 339 | 340 | // calculate the used row width 341 | result.rowWide= fixedWide + (result.autoColWideWithBorderAndPad * autoColCount) - result.borderWide; 342 | 343 | // grid height - can't calculate this until dataHigh is defined due to pivot concerns 344 | var testStyleHeight = null; 345 | if(props.style){ testStyleHeight = props.style.height}; // needed for null safety on props.style. Needs to remove trailing px or % !!! 346 | result.gridHigh = testStyleHeight || 347 | this.makeValidInt(props.gridHigh) || 348 | Math.min(result.rowHighWithPad*result.dataHigh+result.headerUsage+(result.borderWide*3)+10 ,400); // read from style, read from attributes, read from gridHigh attribute, default to 300 349 | if (result.gridHigh === -1) { 350 | result.gridHigh = 400; 351 | } 352 | 353 | // How high should the grid be? 354 | result.dataFullHigh = result.dataHigh * (result.rowHighWithPad + result.borderWide); 355 | result.dataAvailableHigh = result.gridHigh-result.headerUsage-result.toolUsage; // this is the virtList height. 356 | 357 | if(result.dataFullHigh > result.dataAvailableHigh){ 358 | result.showBottomGridLine=true; // less data than space: no bottom line is needed. 359 | } 360 | else{ 361 | result.showBottomGridLine=false; 362 | result.dataAvailableHigh -= (result.borderWide) 363 | } 364 | //result.bottomGridLineWide = (result.keyNames.length * (result.autoColWide + result.borderWide + result.padWide + result.padWide))-result.borderWide; 365 | result.bottomGridLineWide = result.rowWide; 366 | 367 | result.bottomLineUsage=0; 368 | if (result.showBottomGridLine) { 369 | result.bottomLineUsage = result.borderWide; 370 | result.dataAvailableHigh -= result.bottomLineUsage; 371 | } 372 | 373 | // fix a degenerate case 374 | if(result.dataAvailableHigh<0){ 375 | result.dataAvailableHigh=100; 376 | result.gridHigh=150; 377 | result.showBottomGridLine=false; 378 | } 379 | 380 | 381 | // check wether to shrink the grid if there is not enough data to fill it. 382 | result.collapseAvailable = result.gridHigh - result.headerUsage 383 | - result.dataFullHigh 384 | - result.bottomLineUsage 385 | - (result.borderWide*2) 386 | - result.toolUsage ; // amount to fill 387 | 388 | if(result.collapseAvailable < 0) { result.collapseAvailable = 0 } // amount to shrink by 389 | result.dataAvailableHigh -= result.collapseAvailable; // don't let the grid consume needless space. 390 | if (props.gridHighCollapse){ 391 | result.gridHigh -= result.collapseAvailable; // shrink whole component if requested 392 | } 393 | 394 | } 395 | else if(props.getRowData){ 396 | // ==== ROW DATA METHOD we have rows of objects to display ( check for an array ) 397 | result.autoColWide= 398 | result.gridWide - 399 | (scrollBarWide||20) - 400 | (result.borderWide*2); 401 | result.colHeaderKeyList = ["No Data Provided"]; 402 | } 403 | else{ 404 | // ==== NO DATA PROVIDED 405 | result.autoColWide = 406 | result.gridWide - 407 | (scrollBarWide||20) - 408 | (result.borderWide * 2); 409 | result.colHeaderKeyList = ["No Data Provided"]; 410 | } 411 | 412 | return result; 413 | } 414 | } 415 | 416 | export default GridMath; 417 | 418 | -------------------------------------------------------------------------------- /src/GridRow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import VirtualList from 'react-tiny-virtual-list'; 4 | import { observable, action } from 'mobx'; 5 | import { observer } from 'mobx-react'; 6 | import ScrollbarSize from 'react-scrollbar-size'; 7 | import autoBind from 'react-autobind'; 8 | import GridCell from './GridCell'; 9 | 10 | 11 | 12 | @observer class GridRow extends React.Component { 13 | constructor(props) { super(props); autoBind(this); } 14 | 15 | showRowHeaderAltText(e,x,y) 16 | { 17 | this.props.GridStore.cursor.showAltX = x; 18 | this.props.GridStore.cursor.showAltY = y; 19 | } 20 | hideRowHeaderAltText(e,x,y) 21 | { 22 | if(this.props.GridStore.cursor.showAltX === x && this.props.GridStore.cursor.showAltY === y){ 23 | this.props.GridStore.cursor.showAltX = -1; // don't mess with it if values were set by someone else. 24 | this.props.GridStore.cursor.showAltY = -1; 25 | } 26 | } 27 | 28 | render() { 29 | 30 | var selRow=false; 31 | if(this.props.GridStore.cursor.y===this.props.index){ 32 | selRow=true; 33 | } 34 | 35 | var cellArray = []; 36 | var marginOffset = Math.floor(-1 * this.props.uiMath.borderWide); 37 | 38 | 39 | 40 | var topBorder = (this.props.colHeaderHide || this.props.uiMath.forceColHeaderHide) ? this.props.uiMath.borderWide:0; 41 | 42 | var sharedBaseStyleLeftCol = { 43 | width: this.props.uiMath.autoColWide, 44 | borderStyle: 'solid', 45 | borderColor: 'black', 46 | padding: (this.props.uiMath.padWide || 0) + 'px', 47 | borderLeftWidth: this.props.uiMath.borderWide, 48 | borderRightWidth: this.props.uiMath.borderWide, 49 | borderBottomWidth: this.props.uiMath.borderWide, 50 | borderTopWidth: topBorder, 51 | height: this.props.uiMath.rowHighNoPad, 52 | display: 'inline-block', 53 | outline: outline, 54 | overflow:'hidden', 55 | }; 56 | 57 | var sharedBaseStyleInput={ width: this.props.uiMath.autoColWide, 58 | borderStyle: 'solid', 59 | borderColor: 'black', 60 | borderLeftWidth: this.props.uiMath.borderWide, 61 | borderRightWidth: this.props.uiMath.borderWide, 62 | borderBottomWidth: this.props.uiMath.borderWide, 63 | borderTopWidth: topBorder, 64 | backgroundColor: '#fffef4', 65 | padding: (this.props.uiMath.padWide||0)+'px', 66 | height: this.props.uiMath.rowHighNoPad, 67 | display: 'inline-block', 68 | outline: outline, 69 | marginLeft: marginOffset , 70 | overflow: 'hidden', 71 | }; 72 | var sharedBaseStyle2={ width: this.props.uiMath.autoColWide, 73 | borderStyle: 'solid', 74 | borderColor: 'black', 75 | borderLeftWidth: this.props.uiMath.borderWide, 76 | borderRightWidth: this.props.uiMath.borderWide, 77 | borderBottomWidth: this.props.uiMath.borderWide, 78 | borderTopWidth: topBorder, 79 | padding: (this.props.uiMath.padWide||0)+'px', 80 | height: this.props.uiMath.rowHighNoPad, 81 | display: 'inline-block', 82 | outline: outline, 83 | marginLeft: marginOffset , 84 | overflow: 'hidden', 85 | }; 86 | 87 | var cellStyleFirst = Object.assign(sharedBaseStyleLeftCol, (this.props.styleCell||{})); 88 | var cellStyleLocal = Object.assign(sharedBaseStyle2, (this.props.styleCell || {})); 89 | 90 | var inputStyleFirst = Object.assign(sharedBaseStyleLeftCol, (this.props.styleCell||{})); 91 | var inputStyleLocal = Object.assign(sharedBaseStyleInput, (this.props.styleInput || {})); 92 | 93 | 94 | var keyName = null; // used for pivoted data only 95 | 96 | var columnCount = this.props.GridStore.cursor.maxX+1; 97 | if (this.props.pivotOn || this.props.pivotOn === 0){ 98 | columnCount = this.props.data.length; 99 | } 100 | var isFirst=true; 101 | for (var ctr = -1; ctr < columnCount; ctr++) { 102 | var borderColor='black'; 103 | var zIndex=0; 104 | var outline=''; 105 | 106 | cellStyleLocal.marginLeft=marginOffset+'px'; 107 | 108 | // row header / pivot work 109 | if(ctr===-1){ 110 | cellStyleFirst = {width: this.props.uiMath.autoColWide, 111 | borderStyle: 'solid', 112 | borderColor: 'black', 113 | padding: (this.props.uiMath.padWide || 0) + 'px', 114 | borderLeftWidth: this.props.uiMath.borderWide, 115 | borderRightWidth: this.props.uiMath.borderWide, 116 | borderBottomWidth: this.props.uiMath.borderWide, 117 | borderTopWidth: topBorder, 118 | height: this.props.uiMath.rowHighNoPad, 119 | display: 'inline-block', 120 | outline: outline, 121 | overflow:'hidden'} 122 | ; // don't add global cell style on row headers 123 | 124 | if(this.props.uiMath.rowHeaderList && this.props.uiMath.rowHeaderList.length > this.props.index) { 125 | var keyName = this.props.uiMath.rowHeaderList[this.props.index]; // what key am I on? 126 | var titleText = keyName; 127 | if (this.props.GridStore.colDefListByKey[keyName]) { // is there a colDef that uses this key? 128 | titleText = this.props.GridStore.colDefListByKey[keyName].title || keyName; // if there is a title for the colDef use it, or just stick with thekey 129 | } 130 | 131 | if(this.props.uiMath.pivotRowHeaderWide){ 132 | cellStyleFirst.width = Number(this.props.uiMath.pivotRowHeaderWide); 133 | } 134 | isFirst=false; 135 | 136 | // add in the row header style 137 | var gridColLocalStyle = {}; 138 | if(this.props.GridStore.colDefListByKey[keyName]){ 139 | gridColLocalStyle = (this.props.GridStore.colDefListByKey[keyName].styleHeader || {}); 140 | } 141 | 142 | var rowHeaderStyle={...cellStyleFirst,...gridColLocalStyle}; 143 | 144 | cellArray.push( 145 | 159 | ); 160 | } 161 | } 162 | else{ 163 | 164 | var curColKey = this.props.uiMath.colHeaderKeyList[ctr]; // the xth column defined in the colHeaderKey list 165 | if (this.props.pivotOn || this.props.pivotOn === 0) { 166 | if (this.props.GridStore.colDefListByIdx){ 167 | // pvt ON, colDef ON 168 | //console.log(this.props.index, this.props.GridStore.colDefListByIdx[this.props.index]); 169 | curColKey = this.props.uiMath.rowHeaderList[this.props.index]; 170 | } 171 | else{ 172 | curColKey = this.props.uiMath.colHeaderKeyList[this.props.index + 1]; // use Y instead of X and ofset for the row header. this seems like it could be smoother. 173 | //curColKey = this.props.index; // use Y instead of X and ofset for the row header. this seems like it could be smoother. 174 | } 175 | } 176 | var cellData = this.props.GridStore.getDataRespectingPivotAtLocation(this.props.data,ctr,this.props.index); 177 | 178 | cellArray.push( 179 | 194 | ); 195 | isFirst = false; 196 | 197 | } 198 | } 199 | 200 | return( 201 |
202 | {cellArray} 203 |
204 | ); 205 | 206 | } 207 | } 208 | 209 | 210 | export default GridRow; -------------------------------------------------------------------------------- /src/GridStore.jsx: -------------------------------------------------------------------------------- 1 | import { observable, computed, action } from 'mobx'; 2 | import GridMath from './GridMath'; 3 | 4 | class GridStore { // Just a class. Nothing fancy here. 5 | constructor() { 6 | this.uiMathInst = new GridMath(); 7 | 8 | } 9 | 10 | @observable cursor = {x:0,y:0, // cursor x and y values 11 | maxX:-1,maxY:-1, // max legal x and y values. 12 | selectToX:-1,selectToY:-1, // selection box for shift selection 13 | editX:-1,editY:-1,editObjKey:-1, // cell being edited 14 | showAltX:-1,showAltY:-1, // for displaying alt text help 15 | shiftSelInProgress:false // for shift arrow selecting cells 16 | }; 17 | @observable selectedCells = []; // for control clicking cells. 18 | @observable curEditingValue=''; // value being edited before it is applied. 19 | 20 | @observable autoFocus=false; // if true, rendering a selected cell will cause that cell to take focus 21 | @observable inst = ''; // if true, rendering a selected cell will cause that cell to take focus 22 | @observable pivotOn = ''; // from props. Here for easy cell access. 23 | 24 | @observable colDefListByKey = {}; // column definition meta data accessible by keyName 25 | @observable colDefListByIdx = []; // column definition meta data accessible by ordered index 26 | @observable keyList=[] // list of the keys in the given data object. 27 | 28 | @observable showDatePicker = false; // due to scrolling issues, the react-datepicker popup cannot be used. This add a non-scrolled over-lay picker. 29 | @observable showDateTimePicker = false; // due to scrolling issues, the react-datepicker popup cannot be used. This add a non-scrolled over-lay picker. 30 | @observable showMenuPicker = false; // due to scrolling issues, the react-datepicker popup cannot be used. This add a non-scrolled over-lay picker. 31 | @observable showOverlayComp = false; // due to scrolling issues, the react-datepicker popup cannot be used. This add a non-scrolled over-lay picker. 32 | 33 | @observable scrollBarWide = 20; 34 | 35 | 36 | // javascript sucks at math 37 | makeValidInt(inputVal, defaultVal) { 38 | var res = (inputVal || defaultVal); 39 | if (inputVal === 0) { res = inputVal; } 40 | if (res < 0) { res = defaultVal; } 41 | return Number(res); 42 | } 43 | 44 | // all the default handlers inform the user that there is no handler specified. 45 | @action logNoChangeHandlerMessage() { console.log('no onChange handler supplied.');} 46 | @action logNoOnRowAddHandlerMessage() { console.log('no onRowAdd handler supplied.'); } 47 | @action logNoOnRowCutHandlerMessage() { console.log('no onRowCut handler supplied.'); } 48 | @action logNoOnGotoPageHandlerMessage() { console.log('no onGotoPage handler supplied.'); } 49 | @action logNoOnImportHandlerMessage() { console.log('no onImport handler supplied.'); } 50 | @action logNoOnExportHandlerMessage() { console.log('no onExport handler supplied.'); } 51 | @action logNoOnReplaceDataHandlerMessage() { console.log('no onReplaceData handler supplied. (Required for text mode editing)'); } 52 | 53 | // don't call the user supplied on change directly. 54 | // call this to ensure that the pivot variables get swizzled correctly. 55 | onChangePivotWrapper(x,y,objKey,val){ 56 | if(val===null) return; // cannot set null via the UI. prevents unintended changes. 57 | if (this.pivotOn || 0 === this.pivotOn) { 58 | this.onChange(y, x, objKey, val); 59 | } 60 | else { 61 | this.onChange(x, y, objKey, val); 62 | } 63 | } 64 | 65 | /* 66 | this method takes the props and updates the grid store. 67 | */ 68 | @action prepSelectionField(props) 69 | { 70 | // make the calculations 71 | this.uiMath = this.uiMathInst.calcGridBody(props, (this.scrollBarWide||20)); 72 | 73 | // ease of access 74 | this.colDefListByKey = this.uiMath.colDefListByKey; 75 | this.colDefListByIdx = props.columnList; 76 | 77 | //--- style data holders 78 | this.styleCell = props.styleCell||{}; 79 | this.styleCellOddRow = props.styleCellOddRow||{}; 80 | this.styleHeaderCell = props.styleHeaderCell||{}; 81 | this.styleRowHeaderCell = props.styleRowHeaderCell||{}; 82 | 83 | this.styleData = props.styleData||{}; 84 | this.styleDataOddRow = props.styleDataOddRow||{}; 85 | this.styleHeaderData = props.styleHeaderData||{}; 86 | this.styleRowHeaderData = props.styleRowHeaderData||{}; 87 | 88 | this.styleInput = props.styleInput||{}; 89 | this.styleSelected = props.styleSelected||{}; 90 | 91 | 92 | //--- class data holders 93 | this.classCell = props.classCell||''; 94 | this.classCellOddRow = props.classCellOddRow||''; 95 | this.classHeaderCell = props.classHeaderCell||''; 96 | this.classRowHeaderCell = props.classRowHeaderCell||''; 97 | 98 | this.classData = props.classData||''; 99 | this.classDataOddRow = props.classDataOddRow||''; 100 | this.classHeaderData = props.classHeaderData||''; 101 | this.classRowHeaderData = props.classRowHeaderData||''; 102 | 103 | this.classInput = props.classInput||''; 104 | this.classSelected = props.classSelected||''; 105 | 106 | this.keyList = this.uiMath.keyNames; 107 | 108 | 109 | var dataWide = this.uiMath.dataWide; 110 | var dataHigh = this.uiMath.dataHigh; 111 | 112 | // ensure that we only set the values if they've actuallly changed. 113 | if (this.cursor.maxX !== dataWide - 1 || this.cursor.maxY !== dataHigh - 1) { 114 | this.cursor.maxX = dataWide-1; 115 | this.cursor.maxY = dataHigh-1; 116 | } 117 | 118 | if(props.testCursor){ 119 | this.cursor.x = props.testCursor.x; 120 | this.cursor.y = props.testCursor.y; 121 | this.cursor.selectToX = (props.testCursor.selectToX||-1); 122 | this.cursor.selectToY = (props.testCursor.selectToY||-1); 123 | this.cursor.editX = (props.testCursor.editX||-1); 124 | this.cursor.editY = (props.testCursor.editY||-1); 125 | this.cursor.shiftSelInProgress = props.testCursor.shiftSelInProgress; 126 | } 127 | 128 | // make handlers easily available and log messages if they're missing. 129 | this.onChange = (props.onChange || this.logNoChangeHandlerMessage ); 130 | this.onRowAdd = (props.onRowAdd || this.logNoOnRowAddHandlerMessage); 131 | this.onRowCut = (props.onRowCut || this.logNoOnRowCutHandlerMessage); 132 | this.onGotoPage = (props.onGotoPage || this.logNoGotoPageHandlerMessage); 133 | this.onExport = (props.onExport || this.logNoOnExportHandlerMessage); 134 | this.onImport = (props.onImport || this.logNoOnImportHandlerMessage); 135 | this.onReplaceData = (props.onReplaceData || this.logNoOnReplaceDataHandlerMessage); 136 | 137 | this.pivotOn = props.pivotOn; // easy availability to cells 138 | } 139 | 140 | 141 | // sets a selected cell 142 | @action cellSelectSet(x,y,val){ 143 | selectedCells[y][x]=val; 144 | } 145 | 146 | // handles keyboard movement. takes an event. 147 | @action cellMoveKey(e) 148 | { 149 | // only worry about arrow keys: 150 | if(e.shiftKey){ 151 | // was it already down? if no, start a selection 152 | // note: google sheets does not allow 2 separate shift-cell-selections at a time. It's one block+click collection , but not 2 blocks. 153 | if(!this.cursor.shiftSelInProgress){ 154 | this.cursor.selectToX=this.cursor.x; 155 | this.cursor.selectToY=this.cursor.y; 156 | this.cursor.shiftSelInProgress=true; 157 | } 158 | } 159 | else{ 160 | this.cursor.shiftSelInProgress = false; 161 | } 162 | if (e.keyCode == '38') { 163 | // up arrow 164 | this.cursor.y--; 165 | if (this.cursor.y < 0) this.cursor.y = 0; 166 | } 167 | else if (e.keyCode == '40') { 168 | // down arrow 169 | if (this.cursor.y < this.cursor.maxY) this.cursor.y++; 170 | } 171 | else if (e.keyCode == '37') { 172 | // left arrow 173 | this.cursor.x--; 174 | if (this.cursor.x < 0) this.cursor.x = 0; 175 | } 176 | else if (e.keyCode == '39') { 177 | // right arrow 178 | if (this.cursor.x < this.cursor.maxX) this.cursor.x++; 179 | } 180 | e.stopPropagation(); 181 | e.preventDefault(); 182 | } 183 | 184 | // computes the selection bounds based on changes to the selected cell position and shift key. 185 | @computed get selectionBounds(){ 186 | var res={l:-1,r:-1,t:-1,b:-1}; 187 | // block selection 188 | if(this.cursor.shiftSelInProgress){ 189 | res.l = Math.min(this.cursor.x, this.cursor.selectToX); 190 | res.r = Math.max(this.cursor.x, this.cursor.selectToX); 191 | res.t = Math.min(this.cursor.y, this.cursor.selectToY); 192 | res.b = Math.max(this.cursor.y, this.cursor.selectToY); 193 | } 194 | else{ 195 | res.l = res.r = this.cursor.x; 196 | res.t = res.b = this.cursor.y; 197 | } 198 | // click selection 199 | 200 | return res; 201 | } 202 | 203 | // validates some input fields 204 | @computed get curEditIsValidFor() { 205 | var res={}; 206 | res.isValidInt = this.checkValidInt(this.curEditingValue); 207 | res.isValidFloat = this.checkValidFloat (this.curEditingValue); 208 | return res; 209 | } 210 | 211 | // very basic validity checks. 212 | checkValidInt(t){ 213 | if(!t) return true; // blank+null is ok. 214 | return (!isNaN(t) && (function (x) { return (x | 0) === x; })(parseFloat(t))); 215 | } 216 | checkValidFloat(t) { 217 | if(!t) return true; // blank+null is ok. 218 | return (!isNaN(t)); 219 | } 220 | 221 | // because the data may be in a pivot display, use this layer to find the real coordinates of the data for the cell 222 | getDataRespectingPivotAtEditCursor(clientData) 223 | { 224 | if(this.uiMath.isPrimitiveData){ 225 | // non-object data. Just pretend it's a grid. Only one coordinate will matter. 226 | if (this.pivotOn || this.pivotOn === 0){ 227 | return clientData[this.cursor.editY]; // y is rows down / outer array 228 | } 229 | else{ 230 | return clientData[this.cursor.editX]; 231 | } 232 | } 233 | else{ 234 | if(this.pivotOn || this.pivotOn===0){ 235 | return clientData[this.cursor.editX][this.uiMath.rowHeaderList[this.cursor.editY]]; // y is rows down / outer array 236 | } 237 | else{ 238 | return clientData[this.cursor.editY][this.uiMath.colHeaderKeyList[this.cursor.editX]]; 239 | } 240 | } 241 | } 242 | 243 | // because the data may be in a pivot display, use this layer to find the real coordinates of the data for the cell. arbirary location 244 | getDataRespectingPivotAtLocation(clientData,x,y) 245 | { 246 | if(x<0 && y<0){ return '/' } 247 | if (x < 0) { return '/' } 248 | if (y < 0) { return '/' } 249 | if(typeof clientData !== "object"){ console.log('Remember that the first parameter of this method must be the user client data. Currently it is '+clientData);} 250 | if(this.uiMath.isPrimitiveData){ 251 | // non-object data. Just pretend it's a grid. Only one coordinate will matter. 252 | if (this.pivotOn || 0 === this.pivotOn){ 253 | return clientData[x]; // y is rows down / outer array 254 | } 255 | else{ 256 | return clientData[y]; 257 | } 258 | } 259 | else{ 260 | // object data. Use the real stuff. 261 | if (this.pivotOn || 0 === this.pivotOn){ 262 | if (this.uiMath.rowHeaderList){ 263 | return clientData[x][this.uiMath.rowHeaderList[y]]; // x data items into outer list. Y+1 adjusts for the "/" column heading 264 | } 265 | else{ 266 | return clientData[x][y]; // x data items into outer list. Y+1 adjusts for the "/" column heading 267 | } 268 | } 269 | else{ 270 | return clientData[y][this.uiMath.colHeaderKeyList[x]]; // y is depth in outer list, x is the column into the inner list/object 271 | } 272 | } 273 | } 274 | 275 | 276 | // some error handling helpers 277 | @observable jsonAsTxtError = ''; 278 | @observable textDataLinesLength=0; 279 | @observable textGoalFormat = ''; 280 | 281 | // data conversion for text editor support 282 | @action convertJSONtoTXT(data){ 283 | this.textDataLinesLength=0; 284 | this.keyList=[]; 285 | var res = ''; 286 | 287 | if(!data || data.length===0){ 288 | return ''; 289 | } 290 | this.textDataLinesLength = data.length; 291 | 292 | if (typeof data[0] === 'object' || typeof data[0] === 'array'){ 293 | 294 | this.textGoalFormat=typeof data[0]; 295 | this.textDataLinesLength = data.length; 296 | this.keyList = Object.keys(data[0]); 297 | if(this.keyList.length===0){ 298 | // fake object. Treat it as an array 299 | this.textGoalFormat='array'; 300 | if(data[0].length){ 301 | for(var fi=0;fix[this.cursor.objKey]).join('\n'); // get the "objKey"th item from each object into an array of primitives then newline-join it together for the response. 317 | } 318 | else{ 319 | this.textGoalFormat='prims'; // single array of primitives 320 | return data.join('\n').trim(); 321 | } 322 | } 323 | 324 | // TODO 325 | // - tool button to switch between editor types. 326 | // - remember if the input was obj, array, or prim array and use it to convert back 327 | // - Get user specified components working everywhere 328 | @action convertTXTtoJSON(txt){ 329 | var lines = txt.split('\n'); 330 | // bonehead implementation. this is intended for lists of less than 200 items. 331 | var result = []; 332 | for(var ctr=0;ctrkctr){ 345 | curObj[ this.keyList[kctr] ]=items[kctr]; 346 | } 347 | else{ 348 | curObj[ this.keyList[kctr] ]=''; 349 | } 350 | } 351 | result.push(curObj); 352 | } 353 | } 354 | 355 | if(this.uiMath.debugGridMath){ 356 | console.log(result); 357 | } 358 | this.onReplaceData(result); 359 | 360 | } 361 | 362 | } 363 | 364 | export default GridStore; 365 | 366 | 367 | -------------------------------------------------------------------------------- /src/GridTextBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observable,action,trace } from 'mobx'; 4 | import { observer } from 'mobx-react'; 5 | import autoBind from 'react-autobind'; 6 | 7 | import GridHeader from './GridHeader'; 8 | import GridTools from './GridTools'; 9 | 10 | 11 | // Wrapper for the grid 12 | // This grid allows deep styling via style objects, 13 | // but retains control of border and padding to ensure that the grid lines up. 14 | @observer class GridTextBox extends React.Component 15 | { 16 | 17 | @observable txt = ''; // keep track of how wide the scroll bar will be 18 | 19 | constructor(props) { 20 | super(props); 21 | autoBind(this); 22 | this.componentWillReceiveProps(props); // first call needs to set up the data store 23 | //this.refuseUpdates=false; 24 | } 25 | 26 | componentWillReceiveProps(nextProps) 27 | { 28 | //if(this.refuseUpdates){return;} 29 | 30 | var testTxt = this.props.GridStore.convertJSONtoTXT(nextProps.data); 31 | if(this.txt!==testTxt){ 32 | this.txt=testTxt; // don't set the value unless you have to, it will move the cursor! 33 | } 34 | } 35 | 36 | 37 | @action onChange(evt){ 38 | //if (this.refuseUpdates) return; 39 | var curText = evt.target.value; 40 | 41 | if (this.txt !== curText) { 42 | this.refuseUpdates=true; 43 | this.props.GridStore.convertTXTtoJSON(curText); 44 | this.txt = curText; 45 | this.refuseUpdates = false; 46 | } 47 | } 48 | 49 | render(){ 50 | var ui = this.props.uiMath; 51 | 52 | return( 53 |
54 | {/* put the header in place */} 55 | 56 | 57 | {/* render the text area */} 58 | 69 | 70 | {/* put the tools in place */} 71 | 72 |
73 | ); 74 | } 75 | } 76 | 77 | export default GridTextBox; 78 | -------------------------------------------------------------------------------- /src/GridTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observable, action } from 'mobx'; 4 | import { observer } from 'mobx-react'; 5 | import autoBind from 'react-autobind'; 6 | 7 | import PlaylistRemoveIcon from 'mdi-react/PlaylistRemoveIcon'; 8 | import PlaylistPlusIcon from 'mdi-react/PlaylistPlusIcon'; 9 | 10 | import PackageUpIcon from 'mdi-react/PackageUpIcon'; 11 | import PackageDownIcon from 'mdi-react/PackageDownIcon'; 12 | 13 | import FirstIcon from 'mdi-react/ArrowCollapseLeftIcon'; 14 | import LeftIcon from 'mdi-react/ArrowLeftIcon'; 15 | import RightIcon from 'mdi-react/ArrowRightIcon'; 16 | import LastIcon from 'mdi-react/ArrowCollapseRightIcon'; 17 | 18 | import ReactTooltip from 'react-tooltip'; 19 | 20 | 21 | 22 | @observer class GridTools extends React.Component { 23 | constructor(props) { super(props); autoBind(this); } 24 | 25 | 26 | @observable curPage=1; 27 | 28 | @action addRow(){ 29 | this.props.GridStore.onRowAdd(this.props.GridStore.cursor.x, this.props.GridStore.cursor.y, 30 | this.props.GridStore.keyList[this.props.GridStore.cursor.x]); 31 | } 32 | @action cutRow() { 33 | this.props.GridStore.onRowCut(this.props.GridStore.cursor.x, this.props.GridStore.cursor.y, 34 | this.props.GridStore.keyList[this.props.GridStore.cursor.x]); 35 | } 36 | 37 | @action onImport(){this.props.GridStore.onImport()} 38 | @action onExport(){this.props.GridStore.onExport()} 39 | 40 | @action onPageFirst(){this.props.onGotoPage(0);} 41 | @action onPagePrev(){ this.curPage--; if(this.curPage<1){this.curPage=1;}; this.props.onGotoPage(this.curPage); } 42 | @action onPageNext(){ var maxPage = (this.props.pageCount||1); this.curPage++; if(this.curPage>=maxPage){this.curPage=maxPage;}; this.props.onGotoPage(this.curPage); } 43 | @action onPageLast(){ var maxPage = (this.props.pageCount||1); this.curPage=maxPage; this.props.onGotoPage(this.curPage);} 44 | 45 | @action onChange(evt){ 46 | var tst = evt.target.value; 47 | if(!evt.target.value){this.curPage='';return;} 48 | this.curPage = (Number(tst)||1); 49 | if(this.curPage<0) this.curPage=1; 50 | if(this.curPage>(this.props.pageCount||1)){this.curPage=(this.props.pageCount||1)}; 51 | this.props.onGotoPage(this.curPage) 52 | } 53 | 54 | render(){ 55 | var ui = this.props.uiMath; 56 | if(this.props.editDisabled){ 57 | return ''; 58 | } 59 | 60 | return( 61 | 113 | ); 114 | } 115 | } 116 | 117 | export default GridTools; -------------------------------------------------------------------------------- /src/animTest/AnimTest.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import autoBind from 'react-autobind'; 3 | 4 | import { observable } from 'mobx'; 5 | import { observer } from 'mobx-react'; 6 | 7 | 8 | @observer class AnimTest extends React.Component 9 | { 10 | constructor(props) { 11 | super(props); 12 | autoBind(this); 13 | } 14 | @observable x = 50; 15 | @observable y = 50; 16 | @observable rot = 0; 17 | 18 | click(evt){ 19 | this.x=evt.clientX-10-20; 20 | this.y = evt.clientY-10-20; 21 | } 22 | 23 | move(evt) { 24 | var cx = this.x+30; 25 | var cy = this.y+30; 26 | var ex = evt.clientX; 27 | var ey = evt.clientY; 28 | var n = ey-cy; 29 | var d = ex-cx; 30 | this.rot = (Math.atan(n/(d||0.0001)) * 180/Math.PI); 31 | if(ex>cx){ this.rot+=180; } 32 | this.rot-=90; 33 | } 34 | 35 | render() { 36 | 37 | return ( 38 |
46 | Click to move the box. 47 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | 63 | export default AnimTest; 64 | 65 | -------------------------------------------------------------------------------- /src/docTool/ColumnUI.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import autoBind from 'react-autobind'; 3 | 4 | 5 | class ColumnUI extends React.Component { 6 | constructor(props) { super(props); autoBind(this); } 7 | 8 | 9 | render() { 10 | return ( 11 |
12 | Column 13 |
{this.props.label}
14 |   15 |
16 | ); 17 | } 18 | } 19 | 20 | 21 | export default ColumnUI; -------------------------------------------------------------------------------- /src/docTool/CompactObjView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import autoBind from 'react-autobind'; 3 | 4 | 5 | class CompactObjView extends React.Component { 6 | constructor(props) { super(props); autoBind(this); } 7 | 8 | formatThing(val){ 9 | if(val===true || val==='true' || val==='TRUE') return true; 10 | if(val===false || val==='false' || val==='FALSE') return false; 11 | return "'"+val+"'"; 12 | } 13 | 14 | render() { 15 | var items=[]; 16 | var keyList = Object.keys( this.props.target ); 17 | for(var ctr=0;ctr{keyList[ctr] + ":" + this.formatThing(this.props.target[keyList[ctr]])+", "}) 20 | } 21 | } 22 | 23 | return ( 24 |
    {{items}},
25 | ); 26 | } 27 | } 28 | 29 | 30 | export default CompactObjView; -------------------------------------------------------------------------------- /src/docTool/DataMaker.jsx: -------------------------------------------------------------------------------- 1 | import autoBind from 'react-autobind'; 2 | 3 | class DataMaker 4 | { 5 | 6 | 7 | constructor(updateDataFunction) { 8 | autoBind(this); 9 | this.updateDataText = updateDataFunction; //must be specificed by the owning object before these data makers are useful. 10 | } 11 | 12 | makeS() { 13 | var res = "["; // faster to generate strings 14 | for(var ctr=0;ctr<5;ctr++){res+='{r:'+(5-ctr)+',a:5,b:6,c:8,d:90},';} 15 | res = res.substring(0, res.length-1);res+=']'; 16 | this.updateDataText(res); 17 | } 18 | makeM() { 19 | var res = "["; // faster to generate strings 20 | for(var ctr=0;ctr<150;ctr++){res+='{r:'+(150-ctr)+',a:5,b:6,c:8,d:90},';} 21 | res = res.substring(0, res.length-1);res+=']'; 22 | this.updateDataText(res); 23 | } 24 | makeL() { 25 | var res = "["; // faster to generate strings 26 | for(var ctr=0;ctr<50000;ctr++){res+='{"r":'+(50000-ctr)+',"a":5,"b":6,"c":8,"d":90},';} 27 | res = res.substring(0, res.length-1);res+=']'; 28 | this.updateDataText(res); 29 | } 30 | makeSA() { 31 | var res = "["; // faster to generate strings 32 | for(var ctr=0;ctr<5;ctr++){res+='[1'+ctr+','+(2*ctr)+',3,4,5],';} 33 | res = res.substring(0, res.length-1);res+=']'; 34 | this.updateDataText(res); 35 | } 36 | makeMA() { 37 | var res = "["; // faster to generate strings 38 | for(var ctr=0;ctr<150;ctr++){res+='[1'+ctr+','+(2*ctr)+',3,4,5],';} 39 | res = res.substring(0, res.length-1);res+=']'; 40 | this.updateDataText(res); 41 | } 42 | makeLA() { 43 | var res = "["; // faster to generate strings 44 | for(var ctr=0;ctr<50000;ctr++){res+='[1'+ctr+','+(2*ctr)+',3,4,5],';} 45 | res = res.substring(0, res.length-1);res+=']'; 46 | this.updateDataText(res); 47 | } 48 | makeAInt() { 49 | var res = "["; // faster to generate strings 50 | for(var ctr=0;ctr<50;ctr++){res+=ctr+','} 51 | res = res.substring(0, res.length-1);res+=']'; 52 | this.updateDataText(res); 53 | } 54 | makeAWords() { 55 | var res = "["; // faster to generate strings 56 | for(var ctr=0;ctr<25;ctr++){res+='"arnold","bernard","clementine","dolores",'} 57 | res = res.substring(0, res.length-1);res+=']'; 58 | this.updateDataText(res); 59 | } 60 | makeKVP() { 61 | var res = "["; // faster to generate strings 62 | for(var ctr=0;ctr<50;ctr++){res+='{a:"test '+ctr+'"},'} 63 | res = res.substring(0, res.length-1);res+=']'; 64 | this.updateDataText(res); 65 | } 66 | makeCSV() { 67 | var res = "Col A,Col B,Col C,Col D,Col E\n" ; // faster to generate strings 68 | for(var ctr=0;ctr<50;ctr++){res+='a '+ctr+',b '+(ctr*3)+',c,d,e\n'} 69 | this.updateDataText(res); 70 | } 71 | makePSV() { 72 | var res = "Col A|Col B|Col C|Col D|Col E\n" ; // faster to generate strings 73 | for(var ctr=0;ctr<50;ctr++){res+='a '+ctr+'|b '+(ctr*3)+'|c|d|e\n'} 74 | this.updateDataText(res); 75 | } 76 | makeKVE() { 77 | var res = '' ; // faster to generate strings 78 | for(var ctr=0;ctr<50;ctr++){res+=''+ctr.toString(16)+'='+ctr+'\n'} 79 | this.updateDataText(res); 80 | } 81 | 82 | 83 | 84 | 85 | 86 | } 87 | 88 | export default DataMaker; -------------------------------------------------------------------------------- /src/docTool/DocStore.jsx: -------------------------------------------------------------------------------- 1 | import { observable, computed, action } from 'mobx'; 2 | import autoBind from 'react-autobind'; 3 | 4 | 5 | class DocStore { // Just a class. Nothing fancy here. 6 | constructor() { 7 | autoBind(this); 8 | this.addColDefRow('a'); 9 | this.addColDefRow('b'); 10 | this.addColDefRow('c'); 11 | this.addColDefRow('d'); 12 | } 13 | 14 | @action addColDefRow(keyName) 15 | { 16 | this.colDef.push( 17 | observable.object({ 18 | key: (keyName||'key'), title: (keyName||'key')+' Col', 19 | editDisabled: '', 20 | widePct: '', widePx: '', 21 | easyBool: '', easyInt: '', easyFloat: '', easyDollar: '', easyEuro: '', easyPound: '', 22 | easyDate: '',easyDateTime: '', easyMenu: '', 23 | altText: '', 24 | styleCell: '', styleData:'', styleHeaderCell: '', styleHeaderData: '', 25 | classCell: '', classData:'', classHeaderCell: '', classHeaderData: '', 26 | compHeader: '', compInput: '', compCell: '' 27 | }) ); 28 | } 29 | 30 | 31 | @observable showOutline = false; 32 | @action toggleOutline() { this.showOutline = !this.showOutline; } 33 | 34 | @observable hideEditor = false; 35 | @action toggleEditor() { this.hideEditor = !this.hideEditor; } 36 | 37 | // determine which onChangeHandler help and variables to use 38 | @observable onChangeHandlerType = 'normal'; // fast,primitive, rowReplace 39 | 40 | @observable showToolsAddCut = false; 41 | @observable showToolsPage = false; 42 | @observable showToolsImpExp = false; 43 | @observable showToolsCustom = false; 44 | @observable toolsButtonClass = ''; 45 | @observable pageCount = 0; 46 | @action toggleToolsAddCut() { this.showToolsAddCut = !this.showToolsAddCut; } 47 | @action toggleToolsPage() { this.showToolsPage = !this.showToolsPage; } 48 | @action toggleToolsImpExp() { this.showToolsImpExp = !this.showToolsImpExp; } 49 | @action toggleToolsCustom() { this.showToolsCustom = !this.showToolsCustom; } 50 | @action setToolsButtonClass(evt) { this.toolsButtonClass = evt.target.value; } 51 | @action setPageCount(val) { this.pageCount = val; if(this.pageCount<0){this.pageCount=0;} } 52 | 53 | 54 | @observable editDisabled = false; 55 | @action toggleEditDisabled() { this.editDisabled = !this.editDisabled; } 56 | 57 | @observable editAsText = false; 58 | @action toggleEditAsText() { this.editAsText = !this.editAsText; } 59 | 60 | @observable propBorderWide = -1; 61 | @action setBorderWidth(val) { this.propBorderWide = val; } 62 | 63 | @observable propPadWide = -1; 64 | @action setPadWidth(val) { this.propPadWide = val; } 65 | 66 | @observable propGridWide = -1; 67 | @action setGridWide(val) { this.propGridWide = val; } 68 | 69 | @observable propGridHigh = -1; 70 | @action setGridHigh(val) { this.propGridHigh = val; } 71 | 72 | @observable gridHighCollapse = false; 73 | @action toggleGridHighCollapse(val) { this.gridHighCollapse = !this.gridHighCollapse; } 74 | 75 | @observable propRowHigh = -1; 76 | @action setRowHigh(val) { this.propRowHigh = val; } 77 | 78 | @observable propRowHeaderHigh = -1; 79 | @action setRowHeaderHigh(val) { this.propRowHeaderHigh = val; } 80 | 81 | @observable propMinColWide = -1; 82 | @action setMinColWide(val) { this.propMinColWide = val; } 83 | 84 | @observable colHeaderHide = false; 85 | @action toggleColHeaderHide() { this.colHeaderHide = !this.colHeaderHide; } 86 | 87 | @observable pivotOn = false; 88 | @action togglePivotOn() { this.pivotOn = !this.pivotOn; } 89 | 90 | @observable pivotRowHeaderWide = -1; 91 | @action setPivotRowHeaderWide(val) { this.pivotRowHeaderWide = val; } 92 | 93 | @observable columnList = false; 94 | @action toggleColumnList() { this.columnList = !this.columnList; } 95 | 96 | @observable showSizeStuff = false; 97 | @observable showFormatStuff = false; 98 | @observable showStyleStuff = false; 99 | @observable showClassStuff = false; 100 | @observable showPivotStuff = false; 101 | @observable showEditStuff = false; 102 | @observable showDebugStuff = false; 103 | @observable showKeyboardStuff = false; 104 | @observable showCompStuff = false; 105 | @action toggleShowStyleStuff() { this.showStyleStuff = !this.showStyleStuff; } 106 | @action toggleShowSizeStuff() { this.showSizeStuff = !this.showSizeStuff; } 107 | @action toggleShowFormatStuff() { this.showFormatStuff = !this.showFormatStuff; } 108 | @action toggleShowClassStuff() { this.showClassStuff = !this.showClassStuff; } 109 | @action toggleShowPivotStuff() { this.showPivotStuff = !this.showPivotStuff; } 110 | @action toggleShowEditStuff() { this.showEditStuff = !this.showEditStuff; } 111 | @action toggleShowDebugStuff() { this.showDebugStuff = !this.showDebugStuff; } 112 | @action toggleShowKeyboardStuff() { this.showKeyboardStuff = !this.showKeyboardStuff ; } 113 | @action toggleShowCompStuff() { this.showCompStuff = !this.showCompStuff; } 114 | 115 | @observable debugGridMath = false; 116 | @action toggleDebugGridMath() { this.debugGridMath = !this.debugGridMath; } 117 | 118 | 119 | 120 | 121 | @observable classCell = ''; 122 | @observable classData = ''; 123 | @observable classHeaderCell = ''; 124 | @observable classHeaderData = ''; 125 | @observable classRowHeaderCell = ''; 126 | @observable classRowHeaderData = ''; 127 | @observable classCellOddRow = ''; 128 | @observable classDataOddRow = ''; 129 | @observable classInput = ''; 130 | @observable classSelected = ''; 131 | 132 | @action setClassCell(evt) { this.classCell = evt.target.value; } 133 | @action setClassData(evt) { this.classData = evt.target.value; } 134 | @action setClassHeaderCell(evt) { this.classHeaderCell = evt.target.value; } 135 | @action setClassHeaderData(evt) { this.classHeaderData = evt.target.value; } 136 | @action setClassRowHeaderCell(evt) { this.classRowHeaderCell = evt.target.value; } 137 | @action setClassRowHeaderData(evt) { this.classRowHeaderData = evt.target.value; } 138 | @action setClassCellOddRow(evt) { this.classCellOddRow = evt.target.value; } 139 | @action setClassDataOddRow(evt) { this.classDataOddRow = evt.target.value; } 140 | @action setClassInput(evt) { this.classInput = evt.target.value; } 141 | @action setClassSelected(evt) { this.classSelected = evt.target.value; } 142 | 143 | @observable formatDate = ''; 144 | @observable formatTime = ''; 145 | @action setFormatDate(evt) { this.formatDate = evt.target.value; } 146 | @action setFormatTime(evt) { this.formatTime = evt.target.value; } 147 | 148 | @observable colDef = []; 149 | 150 | @action setColDefValue(x, y, objKey, newValue) { 151 | this.colDef[y][objKey] = newValue; 152 | } 153 | 154 | @computed get outlineCSS() { 155 | if (this.showOutline) { return '2px green dashed';} 156 | else{return '';} 157 | } 158 | 159 | 160 | @observable styleCell = ''; 161 | @observable styleCellOddRow = ''; 162 | @observable styleHeaderCell = ''; 163 | @observable styleRowHeaderCell = ''; 164 | 165 | @observable styleData = ''; 166 | @observable styleDataOddRow = ''; 167 | @observable styleHeaderData = ''; 168 | @observable styleRowHeaderData = ''; 169 | 170 | @observable styleInput = ''; 171 | @observable styleSelected = ''; 172 | 173 | @action setStyleCell(evt) { this.styleCell = evt.target.value; } 174 | @action setStyleCellOddRow(evt) { this.styleCellOddRow = evt.target.value; } 175 | @action setStyleHeaderCell(evt) { this.styleHeaderCell = evt.target.value; } 176 | @action setStyleRowHeaderCell(evt) { this.styleRowHeaderCell = evt.target.value; } 177 | 178 | @action setStyleData(evt) { this.styleData = evt.target.value; } 179 | @action setStyleDataOddRow(evt) { this.styleDataOddRow = evt.target.value; } 180 | @action setStyleHeaderData(evt) { this.styleHeaderData = evt.target.value; } 181 | @action setStyleRowHeaderData(evt) { this.styleRowHeaderData = evt.target.value; } 182 | 183 | @action setStyleInput(evt) { this.styleInput = evt.target.value; } 184 | @action setStyleSelected(evt) { this.styleSelected = evt.target.value; } 185 | 186 | 187 | makeJSON(value,error){ 188 | var res={}; 189 | if(value){ 190 | try { res = JSON.parse(this.rrjs.stringToJson(value));} 191 | catch(e) { res={backgroundColor:'red',err:error}; console.log('ERROR',e,error);} 192 | } 193 | return res; 194 | } 195 | 196 | @computed get jsonStyleCell(){ return this.makeJSON(this.styleCell,'invalid Cell JSX Style');} 197 | @computed get jsonStyleCellOddRow(){ return this.makeJSON(this.styleCellOddRow,'invalid Odd Row Cell JSX Style');} 198 | @computed get jsonStyleHeaderCell(){ return this.makeJSON(this.styleHeaderCell,'Invalid JSX Header Cell Style');} 199 | @computed get jsonStyleRowHeaderCell(){ return this.makeJSON(this.styleRowHeaderCell,'Invalid JSX Header Cell Style');} 200 | 201 | @computed get jsonStyleData(){ return this.makeJSON(this.styleData,'invalid Data JSX Style');} 202 | @computed get jsonStyleDataOddRow(){ return this.makeJSON(this.styleDataOddRow,'invalid JSX Odd Row Data Style');} 203 | @computed get jsonStyleHeaderData(){ return this.makeJSON(this.styleHeaderData,'Invalid JSX Header Data Style');} 204 | @computed get jsonStyleRowHeaderData(){ return this.makeJSON(this.styleRowHeaderData,'Invalid JSX Header Data Style');} 205 | 206 | @computed get jsonStyleInput(){ return this.makeJSON(this.styleInput,'Invalid JSX Input Cell Style');} 207 | @computed get jsonStyleSelected(){ return this.makeJSON(this.styleSelected,'Invalid JSX Selected Cell Style'); } 208 | 209 | 210 | } 211 | 212 | export default DocStore; -------------------------------------------------------------------------------- /src/docTool/DocUI.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { toJS,observable,action,computed } from 'mobx'; 3 | import { observer } from 'mobx-react'; 4 | import autoBind from 'react-autobind'; 5 | import Grid from '../Grid'; 6 | import rrjsTool from '../rrj-compile/index.js'; 7 | 8 | import '../../static/TestCSS.css'; 9 | import Toggle from './Toggle'; 10 | import ToggleFolder from './ToggleFolder'; 11 | import NumWheel from './NumWheel'; 12 | import TextParam from './TextParam'; 13 | import CompactObjView from './CompactObjView'; 14 | import DataMaker from './DataMaker'; 15 | 16 | import DocStore from './DocStore'; 17 | 18 | 19 | @observer class DocUI extends React.Component { 20 | 21 | constructor(props) { 22 | super(props); autoBind(this); 23 | this.rrjs = rrjsTool.createParser(); 24 | this.printer = new rrjsTool.PrettyPrinter( rrjsTool.PrettyPrinter.Options.Companion.JsonPretty); 25 | this.dm = new DataMaker(this.updateDataText); 26 | this.ds = new DocStore(); 27 | this.ds.rrjs = this.rrjs; // for convenience in the computed values. 28 | 29 | this.dataAsObject(); 30 | } 31 | 32 | @observable data = `[ 33 | { "a": 1, "b": 20, "c": 3, "d": 4.5 }, 34 | { "a": 21, "b": 30, "c": 33, "d": "asdf" }, 35 | { "a": 31, "b": 40, "c": 333, "d": "zing" }, 36 | { "a": 41, "b": 30, "c": 33, "d": "asdf" }, 37 | { "a": 51, "b": 40, "c": 333, "d": "zing" }, 38 | { "a": 61, "b": 30, "c": 33, "d": "asdf" }, 39 | { "a": 71, "b": 40, "c": 333, "d": "zing" }, 40 | { "a": 81, "b": 30, "c": 33, "d": "asdf" }, 41 | { "a": 91, "b": 40, "c": 333, "d": "zing" }, 42 | { "a": 101, "b": 30, "c": 33, "d": "asdf" }, 43 | { "a": 111, "b": 40, "c": 333, "d": "zing" }, 44 | { "a": 121, "b": 30, "c": 33, "d": "asdf" }, 45 | { "a": 131, "b": 40, "c": 333, "d": "zing" }, 46 | { "a": 131, "b": 30, "c": 33, "d": "asdf" }, 47 | { "a": 141, "b": 40, "c": 333, "d": "zing" }, 48 | { "a": 151, "b": 30, "c": 33, "d": "asdf" }, 49 | { "a": 161, "b": 40, "c": 333, "d": "zing" }, 50 | { "a": 171, "b": 30, "c": 33, "d": "asdf" }, 51 | { "a": 181, "b": 40, "c": 333, "d": "zing" }, 52 | { "a": 191, "b": 50, "c": 3333, "d": "zong" } 53 | ] 54 | `; // the data AS A STRING for the test editor 55 | @observable cleanData = {}; // the data AS JSON for the fast editor 56 | @observable dataErr = ''; 57 | 58 | @action updateData(evt) { 59 | this.data = evt.target.value; 60 | this.dataAsObject(); 61 | } 62 | @action updateDataText(txt) { 63 | this.data = txt; 64 | this.dataAsObject(); 65 | } 66 | 67 | 68 | 69 | @action setValue(x,y,objKey,newValue) 70 | { 71 | // this is just for the test UI. By making the "source of truth" the text file, i keep things in sync 72 | // cost is that I lose update performance for this test UI. Try another test gui for perf testing. 73 | if(!this.ds.hideEditor){ 74 | this.cleanData = JSON.parse(this.rrjs.stringToJson(this.data)); 75 | this.cleanData[y][objKey]=newValue; 76 | this.data = JSON.stringify(this.cleanData); 77 | } 78 | else{ 79 | this.cleanData[y][objKey]=newValue; 80 | } 81 | } 82 | 83 | cleanClone(){ 84 | var target={}; 85 | Object.assign(target,this.cleanData[0]); 86 | var keyList = Object.keys(this.cleanData[0]); 87 | for (var kc = 0; kc < keyList.length; kc++) { 88 | target[keyList[kc]]=''; 89 | } 90 | return target; 91 | } 92 | 93 | @action onRowAdd(x, y, objKey) { 94 | if(!this.ds.hideEditor){ 95 | this.cleanData = JSON.parse(this.rrjs.stringToJson(this.data)); 96 | this.cleanData.splice(y + 1, 0, this.cleanClone()); 97 | this.data = JSON.stringify(this.cleanData); 98 | } 99 | else{ 100 | this.cleanData.splice(y + 1, 0, this.cleanClone()); 101 | } 102 | } 103 | 104 | @action onRowCut(x, y, objKey) { 105 | if(!this.ds.hideEditor){ 106 | this.cleanData = JSON.parse(this.rrjs.stringToJson(this.data)); 107 | this.cleanData.splice(y, 1); 108 | this.data = JSON.stringify(this.cleanData); 109 | } 110 | else{ 111 | this.cleanData.splice(y, 1); 112 | } 113 | } 114 | 115 | @action onReplaceData(newData) { 116 | if(!this.ds.hideEditor){ 117 | this.cleanData = newData; 118 | this.data = JSON.stringify(this.cleanData); 119 | } 120 | else{ 121 | this.cleanData = newData; 122 | } 123 | } 124 | 125 | 126 | @action onColAdd(x, y, objKey) { }// not implemented yet. 127 | @action onColCut(x, y, objKey) { }// not implemented yet. 128 | @action onDataClear(x, y, objKey) { }// not implemented yet. 129 | @action onGotoPage(page) { window.alert('please load page '+page); } 130 | @action onExport() { window.alert('please export'); } 131 | @action onImport() { window.alert('please import'); } 132 | 133 | @action addColDefRow() { this.ds.addColDefRow(); } 134 | 135 | @action cutColDefRow(x, y, objKey) { 136 | this.ds.colDef.splice(x, 1); 137 | } 138 | 139 | @observable curParamHelp = ""; 140 | 141 | dataAsObject(){ 142 | this.cleanData=[]; 143 | this.dataErr=""; 144 | try { 145 | this.cleanData = JSON.parse(this.rrjs.stringToJson(this.data)); 146 | if (this.cleanData && typeof this.cleanData === "object") { this.dataErr="";} 147 | else{this.cleanData=[];this.dataErr="Invalid JSON."} 148 | } 149 | catch (e) { 150 | try{ 151 | this.cleanData = JSON.parse(this.data); 152 | } 153 | catch(e2){ 154 | this.dataErr="Invalid JSON. Keys and values need to be in quotes."; console.log(e); 155 | } 156 | } 157 | }; 158 | 159 | goMakeL(){ 160 | this.dataErr="Building 50K objects... Disabling text data editor for performance."; 161 | this.ds.hideEditor=true; 162 | this.ds.showDebugStuff=true; 163 | var saneThis = this; 164 | setTimeout(function(){ saneThis.dm.makeL(); saneThis.dataErr=''; }, 100); 165 | } 166 | goMakeLA(){ 167 | this.dataErr="Building 50K arrays... Disabling text data editor for performance."; 168 | this.ds.hideEditor=true; 169 | this.ds.showDebugStuff=true; 170 | var saneThis = this; 171 | setTimeout(function(){ saneThis.dm.makeLA(); saneThis.dataErr=''; }, 100); 172 | } 173 | 174 | 175 | 176 | render() { 177 | 178 | var colListAsText=[]; 179 | for(var cctr=0;cctr); 181 | } 182 | 183 | 184 | toJS(this.ds.jsonStyleCell); 185 | toJS(this.ds.jsonStyleCellOddRow); 186 | toJS(this.ds.jsonStyleHeaderCell); 187 | toJS(this.ds.jsonStyleRowHeaderCell); 188 | toJS(this.ds.jsonStyleData); 189 | toJS(this.ds.jsonStyleDataOddRow); 190 | toJS(this.ds.jsonStyleHeaderData); 191 | toJS(this.ds.jsonStyleRowHeaderData); 192 | toJS(this.ds.jsonStyleInput); 193 | toJS(this.ds.jsonStyleSelected); 194 | 195 | 196 | return ( 197 |
198 |
200 |
201 |
202 | 203 |
Easy to use, high performance, JSON Grid Editor.
204 | 205 |

Mouseover each tag attribute to see it's purpose.
206 | Click the attribute to show a description.
207 | Shift-Click text boxes to paste in a sample value.
208 | Please report issues at the github page linked above.

Thanks for trying it out!

209 |
210 | Example Data Generators
211 | 212 | 213 | 214 |
215 | 216 | 217 | 218 |
219 | 220 | 221 | 222 |
223 | 224 | 225 |
226 | {this.dataErr}
227 | {this.data}   Use this to see that the grid and the data are in sync.
228 | {!this.ds.hideEditor && 229 |