UPDATE: Adds cell and formula editing
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
#root {
|
#root {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 10px;
|
inset: 0px;
|
||||||
|
margin: 10px;
|
||||||
border: 1px solid #aaa;
|
border: 1px solid #aaa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function App() {
|
|||||||
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
|
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function start() {
|
async function start() {
|
||||||
await init();
|
await init();
|
||||||
@@ -42,6 +43,7 @@ function App() {
|
|||||||
|
|
||||||
// We could use context for model, but the problem is that it should initialized to null.
|
// We could use context for model, but the problem is that it should initialized to null.
|
||||||
// Passing the property down makes sure it is always defined.
|
// Passing the property down makes sure it is always defined.
|
||||||
|
|
||||||
return <Workbook model={model} workbookState={workbookState} />;
|
return <Workbook model={model} workbookState={workbookState} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface CanvasSettings {
|
|||||||
columnGuide: HTMLDivElement;
|
columnGuide: HTMLDivElement;
|
||||||
rowGuide: HTMLDivElement;
|
rowGuide: HTMLDivElement;
|
||||||
columnHeaders: HTMLDivElement;
|
columnHeaders: HTMLDivElement;
|
||||||
|
editor: HTMLDivElement;
|
||||||
};
|
};
|
||||||
onColumnWidthChanges: (sheet: number, column: number, width: number) => void;
|
onColumnWidthChanges: (sheet: number, column: number, width: number) => void;
|
||||||
onRowHeightChanges: (sheet: number, row: number, height: number) => void;
|
onRowHeightChanges: (sheet: number, row: number, height: number) => void;
|
||||||
@@ -48,6 +49,23 @@ export const defaultCellFontFamily = fonts.regular;
|
|||||||
export const headerFontFamily = fonts.regular;
|
export const headerFontFamily = fonts.regular;
|
||||||
export const frozenSeparatorWidth = 3;
|
export const frozenSeparatorWidth = 3;
|
||||||
|
|
||||||
|
// Get a 10% transparency of an hex color
|
||||||
|
function hexToRGBA10Percent(colorHex: string): string {
|
||||||
|
// Remove the leading hash (#) if present
|
||||||
|
const hex = colorHex.replace(/^#/, "");
|
||||||
|
|
||||||
|
// Parse the hex color
|
||||||
|
const red = Number.parseInt(hex.substring(0, 2), 16);
|
||||||
|
const green = Number.parseInt(hex.substring(2, 4), 16);
|
||||||
|
const blue = Number.parseInt(hex.substring(4, 6), 16);
|
||||||
|
|
||||||
|
// Set the alpha (opacity) to 0.1 (10%)
|
||||||
|
const alpha = 0.1;
|
||||||
|
|
||||||
|
// Return the RGBA color string
|
||||||
|
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
export default class WorksheetCanvas {
|
export default class WorksheetCanvas {
|
||||||
sheetWidth: number;
|
sheetWidth: number;
|
||||||
|
|
||||||
@@ -61,6 +79,8 @@ export default class WorksheetCanvas {
|
|||||||
|
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
editor: HTMLDivElement;
|
||||||
|
|
||||||
areaOutline: HTMLDivElement;
|
areaOutline: HTMLDivElement;
|
||||||
|
|
||||||
cellOutline: HTMLDivElement;
|
cellOutline: HTMLDivElement;
|
||||||
@@ -92,6 +112,7 @@ export default class WorksheetCanvas {
|
|||||||
this.height = options.height;
|
this.height = options.height;
|
||||||
this.ctx = this.setContext();
|
this.ctx = this.setContext();
|
||||||
this.workbookState = options.workbookState;
|
this.workbookState = options.workbookState;
|
||||||
|
this.editor = options.elements.editor;
|
||||||
|
|
||||||
this.cellOutline = options.elements.cellOutline;
|
this.cellOutline = options.elements.cellOutline;
|
||||||
this.cellOutlineHandle = options.elements.cellOutlineHandle;
|
this.cellOutlineHandle = options.elements.cellOutlineHandle;
|
||||||
@@ -1092,7 +1113,7 @@ export default class WorksheetCanvas {
|
|||||||
this.getColumnWidth(selectedSheet, selectedColumn) + 2 * padding;
|
this.getColumnWidth(selectedSheet, selectedColumn) + 2 * padding;
|
||||||
const height = this.getRowHeight(selectedSheet, selectedRow) + 2 * padding;
|
const height = this.getRowHeight(selectedSheet, selectedRow) + 2 * padding;
|
||||||
|
|
||||||
const { cellOutline, areaOutline, cellOutlineHandle } = this;
|
const { cellOutline, editor, areaOutline, cellOutlineHandle } = this;
|
||||||
const cellEditing = null;
|
const cellEditing = null;
|
||||||
|
|
||||||
cellOutline.style.visibility = "visible";
|
cellOutline.style.visibility = "visible";
|
||||||
@@ -1105,6 +1126,11 @@ export default class WorksheetCanvas {
|
|||||||
cellOutlineHandle.style.visibility = "hidden";
|
cellOutlineHandle.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editor.style.left = `${x + 3}px`;
|
||||||
|
editor.style.top = `${y + 3}px`;
|
||||||
|
editor.style.width = `${width - 1}px`;
|
||||||
|
editor.style.height = `${height - 1}px`;
|
||||||
|
|
||||||
// Position the cell outline and clip it
|
// Position the cell outline and clip it
|
||||||
cellOutline.style.left = `${x - padding}px`;
|
cellOutline.style.left = `${x - padding}px`;
|
||||||
cellOutline.style.top = `${y - padding}px`;
|
cellOutline.style.top = `${y - padding}px`;
|
||||||
@@ -1214,6 +1240,52 @@ export default class WorksheetCanvas {
|
|||||||
cellOutlineHandle.style.top = `${handleY - handleHeight / 2}px`;
|
cellOutlineHandle.style.top = `${handleY - handleHeight / 2}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private drawActiveRanges(topLeftCell: Cell, bottomRightCell: Cell): void {
|
||||||
|
const activeRanges = this.workbookState.getActiveRanges();
|
||||||
|
const activeRangesCount = activeRanges.length;
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.setLineDash([2, 2]);
|
||||||
|
for (let rangeIndex = 0; rangeIndex < activeRangesCount; rangeIndex += 1) {
|
||||||
|
const range = activeRanges[rangeIndex];
|
||||||
|
|
||||||
|
const allowedOffset = 1; // to make borders look nicer
|
||||||
|
const minRow = topLeftCell.row - allowedOffset;
|
||||||
|
const maxRow = bottomRightCell.row + allowedOffset;
|
||||||
|
const minColumn = topLeftCell.column - allowedOffset;
|
||||||
|
const maxColumn = bottomRightCell.column + allowedOffset;
|
||||||
|
|
||||||
|
if (
|
||||||
|
minRow <= range.rowEnd &&
|
||||||
|
range.rowStart <= maxRow &&
|
||||||
|
minColumn <= range.columnEnd &&
|
||||||
|
range.columnStart < maxColumn
|
||||||
|
) {
|
||||||
|
// Range in the viewport.
|
||||||
|
const displayRange: typeof range = {
|
||||||
|
...range,
|
||||||
|
rowStart: Math.max(minRow, range.rowStart),
|
||||||
|
rowEnd: Math.min(maxRow, range.rowEnd),
|
||||||
|
columnStart: Math.max(minColumn, range.columnStart),
|
||||||
|
columnEnd: Math.min(maxColumn, range.columnEnd),
|
||||||
|
};
|
||||||
|
const [xStart, yStart] = this.getCoordinatesByCell(
|
||||||
|
displayRange.rowStart,
|
||||||
|
displayRange.columnStart,
|
||||||
|
);
|
||||||
|
const [xEnd, yEnd] = this.getCoordinatesByCell(
|
||||||
|
displayRange.rowEnd + 1,
|
||||||
|
displayRange.columnEnd + 1,
|
||||||
|
);
|
||||||
|
ctx.strokeStyle = range.color;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(xStart, yStart, xEnd - xStart, yEnd - yStart);
|
||||||
|
ctx.fillStyle = hexToRGBA10Percent(range.color);
|
||||||
|
ctx.fillRect(xStart, yStart, xEnd - xStart, yEnd - yStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
renderSheet(): void {
|
renderSheet(): void {
|
||||||
console.time("renderSheet");
|
console.time("renderSheet");
|
||||||
this._renderSheet();
|
this._renderSheet();
|
||||||
@@ -1352,5 +1424,6 @@ export default class WorksheetCanvas {
|
|||||||
|
|
||||||
this.drawCellOutline();
|
this.drawCellOutline();
|
||||||
this.drawExtendToArea();
|
this.drawExtendToArea();
|
||||||
|
this.drawActiveRanges(topLeftCell, bottomRightCell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
243
webapp/src/components/editor/editor.tsx
Normal file
243
webapp/src/components/editor/editor.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// This is the cell editor for IronCalc
|
||||||
|
// It is also the most difficult part of the UX. It is based on an idea of Mateusz Kopec.
|
||||||
|
// There is a hidden texarea and we only show the caret. What we see is a div with the same text content
|
||||||
|
// but in HTML so we can have different colors.
|
||||||
|
// Some keystrokes have different behaviour than a raw HTML text area.
|
||||||
|
// For those cases we capture the keydown event and stop its propagation.
|
||||||
|
// As the editor changes content we need to propagate those changes so the spreadsheet can
|
||||||
|
// mark with colors the active ranges or update the formula in the formula bar
|
||||||
|
|
||||||
|
import type { Model } from "@ironcalc/wasm";
|
||||||
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
type KeyboardEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import type { WorkbookState } from "../workbookState";
|
||||||
|
import getFormulaHTML from "./util";
|
||||||
|
|
||||||
|
const commonCSS: CSSProperties = {
|
||||||
|
fontWeight: "inherit",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
fontSize: "inherit",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
whiteSpace: "pre",
|
||||||
|
width: "100%",
|
||||||
|
padding: 0,
|
||||||
|
lineHeight: "22px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const caretColor = "#FF8899";
|
||||||
|
|
||||||
|
interface EditorOptions {
|
||||||
|
minimalWidth: number | string;
|
||||||
|
minimalHeight: number | string;
|
||||||
|
display: boolean;
|
||||||
|
expand: boolean;
|
||||||
|
originalText: string;
|
||||||
|
onEditEnd: () => void;
|
||||||
|
onTextUpdated: () => void;
|
||||||
|
model: Model;
|
||||||
|
workbookState: WorkbookState;
|
||||||
|
type: "cell" | "formula-bar";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Editor = (options: EditorOptions) => {
|
||||||
|
const {
|
||||||
|
display,
|
||||||
|
expand,
|
||||||
|
minimalHeight,
|
||||||
|
minimalWidth,
|
||||||
|
model,
|
||||||
|
onEditEnd,
|
||||||
|
onTextUpdated,
|
||||||
|
originalText,
|
||||||
|
workbookState,
|
||||||
|
type,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const [width, setWidth] = useState(minimalWidth);
|
||||||
|
const [height, setHeight] = useState(minimalHeight);
|
||||||
|
const [text, setText] = useState(originalText);
|
||||||
|
const [styledFormula, setStyledFormula] = useState(
|
||||||
|
getFormulaHTML(model, text).html,
|
||||||
|
);
|
||||||
|
|
||||||
|
const formulaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const maskRef = useRef<HTMLDivElement>(null);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setText(originalText);
|
||||||
|
setStyledFormula(getFormulaHTML(model, originalText).html);
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.value = originalText;
|
||||||
|
}
|
||||||
|
}, [originalText, model]);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent) => {
|
||||||
|
const { key, shiftKey, altKey } = event;
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (!textarea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case "Enter": {
|
||||||
|
if (altKey) {
|
||||||
|
// new line
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const newText = `${text.slice(0, start)}\n${text.slice(end)}`;
|
||||||
|
setText(newText);
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.setSelectionRange(start + 1, start + 1);
|
||||||
|
}, 0);
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// end edit and select cell bellow
|
||||||
|
setTimeout(() => {
|
||||||
|
const cell = workbookState.getEditingCell();
|
||||||
|
if (cell) {
|
||||||
|
workbookState.clearEditingCell();
|
||||||
|
model.setUserInput(cell.sheet, cell.row, cell.column, cell.text);
|
||||||
|
const sign = shiftKey ? -1 : 1;
|
||||||
|
model.setSelectedCell(cell.row + sign, cell.column);
|
||||||
|
}
|
||||||
|
onEditEnd();
|
||||||
|
}, 0);
|
||||||
|
// event bubbles up
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "Tab": {
|
||||||
|
// end edit and select cell to the right
|
||||||
|
const cell = workbookState.getEditingCell();
|
||||||
|
if (cell) {
|
||||||
|
workbookState.clearEditingCell();
|
||||||
|
model.setUserInput(cell.sheet, cell.row, cell.column, cell.text);
|
||||||
|
const sign = shiftKey ? -1 : 1;
|
||||||
|
model.setSelectedCell(cell.row, cell.column + sign);
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.value = "";
|
||||||
|
setStyledFormula(getFormulaHTML(model, "").html);
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
onEditEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "Escape": {
|
||||||
|
// quit editing without modifying the cell
|
||||||
|
workbookState.clearEditingCell();
|
||||||
|
onEditEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: Arrow keys navigate in Excel
|
||||||
|
case "ArrowRight": {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// We run this in a timeout because the value is not yet in the textarea
|
||||||
|
// since we are capturing the keydown event
|
||||||
|
setTimeout(() => {
|
||||||
|
const value = textarea.value;
|
||||||
|
const styledFormula = getFormulaHTML(model, value);
|
||||||
|
const cell = workbookState.getEditingCell();
|
||||||
|
if (cell) {
|
||||||
|
cell.text = value;
|
||||||
|
workbookState.setEditingCell(cell);
|
||||||
|
|
||||||
|
workbookState.setActiveRanges(styledFormula.activeRanges);
|
||||||
|
setStyledFormula(styledFormula.html);
|
||||||
|
|
||||||
|
onTextUpdated();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[model, text, onEditEnd, onTextUpdated, workbookState],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (display) {
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [display]);
|
||||||
|
|
||||||
|
const onChange = useCallback(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.value = "";
|
||||||
|
setStyledFormula(getFormulaHTML(model, "").html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This happens if the blur hasn't been taken care before by
|
||||||
|
// onclick or onpointerdown events
|
||||||
|
// If we are editing a cell finish that
|
||||||
|
const cell = workbookState.getEditingCell();
|
||||||
|
if (cell) {
|
||||||
|
workbookState.clearEditingCell();
|
||||||
|
model.setUserInput(cell.sheet, cell.row, cell.column, cell.text);
|
||||||
|
}
|
||||||
|
onEditEnd();
|
||||||
|
}, [model, workbookState, onEditEnd]);
|
||||||
|
|
||||||
|
const isCellEditing = workbookState.getEditingCell() !== null;
|
||||||
|
|
||||||
|
const showEditor =
|
||||||
|
isCellEditing && (display || type === "formula-bar") ? "block" : "none";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
overflow: "hidden",
|
||||||
|
display: showEditor,
|
||||||
|
background: "#FFF",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={maskRef}
|
||||||
|
style={{
|
||||||
|
...commonCSS,
|
||||||
|
textAlign: "left",
|
||||||
|
pointerEvents: "none",
|
||||||
|
height,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div ref={formulaRef}>{styledFormula}</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
rows={1}
|
||||||
|
style={{
|
||||||
|
...commonCSS,
|
||||||
|
color: "transparent",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
caretColor,
|
||||||
|
outline: "none",
|
||||||
|
resize: "none",
|
||||||
|
border: "none",
|
||||||
|
height,
|
||||||
|
display: display ? "block" : "none",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
defaultValue={text}
|
||||||
|
spellCheck="false"
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onBlur={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Editor;
|
||||||
189
webapp/src/components/editor/util.tsx
Normal file
189
webapp/src/components/editor/util.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import {
|
||||||
|
type Model,
|
||||||
|
type Range,
|
||||||
|
type Reference,
|
||||||
|
type TokenType,
|
||||||
|
getTokens,
|
||||||
|
} from "@ironcalc/wasm";
|
||||||
|
import type { ActiveRange } from "../workbookState";
|
||||||
|
|
||||||
|
export function tokenIsReferenceType(token: TokenType): token is Reference {
|
||||||
|
return typeof token === "object" && "Reference" in token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tokenIsRangeType(token: TokenType): token is Range {
|
||||||
|
return typeof token === "object" && "Range" in token;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInReferenceMode(text: string, cursor: number): boolean {
|
||||||
|
// FIXME
|
||||||
|
// This is a gross oversimplification
|
||||||
|
// Returns true if both are true:
|
||||||
|
// 1. Cursor is at the end
|
||||||
|
// 2. Last char is one of [',', '(', '+', '*', '-', '/', '<', '>', '=', '&']
|
||||||
|
// This has many false positives like '="1+' and also likely some false negatives
|
||||||
|
// The right way of doing this is to have a partial parse of the formula tree
|
||||||
|
// and check if the next token could be a reference
|
||||||
|
if (!text.startsWith("=")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (text === "=") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const l = text.length;
|
||||||
|
const chars = [",", "(", "+", "*", "-", "/", "<", ">", "=", "&"];
|
||||||
|
if (cursor === l && chars.includes(text[l - 1])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IronCalc Color Palette
|
||||||
|
export function getColor(index: number, alpha = 1): string {
|
||||||
|
const colors = [
|
||||||
|
{
|
||||||
|
name: "Cyan",
|
||||||
|
rgba: [89, 185, 188, 1],
|
||||||
|
hex: "#59B9BC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Flamingo",
|
||||||
|
rgba: [236, 87, 83, 1],
|
||||||
|
hex: "#EC5753",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#3358B7",
|
||||||
|
rgba: [51, 88, 183, 1],
|
||||||
|
name: "Blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#F8CD3C",
|
||||||
|
rgba: [248, 205, 60, 1],
|
||||||
|
name: "Yellow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#3BB68A",
|
||||||
|
rgba: [59, 182, 138, 1],
|
||||||
|
name: "Emerald",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#523E93",
|
||||||
|
rgba: [82, 62, 147, 1],
|
||||||
|
name: "Violet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#A23C52",
|
||||||
|
rgba: [162, 60, 82, 1],
|
||||||
|
name: "Burgundy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#8CB354",
|
||||||
|
rgba: [162, 60, 82, 1],
|
||||||
|
name: "Wasabi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#D03627",
|
||||||
|
rgba: [208, 54, 39, 1],
|
||||||
|
name: "Red",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#1B717E",
|
||||||
|
rgba: [27, 113, 126, 1],
|
||||||
|
name: "Teal",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (alpha === 1) {
|
||||||
|
return colors[index % 10].hex;
|
||||||
|
}
|
||||||
|
const { rgba } = colors[index % 10];
|
||||||
|
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormulaHTML(
|
||||||
|
model: Model,
|
||||||
|
text: string,
|
||||||
|
): { html: JSX.Element[]; activeRanges: ActiveRange[] } {
|
||||||
|
let html: JSX.Element[] = [];
|
||||||
|
const activeRanges: ActiveRange[] = [];
|
||||||
|
let colorCount = 0;
|
||||||
|
if (text.startsWith("=")) {
|
||||||
|
const formula = text.slice(1);
|
||||||
|
const tokens = getTokens(formula);
|
||||||
|
const tokenCount = tokens.length;
|
||||||
|
const usedColors: Record<string, string> = {};
|
||||||
|
const sheet = model.getSelectedSheet();
|
||||||
|
const sheetList = model.getWorksheetsProperties().map((s) => s.name);
|
||||||
|
for (let index = 0; index < tokenCount; index += 1) {
|
||||||
|
const { token, start, end } = tokens[index];
|
||||||
|
if (tokenIsReferenceType(token)) {
|
||||||
|
const { sheet: refSheet, row, column } = token.Reference;
|
||||||
|
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
|
||||||
|
const key = `${sheetIndex}-${row}-${column}`;
|
||||||
|
let color = usedColors[key];
|
||||||
|
if (!color) {
|
||||||
|
color = getColor(colorCount);
|
||||||
|
usedColors[key] = color;
|
||||||
|
colorCount += 1;
|
||||||
|
}
|
||||||
|
html.push(
|
||||||
|
<span key={index} style={{ color }}>
|
||||||
|
{formula.slice(start, end)}
|
||||||
|
</span>,
|
||||||
|
);
|
||||||
|
activeRanges.push({
|
||||||
|
sheet: sheetIndex,
|
||||||
|
rowStart: row,
|
||||||
|
columnStart: column,
|
||||||
|
rowEnd: row,
|
||||||
|
columnEnd: column,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
} else if (tokenIsRangeType(token)) {
|
||||||
|
let {
|
||||||
|
sheet: refSheet,
|
||||||
|
left: { row: rowStart, column: columnStart },
|
||||||
|
right: { row: rowEnd, column: columnEnd },
|
||||||
|
} = token.Range;
|
||||||
|
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
|
||||||
|
|
||||||
|
const key = `${sheetIndex}-${rowStart}-${columnStart}:${rowEnd}-${columnEnd}`;
|
||||||
|
let color = usedColors[key];
|
||||||
|
if (!color) {
|
||||||
|
color = getColor(colorCount);
|
||||||
|
usedColors[key] = color;
|
||||||
|
colorCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowStart > rowEnd) {
|
||||||
|
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||||
|
}
|
||||||
|
if (columnStart > columnEnd) {
|
||||||
|
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||||
|
}
|
||||||
|
html.push(
|
||||||
|
<span key={index} style={{ color }}>
|
||||||
|
{formula.slice(start, end)}
|
||||||
|
</span>,
|
||||||
|
);
|
||||||
|
colorCount += 1;
|
||||||
|
|
||||||
|
activeRanges.push({
|
||||||
|
sheet: sheetIndex,
|
||||||
|
rowStart,
|
||||||
|
columnStart,
|
||||||
|
rowEnd,
|
||||||
|
columnEnd,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html.push(<span key={index}>{formula.slice(start, end)}</span>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html = [<span key="equals">=</span>].concat(html);
|
||||||
|
} else {
|
||||||
|
html = [<span key="single">{text}</span>];
|
||||||
|
}
|
||||||
|
return { html, activeRanges };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getFormulaHTML;
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
TextField,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
interface FormulaDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
close: () => void;
|
|
||||||
onFormulaChanged: (name: string) => void;
|
|
||||||
defaultFormula: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FormulaDialog = (properties: FormulaDialogProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [formula, setFormula] = useState(properties.defaultFormula);
|
|
||||||
return (
|
|
||||||
<Dialog open={properties.isOpen} onClose={properties.close}>
|
|
||||||
<DialogTitle>{t("formula_input.title")}</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<TextField
|
|
||||||
defaultValue={formula}
|
|
||||||
label={t("formula_input.label")}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onChange={(event) => {
|
|
||||||
setFormula(event.target.value);
|
|
||||||
}}
|
|
||||||
spellCheck="false"
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
properties.onFormulaChanged(formula);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("formula_input.update")}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +1,39 @@
|
|||||||
|
import type { Model } from "@ironcalc/wasm";
|
||||||
import { Button, styled } from "@mui/material";
|
import { Button, styled } from "@mui/material";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Fx } from "../icons";
|
import { Fx } from "../icons";
|
||||||
import { FormulaDialog } from "./formulaDialog";
|
import Editor from "./editor/editor";
|
||||||
|
import type { WorkbookState } from "./workbookState";
|
||||||
|
|
||||||
type FormulaBarProps = {
|
type FormulaBarProps = {
|
||||||
cellAddress: string;
|
cellAddress: string;
|
||||||
formulaValue: string;
|
formulaValue: string;
|
||||||
onChange: (value: string) => void;
|
model: Model;
|
||||||
|
workbookState: WorkbookState;
|
||||||
|
onChange: () => void;
|
||||||
|
onTextUpdated: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formulaBarHeight = 30;
|
const formulaBarHeight = 30;
|
||||||
const headerColumnWidth = 30;
|
const headerColumnWidth = 35;
|
||||||
|
|
||||||
function FormulaBar(properties: FormulaBarProps) {
|
function FormulaBar(properties: FormulaBarProps) {
|
||||||
const [formulaDialogOpen, setFormulaDialogOpen] = useState(false);
|
const {
|
||||||
const handleCloseFormulaDialog = () => {
|
cellAddress,
|
||||||
setFormulaDialogOpen(false);
|
formulaValue,
|
||||||
};
|
model,
|
||||||
|
onChange,
|
||||||
|
onTextUpdated,
|
||||||
|
workbookState,
|
||||||
|
} = properties;
|
||||||
|
|
||||||
|
const [display, setDisplay] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<AddressContainer>
|
<AddressContainer>
|
||||||
<CellBarAddress>{properties.cellAddress}</CellBarAddress>
|
<CellBarAddress>{cellAddress}</CellBarAddress>
|
||||||
<StyledButton>
|
<StyledButton>
|
||||||
<ChevronDown />
|
<ChevronDown />
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
@@ -32,23 +43,40 @@ function FormulaBar(properties: FormulaBarProps) {
|
|||||||
<FormulaSymbolButton>
|
<FormulaSymbolButton>
|
||||||
<Fx />
|
<Fx />
|
||||||
</FormulaSymbolButton>
|
</FormulaSymbolButton>
|
||||||
<Editor
|
<EditorWrapper
|
||||||
onClick={() => {
|
onClick={(event) => {
|
||||||
setFormulaDialogOpen(true);
|
const [sheet, row, column] = model.getSelectedCell();
|
||||||
|
workbookState.setEditingCell({
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
text: formulaValue,
|
||||||
|
cursor: 0,
|
||||||
|
focus: "formula-bar",
|
||||||
|
activeRanges: [],
|
||||||
|
});
|
||||||
|
setDisplay(true);
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{properties.formulaValue}
|
<Editor
|
||||||
</Editor>
|
minimalWidth={"100%"}
|
||||||
|
minimalHeight={"100%"}
|
||||||
|
display={display}
|
||||||
|
expand={false}
|
||||||
|
originalText={formulaValue}
|
||||||
|
model={model}
|
||||||
|
workbookState={workbookState}
|
||||||
|
onEditEnd={() => {
|
||||||
|
setDisplay(false);
|
||||||
|
onChange();
|
||||||
|
}}
|
||||||
|
onTextUpdated={onTextUpdated}
|
||||||
|
type="formula-bar"
|
||||||
|
/>
|
||||||
|
</EditorWrapper>
|
||||||
</FormulaContainer>
|
</FormulaContainer>
|
||||||
<FormulaDialog
|
|
||||||
isOpen={formulaDialogOpen}
|
|
||||||
close={handleCloseFormulaDialog}
|
|
||||||
defaultFormula={properties.formulaValue}
|
|
||||||
onFormulaChanged={(newName) => {
|
|
||||||
properties.onChange(newName);
|
|
||||||
setFormulaDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,7 +141,7 @@ const CellBarAddress = styled("div")`
|
|||||||
text-align: "center";
|
text-align: "center";
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Editor = styled("div")`
|
const EditorWrapper = styled("div")`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@@ -127,6 +155,7 @@ const Editor = styled("div")`
|
|||||||
span {
|
span {
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
}
|
}
|
||||||
|
font-family: monospace;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default FormulaBar;
|
export default FormulaBar;
|
||||||
|
|||||||
@@ -143,11 +143,31 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
},
|
},
|
||||||
onEditKeyPressStart: (initText: string): void => {
|
onEditKeyPressStart: (initText: string): void => {
|
||||||
console.log(initText);
|
const { sheet, row, column } = model.getSelectedView();
|
||||||
throw new Error("Function not implemented.");
|
workbookState.setEditingCell({
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
text: initText,
|
||||||
|
cursor: 0,
|
||||||
|
focus: "cell",
|
||||||
|
activeRanges: [],
|
||||||
|
});
|
||||||
|
setRedrawId((id) => id + 1);
|
||||||
},
|
},
|
||||||
onCellEditStart: (): void => {
|
onCellEditStart: (): void => {
|
||||||
throw new Error("Function not implemented.");
|
const { sheet, row, column } = model.getSelectedView();
|
||||||
|
const text = model.getCellContent(sheet, row, column);
|
||||||
|
workbookState.setEditingCell({
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
text,
|
||||||
|
cursor: text.length,
|
||||||
|
focus: "cell",
|
||||||
|
activeRanges: [],
|
||||||
|
});
|
||||||
|
setRedrawId((id) => id + 1);
|
||||||
},
|
},
|
||||||
onBold: () => {
|
onBold: () => {
|
||||||
const { sheet, row, column } = model.getSelectedView();
|
const { sheet, row, column } = model.getSelectedView();
|
||||||
@@ -237,7 +257,9 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
if (!rootRef.current) {
|
if (!rootRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rootRef.current.focus();
|
if (!workbookState.getEditingCell()) {
|
||||||
|
rootRef.current.focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -251,12 +273,25 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
{ rowStart, rowEnd, columnStart, columnEnd },
|
{ rowStart, rowEnd, columnStart, columnEnd },
|
||||||
{ row, column },
|
{ row, column },
|
||||||
);
|
);
|
||||||
const formulaValue = model.getCellContent(sheet, row, column);
|
const formulaValue = (() => {
|
||||||
|
const cell = workbookState.getEditingCell();
|
||||||
|
if (cell) {
|
||||||
|
return cell.text;
|
||||||
|
}
|
||||||
|
return model.getCellContent(sheet, row, column);
|
||||||
|
})();
|
||||||
|
|
||||||
const style = model.getCellStyle(sheet, row, column);
|
const style = model.getCellStyle(sheet, row, column);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container ref={rootRef} onKeyDown={onKeyDown} tabIndex={0}>
|
<Container
|
||||||
|
ref={rootRef}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
rootRef.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
canUndo={model.canUndo()}
|
canUndo={model.canUndo()}
|
||||||
canRedo={model.canRedo()}
|
canRedo={model.canRedo()}
|
||||||
@@ -313,10 +348,15 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
<FormulaBar
|
<FormulaBar
|
||||||
cellAddress={cellAddress}
|
cellAddress={cellAddress}
|
||||||
formulaValue={formulaValue}
|
formulaValue={formulaValue}
|
||||||
onChange={(value) => {
|
onChange={() => {
|
||||||
model.setUserInput(sheet, row, column, value);
|
setRedrawId((id) => id + 1);
|
||||||
|
rootRef.current?.focus();
|
||||||
|
}}
|
||||||
|
onTextUpdated={() => {
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
}}
|
}}
|
||||||
|
model={model}
|
||||||
|
workbookState={workbookState}
|
||||||
/>
|
/>
|
||||||
<Worksheet
|
<Worksheet
|
||||||
model={model}
|
model={model}
|
||||||
@@ -325,6 +365,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Navigation
|
<Navigation
|
||||||
sheets={info}
|
sheets={info}
|
||||||
selectedIndex={model.getSelectedSheet()}
|
selectedIndex={model.getSelectedSheet()}
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
// This are properties of the workbook that are not permanently stored
|
||||||
|
// They only happen at 'runtime' while the workbook is being used:
|
||||||
|
//
|
||||||
|
// * What are we editing
|
||||||
|
// * Are we copying styles?
|
||||||
|
// * Are we extending a cell? (by pulling the cell outline handle down, for instance)
|
||||||
|
//
|
||||||
|
// Editing the cell is the most complex operation.
|
||||||
|
//
|
||||||
|
// * What cell are we editing?
|
||||||
|
// * Are we doing that from the cell editor or the formula editor?
|
||||||
|
// * What is the text content of the cell right now
|
||||||
|
// * The active ranges can technically be computed from the text.
|
||||||
|
// Those are the ranges or cells that appear in the formula
|
||||||
|
|
||||||
import type { CellStyle } from "@ironcalc/wasm";
|
import type { CellStyle } from "@ironcalc/wasm";
|
||||||
|
|
||||||
export enum AreaType {
|
export enum AreaType {
|
||||||
@@ -15,15 +30,44 @@ export interface Area {
|
|||||||
columnEnd: number;
|
columnEnd: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Active ranges are ranges in the sheet that are highlighted when editing a formula
|
||||||
|
export interface ActiveRange {
|
||||||
|
sheet: number;
|
||||||
|
rowStart: number;
|
||||||
|
rowEnd: number;
|
||||||
|
columnStart: number;
|
||||||
|
columnEnd: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Focus = "cell" | "formula-bar";
|
||||||
|
|
||||||
|
// The cell that we are editing
|
||||||
|
export interface EditingCell {
|
||||||
|
sheet: number;
|
||||||
|
row: number;
|
||||||
|
column: number;
|
||||||
|
// raw text in the editor
|
||||||
|
text: string;
|
||||||
|
// position of the cursor
|
||||||
|
cursor: number;
|
||||||
|
focus: Focus;
|
||||||
|
activeRanges: ActiveRange[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Those are styles that are copied
|
||||||
type AreaStyles = CellStyle[][];
|
type AreaStyles = CellStyle[][];
|
||||||
|
|
||||||
export class WorkbookState {
|
export class WorkbookState {
|
||||||
private extendToArea: Area | null;
|
private extendToArea: Area | null;
|
||||||
private copyStyles: AreaStyles | null;
|
private copyStyles: AreaStyles | null;
|
||||||
|
private cell: EditingCell | null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// the extendTo area is the area we are covering
|
||||||
this.extendToArea = null;
|
this.extendToArea = null;
|
||||||
this.copyStyles = null;
|
this.copyStyles = null;
|
||||||
|
this.cell = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtendToArea(): Area | null {
|
getExtendToArea(): Area | null {
|
||||||
@@ -45,4 +89,41 @@ export class WorkbookState {
|
|||||||
getCopyStyles(): AreaStyles | null {
|
getCopyStyles(): AreaStyles | null {
|
||||||
return this.copyStyles;
|
return this.copyStyles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveRanges(activeRanges: ActiveRange[]) {
|
||||||
|
if (!this.cell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cell.activeRanges = activeRanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveRanges(): ActiveRange[] {
|
||||||
|
return this.cell?.activeRanges || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditingCell(): EditingCell | null {
|
||||||
|
return this.cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditingCell(cell: EditingCell) {
|
||||||
|
this.cell = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearEditingCell() {
|
||||||
|
this.cell = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCellEditorActive(): boolean {
|
||||||
|
if (this.cell) {
|
||||||
|
return this.cell.focus === "cell";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFormulaEditorActive(): boolean {
|
||||||
|
if (this.cell) {
|
||||||
|
return this.cell.focus === "formula-bar";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
outlineColor,
|
outlineColor,
|
||||||
} from "./WorksheetCanvas/constants";
|
} from "./WorksheetCanvas/constants";
|
||||||
import WorksheetCanvas from "./WorksheetCanvas/worksheetCanvas";
|
import WorksheetCanvas from "./WorksheetCanvas/worksheetCanvas";
|
||||||
|
import Editor from "./editor/editor";
|
||||||
import type { Cell } from "./types";
|
import type { Cell } from "./types";
|
||||||
import usePointer from "./usePointer";
|
import usePointer from "./usePointer";
|
||||||
import { AreaType, type WorkbookState } from "./workbookState";
|
import { AreaType, type WorkbookState } from "./workbookState";
|
||||||
@@ -32,7 +33,8 @@ function Worksheet(props: {
|
|||||||
|
|
||||||
const worksheetElement = useRef<HTMLDivElement>(null);
|
const worksheetElement = useRef<HTMLDivElement>(null);
|
||||||
const scrollElement = useRef<HTMLDivElement>(null);
|
const scrollElement = useRef<HTMLDivElement>(null);
|
||||||
// const rootElement = useRef<HTMLDivElement>(null);
|
|
||||||
|
const editorElement = useRef<HTMLDivElement>(null);
|
||||||
const spacerElement = useRef<HTMLDivElement>(null);
|
const spacerElement = useRef<HTMLDivElement>(null);
|
||||||
const cellOutline = useRef<HTMLDivElement>(null);
|
const cellOutline = useRef<HTMLDivElement>(null);
|
||||||
const areaOutline = useRef<HTMLDivElement>(null);
|
const areaOutline = useRef<HTMLDivElement>(null);
|
||||||
@@ -45,8 +47,12 @@ function Worksheet(props: {
|
|||||||
|
|
||||||
const ignoreScrollEventRef = useRef(false);
|
const ignoreScrollEventRef = useRef(false);
|
||||||
|
|
||||||
|
const [display, setDisplay] = useState(false);
|
||||||
|
const [originalText, setOriginalText] = useState("");
|
||||||
|
|
||||||
const { model, workbookState, refresh } = props;
|
const { model, workbookState, refresh } = props;
|
||||||
const [clientWidth, clientHeight] = useWindowSize();
|
const [clientWidth, clientHeight] = useWindowSize();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvasRef = canvasElement.current;
|
const canvasRef = canvasElement.current;
|
||||||
const columnGuideRef = columnResizeGuide.current;
|
const columnGuideRef = columnResizeGuide.current;
|
||||||
@@ -58,6 +64,7 @@ function Worksheet(props: {
|
|||||||
const handle = cellOutlineHandle.current;
|
const handle = cellOutlineHandle.current;
|
||||||
const area = areaOutline.current;
|
const area = areaOutline.current;
|
||||||
const extendTo = extendToOutline.current;
|
const extendTo = extendToOutline.current;
|
||||||
|
const editor = editorElement.current;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!canvasRef ||
|
!canvasRef ||
|
||||||
@@ -69,7 +76,8 @@ function Worksheet(props: {
|
|||||||
!handle ||
|
!handle ||
|
||||||
!area ||
|
!area ||
|
||||||
!extendTo ||
|
!extendTo ||
|
||||||
!scrollElement.current
|
!scrollElement.current ||
|
||||||
|
!editor
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
model.setWindowWidth(clientWidth - 37);
|
model.setWindowWidth(clientWidth - 37);
|
||||||
@@ -88,6 +96,7 @@ function Worksheet(props: {
|
|||||||
cellOutlineHandle: handle,
|
cellOutlineHandle: handle,
|
||||||
areaOutline: area,
|
areaOutline: area,
|
||||||
extendToOutline: extendTo,
|
extendToOutline: extendTo,
|
||||||
|
editor: editor,
|
||||||
},
|
},
|
||||||
onColumnWidthChanges(sheet, column, width) {
|
onColumnWidthChanges(sheet, column, width) {
|
||||||
model.setColumnWidth(sheet, column, width);
|
model.setColumnWidth(sheet, column, width);
|
||||||
@@ -100,7 +109,7 @@ function Worksheet(props: {
|
|||||||
});
|
});
|
||||||
const scrollX = model.getScrollX();
|
const scrollX = model.getScrollX();
|
||||||
const scrollY = model.getScrollY();
|
const scrollY = model.getScrollY();
|
||||||
const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000]; //canvas.getSheetDimensions();
|
const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000];
|
||||||
if (spacerElement.current) {
|
if (spacerElement.current) {
|
||||||
spacerElement.current.style.height = `${sheetHeight}px`;
|
spacerElement.current.style.height = `${sheetHeight}px`;
|
||||||
spacerElement.current.style.width = `${sheetWidth}px`;
|
spacerElement.current.style.width = `${sheetWidth}px`;
|
||||||
@@ -127,10 +136,6 @@ function Worksheet(props: {
|
|||||||
worksheetCanvas.current = canvas;
|
worksheetCanvas.current = canvas;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sheetNames = model
|
|
||||||
.getWorksheetsProperties()
|
|
||||||
.map((s: { name: string }) => s.name);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
onPointerMove,
|
onPointerMove,
|
||||||
onPointerDown,
|
onPointerDown,
|
||||||
@@ -311,20 +316,55 @@ function Worksheet(props: {
|
|||||||
className="sheet-container"
|
className="sheet-container"
|
||||||
ref={worksheetElement}
|
ref={worksheetElement}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
|
// if we are editing a cell finish that
|
||||||
|
const cell = workbookState.getEditingCell();
|
||||||
|
if (cell) {
|
||||||
|
workbookState.clearEditingCell();
|
||||||
|
model.setUserInput(cell.sheet, cell.row, cell.column, cell.text);
|
||||||
|
}
|
||||||
onPointerDown(event);
|
onPointerDown(event);
|
||||||
}}
|
}}
|
||||||
onPointerMove={onPointerMove}
|
onPointerMove={onPointerMove}
|
||||||
onPointerUp={onPointerUp}
|
onPointerUp={onPointerUp}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
const { sheet, row, column } = model.getSelectedView();
|
const { sheet, row, column } = model.getSelectedView();
|
||||||
const _text = model.getCellContent(sheet, row, column) || "";
|
const text = model.getCellContent(sheet, row, column) || "";
|
||||||
// TODO
|
workbookState.setEditingCell({
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
text,
|
||||||
|
cursor: 0,
|
||||||
|
focus: "cell",
|
||||||
|
activeRanges: [],
|
||||||
|
});
|
||||||
|
setDisplay(true);
|
||||||
|
setOriginalText(text);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SheetCanvas ref={canvasElement} />
|
<SheetCanvas ref={canvasElement} />
|
||||||
<CellOutline ref={cellOutline} />
|
<CellOutline ref={cellOutline} />
|
||||||
|
<EditorWrapper ref={editorElement}>
|
||||||
|
<Editor
|
||||||
|
minimalWidth={"100%"}
|
||||||
|
minimalHeight={"100%"}
|
||||||
|
display={workbookState.getEditingCell()?.focus === "cell"}
|
||||||
|
expand={true}
|
||||||
|
originalText={workbookState.getEditingCell()?.text || originalText}
|
||||||
|
onEditEnd={(): void => {
|
||||||
|
setDisplay(false);
|
||||||
|
props.refresh();
|
||||||
|
}}
|
||||||
|
onTextUpdated={(): void => {
|
||||||
|
props.refresh();
|
||||||
|
}}
|
||||||
|
model={model}
|
||||||
|
workbookState={workbookState}
|
||||||
|
type={"cell"}
|
||||||
|
/>
|
||||||
|
</EditorWrapper>
|
||||||
<AreaOutline ref={areaOutline} />
|
<AreaOutline ref={areaOutline} />
|
||||||
<ExtendToOutline ref={extendToOutline} />
|
<ExtendToOutline ref={extendToOutline} />
|
||||||
<CellOutlineHandle
|
<CellOutlineHandle
|
||||||
@@ -461,4 +501,21 @@ const ExtendToOutline = styled("div")`
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const EditorWrapper = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
border-width: 0px;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
vertical-align: bottom;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
span {
|
||||||
|
min-width: 1px;
|
||||||
|
}
|
||||||
|
font-family: monospace;
|
||||||
|
`;
|
||||||
|
|
||||||
export default Worksheet;
|
export default Worksheet;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
body {
|
body {
|
||||||
|
inset: 0px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user