Last active
June 17, 2016 03:32
-
-
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 ^^,
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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