diff --git a/webapp/src/components/WorksheetCanvas/constants.ts b/webapp/src/components/WorksheetCanvas/constants.ts index ff1266f..5084bb5 100644 --- a/webapp/src/components/WorksheetCanvas/constants.ts +++ b/webapp/src/components/WorksheetCanvas/constants.ts @@ -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; diff --git a/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts b/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts index 79338cf..d385632 100644 --- a/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts +++ b/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts @@ -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); } diff --git a/webapp/src/components/editor/editor.tsx b/webapp/src/components/editor/editor.tsx index 7421dc3..5e8d3b3 100644 --- a/webapp/src/components/editor/editor.tsx +++ b/webapp/src/components/editor/editor.tsx @@ -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 (
{ ...commonCSS, textAlign: "left", pointerEvents: "none", - height, + height: "100%", }} > -
{styledFormula}
+
+ {styledFormula} +