FIX: Diverse fixes to the editor

* Editor now expands as you write
* You can switch between the formula bar and cell editor
* While editing in the formula bar you see the results in the editor
* Give Mateusz more credit
This commit is contained in:
Nicolás Hatcher
2024-10-12 13:42:33 +02:00
committed by Nicolás Hatcher Andrés
parent 248ef66e7c
commit 585e594d8d
7 changed files with 170 additions and 93 deletions

View File

@@ -16,3 +16,6 @@ export const outlineBackgroundColor = "#F2994A1A";
export const LAST_COLUMN = 16_384;
export const LAST_ROW = 1_048_576;
export const ROW_HEIGH_SCALE = 1.25;
export const COLUMN_WIDTH_SCALE = 1.25;

View File

@@ -3,8 +3,10 @@ import { columnNameFromNumber } from "@ironcalc/wasm";
import type { Cell } from "../types";
import type { WorkbookState } from "../workbookState";
import {
COLUMN_WIDTH_SCALE,
LAST_COLUMN,
LAST_ROW,
ROW_HEIGH_SCALE,
defaultTextColor,
gridColor,
gridSeparatorColor,
@@ -1089,35 +1091,76 @@ export default class WorksheetCanvas {
}
private getColumnWidth(sheet: number, column: number): number {
return Math.round(this.model.getColumnWidth(sheet, column) * 1.25);
return Math.round(
this.model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE,
);
}
private getRowHeight(sheet: number, row: number): number {
return Math.round(this.model.getRowHeight(sheet, row) * 1.25);
return Math.round(this.model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE);
}
private drawCellEditor(): void {
const cell = this.workbookState.getEditingCell();
const [selectedSheet, selectedRow, selectedColumn] =
this.model.getSelectedCell();
const { editor } = this;
if (!cell || cell.sheet !== selectedSheet) {
// If the editing cell is not in the same sheet as the selected sheet
// we take the editor out of view
editor.style.left = "-9999px";
editor.style.top = "-9999px";
return;
}
const { sheet, row, column } = cell;
// const style = this.model.getCellStyle(
// selectedSheet,
// selectedRow,
// selectedColumn
// );
// cellOutline.style.fontWeight = style.font.b ? "bold" : "normal";
// cellOutline.style.fontStyle = style.font.i ? "italic" : "normal";
// cellOutline.style.backgroundColor = style.fill.fg_color;
// TODO: Should we add the same color as the text?
// Only if it is not a formula?
// cellOutline.style.color = style.font.color;
const [x, y] = this.getCoordinatesByCell(row, column);
const padding = -1;
const width = cell.editorWidth + 2 * padding;
const height = cell.editorHeight + 2 * padding;
// const width =
// this.getColumnWidth(sheet, column) + 2 * padding;
// const height = this.getRowHeight(sheet, row) + 2 * padding;
editor.style.left = `${x}px`;
editor.style.top = `${y}px`;
editor.style.width = `${width - 1}px`;
editor.style.height = `${height - 1}px`;
}
private drawCellOutline(): void {
const { cellOutline, areaOutline, cellOutlineHandle } = this;
if (this.workbookState.getEditingCell()) {
cellOutline.style.visibility = "hidden";
cellOutlineHandle.style.visibility = "hidden";
areaOutline.style.visibility = "hidden";
return;
}
cellOutline.style.visibility = "visible";
cellOutlineHandle.style.visibility = "visible";
areaOutline.style.visibility = "visible";
const [selectedSheet, selectedRow, selectedColumn] =
this.model.getSelectedCell();
const { topLeftCell } = this.getVisibleCells();
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
const [x, y] = this.getCoordinatesByCell(selectedRow, selectedColumn);
const style = this.model.getCellStyle(
selectedSheet,
selectedRow,
selectedColumn,
);
const padding = -1;
const width =
this.getColumnWidth(selectedSheet, selectedColumn) + 2 * padding;
const height = this.getRowHeight(selectedSheet, selectedRow) + 2 * padding;
const { cellOutline, editor, areaOutline, cellOutlineHandle } = this;
const cellEditing = null;
cellOutline.style.visibility = "visible";
cellOutlineHandle.style.visibility = "visible";
if (
(selectedRow < topLeftCell.row && selectedRow > frozenRows) ||
(selectedColumn < topLeftCell.column && selectedColumn > frozenColumns)
@@ -1126,19 +1169,6 @@ export default class WorksheetCanvas {
cellOutlineHandle.style.visibility = "hidden";
}
if (this.workbookState.getEditingCell()?.sheet === selectedSheet) {
editor.style.left = `${x + 3}px`;
editor.style.top = `${y + 3}px`;
} else {
// If the editing cell is not in the same sheet as the selected sheet
// we take the editor out of view
editor.style.left = "-9999px";
editor.style.top = "-9999px";
}
editor.style.width = `${width - 1}px`;
editor.style.height = `${height - 1}px`;
// Position the cell outline and clip it
cellOutline.style.left = `${x - padding - 1}px`;
cellOutline.style.top = `${y - padding - 1}px`;
@@ -1151,16 +1181,9 @@ export default class WorksheetCanvas {
// New properties
cellOutline.style.width = `${width}px`;
cellOutline.style.height = `${height}px`;
if (cellEditing) {
cellOutline.style.fontWeight = style.font.b ? "bold" : "normal";
cellOutline.style.fontStyle = style.font.i ? "italic" : "normal";
// cellOutline.style.backgroundColor = style.fill.fg_color;
// TODO: Should we add the same color as the text?
// Only if it is not a formula?
// cellOutline.style.color = style.font.color;
} else {
cellOutline.style.background = "none";
}
cellOutline.style.background = "none";
// border is 2px so line-height must be height - 4
cellOutline.style.lineHeight = `${height - 4}px`;
let {
@@ -1236,11 +1259,6 @@ export default class WorksheetCanvas {
}
}
// draw the handle
if (cellEditing !== null) {
cellOutlineHandle.style.visibility = "hidden";
return;
}
const handleBBox = cellOutlineHandle.getBoundingClientRect();
const handleWidth = handleBBox.width;
const handleHeight = handleBBox.height;
@@ -1442,6 +1460,7 @@ export default class WorksheetCanvas {
context.fillRect(0, 0, headerColumnWidth, headerRowHeight);
this.drawCellOutline();
this.drawCellEditor();
this.drawExtendToArea();
this.drawActiveRanges(topLeftCell, bottomRightCell);
}

View File

@@ -1,5 +1,6 @@
// 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.
// It is also the single most difficult part of the UX. It is based on an idea of the
// celebrated Polish developer 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.
@@ -63,10 +64,6 @@ const commonCSS: CSSProperties = {
const caretColor = "#FF8899";
interface EditorOptions {
minimalWidth: number | string;
minimalHeight: number | string;
display: boolean;
expand: boolean;
originalText: string;
onEditEnd: () => void;
onTextUpdated: () => void;
@@ -76,21 +73,9 @@ interface EditorOptions {
}
const Editor = (options: EditorOptions) => {
const {
display,
expand,
minimalHeight,
minimalWidth,
model,
onEditEnd,
onTextUpdated,
originalText,
workbookState,
type,
} = options;
const { 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,
@@ -120,10 +105,27 @@ const Editor = (options: EditorOptions) => {
});
useEffect(() => {
if (display) {
textareaRef.current?.focus();
if (text.length === 0) {
// noop
}
}, [display]);
}, [text]);
useEffect(() => {
const cell = workbookState.getEditingCell();
if (text.length === 0) {
// noop, just to keep the linter happy
}
if (!cell) {
return;
}
const { editorWidth, editorHeight } = cell;
if (formulaRef.current) {
const scrollWidth = formulaRef.current.scrollWidth;
if (scrollWidth > editorWidth - 5) {
cell.editorWidth = scrollWidth + 10;
}
}
}, [text, workbookState]);
const onChange = useCallback(() => {
const textarea = textareaRef.current;
@@ -137,7 +139,7 @@ const Editor = (options: EditorOptions) => {
cell.cursorStart = textarea.selectionStart;
cell.cursorEnd = textarea.selectionEnd;
const styledFormula = getFormulaHTML(model, cell.text, "");
if (value === "") {
if (value === "" && type === "cell") {
// When we delete the content of a cell we jump to accept mode
cell.mode = "accept";
}
@@ -152,9 +154,15 @@ const Editor = (options: EditorOptions) => {
// Should we stop propagations?
// event.stopPropagation();
// event.preventDefault();
}, [workbookState, model, onTextUpdated]);
}, [workbookState, model, onTextUpdated, type]);
const onBlur = useCallback(() => {
const cell = workbookState.getEditingCell();
if (type !== cell?.focus) {
// If the onBlur event is called because we switch from the cell editor to the formula editor
// or vice versa, do nothing
return;
}
if (textareaRef.current) {
textareaRef.current.value = "";
setStyledFormula(getFormulaHTML(model, "", "").html);
@@ -163,7 +171,6 @@ const Editor = (options: EditorOptions) => {
// 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) {
model.setUserInput(
cell.sheet,
@@ -174,19 +181,25 @@ const Editor = (options: EditorOptions) => {
workbookState.clearEditingCell();
}
onEditEnd();
}, [model, workbookState, onEditEnd]);
}, [model, workbookState, onEditEnd, type]);
const isCellEditing = workbookState.getEditingCell() !== null;
const cell = workbookState.getEditingCell();
const showEditor =
(isCellEditing && display) || type === "formula-bar" ? "block" : "none";
// If we are the focus, the get it
if (cell) {
if (type === cell.focus) {
textareaRef.current?.focus();
}
}
const showEditor = cell !== null || type === "formula-bar" ? "block" : "none";
return (
<div
style={{
position: "relative",
width,
height,
width: "100%",
height: "100%",
overflow: "hidden",
display: showEditor,
background: "#FFF",
@@ -198,10 +211,17 @@ const Editor = (options: EditorOptions) => {
...commonCSS,
textAlign: "left",
pointerEvents: "none",
height,
height: "100%",
}}
>
<div ref={formulaRef}>{styledFormula}</div>
<div
style={{
display: "inline-block",
}}
ref={formulaRef}
>
{styledFormula}
</div>
</div>
<textarea
ref={textareaRef}
@@ -214,25 +234,34 @@ const Editor = (options: EditorOptions) => {
outline: "none",
resize: "none",
border: "none",
height,
height: "100%",
overflow: "hidden",
alignContent: "baseline",
}}
defaultValue={text}
spellCheck="false"
onKeyDown={onKeyDown}
onChange={onChange}
onBlur={onBlur}
onClick={(event) => {
// Prevents this from bubbling up and focusing on the spreadsheet
if (isCellEditing && type === "cell") {
const cell = workbookState.getEditingCell();
if (cell) {
cell.mode = "edit";
workbookState.setEditingCell(cell);
}
onPointerDown={(event) => {
// We are either clicking in the same cell we are editing,
// in which case we just change the mode to edit, or we click
// in a different editor, in which case we switch the focus
const cell = workbookState.getEditingCell();
if (cell) {
// We make sure the mode is edit
cell.mode = "edit";
cell.focus = type;
workbookState.setEditingCell(cell);
event.stopPropagation();
}
}}
onScroll={() => {
if (maskRef.current && textareaRef.current) {
maskRef.current.style.left = `-${textareaRef.current.scrollLeft}px`;
maskRef.current.style.top = `-${textareaRef.current.scrollTop}px`;
}
}}
/>
</div>
);

View File

@@ -2,6 +2,10 @@ import type { Model } from "@ironcalc/wasm";
import { Button, styled } from "@mui/material";
import { ChevronDown } from "lucide-react";
import { Fx } from "../icons";
import {
COLUMN_WIDTH_SCALE,
ROW_HEIGH_SCALE,
} from "./WorksheetCanvas/constants";
import { FORMULA_BAR_HEIGH } from "./constants";
import Editor from "./editor/editor";
import type { WorkbookState } from "./workbookState";
@@ -42,6 +46,10 @@ function FormulaBar(properties: FormulaBarProps) {
<EditorWrapper
onClick={(event) => {
const [sheet, row, column] = model.getSelectedCell();
const editorWidth =
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
const editorHeight =
model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
workbookState.setEditingCell({
sheet,
row,
@@ -52,17 +60,15 @@ function FormulaBar(properties: FormulaBarProps) {
cursorEnd: formulaValue.length,
focus: "formula-bar",
activeRanges: [],
mode: "accept",
mode: "edit",
editorWidth,
editorHeight,
});
event.stopPropagation();
event.preventDefault();
}}
>
<Editor
minimalWidth={"100%"}
minimalHeight={"100%"}
display={true}
expand={false}
originalText={formulaValue}
model={model}
workbookState={workbookState}

View File

@@ -1,7 +1,11 @@
import type { BorderOptions, Model, WorksheetProperties } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import { useCallback, useEffect, useRef, useState } from "react";
import { LAST_COLUMN } from "./WorksheetCanvas/constants";
import {
COLUMN_WIDTH_SCALE,
LAST_COLUMN,
ROW_HEIGH_SCALE,
} from "./WorksheetCanvas/constants";
import FormulaBar from "./formulabar";
import Navigation from "./navigation/navigation";
import Toolbar from "./toolbar";
@@ -144,6 +148,9 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
},
onEditKeyPressStart: (initText: string): void => {
const { sheet, row, column } = model.getSelectedView();
const editorWidth =
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
const editorHeight = model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
workbookState.setEditingCell({
sheet,
row,
@@ -155,6 +162,8 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
referencedRange: null,
activeRanges: [],
mode: "accept",
editorWidth,
editorHeight,
});
setRedrawId((id) => id + 1);
},
@@ -162,6 +171,9 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
// User presses F2, we start editing at the edn of the text
const { sheet, row, column } = model.getSelectedView();
const text = model.getCellContent(sheet, row, column);
const editorWidth =
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
const editorHeight = model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
workbookState.setEditingCell({
sheet,
row,
@@ -173,6 +185,8 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
focus: "cell",
activeRanges: [],
mode: "edit",
editorWidth,
editorHeight,
});
setRedrawId((id) => id + 1);
},

View File

@@ -72,6 +72,8 @@ export interface EditingCell {
focus: Focus;
activeRanges: ActiveRange[];
mode: EditorMode;
editorWidth: number;
editorHeight: number;
}
// Those are styles that are copied

View File

@@ -2,6 +2,8 @@ import type { Model } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import {
COLUMN_WIDTH_SCALE,
ROW_HEIGH_SCALE,
outlineBackgroundColor,
outlineColor,
} from "./WorksheetCanvas/constants";
@@ -329,6 +331,9 @@ function Worksheet(props: {
// Starts editing cell
const { sheet, row, column } = model.getSelectedView();
const text = model.getCellContent(sheet, row, column) || "";
const editorWidth =
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
const editorHeight = model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
workbookState.setEditingCell({
sheet,
row,
@@ -340,20 +345,19 @@ function Worksheet(props: {
referencedRange: null,
activeRanges: [],
mode: "accept",
editorWidth,
editorHeight,
});
setOriginalText(text);
event.stopPropagation();
event.preventDefault();
props.refresh();
}}
>
<SheetCanvas ref={canvasElement} />
<CellOutline ref={cellOutline} />
<EditorWrapper ref={editorElement}>
<Editor
minimalWidth={"100%"}
minimalHeight={"100%"}
display={workbookState.getEditingCell()?.focus === "cell"}
expand={true}
originalText={workbookState.getEditingText() || originalText}
onEditEnd={(): void => {
props.refresh();
@@ -492,7 +496,6 @@ const CellOutlineHandle = styled("div")`
height: 5px;
background: ${outlineColor};
cursor: crosshair;
// border: 1px solid white;
border-radius: 1px;
`;
@@ -517,6 +520,7 @@ const EditorWrapper = styled("div")`
min-width: 1px;
}
font-family: monospace;
border: 2px solid ${outlineColor};
`;
export default Worksheet;