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:
committed by
Nicolás Hatcher Andrés
parent
248ef66e7c
commit
585e594d8d
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -72,6 +72,8 @@ export interface EditingCell {
|
||||
focus: Focus;
|
||||
activeRanges: ActiveRange[];
|
||||
mode: EditorMode;
|
||||
editorWidth: number;
|
||||
editorHeight: number;
|
||||
}
|
||||
|
||||
// Those are styles that are copied
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user