WIP
commit
e8471b60c6
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
yarn-error.log
|
@ -0,0 +1,3 @@
|
|||||||
|
TODO:
|
||||||
|
- Keyboard navigation of the active table; arrow keys and enter-to-edit, as well as escape cancellation
|
||||||
|
- Reworking event handling to work with nested tables, where necessary
|
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "websheets",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": "git@git.cryto.net:joepie91/websheets.git",
|
||||||
|
"author": "Sven Slootweg <admin@cryto.net>",
|
||||||
|
"license": "WTFPL OR CC0-1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"as-expression": "^1.0.0",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"css-extract": "^2.0.0",
|
||||||
|
"express": "^4.17.3",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.17.9",
|
||||||
|
"@babel/preset-env": "^7.16.11",
|
||||||
|
"@babel/preset-react": "^7.16.7",
|
||||||
|
"autoprefixer": "^10.4.4",
|
||||||
|
"babelify": "^10.0.0",
|
||||||
|
"browserify": "^17.0.0",
|
||||||
|
"budo-express": "^1.0.8",
|
||||||
|
"icssify": "^1.2.1",
|
||||||
|
"nodemon": "^2.0.15",
|
||||||
|
"postcss-nested": "^5.0.6",
|
||||||
|
"postcss-nested-props": "^2.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "NODE_ENV=development nodemon ./src/server/index.js"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
const Table = require("../table");
|
||||||
|
|
||||||
|
let sheet = {
|
||||||
|
columnCount: 15,
|
||||||
|
rowCount: 15,
|
||||||
|
cells: [
|
||||||
|
[ "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" ],
|
||||||
|
[ "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz" ],
|
||||||
|
[ "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo" ],
|
||||||
|
[ "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" ],
|
||||||
|
[ "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz" ],
|
||||||
|
[ "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo" ],
|
||||||
|
[ "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" ],
|
||||||
|
[ "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz" ],
|
||||||
|
[ "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo" ],
|
||||||
|
[ "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" ],
|
||||||
|
[ "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz" ],
|
||||||
|
[ "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo" ],
|
||||||
|
[ "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" ],
|
||||||
|
[ "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz", "asdf", "Qux", "Quz" ],
|
||||||
|
[ "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo", "FooBar", "BazQux", "QuzFoo" ]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = function App() {
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<Table sheet={sheet} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
/* HACK: https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/ */
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No nesting here, because we want to keep precedence as low as possible */
|
||||||
|
|
||||||
|
.editable, .dummy {
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: normal;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
|
||||||
|
font: {
|
||||||
|
family: inherit;
|
||||||
|
size: inherit;
|
||||||
|
weight: inherit;
|
||||||
|
style: inherit;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
.dummy {
|
||||||
|
visibility: hidden;
|
||||||
|
width: max-content; /* NOTE: This prevents the pseudo-element from line-wrapping when the overall page content is bigger than the viewport, which is necessary to get consistent behaviour with regular elements. */
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable {
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const classnames = require("classnames");
|
||||||
|
|
||||||
|
const style = require("./autoresize-textarea.css");
|
||||||
|
|
||||||
|
const NBSP = String.fromCharCode(160);
|
||||||
|
|
||||||
|
module.exports = React.forwardRef(function AutosizeTextarea({ value, className, onChange, ... args }, ref) {
|
||||||
|
// TODO: Handle this outside of the React state cycle using raw JS events, for improved performance
|
||||||
|
let [ currentValue, setCurrentValue ] = React.useState(value);
|
||||||
|
|
||||||
|
// This is mainly to ensure that an empty newline is taken into account for the size measurement, otherwise we get a scrollbar in the textarea
|
||||||
|
let displayValue = currentValue + (currentValue.endsWith("\n") ? NBSP : "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classnames(style.wrapper)}>
|
||||||
|
<div className={classnames(style.dummy, className)}>{displayValue}</div>
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
rows="1" cols="1"
|
||||||
|
name="value"
|
||||||
|
className={classnames(style.editable, className)}
|
||||||
|
value={currentValue}
|
||||||
|
onChange={(event) => {
|
||||||
|
let newValue = event.target.value;
|
||||||
|
|
||||||
|
setCurrentValue(newValue);
|
||||||
|
|
||||||
|
if (onChange != null) {
|
||||||
|
onChange(newValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{... args}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,242 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
.table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
/* width: max(fit-content, 100%); */
|
||||||
|
border: 1px solid silver;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
/* Border-collapsing hack */
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -1px;
|
||||||
|
|
||||||
|
&.selectionPart {
|
||||||
|
background-color: #EEEEEE;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selectionLeftEdge {
|
||||||
|
border-left: 2px solid rgb(36, 36, 36);
|
||||||
|
margin-left: -2px; /* Includes border-collapsing compensation */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selectionRightEdge {
|
||||||
|
border-right: 2px solid rgb(36, 36, 36);
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selectionTopEdge {
|
||||||
|
border-top: 2px solid rgb(36, 36, 36);
|
||||||
|
margin-top: -2px; /* Includes border-collapsing compensation */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selectionBottomEdge {
|
||||||
|
border-bottom: 2px solid rgb(36, 36, 36);
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellContents, .editableCell {
|
||||||
|
padding: 3px 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "sans-serif";
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .preview {
|
||||||
|
background-color: chartreuse;
|
||||||
|
/* overflow: visible; */
|
||||||
|
width: 80px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .previewContents {
|
||||||
|
/* NOTE: *Must* be a block element for ResizeObserver to work */
|
||||||
|
display: inline-block;
|
||||||
|
background-color: beige;
|
||||||
|
width: max-content;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
require("./global.css");
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const { createRoot } = require("react-dom/client");
|
||||||
|
|
||||||
|
const App = require("./components/app");
|
||||||
|
|
||||||
|
let root = createRoot(document.querySelector("#app"));
|
||||||
|
root.render(<App />);
|
@ -0,0 +1,10 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const express = require("express");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
let app = express();
|
||||||
|
|
||||||
|
app.use(express.static(path.join(__dirname, "../static")));
|
||||||
|
|
||||||
|
module.exports = app;
|
@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const budoExpress = require("budo-express");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
budoExpress({
|
||||||
|
port: 3500,
|
||||||
|
allowUnsafeHost: true,
|
||||||
|
expressApp: require("./app"),
|
||||||
|
basePath: path.join(__dirname, "../.."),
|
||||||
|
entryFiles: "src/client/index.jsx",
|
||||||
|
staticPath: "src/static",
|
||||||
|
bundlePath: "js/bundle.js",
|
||||||
|
livereloadPattern: "**/*.{css,html,js,svg}",
|
||||||
|
browserify: {
|
||||||
|
extensions: [ ".jsx" ],
|
||||||
|
transform: [
|
||||||
|
[ "babelify", {
|
||||||
|
presets: [ "@babel/preset-env", "@babel/preset-react" ]
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
plugin: [
|
||||||
|
[ "icssify", {
|
||||||
|
before: [
|
||||||
|
require("postcss-nested"),
|
||||||
|
require("postcss-nested-props"),
|
||||||
|
require("autoprefixer"),
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
[ "css-extract", {
|
||||||
|
out: "src/static/css/bundle.css"
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,127 @@
|
|||||||
|
/* from src/client/components/autoresize-textarea/autoresize-textarea.css */
|
||||||
|
|
||||||
|
/* HACK: https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/ */
|
||||||
|
|
||||||
|
.autoresize-textarea__wrapper---2VZA1 {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No nesting here, because we want to keep precedence as low as possible */
|
||||||
|
|
||||||
|
.autoresize-textarea__editable---LrrqY, .autoresize-textarea__dummy---mFLYc {
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: normal;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
font-style: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoresize-textarea__dummy---mFLYc {
|
||||||
|
visibility: hidden;
|
||||||
|
width: -webkit-max-content;
|
||||||
|
width: -moz-max-content;
|
||||||
|
width: max-content; /* NOTE: This prevents the pseudo-element from line-wrapping when the overall page content is bigger than the viewport, which is necessary to get consistent behaviour with regular elements. */
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoresize-textarea__editable---LrrqY {
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* from src/client/components/table/table.css */
|
||||||
|
|
||||||
|
.table__table---22_r7 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto auto;
|
||||||
|
width: -webkit-fit-content;
|
||||||
|
width: -moz-fit-content;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__cell---3PaNC {
|
||||||
|
/* width: max(fit-content, 100%); */
|
||||||
|
border: 1px solid silver;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
/* Border-collapsing hack */
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__cell---3PaNC.table__selectionPart---x6YGc {
|
||||||
|
background-color: #EEEEEE;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__cell---3PaNC.table__selectionLeftEdge---1N5B_ {
|
||||||
|
border-left: 2px solid rgb(36, 36, 36);
|
||||||
|
margin-left: -2px; /* Includes border-collapsing compensation */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__cell---3PaNC.table__selectionRightEdge---3HQMN {
|
||||||
|
border-right: 2px solid rgb(36, 36, 36);
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__cell---3PaNC.table__selectionTopEdge---3rpyb {
|
||||||
|
border-top: 2px solid rgb(36, 36, 36);
|
||||||
|
margin-top: -2px; /* Includes border-collapsing compensation */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__cell---3PaNC.table__selectionBottomEdge---1YFE2 {
|
||||||
|
border-bottom: 2px solid rgb(36, 36, 36);
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table__cellContents---Tzzg7, .table__editableCell---35lZy {
|
||||||
|
padding: 3px 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* from src/client/global.css */
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "sans-serif";
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
background-color: chartreuse;
|
||||||
|
/* overflow: visible; */
|
||||||
|
width: 80px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewContents {
|
||||||
|
/* NOTE: *Must* be a block element for ResizeObserver to work */
|
||||||
|
display: inline-block;
|
||||||
|
background-color: beige;
|
||||||
|
width: -webkit-max-content;
|
||||||
|
width: -moz-max-content;
|
||||||
|
width: max-content;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Websheets</title>
|
||||||
|
<link rel="stylesheet" href="/css/bundle.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">Loading Websheets...</div>
|
||||||
|
<script src="/js/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue