FIX: Minimal implementation of browse mode

This commit is contained in:
Nicolás Hatcher
2024-09-28 13:55:39 +02:00
parent 90cf5f74f7
commit fde1e13ffb
9 changed files with 191 additions and 57 deletions

View File

@@ -1241,10 +1241,20 @@ export default class WorksheetCanvas {
} }
private drawActiveRanges(topLeftCell: Cell, bottomRightCell: Cell): void { private drawActiveRanges(topLeftCell: Cell, bottomRightCell: Cell): void {
const activeRanges = this.workbookState.getActiveRanges(); let activeRanges = this.workbookState.getActiveRanges();
const activeRangesCount = activeRanges.length;
const ctx = this.ctx; const ctx = this.ctx;
ctx.setLineDash([2, 2]); 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) { for (let rangeIndex = 0; rangeIndex < activeRangesCount; rangeIndex += 1) {
const range = activeRanges[rangeIndex]; const range = activeRanges[rangeIndex];

View File

@@ -6,6 +6,18 @@
// For those cases we capture the keydown event and stop its propagation. // 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 // 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 // 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 type { Model } from "@ironcalc/wasm";
import { import {
@@ -65,7 +77,7 @@ const Editor = (options: EditorOptions) => {
const [height, setHeight] = useState(minimalHeight); const [height, setHeight] = useState(minimalHeight);
const [text, setText] = useState(originalText); const [text, setText] = useState(originalText);
const [styledFormula, setStyledFormula] = useState( const [styledFormula, setStyledFormula] = useState(
getFormulaHTML(model, text).html, getFormulaHTML(model, text, "").html,
); );
const formulaRef = useRef<HTMLDivElement>(null); const formulaRef = useRef<HTMLDivElement>(null);
@@ -74,7 +86,7 @@ const Editor = (options: EditorOptions) => {
useEffect(() => { useEffect(() => {
setText(originalText); setText(originalText);
setStyledFormula(getFormulaHTML(model, originalText).html); setStyledFormula(getFormulaHTML(model, originalText, "").html);
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.value = originalText; textareaRef.current.value = originalText;
} }
@@ -107,7 +119,12 @@ const Editor = (options: EditorOptions) => {
const cell = workbookState.getEditingCell(); const cell = workbookState.getEditingCell();
if (cell) { if (cell) {
workbookState.clearEditingCell(); 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; const sign = shiftKey ? -1 : 1;
model.setSelectedCell(cell.row + sign, cell.column); model.setSelectedCell(cell.row + sign, cell.column);
} }
@@ -121,12 +138,17 @@ const Editor = (options: EditorOptions) => {
const cell = workbookState.getEditingCell(); const cell = workbookState.getEditingCell();
if (cell) { if (cell) {
workbookState.clearEditingCell(); 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; const sign = shiftKey ? -1 : 1;
model.setSelectedCell(cell.row, cell.column + sign); model.setSelectedCell(cell.row, cell.column + sign);
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.value = ""; textareaRef.current.value = "";
setStyledFormula(getFormulaHTML(model, "").html); setStyledFormula(getFormulaHTML(model, "", "").html);
} }
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); 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 // We run this in a timeout because the value is not yet in the textarea
// since we are capturing the keydown event // since we are capturing the keydown event
setTimeout(() => { setTimeout(() => {
const value = textarea.value;
const styledFormula = getFormulaHTML(model, value);
const cell = workbookState.getEditingCell(); const cell = workbookState.getEditingCell();
if (cell) { if (cell) {
// accept whatever is in the referenced range
const value = textarea.value;
const styledFormula = getFormulaHTML(model, value, "");
cell.text = value; cell.text = value;
cell.referencedRange = null;
cell.cursorStart = textarea.selectionStart;
cell.cursorEnd = textarea.selectionEnd;
workbookState.setEditingCell(cell); workbookState.setEditingCell(cell);
workbookState.setActiveRanges(styledFormula.activeRanges); workbookState.setActiveRanges(styledFormula.activeRanges);
@@ -176,7 +203,7 @@ const Editor = (options: EditorOptions) => {
const onChange = useCallback(() => { const onChange = useCallback(() => {
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.value = ""; textareaRef.current.value = "";
setStyledFormula(getFormulaHTML(model, "").html); setStyledFormula(getFormulaHTML(model, "", "").html);
} }
// This happens if the blur hasn't been taken care before by // 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 // If we are editing a cell finish that
const cell = workbookState.getEditingCell(); const cell = workbookState.getEditingCell();
if (cell) { if (cell) {
model.setUserInput(
cell.sheet,
cell.row,
cell.column,
workbookState.getEditingText(),
);
workbookState.clearEditingCell(); workbookState.clearEditingCell();
model.setUserInput(cell.sheet, cell.row, cell.column, cell.text);
} }
onEditEnd(); onEditEnd();
}, [model, workbookState, onEditEnd]); }, [model, workbookState, onEditEnd]);

View File

@@ -15,7 +15,7 @@ export function tokenIsRangeType(token: TokenType): token is Range {
return typeof token === "object" && "Range" in token; return typeof token === "object" && "Range" in token;
} }
function isInReferenceMode(text: string, cursor: number): boolean { export function isInReferenceMode(text: string, cursor: number): boolean {
// FIXME // FIXME
// This is a gross oversimplification // This is a gross oversimplification
// Returns true if both are true: // Returns true if both are true:
@@ -102,6 +102,7 @@ export function getColor(index: number, alpha = 1): string {
function getFormulaHTML( function getFormulaHTML(
model: Model, model: Model,
text: string, text: string,
referenceRange: string,
): { html: JSX.Element[]; activeRanges: ActiveRange[] } { ): { html: JSX.Element[]; activeRanges: ActiveRange[] } {
let html: JSX.Element[] = []; let html: JSX.Element[] = [];
const activeRanges: ActiveRange[] = []; const activeRanges: ActiveRange[] = [];
@@ -179,6 +180,10 @@ function getFormulaHTML(
html.push(<span key={index}>{formula.slice(start, end)}</span>); html.push(<span key={index}>{formula.slice(start, end)}</span>);
} }
} }
// If there is a reference range add it at the end
if (referenceRange !== "") {
html.push(<span key="reference">{referenceRange}</span>);
}
html = [<span key="equals">=</span>].concat(html); html = [<span key="equals">=</span>].concat(html);
} else { } else {
html = [<span key="single">{text}</span>]; html = [<span key="single">{text}</span>];

View File

@@ -47,7 +47,9 @@ function FormulaBar(properties: FormulaBarProps) {
row, row,
column, column,
text: formulaValue, text: formulaValue,
cursor: 0, referencedRange: null,
cursorStart: formulaValue.length,
cursorEnd: formulaValue.length,
focus: "formula-bar", focus: "formula-bar",
activeRanges: [], activeRanges: [],
}); });

View File

@@ -5,7 +5,9 @@ import {
headerColumnWidth, headerColumnWidth,
headerRowHeight, headerRowHeight,
} from "./WorksheetCanvas/worksheetCanvas"; } from "./WorksheetCanvas/worksheetCanvas";
import { isInReferenceMode } from "./editor/util";
import type { Cell } from "./types"; import type { Cell } from "./types";
import { rangeToStr } from "./util";
import type { WorkbookState } from "./workbookState"; import type { WorkbookState } from "./workbookState";
interface PointerSettings { interface PointerSettings {
@@ -19,6 +21,7 @@ interface PointerSettings {
onExtendToEnd: () => void; onExtendToEnd: () => void;
model: Model; model: Model;
workbookState: WorkbookState; workbookState: WorkbookState;
refresh: () => void;
} }
interface PointerEvents { interface PointerEvents {
@@ -31,6 +34,7 @@ interface PointerEvents {
const usePointer = (options: PointerSettings): PointerEvents => { const usePointer = (options: PointerSettings): PointerEvents => {
const isSelecting = useRef(false); const isSelecting = useRef(false);
const isExtending = useRef(false); const isExtending = useRef(false);
const isInsertingRef = useRef(false);
const onPointerMove = useCallback( const onPointerMove = useCallback(
(event: PointerEvent): void => { (event: PointerEvent): void => {
@@ -40,43 +44,43 @@ const usePointer = (options: PointerSettings): PointerEvents => {
return; 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) { if (isSelecting.current) {
const { canvasElement, worksheetCanvas } = options; options.onAreaSelecting(cell);
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");
}
} else if (isExtending.current) { } 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); 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], [options],
@@ -94,6 +98,10 @@ const usePointer = (options: PointerSettings): PointerEvents => {
isExtending.current = false; isExtending.current = false;
worksheetElement.current?.releasePointerCapture(event.pointerId); worksheetElement.current?.releasePointerCapture(event.pointerId);
options.onExtendToEnd(); options.onExtendToEnd();
} else if (isInsertingRef.current) {
const { worksheetElement } = options;
isInsertingRef.current = false;
worksheetElement.current?.releasePointerCapture(event.pointerId);
} }
}, },
[options], [options],
@@ -106,6 +114,7 @@ const usePointer = (options: PointerSettings): PointerEvents => {
const { const {
canvasElement, canvasElement,
model, model,
refresh,
worksheetElement, worksheetElement,
worksheetCanvas, worksheetCanvas,
workbookState, workbookState,
@@ -142,9 +151,8 @@ const usePointer = (options: PointerSettings): PointerEvents => {
} }
return; return;
} }
// if we are editing a cell finish that
const editingCell = workbookState.getEditingCell();
const editingCell = workbookState.getEditingCell();
const cell = worksheet.getCellByCoordinates(x, y); const cell = worksheet.getCellByCoordinates(x, y);
if (cell) { if (cell) {
if (editingCell) { if (editingCell) {
@@ -156,6 +164,31 @@ const usePointer = (options: PointerSettings): PointerEvents => {
// we do nothing // we do nothing
return; 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(); workbookState.clearEditingCell();
model.setUserInput( model.setUserInput(
editingCell.sheet, editingCell.sheet,
@@ -163,6 +196,7 @@ const usePointer = (options: PointerSettings): PointerEvents => {
editingCell.column, editingCell.column,
editingCell.text, editingCell.text,
); );
// we continue to select the new cell
} }
options.onCellSelected(cell, event); options.onCellSelected(cell, event);
isSelecting.current = true; isSelecting.current = true;

View File

@@ -40,3 +40,21 @@ export const getCellAddress = (selectedArea: Area, selectedCell?: Cell) => {
selectedArea.rowStart selectedArea.rowStart
}:${columnNameFromNumber(selectedArea.columnEnd)}${selectedArea.rowEnd}`; }:${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}`;
}

View File

@@ -149,8 +149,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
row, row,
column, column,
text: initText, text: initText,
cursor: 0, cursorStart: initText.length,
cursorEnd: initText.length,
focus: "cell", focus: "cell",
referencedRange: null,
activeRanges: [], activeRanges: [],
}); });
setRedrawId((id) => id + 1); setRedrawId((id) => id + 1);
@@ -163,7 +165,9 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
row, row,
column, column,
text, text,
cursor: text.length, cursorStart: text.length,
cursorEnd: text.length,
referencedRange: null,
focus: "cell", focus: "cell",
activeRanges: [], activeRanges: [],
}); });
@@ -277,7 +281,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const formulaValue = () => { const formulaValue = () => {
const cell = workbookState.getEditingCell(); const cell = workbookState.getEditingCell();
if (cell) { if (cell) {
return cell.text; return workbookState.getEditingText();
} }
const { sheet, row, column } = model.getSelectedView(); const { sheet, row, column } = model.getSelectedView();
return model.getCellContent(sheet, row, column); return model.getCellContent(sheet, row, column);
@@ -295,8 +299,12 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
ref={rootRef} ref={rootRef}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
tabIndex={0} tabIndex={0}
onClick={() => { onClick={(event) => {
rootRef.current?.focus(); if (!workbookState.getEditingCell()) {
rootRef.current?.focus();
} else {
event.stopPropagation();
}
}} }}
> >
<Toolbar <Toolbar

View File

@@ -40,6 +40,17 @@ export interface ActiveRange {
color: string; color: string;
} }
export interface ReferencedRange {
range: {
sheet: number;
rowStart: number;
rowEnd: number;
columnStart: number;
columnEnd: number;
};
str: string;
}
type Focus = "cell" | "formula-bar"; type Focus = "cell" | "formula-bar";
// The cell that we are editing // The cell that we are editing
@@ -50,7 +61,10 @@ export interface EditingCell {
// raw text in the editor // raw text in the editor
text: string; text: string;
// position of the cursor // position of the cursor
cursor: number; cursorStart: number;
cursorEnd: number;
// referenced range
referencedRange: ReferencedRange | null;
focus: Focus; focus: Focus;
activeRanges: ActiveRange[]; activeRanges: ActiveRange[];
} }
@@ -126,4 +140,12 @@ export class WorkbookState {
} }
return false; return false;
} }
getEditingText(): string {
const cell = this.cell;
if (cell) {
return cell.text + (cell.referencedRange?.str || "");
}
return "";
}
} }

View File

@@ -144,6 +144,7 @@ function Worksheet(props: {
} = usePointer({ } = usePointer({
model, model,
workbookState, workbookState,
refresh,
onCellSelected: (cell: Cell, event: React.MouseEvent) => { onCellSelected: (cell: Cell, event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -328,8 +329,10 @@ function Worksheet(props: {
row, row,
column, column,
text, text,
cursor: text.length, cursorStart: text.length,
cursorEnd: text.length,
focus: "cell", focus: "cell",
referencedRange: null,
activeRanges: [], activeRanges: [],
}); });
setOriginalText(text); setOriginalText(text);
@@ -345,7 +348,7 @@ function Worksheet(props: {
minimalHeight={"100%"} minimalHeight={"100%"}
display={workbookState.getEditingCell()?.focus === "cell"} display={workbookState.getEditingCell()?.focus === "cell"}
expand={true} expand={true}
originalText={workbookState.getEditingCell()?.text || originalText} originalText={workbookState.getEditingText() || originalText}
onEditEnd={(): void => { onEditEnd={(): void => {
props.refresh(); props.refresh();
}} }}