diff --git a/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts b/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts index cb5e9a8..ec73152 100644 --- a/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts +++ b/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts @@ -1241,10 +1241,20 @@ export default class WorksheetCanvas { } private drawActiveRanges(topLeftCell: Cell, bottomRightCell: Cell): void { - const activeRanges = this.workbookState.getActiveRanges(); - const activeRangesCount = activeRanges.length; + let activeRanges = this.workbookState.getActiveRanges(); const ctx = this.ctx; ctx.setLineDash([2, 2]); + const referencedRange = + this.workbookState.getEditingCell()?.referencedRange || null; + if (referencedRange) { + activeRanges = activeRanges.concat([ + { + ...referencedRange.range, + color: "#343423", + }, + ]); + } + const activeRangesCount = activeRanges.length; for (let rangeIndex = 0; rangeIndex < activeRangesCount; rangeIndex += 1) { const range = activeRanges[rangeIndex]; diff --git a/webapp/src/components/editor/editor.tsx b/webapp/src/components/editor/editor.tsx index a305347..bad7dac 100644 --- a/webapp/src/components/editor/editor.tsx +++ b/webapp/src/components/editor/editor.tsx @@ -6,6 +6,18 @@ // 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 +// +// Events outside the editor might influence the editor +// 1. Clicking on a different cell: +// * might either terminate the editing +// * or add the external cell to the formula +// 2. Clicking on a sheet tab would open the new sheet or terminate editing +// 3. Clicking somewhere else will finish editing +// +// Keyboard navigation is also fairly complex. For instance RightArrow might: +// 1. End editing and navigate to the cell on the right +// 2. Move the cursor to the right +// 3. Insert in the formula the cell name on the right import type { Model } from "@ironcalc/wasm"; import { @@ -65,7 +77,7 @@ const Editor = (options: EditorOptions) => { const [height, setHeight] = useState(minimalHeight); const [text, setText] = useState(originalText); const [styledFormula, setStyledFormula] = useState( - getFormulaHTML(model, text).html, + getFormulaHTML(model, text, "").html, ); const formulaRef = useRef(null); @@ -74,7 +86,7 @@ const Editor = (options: EditorOptions) => { useEffect(() => { setText(originalText); - setStyledFormula(getFormulaHTML(model, originalText).html); + setStyledFormula(getFormulaHTML(model, originalText, "").html); if (textareaRef.current) { textareaRef.current.value = originalText; } @@ -107,7 +119,12 @@ const Editor = (options: EditorOptions) => { const cell = workbookState.getEditingCell(); if (cell) { workbookState.clearEditingCell(); - model.setUserInput(cell.sheet, cell.row, cell.column, cell.text); + model.setUserInput( + cell.sheet, + cell.row, + cell.column, + cell.text + (cell.referencedRange?.str || ""), + ); const sign = shiftKey ? -1 : 1; model.setSelectedCell(cell.row + sign, cell.column); } @@ -121,12 +138,17 @@ const Editor = (options: EditorOptions) => { const cell = workbookState.getEditingCell(); if (cell) { workbookState.clearEditingCell(); - model.setUserInput(cell.sheet, cell.row, cell.column, cell.text); + model.setUserInput( + cell.sheet, + cell.row, + cell.column, + cell.text + (cell.referencedRange?.str || ""), + ); const sign = shiftKey ? -1 : 1; model.setSelectedCell(cell.row, cell.column + sign); if (textareaRef.current) { textareaRef.current.value = ""; - setStyledFormula(getFormulaHTML(model, "").html); + setStyledFormula(getFormulaHTML(model, "", "").html); } event.stopPropagation(); event.preventDefault(); @@ -148,11 +170,16 @@ const Editor = (options: EditorOptions) => { // 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) { + // accept whatever is in the referenced range + const value = textarea.value; + const styledFormula = getFormulaHTML(model, value, ""); + cell.text = value; + cell.referencedRange = null; + cell.cursorStart = textarea.selectionStart; + cell.cursorEnd = textarea.selectionEnd; workbookState.setEditingCell(cell); workbookState.setActiveRanges(styledFormula.activeRanges); @@ -176,7 +203,7 @@ const Editor = (options: EditorOptions) => { const onChange = useCallback(() => { if (textareaRef.current) { textareaRef.current.value = ""; - setStyledFormula(getFormulaHTML(model, "").html); + setStyledFormula(getFormulaHTML(model, "", "").html); } // This happens if the blur hasn't been taken care before by @@ -184,8 +211,13 @@ const Editor = (options: EditorOptions) => { // If we are editing a cell finish that const cell = workbookState.getEditingCell(); if (cell) { + model.setUserInput( + cell.sheet, + cell.row, + cell.column, + workbookState.getEditingText(), + ); workbookState.clearEditingCell(); - model.setUserInput(cell.sheet, cell.row, cell.column, cell.text); } onEditEnd(); }, [model, workbookState, onEditEnd]); diff --git a/webapp/src/components/editor/util.tsx b/webapp/src/components/editor/util.tsx index 1712d84..91f6054 100644 --- a/webapp/src/components/editor/util.tsx +++ b/webapp/src/components/editor/util.tsx @@ -15,7 +15,7 @@ export function tokenIsRangeType(token: TokenType): token is Range { return typeof token === "object" && "Range" in token; } -function isInReferenceMode(text: string, cursor: number): boolean { +export function isInReferenceMode(text: string, cursor: number): boolean { // FIXME // This is a gross oversimplification // Returns true if both are true: @@ -102,6 +102,7 @@ export function getColor(index: number, alpha = 1): string { function getFormulaHTML( model: Model, text: string, + referenceRange: string, ): { html: JSX.Element[]; activeRanges: ActiveRange[] } { let html: JSX.Element[] = []; const activeRanges: ActiveRange[] = []; @@ -179,6 +180,10 @@ function getFormulaHTML( html.push({formula.slice(start, end)}); } } + // If there is a reference range add it at the end + if (referenceRange !== "") { + html.push({referenceRange}); + } html = [=].concat(html); } else { html = [{text}]; diff --git a/webapp/src/components/formulabar.tsx b/webapp/src/components/formulabar.tsx index f1bac38..fa23cb1 100644 --- a/webapp/src/components/formulabar.tsx +++ b/webapp/src/components/formulabar.tsx @@ -47,7 +47,9 @@ function FormulaBar(properties: FormulaBarProps) { row, column, text: formulaValue, - cursor: 0, + referencedRange: null, + cursorStart: formulaValue.length, + cursorEnd: formulaValue.length, focus: "formula-bar", activeRanges: [], }); diff --git a/webapp/src/components/usePointer.ts b/webapp/src/components/usePointer.ts index 7328392..a592dee 100644 --- a/webapp/src/components/usePointer.ts +++ b/webapp/src/components/usePointer.ts @@ -5,7 +5,9 @@ import { headerColumnWidth, headerRowHeight, } from "./WorksheetCanvas/worksheetCanvas"; +import { isInReferenceMode } from "./editor/util"; import type { Cell } from "./types"; +import { rangeToStr } from "./util"; import type { WorkbookState } from "./workbookState"; interface PointerSettings { @@ -19,6 +21,7 @@ interface PointerSettings { onExtendToEnd: () => void; model: Model; workbookState: WorkbookState; + refresh: () => void; } interface PointerEvents { @@ -31,6 +34,7 @@ interface PointerEvents { const usePointer = (options: PointerSettings): PointerEvents => { const isSelecting = useRef(false); const isExtending = useRef(false); + const isInsertingRef = useRef(false); const onPointerMove = useCallback( (event: PointerEvent): void => { @@ -40,43 +44,43 @@ const usePointer = (options: PointerSettings): PointerEvents => { return; } + if ( + !(isSelecting.current || isExtending.current || isInsertingRef.current) + ) { + return; + } + const { canvasElement, worksheetCanvas } = options; + const canvas = canvasElement.current; + const worksheet = worksheetCanvas.current; + // Silence the linter + if (!worksheet || !canvas) { + return; + } + const canvasRect = canvas.getBoundingClientRect(); + const x = event.clientX - canvasRect.x; + const y = event.clientY - canvasRect.y; + + const cell = worksheet.getCellByCoordinates(x, y); + if (!cell) { + return; + } + if (isSelecting.current) { - const { canvasElement, worksheetCanvas } = options; - const canvas = canvasElement.current; - const worksheet = worksheetCanvas.current; - // Silence the linter - if (!worksheet || !canvas) { - return; - } - let x = event.clientX; - let y = event.clientY; - const canvasRect = canvas.getBoundingClientRect(); - x -= canvasRect.x; - y -= canvasRect.y; - const cell = worksheet.getCellByCoordinates(x, y); - if (cell) { - options.onAreaSelecting(cell); - } else { - console.log("Failed"); - } + options.onAreaSelecting(cell); } else if (isExtending.current) { - const { canvasElement, worksheetCanvas } = options; - const canvas = canvasElement.current; - const worksheet = worksheetCanvas.current; - // Silence the linter - if (!worksheet || !canvas) { - return; - } - let x = event.clientX; - let y = event.clientY; - const canvasRect = canvas.getBoundingClientRect(); - x -= canvasRect.x; - y -= canvasRect.y; - const cell = worksheet.getCellByCoordinates(x, y); - if (!cell) { - return; - } options.onExtendToCell(cell); + } else if (isInsertingRef.current) { + const { refresh, workbookState } = options; + const editingCell = workbookState.getEditingCell(); + if (!editingCell || !editingCell.referencedRange) { + return; + } + const range = editingCell.referencedRange.range; + range.rowEnd = cell.row; + range.columnEnd = cell.column; + editingCell.referencedRange.str = rangeToStr(range, 0); + workbookState.setEditingCell(editingCell); + refresh(); } }, [options], @@ -94,6 +98,10 @@ const usePointer = (options: PointerSettings): PointerEvents => { isExtending.current = false; worksheetElement.current?.releasePointerCapture(event.pointerId); options.onExtendToEnd(); + } else if (isInsertingRef.current) { + const { worksheetElement } = options; + isInsertingRef.current = false; + worksheetElement.current?.releasePointerCapture(event.pointerId); } }, [options], @@ -106,6 +114,7 @@ const usePointer = (options: PointerSettings): PointerEvents => { const { canvasElement, model, + refresh, worksheetElement, worksheetCanvas, workbookState, @@ -142,9 +151,8 @@ const usePointer = (options: PointerSettings): PointerEvents => { } return; } - // if we are editing a cell finish that - const editingCell = workbookState.getEditingCell(); + const editingCell = workbookState.getEditingCell(); const cell = worksheet.getCellByCoordinates(x, y); if (cell) { if (editingCell) { @@ -156,6 +164,31 @@ const usePointer = (options: PointerSettings): PointerEvents => { // we do nothing return; } + // now we are editing one cell and we click in another one + // If we can insert a range we do that + const text = editingCell.text; + if (isInReferenceMode(text, editingCell.cursorEnd)) { + const range = { + sheet: 0, + rowStart: cell.row, + rowEnd: cell.row, + columnStart: cell.column, + columnEnd: cell.column, + }; + editingCell.referencedRange = { + range, + str: rangeToStr(range, 0), + }; + workbookState.setEditingCell(editingCell); + event.stopPropagation(); + event.preventDefault(); + isInsertingRef.current = true; + worksheetWrapper.setPointerCapture(event.pointerId); + refresh(); + return; + } + // We are clicking away but we are not in reference mode + // We finish the editing workbookState.clearEditingCell(); model.setUserInput( editingCell.sheet, @@ -163,6 +196,7 @@ const usePointer = (options: PointerSettings): PointerEvents => { editingCell.column, editingCell.text, ); + // we continue to select the new cell } options.onCellSelected(cell, event); isSelecting.current = true; diff --git a/webapp/src/components/util.ts b/webapp/src/components/util.ts index 137608c..4917620 100644 --- a/webapp/src/components/util.ts +++ b/webapp/src/components/util.ts @@ -40,3 +40,21 @@ export const getCellAddress = (selectedArea: Area, selectedCell?: Cell) => { selectedArea.rowStart }:${columnNameFromNumber(selectedArea.columnEnd)}${selectedArea.rowEnd}`; }; + +export function rangeToStr( + range: { + sheet: number; + rowStart: number; + rowEnd: number; + columnStart: number; + columnEnd: number; + }, + referenceSheet: number, +): string { + const { sheet, rowStart, rowEnd, columnStart, columnEnd } = range; + const sheetName = sheet === referenceSheet ? "" : "other!"; + if (rowStart === rowEnd && columnStart === columnEnd) { + return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}`; + } + return `${sheetName}${columnNameFromNumber(columnStart)}${rowStart}:${columnNameFromNumber(columnEnd)}${rowEnd}`; +} diff --git a/webapp/src/components/workbook.tsx b/webapp/src/components/workbook.tsx index e5ef842..0a8e759 100644 --- a/webapp/src/components/workbook.tsx +++ b/webapp/src/components/workbook.tsx @@ -149,8 +149,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { row, column, text: initText, - cursor: 0, + cursorStart: initText.length, + cursorEnd: initText.length, focus: "cell", + referencedRange: null, activeRanges: [], }); setRedrawId((id) => id + 1); @@ -163,7 +165,9 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { row, column, text, - cursor: text.length, + cursorStart: text.length, + cursorEnd: text.length, + referencedRange: null, focus: "cell", activeRanges: [], }); @@ -277,7 +281,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { const formulaValue = () => { const cell = workbookState.getEditingCell(); if (cell) { - return cell.text; + return workbookState.getEditingText(); } const { sheet, row, column } = model.getSelectedView(); return model.getCellContent(sheet, row, column); @@ -295,8 +299,12 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { ref={rootRef} onKeyDown={onKeyDown} tabIndex={0} - onClick={() => { - rootRef.current?.focus(); + onClick={(event) => { + if (!workbookState.getEditingCell()) { + rootRef.current?.focus(); + } else { + event.stopPropagation(); + } }} > { event.preventDefault(); event.stopPropagation(); @@ -328,8 +329,10 @@ function Worksheet(props: { row, column, text, - cursor: text.length, + cursorStart: text.length, + cursorEnd: text.length, focus: "cell", + referencedRange: null, activeRanges: [], }); setOriginalText(text); @@ -345,7 +348,7 @@ function Worksheet(props: { minimalHeight={"100%"} display={workbookState.getEditingCell()?.focus === "cell"} expand={true} - originalText={workbookState.getEditingCell()?.text || originalText} + originalText={workbookState.getEditingText() || originalText} onEditEnd={(): void => { props.refresh(); }}