You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
7.1 KiB
JavaScript

"use strict";
const React = require("react");
const classnames = require("classnames");
const asExpression = require("as-expression");
const AutosizeTextarea = require("../autoresize-textarea");
const style = require("./table.css");
const MOUSE_PRIMARY = 0;
function generateColumnTemplate(columnCount) {
return (new Array(columnCount))
.fill("auto")
.join(" ");
}
function StaticCellContents({ dummy, children }) {
return (
<div className={classnames(style.cellContents, { [style.dummy]: dummy })}>
{children}
</div>
);
}
function EditableCellContents({ onChange, children }) {
let ref = React.useRef();
React.useEffect(() => {
ref.current.focus();
ref.current.select();
}, [ ref ]);
return (
<AutosizeTextarea
ref={ref}
value={children}
className={style.editableCell}
onChange={onChange}
onMouseDown={(event) => {
// Workaround for Chromium, which will try to select the underlying cell when attempting to click the editable text
event.stopPropagation();
}}
/>
);
}
function Cell({ rowIndex, columnIndex, children }) {
let table = React.useContext(TableContext);
let isSingleSelection = (
(table.selection.rowStart != null || table.selection.columnStart != null)
&& rowIndex === table.selection.rowStart
&& rowIndex === table.selection.rowEnd
&& columnIndex === table.selection.columnStart
&& columnIndex === table.selection.columnEnd
);
let isPartOfSelection = (
(table.selection.rowStart != null || table.selection.columnStart != null)
&& rowIndex >= table.selection.rowStart
&& rowIndex <= table.selection.rowEnd
&& columnIndex >= table.selection.columnStart
&& columnIndex <= table.selection.columnEnd
);
let isLeftEdge = (columnIndex === table.selection.columnStart);
let isRightEdge = (columnIndex === table.selection.columnEnd);
let isTopEdge = (rowIndex === table.selection.rowStart);
let isBottomEdge = (rowIndex === table.selection.rowEnd);
let isEditable = (table.isEditing && isPartOfSelection);
let contents = (isEditable)
? <EditableCellContents
children={children}
onChange={(contents) => table.onCellContentsChange(rowIndex, columnIndex, contents)}
/>
: <StaticCellContents children={children} />;
return <div
className={classnames(style.cell, {
[style.selectionLeftEdge]: isPartOfSelection && isLeftEdge,
[style.selectionRightEdge]: isPartOfSelection && isRightEdge,
[style.selectionTopEdge]: isPartOfSelection && isTopEdge,
[style.selectionBottomEdge]: isPartOfSelection && isBottomEdge,
[style.selectionPart]: isPartOfSelection
})}
children={contents}
onMouseDown={(event) => table.onCellDown(rowIndex, columnIndex, event)}
onMouseUp={(event) => table.onCellUp(rowIndex, columnIndex, event)}
onMouseEnter={(event) => table.onCellEnter(rowIndex, columnIndex, event)}
onMouseLeave={(event) => table.onCellLeave(rowIndex, columnIndex, event)}
onDoubleClick={(event) => table.onCellDoubleClick(rowIndex, columnIndex, event)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
table.onCellCompleteEditing(rowIndex, columnIndex);
}
}}
/>;
}
function Row({ columns, index }) {
return columns.map((cell, columnIndex) => {
return <Cell rowIndex={index} columnIndex={columnIndex}>{cell}</Cell>
});
}
let TableContext = React.createContext();
module.exports = function Table({ sheet }) {
let [ selectedRowStart, setSelectedRowStart ] = React.useState();
let [ selectedRowEnd, setSelectedRowEnd ] = React.useState();
let [ selectedColumnStart, setSelectedColumnStart ] = React.useState();
let [ selectedColumnEnd, setSelectedColumnEnd ] = React.useState();
let [ isSelecting, setIsSelecting ] = React.useState(false);
let [ isEditing, setIsEditing ] = React.useState(false);
// FIXME: Ensure that in editing mode, there's always just *one* cell selected, including when editing mode is activated by eg. pressing Enter (at least until we have multi-cell editing implemented!)
React.useEffect(() => {
function mouseupListener(event) {
if (event.button === MOUSE_PRIMARY) {
setIsSelecting(false);
}
};
function mousedownListener(event) {
if (event.button === MOUSE_PRIMARY) {
stopEditing();
deselect();
}
}
document.addEventListener("mouseup", mouseupListener);
document.addEventListener("mousedown", mousedownListener);
return function () {
document.removeEventListener("mouseup", mouseupListener);
document.removeEventListener("mousedown", mousedownListener);
}
}, []);
function deselect() {
setSelectedRowStart(null);
setSelectedRowEnd(null);
setSelectedColumnStart(null);
setSelectedColumnEnd(null);
}
function select(rowIndex, columnIndex) {
setSelectedRowStart(rowIndex);
setSelectedRowEnd(rowIndex);
setSelectedColumnStart(columnIndex);
setSelectedColumnEnd(columnIndex);
}
function selectCellStart(rowIndex, columnIndex) {
setSelectedRowStart(rowIndex);
setSelectedColumnStart(columnIndex);
}
// FIXME: Swap when end < start
function selectCellEnd(rowIndex, columnIndex) {
setSelectedRowEnd(rowIndex);
setSelectedColumnEnd(columnIndex);
}
function startEditing() {
setIsEditing(true);
}
function stopEditing() {
setIsEditing(false);
}
let flippedSelection = asExpression(() => {
let [ rowStart, rowEnd ] = (selectedRowStart < selectedRowEnd)
? [ selectedRowStart, selectedRowEnd ]
: [ selectedRowEnd, selectedRowStart ];
let [ columnStart, columnEnd ] = (selectedColumnStart < selectedColumnEnd)
? [ selectedColumnStart, selectedColumnEnd ]
: [ selectedColumnEnd, selectedColumnStart ];
return { rowStart, rowEnd, columnStart, columnEnd };
});
// TODO: Also stop selecting on focus blur and document mouseleave? Or is this not necessary?
let tableAPI = {
selection: flippedSelection,
isEditing: isEditing,
onCellDown: function (rowIndex, columnIndex, event) {
if (event.button === MOUSE_PRIMARY) {
stopEditing();
setIsSelecting(true);
selectCellStart(rowIndex, columnIndex);
selectCellEnd(rowIndex, columnIndex);
event.stopPropagation();
}
},
onCellUp: function (rowIndex, columnIndex, event) {
if (event.button === MOUSE_PRIMARY) {
setIsSelecting(false);
event.stopPropagation();
}
},
onCellEnter: function (rowIndex, columnIndex, event) {
if (isSelecting) {
selectCellEnd(rowIndex, columnIndex);
}
},
onCellLeave: function (rowIndex, columnIndex, event) {
},
onCellDoubleClick: function (rowIndex, columnIndex, event) {
if (event.button === MOUSE_PRIMARY) {
startEditing();
select(rowIndex, columnIndex);
event.stopPropagation();
}
},
onCellContentsChange: function (rowIndex, columnIndex, contents) {
// FIXME: Properly run this through a state management cycle?
sheet.cells[rowIndex][columnIndex] = contents;
},
onCellCompleteEditing: function (rowIndex, columnIndex) {
stopEditing();
}
};
return (
<div className={style.table} style={{ gridTemplateColumns: generateColumnTemplate(sheet.columnCount) }}>
<TableContext.Provider value={tableAPI}>
{sheet.cells.map((columns, rowIndex) => {
return <Row index={rowIndex} columns={columns} />;
})}
</TableContext.Provider>
</div>
);
}