World's Worst Spreadsheet

by alexb

HTML

<div id="root"></div>

CSS

html {
  box-sizing: border-box;
  font-family: Helvetica;
  font-size: 14px;
}

html * {
  box-sizing: inherit;
}

body {
  background: #20262E;
  padding: 20px;
}

#root {
  background: #fff;
  border-radius: 4px;
  margin: 0 auto;
  max-width: 900px;
  padding: 20px;
  transition: all 0.2s;
}

.editor {
  border: 1px solid lightgray;
  font-family: Helvetica;
  font-size: 1rem;
  padding: 0.25rem 0.5rem;
  width: 100%;
}

.editor + p {
  color: gray;
  font-size: 10px;
  height: 30px;
  opacity: 0;
  padding-top: 5px;
  padding-left: calc(0.5rem + 1px);
  transition: opacity 100ms;
}

.editor:focus + p.show {
  opacity: 1;
}

.cells {
  display: flex;
  flex-wrap: wrap;
  padding: 1px 0 0 1px;
}

.cells > * {
  width: calc((100% - 30px) / 8 + 1px);
}

.cell {
  align-items: center;
  border: 1px solid lightgray;
  cursor: pointer;
  display: flex;
  height: 1.75em;
  margin: -1px 0 0 -1px;
  overflow: hidden;
  padding: 0 0.25em;
  white-space: nowrap;
}

.cell.heading {
  background: lightgray;
  cursor: default;
  justify-content: center;
}

.cell.heading-row {
  width: 30px;
}

.cell.selected {
  border-color: cornflowerblue;
  outline: 2px solid cornflowerblue;
  position: relative;
}

React

const col = cellRef => cellRef.replace(/\d+/, '');
const row = cellRef => cellRef.replace(/\D+/, '');
const pairToRef = (x, y) => String.fromCharCode(64 + x) + String(y);

class Sheet {
	constructor() {
	    // Map of cell references to cell contents. Contents
	    // are strings containing either numbers or formulas.
	    // Examples: '2', '=A2+B3'
	    this._contents = new Map();
	  }
  
  evaluate(cellRef) {
  	const cell = this._contents.get(cellRef) || '';
    if (cell.substr(0, 1) !== '=') {
      return cell;
    }
    let noRefs;
    try {
      const expr = cell.substr(1);
      noRefs = expr.replace(
        /[A-Z]+\d+/g,
        ref => this.evaluate(ref));
      return String(eval(noRefs));
    }
    catch (e) {
	    console.error(
      	'Error evaluating cell',
        cellRef,
        JSON.stringify(cell),
        '→',
        JSON.stringify(noRefs),
        e.message
      );
      return '#ERROR';
    }
  }
  
  get(cellRef) {
  	return this._contents.get(cellRef) || '';
  }
  
  set(cellRef, content) {
  	this._contents.set(cellRef, content);
  }
}

const range = (lo, hi) => {
	if (hi === undefined) {
    hi = lo;
  	lo = 0;
  }
	return new Array(hi - lo).fill(0).map((_, i) => lo + i);
};

const SheetView = ({ sheet }) => {
	const [[selX, selY], setSel] = React.useState([1, 1]);
	const [editorContent, setEditorContent] =
  	React.useState(sheet.get(pairToRef(selX, selY)));
    
  const editorChanged =
  	editorContent !== sheet.get(pairToRef(selX, selY));
   
  const setCell = (x, y) => {
  	setSel([x, y]);
    setEditorContent(sheet.get(pairToRef(x, y)))
  }
    
  const handleEditorChange = event =>
  	setEditorContent(event.currentTarget.value);
  const handleEditorKeyPress = event => {
  	if (event.key === 'Enter') {
    	console.log(sheet, pairToRef(selX, selY), editorContent)
	  	sheet.set(pairToRef(selX, selY), editorContent);
      setSel([selX, selY]); // Force rerender.
    }
  };
	return (
		<div className="sheet">
      <input
       ...