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 FormulaBar from "./formulabar"; import Navigation from "./navigation/navigation"; import Toolbar from "./toolbar"; import useKeyboardNavigation from "./useKeyboardNavigation"; import { type NavigationKey, getCellAddress } from "./util"; import type { WorkbookState } from "./workbookState"; import Worksheet from "./worksheet"; const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { const { model, workbookState } = props; const rootRef = useRef(null); // Calling `setRedrawId((id) => id + 1);` forces a redraw // This is needed because `model` can change without React being aware of it const setRedrawId = useState(0)[1]; const info = model .getWorksheetsProperties() .map(({ name, color, sheet_id }: WorksheetProperties) => { return { name, color: color ? color : "#FFF", sheetId: sheet_id }; }); const onRedo = () => { model.redo(); setRedrawId((id) => id + 1); }; const onUndo = () => { model.undo(); setRedrawId((id) => id + 1); }; const updateRangeStyle = (stylePath: string, value: string) => { const { sheet, range: [rowStart, columnStart, rowEnd, columnEnd], } = model.getSelectedView(); const row = Math.min(rowStart, rowEnd); const column = Math.min(columnStart, columnEnd); const range = { sheet, row, column, width: Math.abs(columnEnd - columnStart) + 1, height: Math.abs(rowEnd - rowStart) + 1, }; model.updateRangeStyle(range, stylePath, value); setRedrawId((id) => id + 1); }; const onToggleUnderline = (value: boolean) => { updateRangeStyle("font.u", `${value}`); }; const onToggleItalic = (value: boolean) => { updateRangeStyle("font.i", `${value}`); }; const onToggleBold = (value: boolean) => { updateRangeStyle("font.b", `${value}`); }; const onToggleStrike = (value: boolean) => { updateRangeStyle("font.strike", `${value}`); }; const onToggleHorizontalAlign = (value: string) => { updateRangeStyle("alignment.horizontal", value); }; const onToggleVerticalAlign = (value: string) => { updateRangeStyle("alignment.vertical", value); }; const onTextColorPicked = (hex: string) => { updateRangeStyle("font.color", hex); }; const onFillColorPicked = (hex: string) => { updateRangeStyle("fill.fg_color", hex); }; const onNumberFormatPicked = (numberFmt: string) => { updateRangeStyle("num_fmt", numberFmt); }; const onCopyStyles = () => { const { sheet, range: [rowStart, columnStart, rowEnd, columnEnd], } = model.getSelectedView(); const row1 = Math.min(rowStart, rowEnd); const column1 = Math.min(columnStart, columnEnd); const row2 = Math.max(rowStart, rowEnd); const column2 = Math.max(columnStart, columnEnd); const styles = []; for (let row = row1; row <= row2; row++) { const styleRow = []; for (let column = column1; column <= column2; column++) { styleRow.push(model.getCellStyle(sheet, row, column)); } styles.push(styleRow); } workbookState.setCopyStyles(styles); // FIXME: This is so that the cursor indicates there are styles to be pasted const el = rootRef.current?.getElementsByClassName("sheet-container")[0]; if (el) { (el as HTMLElement).style.cursor = `url('data:image/svg+xml;utf8,'), auto`; } }; // FIXME: I *think* we should have only one on onKeyPressed function that goes to // the Rust end const { onKeyDown } = useKeyboardNavigation({ onCellsDeleted: (): void => { const { sheet, range: [rowStart, columnStart, rowEnd, columnEnd], } = model.getSelectedView(); const row = Math.min(rowStart, rowEnd); const column = Math.min(columnStart, columnEnd); const width = Math.abs(columnEnd - columnStart); const height = Math.abs(rowEnd - rowStart); model.rangeClearContents( sheet, row, column, row + height, column + width, ); setRedrawId((id) => id + 1); }, onExpandAreaSelectedKeyboard: ( key: "ArrowRight" | "ArrowLeft" | "ArrowUp" | "ArrowDown", ): void => { model.onExpandSelectedRange(key); setRedrawId((id) => id + 1); }, onEditKeyPressStart: (initText: string): void => { const { sheet, row, column } = model.getSelectedView(); workbookState.setEditingCell({ sheet, row, column, text: initText, cursorStart: initText.length, cursorEnd: initText.length, focus: "cell", referencedRange: null, activeRanges: [], mode: "accept", }); setRedrawId((id) => id + 1); }, onCellEditStart: (): void => { // 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); workbookState.setEditingCell({ sheet, row, column, text, cursorStart: text.length, cursorEnd: text.length, referencedRange: null, focus: "cell", activeRanges: [], mode: "edit", }); setRedrawId((id) => id + 1); }, onBold: () => { const { sheet, row, column } = model.getSelectedView(); const value = !model.getCellStyle(sheet, row, column).font.b; onToggleBold(!value); }, onItalic: () => { const { sheet, row, column } = model.getSelectedView(); const value = !model.getCellStyle(sheet, row, column).font.i; onToggleItalic(!value); }, onUnderline: () => { const { sheet, row, column } = model.getSelectedView(); const value = !model.getCellStyle(sheet, row, column).font.u; onToggleUnderline(!value); }, onNavigationToEdge: (direction: NavigationKey): void => { console.log(direction); throw new Error("Function not implemented."); }, onPageDown: (): void => { model.onPageDown(); setRedrawId((id) => id + 1); }, onPageUp: (): void => { model.onPageUp(); setRedrawId((id) => id + 1); }, onArrowDown: (): void => { model.onArrowDown(); setRedrawId((id) => id + 1); }, onArrowUp: (): void => { model.onArrowUp(); setRedrawId((id) => id + 1); }, onArrowLeft: (): void => { model.onArrowLeft(); setRedrawId((id) => id + 1); }, onArrowRight: (): void => { model.onArrowRight(); setRedrawId((id) => id + 1); }, onKeyHome: (): void => { const view = model.getSelectedView(); const cell = model.getSelectedCell(); model.setSelectedCell(cell[1], 1); model.setTopLeftVisibleCell(view.top_row, 1); setRedrawId((id) => id + 1); }, onKeyEnd: (): void => { const view = model.getSelectedView(); const cell = model.getSelectedCell(); model.setSelectedCell(cell[1], LAST_COLUMN); model.setTopLeftVisibleCell(view.top_row, LAST_COLUMN - 5); setRedrawId((id) => id + 1); }, onUndo: (): void => { model.undo(); setRedrawId((id) => id + 1); }, onRedo: (): void => { model.redo(); setRedrawId((id) => id + 1); }, onNextSheet: (): void => { const nextSheet = model.getSelectedSheet() + 1; if (nextSheet >= model.getWorksheetsProperties().length) { model.setSelectedSheet(0); } else { model.setSelectedSheet(nextSheet); } }, onPreviousSheet: (): void => { const nextSheet = model.getSelectedSheet() - 1; if (nextSheet < 0) { model.setSelectedSheet(model.getWorksheetsProperties().length - 1); } else { model.setSelectedSheet(nextSheet); } }, root: rootRef, }); useEffect(() => { if (!rootRef.current) { return; } if (!workbookState.getEditingCell()) { rootRef.current.focus(); } }); const cellAddress = useCallback(() => { const { row, column, range: [rowStart, columnStart, rowEnd, columnEnd], } = model.getSelectedView(); return getCellAddress( { rowStart, rowEnd, columnStart, columnEnd }, { row, column }, ); }, [model]); const formulaValue = () => { const cell = workbookState.getEditingCell(); if (cell) { return workbookState.getEditingText(); } const { sheet, row, column } = model.getSelectedView(); return model.getCellContent(sheet, row, column); }; const getCellStyle = useCallback(() => { const { sheet, row, column } = model.getSelectedView(); return model.getCellStyle(sheet, row, column); }, [model]); const style = getCellStyle(); return ( { if (!workbookState.getEditingCell()) { rootRef.current?.focus(); } else { event.stopPropagation(); } }} > { const { sheet, range: [rowStart, columnStart, rowEnd, columnEnd], } = model.getSelectedView(); const row = Math.min(rowStart, rowEnd); const column = Math.min(columnStart, columnEnd); const width = Math.abs(columnEnd - columnStart) + 1; const height = Math.abs(rowEnd - rowStart) + 1; const borderArea = { type: border.border, item: border, }; model.setAreaWithBorder( { sheet, row, column, width, height }, borderArea, ); setRedrawId((id) => id + 1); }} fillColor={style.fill.fg_color || "#FFF"} fontColor={style.font.color} bold={style.font.b} underline={style.font.u} italic={style.font.i} strike={style.font.strike} horizontalAlign={ style.alignment ? style.alignment.horizontal : "general" } verticalAlign={style.alignment ? style.alignment.vertical : "center"} canEdit={true} numFmt={style.num_fmt} showGridLines={model.getShowGridLines(model.getSelectedSheet())} onToggleShowGridLines={(show) => { const sheet = model.getSelectedSheet(); model.setShowGridLines(sheet, show); setRedrawId((id) => id + 1); }} /> { setRedrawId((id) => id + 1); rootRef.current?.focus(); }} onTextUpdated={() => { setRedrawId((id) => id + 1); }} model={model} workbookState={workbookState} /> { setRedrawId((id) => id + 1); }} /> { model.setSelectedSheet(sheet); setRedrawId((value) => value + 1); }} onAddBlankSheet={(): void => { model.newSheet(); setRedrawId((value) => value + 1); }} onSheetColorChanged={(hex: string): void => { try { model.setSheetColor(model.getSelectedSheet(), hex); setRedrawId((value) => value + 1); } catch (e) { // TODO: Show a proper modal dialog alert(`${e}`); } }} onSheetRenamed={(name: string): void => { try { model.renameSheet(model.getSelectedSheet(), name); setRedrawId((value) => value + 1); } catch (e) { // TODO: Show a proper modal dialog alert(`${e}`); } }} onSheetDeleted={(): void => { const selectedSheet = model.getSelectedSheet(); model.deleteSheet(selectedSheet); setRedrawId((value) => value + 1); }} /> ); }; const Container = styled("div")` display: flex; flex-direction: column; height: 100%; position: relative; font-family: ${({ theme }) => theme.typography.fontFamily}; &:focus { outline: none; } `; export default Workbook;