Skip to content

Instantly share code, notes, and snippets.

@kiasaki
Last active June 17, 2016 03:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kiasaki/e259ffc18f83cce0611a969151d50ecc to your computer and use it in GitHub Desktop.
Save kiasaki/e259ffc18f83cce0611a969151d50ecc to your computer and use it in GitHub Desktop.
An simplistic editable grid/table in React. Support click to edit, moving with arrow keys, escape, enter & looks quite good ^^,
import React, {Component, PropTypes} from 'react';
import {m} from '../../utils';
import styles from '../../styles';
let s = null;
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
rowCount: 25,
columnCount: 25,
editing: null
};
this.handleEditingChange = this.handleEditingChange.bind(this);
this.handleEditingBlur = this.handleEditingBlur.bind(this);
this.handleEditingKeyPress = this.handleEditingKeyPress.bind(this);
}
getValue(data, x, y) {
let value;
if (x < data.length && y < data[x].length) {
value = data[x][y];
}
return value;
}
ensureDataFits(data) {
let {rowCount, columnCount} = this.state;
let changed = false;
if (data.length > columnCount) {
columnCount = data.length;
changed = true;
}
for (let x; x < data.length; x++) {
if (data[x].length > columnCount) {
rowCount = data[x].length;
changed = true;
}
}
if (changed) {
this.setState({rowCount, columnCount});
}
}
// Invert array[x][y] to array[y][x];
invertData(data) {
const {rowCount, columnCount} = this.state;
const gridData = [];
// invert data table
for (let y = 0; y < rowCount; y++) {
gridData[y] = [];
for (let x = 0; x < columnCount; x++) {
if (data.length >= y && data[y].length >= x) {
gridData[y][x] = data[x][y];
} else {
gridData[y][x] = '';
}
}
}
}
columnNameForY(y) {
let yLeft = y + 1;
let name = '';
while (yLeft > 0) {
name = String.fromCharCode(64 + (yLeft % 26)) + name;
yLeft = Math.floor(yLeft / 26);
}
return name;
}
// Handle editing typing
handleEditingChange(event) {
this.setState({editingValue: event.target.value});
}
// Handle exiting edit mode & notifying parent
handleEditingBlur() {
const {editingValue, editing} = this.state;
const [editingX, editingY] = editing;
const {data} = this.props;
// Exit early in case there was no change
const value = this.getValue(data, editingX, editingY) || '';
if (value === editingValue) {
this.setState({editingValue: null, editing: null});
return;
}
if (data.length <= editingX) {
for (let x = data.length; x <= editingX; x++) {
// Create missing columns
data[x] = [];
}
}
data[editingX][editingY] = editingValue;
this.setState({editingValue: null, editing: null});
this.props.onChange(data);
}
// Handle moving around with arrows
handleEditingKeyPress(event) {
const {data} = this.props;
const {rowCount, columnCount, editing} = this.state;
const [editingX, editingY] = editing;
const move = (newX, newY) => {
// Check if it's a valid move
if (
newX >= 0 && newX < columnCount &&
newY >= 0 && newY < rowCount
) {
this.handleEditingBlur();
const value = this.getValue(data, newX, newY) || '';
this.makeCellClickHandler(newX, newY, value)();
}
};
switch (event.keyCode || event.which) {
case 37: // left
move(editingX - 1, editingY);
break;
case 38: // up
move(editingX, editingY - 1);
break;
case 39: // right
move(editingX + 1, editingY);
break;
case 13: // enter
case 40: // down
move(editingX, editingY + 1);
break;
case 27: // escape
this.handleEditingBlur();
break;
default:
break;
}
}
makeCellClickHandler(sx, sy, svalue) {
const x = sx;
const y = sy;
const value = svalue;
return () => {
this.setState({
editing: [x, y],
editingValue: value || ''
}, () => {
this.refs.editingCell.focus();
});
};
}
renderCell(data, x, y) {
const value = this.getValue(data, x, y);
const onClick = this.makeCellClickHandler(x, y, value);
if (this.state.editing) {
const {editingValue, editing} = this.state;
const [editingX, editingY] = editing;
if (x === editingX && y === editingY) {
return (
<input
key={x}
ref="editingCell"
style={m(s.cellEditing, s.cell)}
type="text"
value={editingValue}
onChange={this.handleEditingChange}
onBlur={this.handleEditingBlur}
onKeyDown={this.handleEditingKeyPress}
/>
);
}
}
return (
<div key={x} style={s.cell} onClick={onClick}>
{value || '\u00A0' /* nbsp */}
</div>
);
}
renderRow(data, y) {
const {columnCount} = this.state;
const cells = [];
for (let x = 0; x < columnCount; x++) {
cells.push(this.renderCell(data, x, y));
}
return (
<div key={y} style={s.row}>
{cells}
</div>
);
}
render() {
const {rowCount} = this.state;
const {data, columnNames} = this.props;
this.ensureDataFits(data);
let headerRow = [];
let rows = [];
for (let y = 0; y < rowCount; y++) {
headerRow.push(
<div key={y} style={m(s.cell, s.headerCell)}>
{columnNames[y] || this.columnNameForY(y)}
</div>
);
rows.push(this.renderRow(data, y));
}
return (
<div style={s.container}>
<div style={s.row}>
{headerRow}
</div>
{rows}
</div>
);
}
}
Grid.propTypes = {
columnNames: PropTypes.arrayOf(PropTypes.string).isRequired,
data: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
onChange: PropTypes.func.isRequired
};
export default Grid;
s = {
container: {
},
row: {
display: 'flex'
},
headerCell: {
background: styles.colors.whiteDarkish,
color: styles.colors.gray
},
cell: {
flex: 1,
fontSize: styles.fontSizes.small,
fontFamily: '"CourierNeue", Courier, monospace',
minWidth: '6rem',
padding: '0.4rem 0.6em',
border: `1px solid ${styles.colors.grayPaleish}`,
borderWidth: '0 1px 1px 0',
color: styles.colors.grayDark,
textAlign: 'center',
cursor: 'pointer'
},
cellEditing: {
margin: 0,
borderRadius: 0,
height: 'inherit',
background: styles.colors.blueGrayPale
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment