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
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>
|
|
);
|
|
}
|