├── .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 |
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 |
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 |
230 | }
231 |
232 |
233 | Parameter UI
234 |
235 |
236 | { this.ds.showSizeStuff &&
237 |
238 |
239 |
240 | Forced width of the grid. Not set by CSS because the number is needed for javascript calculations.
} />
241 | Forced height of the grid. Not set by CSS because the number is needed for javascript calculations.
378 |
379 |
380 |
381 | ( The Example Data text editor has much worse performance than react-json-grid. Use "Hide text editor" to disable it while doing performance tests. )
382 |