diff --git a/.gitignore b/.gitignore index 826f87a..2592bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ node_modules +bower_components docs/build -standalone/react-grid.min.js -standalone/react-grid.js +**/build +dist +build +examples/build diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..1df151f --- /dev/null +++ b/.jshintrc @@ -0,0 +1,22 @@ +{ + "node": true, + + "boss": true, + "curly": true, + "devel": true, + "eqnull": true, + "expr": true, + "funcscope": true, + "globalstrict": true, + "laxcomma": true, + "laxbreak": true, + "loopfunc": true, + "newcap": false, + "noempty": true, + "nonstandard": true, + "onecase": true, + "regexdash": true, + "trailing": true, + "undef": true, + "unused": "vars" +} diff --git a/README.md b/README.md index cf267b5..3a59827 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,38 @@ # React Grid -Data grid for [React][]. +Data grid for [React](http://facebook.github.io/react) + + +## Getting started +0. you'll need node, and an editor. Till visual studio does JSX, we think Atom.io is the best, so if you dont already, grab them: + + choco install atom + choco install nodejs.install *if typing npm gives you an error* + git clone https://github.com/prometheusresearch/react-grid *in the root directory you want your files in* + cd .\react-grid + +1. You'll also need Gulp which will perform build tasks such as jsx compilation (specified in gulpfile.js) - Go get it: + + npm install -g gulp + npm install --save-dev gulp + +2. Install Project Dependencies from package.json file + + npm install + +3. Run gulp. It compiles your jsx, jshint, packs your scripts up, and fires up a local webserver and opens the start page + + gulp + +Have a look in the gulpfile for other commands or add your own ## Credits -React Grid is free software created by [Prometheus Research][] and is released -under the MIT. +React Grid is from [Prometheus Research](http://prometheusresearch.github.io/react-grid) and there are some [good examples](http://prometheusresearch.github.io/react-grid/examples/locked-columns.html) +Contributions from [adazzle](https://www.adazzle.com) +It is released under the [MIT](LICENCE). + +For more details, see the [React docs](http://facebook.github.io/react/), especially [thinking in react](http://facebook.github.io/react/docs/thinking-in-react.html) -[React]: http://facebook.github.io/react/ -[Prometheus Research, LLC]: http://prometheusresearch.com +## Work in progress +This is still a work in progress but feel free to comment, add [an issue](https://github.com/prometheusresearch/react-grid/issues) or submit a [pull request](https://github.com/prometheusresearch/react-grid/pulls) diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..9ee137a --- /dev/null +++ b/bower.json @@ -0,0 +1,27 @@ +{ + "name": "react-grid", + "version": "0.0.0", + "homepage": "https://github.com/prometheusresearch/react-grid", + "authors": [ + "Prometheus Research https://github.com/prometheusresearch", + "Adazzle https://github.com/adazzle" + ], + "moduleType": [], + "license": "MIT", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "directory": "ext", + "dependencies": { + "jasmine": "~2.0.1", + "underscore": "~1.6.0", + "requirejs": "~2.1.14", + "jquery": "~2.1.1" + }, + "devDependencies": {} +} diff --git a/docs/contents/styles/sortable.svg b/docs/contents/styles/sortable.svg index a540079..0bf1eb5 100644 --- a/docs/contents/styles/sortable.svg +++ b/docs/contents/styles/sortable.svg @@ -1,10 +1,10 @@ - - - - - - - - + + + + + + + + diff --git a/examples/example1.html b/examples/example1.html new file mode 100644 index 0000000..9eeeae8 --- /dev/null +++ b/examples/example1.html @@ -0,0 +1,11 @@ + + +react grid + +
+ + + + + + diff --git a/examples/excelGrid.html b/examples/excelGrid.html new file mode 100644 index 0000000..de71ac1 --- /dev/null +++ b/examples/excelGrid.html @@ -0,0 +1,17 @@ + + + + + +

Grid with keyboard navigation

+
+ + + + + diff --git a/examples/jsx/example1.jsx b/examples/jsx/example1.jsx new file mode 100644 index 0000000..1254892 --- /dev/null +++ b/examples/jsx/example1.jsx @@ -0,0 +1,49 @@ +/** + * @jsx React.DOM + */ +(function() { + +var React = require('react'); +var ReactGrid = require('../../lib/Grid.js'); + +'use strict'; + +function rows(start, end) { + var rows = []; + for (var i = start; i < end; i++) { + rows.push([i, 'Name ' + i, 'Surname ' + i]); + } + return rows; +} + +var columns = [ + { + name: '№', + width: '10%', + key: 0, + locked:true + }, + { + name: 'Name', + width: '40%', + resizeable: true, + key: 1 + }, + { + name: 'Surname', + width: '50%', + resizeable: true, + key: 2 + } +]; + + +React.renderComponent( + , + document.getElementById('sandbox')); + +})(); diff --git a/examples/jsx/excelGrid.jsx b/examples/jsx/excelGrid.jsx new file mode 100644 index 0000000..cc98b94 --- /dev/null +++ b/examples/jsx/excelGrid.jsx @@ -0,0 +1,69 @@ +/** + * @jsx React.DOM + */ +'use strict'; +var ExcelGrid = require('../../lib/ExcelGrid'); +var React = require('React'); + +var data = []; +for (var i = 0; i < 2000; i++) { + data.push({ + 'key': i, + 'supplier':{'value':'Supplier ' + i, 'editing':true}, + 'format': 'fmt ' + i, + 'start':'start', + 'end':'end', + 'price':i }); +}; + + +function rows(start, end) { + return data.slice(start, end); +} + +var columns = [ + + { + idx: 0, + name: 'Supplier', + key: 'supplier', + width: 300, + locked: true, + }, + { + idx: 1, + name: 'Format', + key: 'format', + width: 350, + }, + { + idx: 2, + name: 'Start', + key: 'start', + width: 250, + }, + { + idx: 3, + name: 'End', + key: 'end', + width: 250, + }, + { + idx: 4, + name: 'Cost', + key: 'cost', + width: 200, + } +]; + + +var renderGrid = function(containerId) { + containerId = containerId || 'sandbox'; + var grid = ExcelGrid({columns:columns, rows: rows, length: data.length, height: 400}); + React.renderComponent(grid, + document.getElementById(containerId)); +}; +renderGrid(); + +//force a global react object, for chrome dev tools if nothing else +window.React = window.React || React; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..87cbeb8 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,163 @@ +var gulp = require('gulp'); +var debug = require('gulp-debug'); +var clean = require('gulp-clean'); + + +gulp.task('clean', function() { + return gulp + .src(['build/*','dist/*'], {read: false}) + .pipe(clean({force: true})); +}); + +var plumber = require('gulp-plumber'); +var react = require('gulp-react'); +var uglify = require('gulp-uglify'); +var rename = require('gulp-rename'); + +gulp.task('js-build',['clean'], function() { + + return gulp.src(['lib/*.js']) + .pipe(plumber()) + // Turn React JSX syntax into regular js-build + .pipe(react({harmony:false})) + // Output each file into the ./build/js-build/ directory + .pipe(gulp.dest('build/js/')); +}); + + + +var jshint = require('gulp-jshint'); +function lint(src) { + return gulp.src(src) + .pipe(jshint()) + .pipe(jshint.reporter('default')); +} +gulp.task('js-lint', ['js-build'],function() { + return lint(['build/**/*.js']); +}); + +var browserify = require('gulp-browserify'); + +gulp.task('js-combine', ['js-build', 'js-lint'], function() { + + + return gulp.src('build/js/index.js') + .pipe(plumber()) + .pipe(browserify({transform: ['envify']})) + .pipe(rename('react-grid.js')) + .pipe(gulp.dest('dist/js/')) + .pipe(uglify()) + .pipe(rename({suffix: '.min'})) + .pipe(gulp.dest('dist/js/')); + + +}); + +gulp.task('examples',['js-build'], function() { + + //process examples + return gulp.src('examples/jsx/*.jsx') + .pipe(plumber()) + .pipe(browserify({transform: ['envify','reactify']})) + .pipe(gulp.dest('build/js/examples/')); + + +}); + + + +var less = require('gulp-less'); +var minifycss = require('gulp-minify-css'); + +// gulp.task('styles', function() { +// return gulp.src('themes/**/*.less') +// .pipe(less()) +// .pipe(gulp.dest('build/')) +// .pipe(minifycss()) +// .pipe(rename({suffix: '.min'})) +// .pipe(gulp.dest('build/')); +// }); + +gulp.task('watch', function() { + return gulp.watch(['lib/**/*.js','examples/**/*.jsx'], ['js-combine','examples']); + + // Watch for .less file changes and re-run the 'styles' task + //gulp.watch('frontend/**/*.less', ['styles']); + +}); + + +var connect = require('gulp-connect'); +gulp.task('connect', function() { + return connect.server({ + livereload : true + }); +}); + +var open = require('gulp-open'); +gulp.task("launch-example", ['connect', 'js-combine'], function(){ + var options = { + url: "http://localhost:8080/examples/example1.html", + app: "chrome" + }; + return gulp.src("examples/example1.html") + .pipe(open("", options)); +}); + +var launchPage = function(page) { + page = page || "index.html"; + var options = { + url: "http://localhost:8080/" + page, + app: "chrome" + }; + return gulp.src(page) + .pipe(open("", options)); + +}; +gulp.task('launch', ['connect', 'js-combine'], function(){ + return launchPage(); +}); + +gulp.task('tests-clean', function () { + return + gulp.src('build/test', {read: false}) + .pipe(clean({force: true})) +}); + +var jasmine = require('gulp-jasmine'); +var concat = require('gulp-concat'); +var addsrc = require('gulp-add-src'); +var flatten = require('gulp-flatten'); +gulp.task('tests-1', function () { + + + return gulp.src('test/**/*.js') + .pipe(plumber()) + .pipe(flatten()) + .pipe(addsrc(['lib/*.js'])) + .pipe(gulp.dest('build/test/temp')) + +}); + +gulp.task('tests-build',['tests-1'], function () { + return gulp.src(['build/test/temp/*.spec.js']) + .pipe(concat('specs.js')) + .pipe(browserify({transform: ['reactify']})) + .pipe(rename("specs-all.js")) + .pipe(gulp.dest('build/test')); +}); + +gulp.task('tests', ['tests-clean','tests-build'],function () { + return gulp.src('build/test/core/*.js').pipe(jasmine()); +}); + + +gulp.task('tests-run', ['tests','connect'],function () { + return launchPage("./test/testRunner.html") + .pipe(gulp.watch(['lib/**/*.js','examples/**/*.jsx','test/**/*.js'], ['tests'])); +;; +}); + +gulp.task('default',['js-combine','examples', 'launch', 'watch'], function() { + +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..43d10ae --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + +

React Grid

+

Examples are all located in the examples folder

+ +

To add a new example, just add your jsx file (under the jsx folder), the html file, a link to the built jsx (under build/js/examples/your-file.jsx) and run gulp

+ + diff --git a/lib/Canvas.js b/lib/Canvas.js index f5b3d9f..f4156cb 100644 --- a/lib/Canvas.js +++ b/lib/Canvas.js @@ -18,24 +18,27 @@ */ "use strict"; + +/* jshint esnext: true */ + var React = require('react/addons'); var shallowEqual = require('./utils').shallowEqual; -var cx = React.addons.classSet; var cloneWithProps = React.addons.cloneWithProps; -var ScrollShim = require('./ScrollShim'); var Row = require('./Row'); var Canvas = React.createClass({ - mixins: [ScrollShim], propTypes: { header: React.PropTypes.component, cellRenderer: React.PropTypes.component, rowRenderer: React.PropTypes.component, rowHeight: React.PropTypes.number.isRequired, + height: React.PropTypes.number.isRequired, displayStart: React.PropTypes.number.isRequired, displayEnd: React.PropTypes.number.isRequired, length: React.PropTypes.number.isRequired, + style: React.PropTypes.object.isRequired, + SelectedCells: React.PropTypes.array.isRequired, rows: React.PropTypes.oneOfType([ React.PropTypes.func.isRequired, React.PropTypes.array.isRequired @@ -54,9 +57,12 @@ var Canvas = React.createClass({ key: displayStart + idx, idx: displayStart + idx, row: row, + isSelected: this.props.SelectedCells && this.props.SelectedCells[(displayStart + idx)] && this.props.SelectedCells[(displayStart + idx)].length, + SelectedCells: this.props.SelectedCells ? this.props.SelectedCells[(displayStart + idx)] : [], height: rowHeight, columns: this.props.columns, - cellRenderer: this.props.cellRenderer + cellRenderer: this.props.cellRenderer, + onRowClick: this.props.onRowClick })); if (displayStart > 0) { @@ -70,7 +76,7 @@ var Canvas = React.createClass({ return this.transferPropsTo(
@@ -82,9 +88,9 @@ var Canvas = React.createClass({ renderRow: function(props) { if (React.isValidComponent(this.props.rowRenderer)) { - return cloneWithProps(this.props.rowRenderer, props); + return cloneWithProps(this.props.rowRenderer(props), props); } else { - return this.props.rowRenderer(props); + return Row(props); } }, @@ -97,10 +103,6 @@ var Canvas = React.createClass({ ); }, - getDefaultProps: function() { - return {rowRenderer: Row}; - }, - getInitialState: function() { return { shouldUpdate: true, @@ -110,8 +112,8 @@ var Canvas = React.createClass({ }, componentWillReceiveProps: function(nextProps) { - var shouldUpdate = !(nextProps.visibleStart > this.state.displayStart - && nextProps.visibleEnd < this.state.displayEnd) + var shouldUpdate = !(nextProps.visibleStart >= this.state.displayStart + && nextProps.visibleEnd <= this.state.displayEnd) || nextProps.length !== this.props.length || nextProps.rowHeight !== this.props.rowHeight || nextProps.columns !== this.props.columns @@ -134,12 +136,10 @@ var Canvas = React.createClass({ }, onScroll: function(e) { - this.appendScrollShim(); if (this.props.onScroll) { this.props.onScroll(e); } }, - setScrollTop: function(scrollTop) { this.getDOMNode().scrollTop = scrollTop; }, diff --git a/lib/Cell.js b/lib/Cell.js index c809641..1c54d5b 100644 --- a/lib/Cell.js +++ b/lib/Cell.js @@ -4,10 +4,8 @@ 'use strict'; var React = require('react/addons'); -var cx = React.addons.classSet; var Cell = React.createClass({ - render: function() { var style = { display: 'block', @@ -18,8 +16,14 @@ var Cell = React.createClass({ textOverflow: 'ellipsis', overflow: 'hidden' }; + var cx = React.addons.classSet; + var classes = cx({ + 'react-grid-Cell': true, + 'active-cell': this.props.isSelected, + 'selected-cell': this.props.isSelected + }); return ( -
+
{this.props.renderer({ value: this.props.value, column: this.props.column @@ -27,7 +31,9 @@ var Cell = React.createClass({
); }, - + onClick: function(ev) { + if(this.props.onClick) { this.props.onClick(ev, this.props); } + }, getDefaultProps: function() { return { renderer: simpleCellRenderer diff --git a/lib/ColumnMetrics.js b/lib/ColumnMetrics.js index bd6ecce..7ecfce1 100644 --- a/lib/ColumnMetrics.js +++ b/lib/ColumnMetrics.js @@ -3,9 +3,9 @@ */ "use strict"; +/* jshint esnext: true, unused: false, undef: false */ var React = require('react'); var shallowCloneObject = require('./shallowCloneObject'); -var DOMMetrics = require('./DOMMetrics'); /** * Update column metrics calculation. @@ -79,17 +79,14 @@ function resizeColumn(metrics, index, width) { } var Mixin = { - mixins: [DOMMetrics.MetricsMixin], propTypes: { columns: React.PropTypes.array, minColumnWidth: React.PropTypes.number }, - DOMMetrics: { - gridWidth: function() { - return this.getDOMNode().offsetWidth - 2; - } + gridWidth: function() { + return this.isMounted() ? this.getDOMNode().offsetWidth - 2 : 0; }, getDefaultProps: function() { @@ -107,7 +104,7 @@ var Mixin = { }, getColumnMetrics: function(props, initial) { - var totalWidth = initial ? null : this.DOMMetrics.gridWidth(); + var totalWidth = initial ? null : this.gridWidth(); return { regularColumns: calculate({ columns: props.columns.filter((c) => !c.locked), diff --git a/lib/DOMMetrics.js b/lib/DOMMetrics.js index cc933c1..ef66a9f 100644 --- a/lib/DOMMetrics.js +++ b/lib/DOMMetrics.js @@ -3,6 +3,9 @@ */ 'use strict'; + +/* jshint esnext: true, browser: true */ + var React = require('react'); var utils = require('./utils'); diff --git a/lib/DraggableMixin.js b/lib/DraggableMixin.js index ea62c9c..8680db7 100644 --- a/lib/DraggableMixin.js +++ b/lib/DraggableMixin.js @@ -1,5 +1,7 @@ 'use strict'; +/* jshint browser: true */ + var DraggableMixin = { componentWillMount: function() { diff --git a/lib/ExcelGrid.js b/lib/ExcelGrid.js new file mode 100644 index 0000000..e7805b3 --- /dev/null +++ b/lib/ExcelGrid.js @@ -0,0 +1,124 @@ +/** + * @jsx React.DOM + */ +'use strict'; +var Grid = require('./Grid'); +var React = require('React'); + +var ExcelGrid = React.createClass({ + propTypes: { + columns: React.PropTypes.oneOfType([ + React.PropTypes.array.isRequired, + React.PropTypes.func.isRequired + ]), + rows: React.PropTypes.oneOfType([ + React.PropTypes.array.isRequired, + React.PropTypes.func.isRequired + ]), + length: React.PropTypes.number.isRequired, + rowRenderer: React.PropTypes.component + }, + getInitialState: function() { + return { + SelectedCells:[], + ActiveCell: {row:0,col:0}, + rowHeight:40, + }; + }, + render: function() { + var grid = this.transferPropsTo(); + return ( +
+ {grid} +
+ ); + + }, + onRowClick: function(ev, row, cell) { + this.navigateTo({col:cell.column.idx, row:row.key, ev:ev}); + }, + handleGridDoubleClick: function(ev) { + + }, + + handleKeyUp: function(ev) { + var key = ev.key; + + if(key === 'ArrowUp') { + this.navigateTo({rowDelta:-1, ev:ev}); + } + else if(key === 'ArrowDown') { + this.navigateTo({rowDelta:1, ev:ev}); + } + else if(key === 'ArrowLeft') { + this.navigateTo({colDelta:-1, ev:ev}); + } + else if(key === 'ArrowRight') { + this.navigateTo({colDelta:1, ev:ev}); + } + }, + navigateTo: function(args) { + //TODO validate args + + //select the cell + var SelectedCells = this.state.SelectedCells; + if(!args.ev || !args.ev.shiftKey) { + //clear selection + SelectedCells=[]; + } + + var row = this.state.ActiveCell.row; + var col = this.state.ActiveCell.col; + if(args.rowDelta) { + row += args.rowDelta; + } + if(args.colDelta) { + col += args.colDelta; + } + if(isFinite(args.col)) { + col=args.col; + } + if(isFinite(args.row)) { + row=args.row; + } + SelectedCells[row]=SelectedCells[row] || []; + SelectedCells[row][col]=SelectedCells[row][col] ? false : true; //toggle if it was already selected + //need to adjust the viewport too + //this happens through the native events for keys + //but we will need to focus the element, unless we clicked it + //so we set focus in componentDidUpdate + //focus if its anythig other than a click, or we dont have an event + var focus = !args.ev || !args.ev.type || args.ev.type!=='click' + this.setState({ + SelectedCells:SelectedCells, + ActiveCell:{row:row,col:col}, + FocusCell:focus + }); + + }, + getActiveCell: function() { + var cells = this.refs.gridComponent.getDOMNode().getElementsByClassName('active-cell'); + return cells && cells.length ? cells[0] : null; + }, + componentDidUpdate: function() { + if(this.state.FocusCell) { + var active=this.getActiveCell(); + if(active) { active.focus(); } + } + } +}); + + +module.exports = ExcelGrid; diff --git a/lib/Grid.js b/lib/Grid.js index 854d27c..e7c7d24 100644 --- a/lib/Grid.js +++ b/lib/Grid.js @@ -21,65 +21,81 @@ var React = require('react'); var Header = require('./Header'); +var getWindowSize = require('./getWindowSize'); var Viewport = require('./Viewport'); var ColumnMetrics = require('./ColumnMetrics'); -var DOMMetrics = require('./DOMMetrics'); var Grid = React.createClass({ - mixins: [ColumnMetrics.Mixin, DOMMetrics.MetricsComputatorMixin], + mixins: [ColumnMetrics.Mixin], propTypes: { rows: React.PropTypes.oneOfType([ React.PropTypes.array.isRequired, React.PropTypes.func.isRequired ]), - rowRenderer: React.PropTypes.component + rowRenderer: React.PropTypes.component, + length: React.PropTypes.number.isRequired, + height: React.PropTypes.number, + width: React.PropTypes.number, + SelectedCells: React.PropTypes.array, + onRowClick: React.PropTypes.func }, - style: { - overflow: 'hidden', - position: 'relative', - outline: 0, - minHeight: 300 - }, + render: function() { + var styles = { + overflow: 'hidden', + position: 'relative', + outline: 0, + minHeight: 300 + }; return this.transferPropsTo( -
+
); }, + getGridWidth: function() { + var colsWidth = this.state.lockedColumns.width + + this.state.regularColumns.width; + return colsWidth > this.props.width ? this.props.width : colsWidth; + }, getDefaultProps: function() { + var winSize = getWindowSize(); return { - rowHeight: 35 + rowHeight: 35, + height: winSize.height, + width: winSize.width, }; }, diff --git a/lib/Header.js b/lib/Header.js index 166e337..a062560 100644 --- a/lib/Header.js +++ b/lib/Header.js @@ -21,7 +21,6 @@ var React = require('react/addons'); var cx = React.addons.classSet; var utils = require('./utils'); var DraggableMixin = require('./DraggableMixin'); -var getScrollbarSize = require('./getScrollbarSize'); var ColumnMetrics = require('./ColumnMetrics'); var Header = React.createClass({ @@ -29,7 +28,7 @@ var Header = React.createClass({ propTypes: { lockedColumns: React.PropTypes.object.isRequired, regularColumns: React.PropTypes.object.isRequired, - totalWidth: React.PropTypes.number, + totalWidth: React.PropTypes.number.isRequired, height: React.PropTypes.number.isRequired }, @@ -41,13 +40,11 @@ var Header = React.createClass({ top: 0, width: state.lockedColumns.width }; - var regularColumnsStyle = { position: 'absolute', top: 0, - left: state.lockedColumns.width, - width: (this.props.totalWidth - - state.lockedColumns.width) + left: lockedColumnsStyle.width - 20, //floats over the scrollbar + width: (this.props.totalWidth - lockedColumnsStyle.width) }; var className = cx({ @@ -163,9 +160,8 @@ var HeaderRow = React.createClass({ }, render: function() { - var scrollbarSize = getScrollbarSize(); var columnsStyle = { - width: this.props.width ? (this.props.width + scrollbarSize) : '100%', + width: this.props.width || '100%', height: this.props.height, position: 'relative', whiteSpace: 'nowrap', @@ -173,7 +169,7 @@ var HeaderRow = React.createClass({ overflowY: 'hidden' }; return this.transferPropsTo( -
+
{this.props.columns.map((column, idx) => HeaderCell({ key: idx, @@ -196,15 +192,6 @@ var HeaderRow = React.createClass({ || nextProps.columns !== this.props.columns || !utils.shallowEqual(nextProps.style, this.props.style) ); - }, - - getStyle: function() { - return { - overflow: 'hidden', - width: '100%', - height: this.props.height, - position: 'relative' - }; } }); diff --git a/lib/Row.js b/lib/Row.js index 838a30f..1d51d0c 100644 --- a/lib/Row.js +++ b/lib/Row.js @@ -3,6 +3,8 @@ */ 'use strict'; +/* jshint esnext: true */ + var React = require('react/addons'); var cx = React.addons.classSet; var Cell = require('./Cell'); @@ -32,18 +34,25 @@ var Row = React.createClass({ } else { children = this.props.columns.map((column, idx) => Cell({ key: idx, + row: this.props.row.key, + isSelected: this.props.isSelected && this.props.SelectedCells && this.props.SelectedCells[column.idx], value: this.props.row[column.key || idx], column: column, height: this.props.height, - renderer: column.renderer || this.props.cellRenderer + renderer: column.renderer || this.props.cellRenderer, + onClick: this.onCellClicked })); } return this.transferPropsTo( -
+
{children}
); + }, + onCellClicked: function(ev, cellProps) { + //bubble, adding row data + if(this.props.onRowClick) { this.props.onRowClick(ev, this.props, cellProps); } } }); diff --git a/lib/ScrollShim.js b/lib/ScrollShim.js deleted file mode 100644 index 91486d0..0000000 --- a/lib/ScrollShim.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -var ScrollShim = { - - appendScrollShim: function() { - if (!this._scrollShim) { - var size = this._scrollShimSize(); - var shim = document.createElement('div'); - shim.classList.add('react-grid-ScrollShim'); - shim.style.position = 'absolute'; - shim.style.top = 0; - shim.style.left = 0; - shim.style.width = '' + size.width + 'px'; - shim.style.height = '' + size.height + 'px'; - this.getDOMNode().appendChild(shim); - this._scrollShim = shim; - } - this._scheduleRemoveScrollShim(); - }, - - _scrollShimSize: function() { - return { - width: this.props.width, - height: this.props.length * this.props.rowHeight - }; - }, - - _scheduleRemoveScrollShim: function() { - if (this._scheduleRemoveScrollShimTimer) { - clearTimeout(this._scheduleRemoveScrollShimTimer); - } - this._scheduleRemoveScrollShimTimer = setTimeout( - this._removeScrollShim, 70); - }, - - _removeScrollShim: function() { - if (this._scrollShim) { - this._scrollShim.parentNode.removeChild(this._scrollShim); - this._scrollShim = undefined; - } - } -}; - -module.exports = ScrollShim; diff --git a/lib/Scrollbar.js b/lib/Scrollbar.js deleted file mode 100644 index 25eb017..0000000 --- a/lib/Scrollbar.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @jsx React.DOM - */ -'use strict'; - -var React = require('react/addons'); -var cx = React.addons.classSet; -var utils = require('./utils'); -var DraggableMixin = require('./DraggableMixin'); - -var floor = Math.floor; - -var MIN_STICK_SIZE = 40; - -var ScrollbarMixin = { - mixins: [DraggableMixin], - - render: function() { - var style = this.props.style ? - utils.merge(this.getStyle(), this.props.style) : - this.getStyle(); - - if (this.props.size >= this.props.totalSize) { - style.display = 'none'; - } - var className = cx("react-grid-Scrollbar", this.className); - - return this.transferPropsTo( -
-
-
-
-
- ); - }, - - getStickPosition: function() { - return floor(this.props.position / - (this.props.totalSize - this.props.size) * - (this.props.size - this.getStickSize())); - }, - - getStickSize: function() { - var size = floor(this.props.size / this.props.totalSize * this.props.size); - return size < MIN_STICK_SIZE ? MIN_STICK_SIZE : size; - }, - - componentWillMount: function() { - this.dragging = null; - }, - - onDrag: function(e) { - this.props.onScrollUpdate( - floor((this.getPositionFromMouseEvent(e) - this.dragging) / - (this.props.size - this.getStickSize()) * - (this.props.totalSize - this.props.size))); - }, - - getDraggingInfo: function(e) { - return this.getPositionFromMouseEvent(e) - this.getStickPosition(); - } -}; - -var VerticalScrollbarMixin = { - - className: 'react-grid-Scrollbar--vertical', - - getStyle: function() { - return { - height: this.props.height, - position: 'absolute', - top: 0, - right: 0 - }; - }, - - getStickStyle: function() { - return { - position: 'absolute', - height: this.getStickSize(), - top: this.getStickPosition() - }; - }, - - getPosition: function() { - return this.getDOMNode().getBoundingClientRect().top; - }, - - getPositionFromMouseEvent: function(e) { - return e.clientY; - } -}; - -var HorizontalScrollbarMixin = { - - className: 'react-grid-Scrollbar--horizontal', - - getStyle: function() { - return { - width: this.props.size, - position: 'absolute', - bottom: 0, - left: 0 - }; - }, - - getStickStyle: function() { - return { - position: 'absolute', - width: this.getStickSize(), - left: this.getStickPosition() - }; - }, - - getPosition: function() { - return this.getDOMNode().getBoundingClientRect().left; - }, - - getPositionFromMouseEvent: function(e) { - return e.clientX; - } -}; - -var VerticalScrollbar = React.createClass({ - mixins: [ScrollbarMixin, VerticalScrollbarMixin] -}); - -var HorizontalScrollbar = React.createClass({ - mixins: [ScrollbarMixin, HorizontalScrollbarMixin] -}); - -module.exports = { - VerticalScrollbar, - HorizontalScrollbar -}; diff --git a/lib/Viewport.js b/lib/Viewport.js index e84def8..a7fb92d 100644 --- a/lib/Viewport.js +++ b/lib/Viewport.js @@ -3,37 +3,28 @@ * * Component hierarchy diagram: * - * +––––––––––––––––––––––––––––––––––––––––––––––––––––+ - * | Viewport | - * | +––––––––––––––––––––+ +–––––––––––––––––––+ +–––+ | - * | | Canvas (locked) | | Canvas (regular) | | S | | - * | | | | | | c | | - * | | | | | | r | | - * | | | | | | o | | - * | | | | | | l | | - * | | | | | | l | | - * | | | | | | b | | - * | | | | | | a | | - * | | | | | | r | | - * | | | +–––––––––––––––––––+ +–––+ | - * | | | +–––––––––––––––––––––––––+ | - * | | | | Scrollbar | | - * | +––––––––––––––––––––+ +–––––––––––––––––––––––––+ | - * +––––––––––––––––––––––––––––––––––––––––––––––––––––+ + * +–––––––––––––––––––––––––––––––––––––––––––––––+ + * | Viewport | + * | +––––––––––––––––––––+ +–––––––––––––––––––+ | + * | | Canvas (locked) | | Canvas (regular) | | + * | | | | | | + * | | | | | | + * | | | | | | + * | | | | | | + * | | | | | | + * | | | | | | + * | | | | | | + * | | | | | | + * | +–––––––––––––––––––-+ +–––––––––––––––––––+ | + * +–––––––––––––––––––––––––––––––––––––––––––––––+ * * @jsx React.DOM */ 'use strict'; var React = require('react'); -var Scrollbar = require('./Scrollbar'); -var getWindowSize = require('./getWindowSize'); -var getScrollbarSize = require('./getScrollbarSize'); -var DOMMetrics = require('./DOMMetrics'); var Canvas = require('./Canvas'); - -var VerticalScrollbar = Scrollbar.VerticalScrollbar; -var HorizontalScrollbar = Scrollbar.HorizontalScrollbar; +var utils = require('./utils'); var min = Math.min; var max = Math.max; @@ -41,16 +32,14 @@ var floor = Math.floor; var ceil = Math.ceil; var ViewportScroll = { - mixins: [DOMMetrics.MetricsMixin], - DOMMetrics: { - viewportHeight: function() { - return this.getDOMNode().offsetHeight; - } + viewportHeight: function() { + return this.isMounted() ? this.getDOMNode().offsetHeight : 0; }, propTypes: { rowHeight: React.PropTypes.number, + height: React.PropTypes.number.isRequired, length: React.PropTypes.number.isRequired }, @@ -65,55 +54,45 @@ var ViewportScroll = { }, getGridState: function(props) { - var height = this.state && this.state.height ? - this.state.height : - getWindowSize().height; + var height = this.props.height; var renderedRowsCount = ceil(height / props.rowHeight); return { - displayStart: 0, + displayStart: this.props.initialRow || 0, displayEnd: renderedRowsCount * 2, - height: height, - scrollTop: 0, - scrollLeft: 0 + height: height }; }, - updateScroll: function(scrollTop, scrollLeft, height, rowHeight, length) { - var renderedRowsCount = ceil(height / rowHeight); + updateScroll: function(height, rowHeight, length, scrollTop, scrollLeft) { + scrollTop = scrollTop || 0; + scrollLeft = scrollLeft || 0; + var nextScrollState = { }; + + nextScrollState.renderedRowsCount = ceil(height / rowHeight); - var visibleStart = floor(scrollTop / rowHeight); + nextScrollState.visibleStart = floor(scrollTop / rowHeight); - var visibleEnd = min( - visibleStart + renderedRowsCount, + nextScrollState.visibleEnd = min( + nextScrollState.visibleStart + nextScrollState.renderedRowsCount, length); - var displayStart = max( + nextScrollState.displayStart = max( 0, - visibleStart - renderedRowsCount * 2); + nextScrollState.visibleStart - nextScrollState.renderedRowsCount * 2); - var displayEnd = min( - visibleStart + renderedRowsCount * 2, + nextScrollState.displayEnd = min( + nextScrollState.visibleStart + nextScrollState.renderedRowsCount * 2, length); - var nextScrollState = { - visibleStart, - visibleEnd, - displayStart, - displayEnd, - height, - scrollTop, - scrollLeft - }; + nextScrollState.height = height; this.setState(nextScrollState); }, metricsUpdated: function() { - var height = this.DOMMetrics.viewportHeight(); + var height = this.viewportHeight(); if (height) { this.updateScroll( - this.state.scrollTop, - this.state.scrollLeft, height, this.props.rowHeight, this.props.length @@ -126,19 +105,35 @@ var ViewportScroll = { this.setState(this.getGridState(nextProps)); } else if (this.props.length !== nextProps.length) { this.updateScroll( - this.state.scrollTop, - this.state.scrollLeft, - this.state.height, + this.props.height, nextProps.rowHeight, nextProps.length ); } - } + }, + }; var Viewport = React.createClass({ mixins: [ViewportScroll], + propTypes: { + regularColumns: React.PropTypes.object.isRequired, + lockedColumns: React.PropTypes.object, + onHorizontalScrollUpdate: React.PropTypes.func, + onVerticalScrollUpdate: React.PropTypes.func, + onViewportScroll: React.PropTypes.func, + length: React.PropTypes.number.isRequired, + rowHeight: React.PropTypes.number.isRequired, + height: React.PropTypes.number.isRequired, + rows: React.PropTypes.oneOfType([ + React.PropTypes.array.isRequired, + React.PropTypes.func.isRequired + ]), + rowRenderer: React.PropTypes.component, + SelectedCells: React.PropTypes.array.isRequired, + totalWidth: React.PropTypes.number.isRequired + }, style: { overflowX: 'hidden', overflowY: 'hidden', @@ -147,28 +142,16 @@ var Viewport = React.createClass({ }, render: function() { - var shift = getScrollbarSize(); var locked = this.renderLockedCanvas(); var regular = this.renderRegularCanvas(); + this.style.height = this.props.height; return this.transferPropsTo(
{locked && locked.canvas} {regular.canvas} - {shift > 0 && } - {shift > 0 && } +
); }, @@ -177,70 +160,44 @@ var Viewport = React.createClass({ if (this.props.lockedColumns.columns.length === 0) { return null; } - - var shift = getScrollbarSize(); - var width = this.props.lockedColumns.width + shift; - var hScroll = this.props.lockedColumns.width > width; - - var style = { - position: 'absolute', - top: 0, - width: width, - overflowX: hScroll ? 'scroll' : 'hidden', - overflowY: 'scroll', - paddingBottom: hScroll ? shift : 0 - }; - - var canvas = ( - - ); - return {canvas, style}; + var width = this.props.lockedColumns.width; + return this.renderAnyCanvas("locked", this.props.lockedColumns,{width:width}); }, renderRegularCanvas: function() { - var shift = getScrollbarSize(); - var width = (this.props.totalWidth - - this.props.lockedColumns.width + - shift); - var hScroll = this.props.regularColumns.width > width; - - var style = { + var width = (this.props.totalWidth - this.props.lockedColumns.width); + return this.renderAnyCanvas("regular", this.props.regularColumns,{ + width:width, + left:this.props.lockedColumns.width - 20,//floats over the scrollbar + backgroundColor:'white' //and 'hides' it + }); + }, + componentWillMount: function() { + this._ignoreNextScroll = this._ignoreNextScroll || null; + }, + renderAnyCanvas: function(canvasId, cols, style) { + var hScroll = cols.width > style.width; + utils.mergeInto(style, { position: 'absolute', top: 0, overflowX: hScroll ? 'scroll' : 'hidden', overflowY: 'scroll', - width: width, - left: this.props.lockedColumns.width, - paddingBottom: hScroll ? shift : 0 - }; + height: this.props.height - 20, + marginBottom: hScroll ? 20 : 0 + }); + var canvasRef=canvasId + "Rows"; + var canvasClass="react-grid-Viewport__" + canvasId; var canvas = ( ); - return {canvas, style}; }, - onScroll: function(rowGroup, e) { if (this._ignoreNextScroll !== null && this._ignoreNextScroll !== rowGroup) { @@ -267,10 +222,9 @@ var Viewport = React.createClass({ // we do this outside of React for better performance... // XXX: we might want to use rAF here var scrollTop = e.target.scrollTop; - var scrollLeft = rowGroup === 'lockedRows' ? - this.state.scrollLeft : e.target.scrollLeft; + var scrollLeft = e.target.scrollLeft; - var toUpdate = rowGroup === 'lockedRows' ? + var toUpdate = rowGroup.canvasRef === 'lockedRows' ? this.refs.regularRows : this.refs.lockedRows; @@ -280,11 +234,11 @@ var Viewport = React.createClass({ } this.updateScroll( - scrollTop, - scrollLeft, - this.state.height, + this.props.height, this.props.rowHeight, - this.props.length + this.props.length, + scrollTop, + scrollLeft ); if (this.props.onViewportScroll) { @@ -292,13 +246,7 @@ var Viewport = React.createClass({ } }, - onVerticalScrollUpdate: function(scrollTop) { - this.refs.regularRows.getDOMNode().scrollTop = scrollTop; - }, - onHorizontalScrollUpdate: function(scrollLeft) { - this.refs.regularRows.getDOMNode().scrollLeft = scrollLeft; - } }); module.exports = Viewport; diff --git a/lib/getScrollbarSize.js b/lib/getScrollbarSize.js deleted file mode 100644 index 79b8589..0000000 --- a/lib/getScrollbarSize.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; - -var size; - -function getScrollbarSize() { - if (size === undefined) { - - var outer = document.createElement('div'); - outer.style.width = '50px'; - outer.style.height = '50px'; - outer.style.overflowY = 'scroll'; - outer.style.position = 'absolute'; - outer.style.top = '-200px'; - outer.style.left = '-200px'; - - var inner = document.createElement('div'); - inner.style.height = '100px'; - inner.style.width = '100%'; - - outer.appendChild(inner); - document.body.appendChild(outer); - - var outerWidth = outer.offsetWidth; - var innerWidth = inner.offsetWidth; - - document.body.removeChild(outer); - - size = outerWidth - innerWidth; - } - - return size; -} - -module.exports = getScrollbarSize; diff --git a/lib/getWindowSize.js b/lib/getWindowSize.js index 502186f..ee3cb6a 100644 --- a/lib/getWindowSize.js +++ b/lib/getWindowSize.js @@ -10,6 +10,7 @@ * * @return {Object} height and width of the window */ + /* jshint browser: true */ function getWindowSize() { var width = window.innerWidth; var height = window.innerHeight; @@ -24,7 +25,7 @@ function getWindowSize() { height = document.body.clientHeight; } - return {width, height}; + return {width: width, height: height}; } module.exports = getWindowSize; diff --git a/lib/index.js b/lib/index.js index 622c1b1..f6fbefe 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,22 @@ /** * @jsx React.DOM */ + 'use strict'; -var Grid = require('./Grid'); -module.exports = Grid; +/* jshint browser: true */ + +//get our top level component +var reactGrid = require('./ExcelGrid'); + +//export it for browserify folks +module.exports = reactGrid; + +if(window) { + //and be nice to AMD peeps (no flame wars here...) + if (typeof window.define == 'function' && global.window.define.amd) { + window.define('reactGrid', function () { return reactGrid; }); + } + //and plain ole' global folks + window.reactGrid = reactGrid; +} diff --git a/lib/utils.js b/lib/utils.js index db3ec61..11e8dd0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,8 +2,9 @@ * @jsx React.DOM */ "use strict"; +var utils = {}; -function mergeInto(dst, src) { +utils.mergeInto = function mergeInto(dst, src) { if (src != null) { for (var k in src) { if (!src.hasOwnProperty(k)) { @@ -12,16 +13,16 @@ function mergeInto(dst, src) { dst[k] = src[k]; } } -} +}; -function merge(a, b) { +utils.merge = function merge(a, b) { var result = {}; - mergeInto(result, a); - mergeInto(result, b); + utils.mergeInto(result, a); + utils.mergeInto(result, b); return result; -} +}; -function shallowEqual(a, b) { +utils.shallowEqual = function shallowEqual(a, b) { if (a === b) { return true; } @@ -42,19 +43,19 @@ function shallowEqual(a, b) { } return true; -} +}; -function emptyFunction() { +utils.emptyFunction = function emptyFunction() { -} +}; -function invariant(condition, message) { +utils.invariant = function invariant(condition, message) { if (!condition) { throw new Error(message || 'invariant violation'); } -} +}; -function shallowCloneObject(obj) { +utils.shallowCloneObject = function shallowCloneObject(obj) { var result = {}; for (var k in obj) { if (obj.hasOwnProperty(k)) { @@ -62,13 +63,6 @@ function shallowCloneObject(obj) { } } return result; -} - -module.exports = { - shallowEqual, - emptyFunction, - invariant, - shallowCloneObject, - mergeInto, - merge }; + +module.exports = utils; diff --git a/package.json b/package.json index 2c4ecf5..5cbe6b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-grid", "version": "0.0.0", - "description": "Data grid fro React", + "description": "Data grid for React", "main": "lib/index.js", "scripts": { "test": "make test" @@ -15,14 +15,42 @@ "grid" ], "peerDependencies": { - "react": "^0.10.0" + "react": "^0.11.1" }, "devDependencies": { "mocha": "^1.18.2", - "mochify": "^0.8.1", - "react": "^0.10.0", + "mochify": "^0.8.2", + "react": "^0.11.1", "sinon": "^1.9.1", - "eslint-jsx": "git+https://github.com/andreypopp/eslint#jsx" + "gulp-rename": "^1.2.0", + "gulp-react": "^0.4.0", + "gulp-clean": "^0.3.1", + "gulp-minify-css": "^0.3.6", + "gulp-browserify": "^0.5.0", + "gulp-less": "^1.3.1", + "reactify": "^0.11.0", + "gulp": "^3.8.5", + "gulp-util": "^2.2.19", + "gulp-uglify": "^0.3.1", + "gulp-jshint": "^1.6.4", + "envify": "^1.2.1", + "gulp-connect": "^2.0.6", + "gulp-open": "^0.2.8", + "glob": "^4.0.4", + "gulp-concat": "^2.3.4", + "gulp-flatten": "0.0.2", + "gulp-plumber": "^0.6.3", + "gulp-template": "^0.1.1", + "jshint-stylish": "^0.4.0", + "gulp-jasmine": "^0.2.0", + "underscore": "^1.6.0", + "gulp-debug": "^1.0.0", + "gulp-add-src": "^0.1.1", + "react-tools": "^0.11.1" + }, + "browser": { + "react-grid": "./build/js/react-grid.js", + "plugin": "./src/javascript/vendor/jquery-plugin.js" }, "browserify": { "transform": [ @@ -41,6 +69,10 @@ }, "homepage": "https://github.com/prometheusresearch/react-grid", "dependencies": { - "reactify": "^0.11.0" + "reactify": "^0.11.0", + "lodash": "^2.4.1", + "envify": "^1.2.1", + "react": "^0.11.1", + "react-tools": "^0.11.1" } } diff --git a/standalone/lib b/standalone/lib deleted file mode 120000 index dc598c5..0000000 --- a/standalone/lib +++ /dev/null @@ -1 +0,0 @@ -../lib \ No newline at end of file diff --git a/standalone/lib b/standalone/lib new file mode 100644 index 0000000..dc598c5 --- /dev/null +++ b/standalone/lib @@ -0,0 +1 @@ +../lib \ No newline at end of file diff --git a/test/browser/ExcelGrid/keyboard_nav.spec.js b/test/browser/ExcelGrid/keyboard_nav.spec.js new file mode 100644 index 0000000..f72619e --- /dev/null +++ b/test/browser/ExcelGrid/keyboard_nav.spec.js @@ -0,0 +1,222 @@ +'use strict'; +var gridHelpers = require('./helpers.js'); +var React = require('React'); +var ReactTests = React.addons.TestUtils; + + +var baseKeyboardTests = { + tests:{ + navigate:function(keyArgs, row, col) { + var initial = this.grid.state.ActiveCell; + var times = keyArgs.times || 1; + for(var i=times;i>0;i--) { + ReactTests.Simulate.keyDown(this.gridNode, keyArgs); + } + if(isFinite(row)) { + expect(this.grid.state.ActiveCell.row).toBe(row); + } + if(isFinite(col)){ + expect(this.grid.state.ActiveCell.col).toBe(col); + } + }, + }, + matchers: { + + }, + suite: function(context) { + describe("Base keyboard navigation tests", function() { + beforeEach(function() { + this.navigateTest = baseKeyboardTests.tests.navigate + }); + it("Should navigate on key left", function() { + this.navigateTest({ key: 'Left' }, 0, -1); + }); + it("Should navigate on key right", function() { + this.navigateTest({ key: 'Right' }, 0, 1); + }); + it("Should navigate on key up", function() { + this.navigateTest({ key: 'Up' }, -1, 0); + }); + it("Should navigate on key down", function() { + this.navigateTest({ key: 'Down' }, 1, 0); + }); + }); + } +}; +describe("Keyboard Navigation", function() { + + beforeEach(function() { + this.grid = ReactTests.renderIntoDocument(gridHelpers.getGrid()); + this.gridNode = this.grid.refs.gridComponent.getDOMNode(); + + }); + it("Should assume the first cell is active if not set", function() { + expect(this.grid.state.ActiveCell.row).toBe(0); + expect(this.grid.state.ActiveCell.col).toBe(0); + }); + it("Should navigate columns via the api", function() { + this.grid.navigateTo({col:2}); + expect(this.grid.state.ActiveCell.row).toBe(0); + expect(this.grid.state.ActiveCell.col).toBe(2); + }); + it("Should navigate rows via the api", function() { + this.grid.navigateTo({row:2}); + expect(this.grid.state.ActiveCell.row).toBe(2); + expect(this.grid.state.ActiveCell.col).toBe(0); + }); + it("Should navigate rows using deltas via the api", function() { + this.grid.navigateTo({rowDelta:3}); + expect(this.grid.state.ActiveCell.row).toBe(3); + expect(this.grid.state.ActiveCell.col).toBe(0); + }); + it("Should navigate col using deltas via the api", function() { + this.grid.navigateTo({colDelta:3}); + expect(this.grid.state.ActiveCell.row).toBe(0); + expect(this.grid.state.ActiveCell.col).toBe(3); + }); + it("Should navigate by click", function() { + //click the grid at the right point + ReactTests.Simulate.click($(this.gridNode).find('div.react-grid-Row:eq(3)').find('div.react-grid-Cell:first')[0], {}); + //should be a better way to do this + //here we are tightly coupled to the structure of our grid (having a frozen column) but also our markup + expect(this.grid.state.ActiveCell.row).toBe(3); + expect(this.grid.state.ActiveCell.col).toBe(0); + }); + baseKeyboardTests.suite(this); + +}); + +describe("Keyboard Navigation - Frozen columns", function() { + + beforeEach(function() { + //by default column 0 is frozen + this.grid = ReactTests.renderIntoDocument(gridHelpers.getGrid()); + this.gridNode = this.grid.refs.gridComponent.getDOMNode(); + this.navigateTest = baseKeyboardTests.tests.navigate; + }); + + baseKeyboardTests.suite(this); + + it("Should navigate on key left from regular column > frozen one", function() { + this.grid.navigateTo({col:1}); //get onto the regular canvas + this.navigateTest({ key: 'Left' }, 0, 0); + }); + + it("Should navigate on key right from frozen column > regular one", function() { + this.navigateTest({ key: 'Right' }, 0, 1); + }); + + it("Should navigate by click in the frozen pane", function() { + //click the grid at the right point + ReactTests.Simulate.click($(this.gridNode).find('div.react-grid-Viewport__locked').find('div.react-grid-Row:eq(3)').find('div.react-grid-Cell:first')[0], {}); + //should be a better way to do this + //here we are tightly coupled to the structure of our grid (having a frozen column) but also our markup + expect(this.grid.state.ActiveCell.row).toBe(3); + expect(this.grid.state.ActiveCell.col).toBe(0); + }); + it("Should navigate by click in the regular pane", function() { + //click the grid at the right point + ReactTests.Simulate.click($(this.gridNode).find('div.react-grid-Viewport__regular').find('div.react-grid-Row:eq(3)').find('div.react-grid-Cell:first')[0], {}); + //should be a better way to do this + //here we are tightly coupled to the structure of our grid (having a frozen column) but also our markup + expect(this.grid.state.ActiveCell.row).toBe(3); + expect(this.grid.state.ActiveCell.col).toBe(1); + }); + + +}); +describe("Keyboard Navigation - Cell Selection", function() { + + beforeEach(function() { + //by default column 0 is frozen + this.grid = ReactTests.renderIntoDocument(gridHelpers.getGrid()); + this.gridNode = this.grid.refs.gridComponent.getDOMNode(); + this.navigateTest = baseKeyboardTests.tests.navigate; + jasmine.addMatchers({ + toContainRowAndCol: function() { + return { + compare: function (actual, expected) { + return { + pass: actual + && actual[expected.row] + && actual[expected.row][expected.col] + } + } + } + } + }); + }); + + it("Should select cells on navigate left", function() { + this.navigateTest({ key: 'Left' }, 0, -1); + expect(this.grid.state.SelectedCells).toContainRowAndCol({row: 0, col: -1}); + }); + it("Should select cells on navigate right", function() { + this.navigateTest({ key: 'Right' }, 0, 1); + expect(this.grid.state.SelectedCells).toContainRowAndCol({row: 0, col: 1}); + }); + it("Should select cells on navigate up", function() { + this.navigateTest({ key: 'Up' }, -1, 0); + expect(this.grid.state.SelectedCells).toContainRowAndCol({row: -1, col: 0}); + + }); + it("Should select cells on navigate down", function() { + this.navigateTest({ key: 'Down' }, 1, 0); + expect(this.grid.state.SelectedCells).toContainRowAndCol({row: 1, col: 0}); + }); + it("Should select multiple cells with shift key down", function() { + this.navigateTest({ key: 'Right', shiftKey: true }, 0, 1); + this.navigateTest({ key: 'Right', shiftKey: true }, 0, 2); + expect(this.grid.state.SelectedCells).toContainRowAndCol({row: 0, col: 1}); + expect(this.grid.state.SelectedCells).toContainRowAndCol({row: 0, col: 2}); + + }); + it("Should toggle multiple cells with shift key down", function() { + this.navigateTest({ key: 'Right', shiftKey: true }, 0, 1); + this.navigateTest({ key: 'Right', shiftKey: true }, 0, 2); + this.navigateTest({ key: 'Left', shiftKey: true }, 0, 1); + expect(this.grid.state.SelectedCells).not.toContainRowAndCol({row: 0, col: 1}); + expect(this.grid.state.SelectedCells).toContainRowAndCol({row: 0, col: 2}); + + }); + +}); + + +describe("Keyboard Navigation - Scroll", function() { + + beforeEach(function() { + //by default column 0 is frozen + this.grid = ReactTests.renderIntoDocument(gridHelpers.getGrid()); + this.gridNode = this.grid.refs.gridComponent.getDOMNode(); + this.navigateTest = baseKeyboardTests.tests.navigate; + }); + + xit("Should scroll on key down", function() { + this.navigateTest({ key: 'Left', times: 100 }); + //TODO scroll is (I think...) happening due to in built browser events + //so when we keyPress:Down in a scrollable div, it then fires scroll + //that doenst happen on simulated events though.... + expect(this.grid.refs.gridComponent.refs.Viewport.state.displayEnd).toBeGreaterThan(99); + }); + Xit("Should not prevent propogation", function() { + var called = false; + $(document).on('keydown', function() { called=true; }); + this.navigateTest({ key: 'Left' }); + //not working, argh. assume its due to being attached to a real dom? + expect(called).toBe(true); + }); +}); + +describe("TODO", function() { + it("Should load the grid", function() { + expect(gridHelpers.getGrid()).not.toBe(null); + }); + + it("Should have the grid DOM node", function() { + expect(this.gridNode).not.toBe(null); + }); + + + +}); diff --git a/test/browser/Viewport/scroll.js b/test/browser/Viewport/scroll.js new file mode 100644 index 0000000..b616206 --- /dev/null +++ b/test/browser/Viewport/scroll.js @@ -0,0 +1,45 @@ +'use strict'; +var gridHelpers = require('./helpers.js'); +var React = require('React'); +var ReactTests = React.addons.TestUtils; + +describe("Viewport scroll tests", function() { + beforeEach(function() { + //we use the gris as a shorthand to create + //strictly, we should create the viewport ourselves + //we have to render to get refs, etc + this.excelGrid = ReactTests.renderIntoDocument(gridHelpers.getGrid()); + + this.grid = this.excelGrid.refs.gridComponent; + this.viewport = this.grid.refs.Viewport; + this.lockedCanvas = this.viewport.refs.lockedRows; + this.regularCanvas = this.viewport.refs.regularRows; + this.viewNode = this.viewport.getDOMNode(); + + //set up some spies + spyOn(this.viewport, 'onScroll').and.callThrough(); + spyOn(this.lockedCanvas, 'setScrollTop').and.callThrough(); + spyOn(this.regularCanvas, 'setScrollTop').and.callThrough(); + + }); + it("Should scroll regular pane when scroll on frozen pane", function() { + ReactTests.Simulate.scroll(this.lockedCanvas.getDOMNode(), {target:{scrollLeft:0, scrollTop:100}}); + expect(this.regularCanvas.setScrollTop).toHaveBeenCalledWith(100); + }); + + it("Should scroll frozen pane when scroll on regular pane", function() { + ReactTests.Simulate.scroll(this.regularCanvas.getDOMNode(), {target:{scrollLeft:0, scrollTop:100}}); + expect(this.lockedCanvas.setScrollTop).toHaveBeenCalledWith(100); + }); + + it("Should scroll horizontally in the frozen pane", function() { + spyOn(this.viewport.props,'onViewportScroll'); + ReactTests.Simulate.scroll(this.lockedCanvas.getDOMNode(), {target:{scrollLeft:20, scrollTop:0}}); + expect(this.viewport.props.onViewportScroll).toHaveBeenCalledWith(0,20); + }); + it("Should scroll horizontally in the regular pane", function() { + spyOn(this.viewport.props,'onViewportScroll'); + ReactTests.Simulate.scroll(this.regularCanvas.getDOMNode(), {target:{scrollLeft:20, scrollTop:0}}); + expect(this.viewport.props.onViewportScroll).toHaveBeenCalledWith(0,20); + }); +}); diff --git a/test/browser/helpers.js b/test/browser/helpers.js new file mode 100644 index 0000000..4067fc5 --- /dev/null +++ b/test/browser/helpers.js @@ -0,0 +1,74 @@ +var _ = require("underscore"); +var ExcelGrid = require('./ExcelGrid'); +var React = require('React'); + +var data = []; +for (var i = 0; i < 2000; i++) { + data.push({ + 'key': i, + 'supplier':{'value':'Supplier ' + i, 'editing':true}, + 'format': 'fmt ' + i, + 'start':'start', + 'end':'end', + 'price':i }); +}; + + +function rows(start, end) { + return data.slice(start, end); +} + +var columns = [ + + { + idx: 0, + name: 'Supplier', + key: 'supplier', + width: 300, + locked: true, + }, + { + idx: 1, + name: 'Format', + key: 'format', + width: 350, + }, + { + idx: 2, + name: 'Start', + key: 'start', + width: 250, + }, + { + idx: 3, + name: 'End', + key: 'end', + width: 250, + }, + { + idx: 4, + name: 'Cost', + key: 'cost', + width: 200, + } +]; + +var getGrid = function(args) { + args = args || {}; + args = _.defaults(args, { + columns: columns, + rows: rows, + removeFreezeCols: false, + dataLength: data.length, + height:400 + }); + return ExcelGrid({columns:args.columns, rows: args.rows, length: args.dataLength, height: args.height}); +}; +var renderGrid = function(args) { + return React.renderComponent(getGrid(args), + document.getElementById(args.containerId)); +}; +module.exports = { + getGrid: getGrid, + renderGrid: renderGrid +}; diff --git a/test/testRunner.html b/test/testRunner.html new file mode 100644 index 0000000..5700ccd --- /dev/null +++ b/test/testRunner.html @@ -0,0 +1,16 @@ + + + + + Jasmine Spec Runner + + + +
+ + + + + + +