785 lines
25 KiB
TypeScript
785 lines
25 KiB
TypeScript
import type {
|
|
BorderOptions,
|
|
ClipboardCell,
|
|
Model,
|
|
WorksheetProperties,
|
|
} from "@ironcalc/wasm";
|
|
import { styled } from "@mui/material/styles";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import {
|
|
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
|
getNewClipboardId,
|
|
} from "../clipboard";
|
|
import { TOOLBAR_HEIGHT } from "../constants";
|
|
import FormulaBar from "../FormulaBar/FormulaBar";
|
|
import RightDrawer, { DEFAULT_DRAWER_WIDTH } from "../RightDrawer/RightDrawer";
|
|
import SheetTabBar from "../SheetTabBar";
|
|
import Toolbar from "../Toolbar/Toolbar";
|
|
import {
|
|
getCellAddress,
|
|
getFullRangeToString,
|
|
type NavigationKey,
|
|
} from "../util";
|
|
import Worksheet from "../Worksheet/Worksheet";
|
|
import {
|
|
COLUMN_WIDTH_SCALE,
|
|
LAST_COLUMN,
|
|
LAST_ROW,
|
|
ROW_HEIGH_SCALE,
|
|
} from "../WorksheetCanvas/constants";
|
|
import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
|
|
import { devicePixelRatio } from "../WorksheetCanvas/worksheetCanvas";
|
|
import type { WorkbookState } from "../workbookState";
|
|
import useKeyboardNavigation from "./useKeyboardNavigation";
|
|
|
|
const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|
const { model, workbookState } = props;
|
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
const worksheetRef = useRef<{
|
|
getCanvas: () => WorksheetCanvas | null;
|
|
}>(null);
|
|
|
|
// Calling `setRedrawId((id) => id + 1);` forces a redraw
|
|
// This is needed because `model` or `workbookState` can change without React being aware of it
|
|
const setRedrawId = useState(0)[1];
|
|
|
|
const [isDrawerOpen, setDrawerOpen] = useState(false);
|
|
const [drawerWidth, setDrawerWidth] = useState(DEFAULT_DRAWER_WIDTH);
|
|
|
|
const worksheets = model.getWorksheetsProperties();
|
|
const info = worksheets.map(
|
|
({ name, color, sheet_id, state }: WorksheetProperties) => {
|
|
return { name, color: color ? color : "#FFF", sheetId: sheet_id, state };
|
|
},
|
|
);
|
|
const focusWorkbook = useCallback(() => {
|
|
if (rootRef.current) {
|
|
rootRef.current.focus({ preventScroll: true });
|
|
// HACK: We need to select something inside the root for onCopy to work
|
|
const selection = window.getSelection();
|
|
if (selection) {
|
|
selection.empty();
|
|
const range = new Range();
|
|
range.setStart(rootRef.current.firstChild as Node, 0);
|
|
range.setEnd(rootRef.current.firstChild as Node, 0);
|
|
selection.addRange(range);
|
|
}
|
|
}
|
|
}, []);
|
|
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 onToggleWrapText = (value: boolean) => {
|
|
updateRangeStyle("alignment.wrap_text", `${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 onIncreaseFontSize = (delta: number) => {
|
|
updateRangeStyle("font.size_delta", `${delta}`);
|
|
};
|
|
|
|
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) {
|
|
// Taken from lucide icons: <PaintRoller /> and rotated.
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paint-roller" style="transform:rotate(-8deg)"><rect width="16" height="6" x="2" y="2" rx="2"></rect><path d="M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect width="4" height="6" x="8" y="16" rx="1"></rect></svg>`;
|
|
(el as HTMLElement).style.cursor =
|
|
`url('data:image/svg+xml;utf8,${encodeURIComponent(svg)}'), 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();
|
|
const editorWidth =
|
|
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
|
|
const editorHeight = model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
|
|
workbookState.setEditingCell({
|
|
sheet,
|
|
row,
|
|
column,
|
|
text: initText,
|
|
cursorStart: initText.length,
|
|
cursorEnd: initText.length,
|
|
focus: "cell",
|
|
referencedRange: null,
|
|
activeRanges: [],
|
|
mode: "accept",
|
|
editorWidth,
|
|
editorHeight,
|
|
});
|
|
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);
|
|
const editorWidth =
|
|
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
|
|
const editorHeight = model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
|
|
workbookState.setEditingCell({
|
|
sheet,
|
|
row,
|
|
column,
|
|
text,
|
|
cursorStart: text.length,
|
|
cursorEnd: text.length,
|
|
referencedRange: null,
|
|
focus: "cell",
|
|
activeRanges: [],
|
|
mode: "edit",
|
|
editorWidth,
|
|
editorHeight,
|
|
});
|
|
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 => {
|
|
model.onNavigateToEdgeInDirection(direction);
|
|
setRedrawId((id) => id + 1);
|
|
},
|
|
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);
|
|
}
|
|
},
|
|
onEscape: (): void => {
|
|
workbookState.clearCutRange();
|
|
setRedrawId((id) => id + 1);
|
|
},
|
|
onSelectColumn: (): void => {
|
|
const { column } = model.getSelectedView();
|
|
model.setSelectedRange(1, column, LAST_ROW, column);
|
|
setRedrawId((id) => id + 1);
|
|
},
|
|
onSelectRow: (): void => {
|
|
const { row } = model.getSelectedView();
|
|
model.setSelectedRange(row, 1, row, LAST_COLUMN);
|
|
setRedrawId((id) => id + 1);
|
|
},
|
|
root: rootRef,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!rootRef.current) {
|
|
return;
|
|
}
|
|
if (!workbookState.getEditingCell()) {
|
|
focusWorkbook();
|
|
}
|
|
});
|
|
|
|
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 (
|
|
<Container
|
|
ref={rootRef}
|
|
onKeyDown={onKeyDown}
|
|
tabIndex={0}
|
|
onClick={(event: React.MouseEvent) => {
|
|
if (!workbookState.getEditingCell()) {
|
|
focusWorkbook();
|
|
} else {
|
|
event.stopPropagation();
|
|
}
|
|
}}
|
|
onPaste={(event: React.ClipboardEvent) => {
|
|
workbookState.clearCutRange();
|
|
const { items } = event.clipboardData;
|
|
if (!items) {
|
|
return;
|
|
}
|
|
const mimeTypes = [
|
|
"application/json",
|
|
"text/plain",
|
|
"text/csv",
|
|
"text/html",
|
|
];
|
|
let mimeType = null;
|
|
let value = null;
|
|
for (let index = 0; index < mimeTypes.length; index += 1) {
|
|
mimeType = mimeTypes[index];
|
|
value = event.clipboardData.getData(mimeType);
|
|
if (value) {
|
|
break;
|
|
}
|
|
}
|
|
if (!mimeType || !value) {
|
|
// No clipboard data to paste
|
|
return;
|
|
}
|
|
if (mimeType === "application/json") {
|
|
// We are copying from within the application
|
|
const source = JSON.parse(value);
|
|
// const clipboardId = sessionStorage.getItem(
|
|
// CLIPBOARD_ID_SESSION_STORAGE_KEY
|
|
// );
|
|
const data: Map<number, Map<number, ClipboardCell>> = new Map();
|
|
const sheetData = source.sheetData;
|
|
for (const row of Object.keys(sheetData)) {
|
|
const dataRow = sheetData[row];
|
|
const rowMap = new Map();
|
|
for (const column of Object.keys(dataRow)) {
|
|
rowMap.set(Number.parseInt(column, 10), dataRow[column]);
|
|
}
|
|
data.set(Number.parseInt(row, 10), rowMap);
|
|
}
|
|
model.pasteFromClipboard(
|
|
source.sheet,
|
|
source.area,
|
|
data,
|
|
source.type === "cut",
|
|
);
|
|
setRedrawId((id) => id + 1);
|
|
} else if (mimeType === "text/plain") {
|
|
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.pasteCsvText(range, value);
|
|
setRedrawId((id) => id + 1);
|
|
} else {
|
|
// NOT IMPLEMENTED
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}}
|
|
onCopy={(event: React.ClipboardEvent) => {
|
|
const data = model.copyToClipboard();
|
|
const sheet = model.getSelectedSheet();
|
|
// '2024-10-18T14:07:37.599Z'
|
|
|
|
let clipboardId = sessionStorage.getItem(
|
|
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
|
);
|
|
if (!clipboardId) {
|
|
clipboardId = getNewClipboardId();
|
|
sessionStorage.setItem(CLIPBOARD_ID_SESSION_STORAGE_KEY, clipboardId);
|
|
}
|
|
const sheetData: {
|
|
[row: number]: {
|
|
[column: number]: ClipboardCell;
|
|
};
|
|
} = {};
|
|
data.data.forEach((value, row) => {
|
|
const rowData: {
|
|
[column: number]: ClipboardCell;
|
|
} = {};
|
|
value.forEach((val, column) => {
|
|
rowData[column] = val;
|
|
});
|
|
sheetData[row] = rowData;
|
|
});
|
|
const clipboardJsonStr = JSON.stringify({
|
|
type: "copy",
|
|
area: data.range,
|
|
sheetData,
|
|
sheet,
|
|
clipboardId,
|
|
});
|
|
event.clipboardData.setData("text/plain", data.csv.trim());
|
|
event.clipboardData.setData("application/json", clipboardJsonStr);
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}}
|
|
onCut={(event: React.ClipboardEvent) => {
|
|
const data = model.copyToClipboard();
|
|
const sheet = model.getSelectedSheet();
|
|
// '2024-10-18T14:07:37.599Z'
|
|
|
|
let clipboardId = sessionStorage.getItem(
|
|
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
|
);
|
|
if (!clipboardId) {
|
|
clipboardId = getNewClipboardId();
|
|
sessionStorage.setItem(CLIPBOARD_ID_SESSION_STORAGE_KEY, clipboardId);
|
|
}
|
|
const sheetData: {
|
|
[row: number]: {
|
|
[column: number]: ClipboardCell;
|
|
};
|
|
} = {};
|
|
data.data.forEach((value, row) => {
|
|
const rowData: {
|
|
[column: number]: ClipboardCell;
|
|
} = {};
|
|
value.forEach((val, column) => {
|
|
rowData[column] = val;
|
|
});
|
|
sheetData[row] = rowData;
|
|
});
|
|
const clipboardJsonStr = JSON.stringify({
|
|
type: "cut",
|
|
area: data.range,
|
|
sheetData,
|
|
sheet,
|
|
clipboardId,
|
|
});
|
|
event.clipboardData.setData("text/plain", data.csv);
|
|
event.clipboardData.setData("application/json", clipboardJsonStr);
|
|
workbookState.setCutRange({
|
|
sheet: model.getSelectedSheet(),
|
|
rowStart: data.range[0],
|
|
rowEnd: data.range[2],
|
|
columnStart: data.range[1],
|
|
columnEnd: data.range[3],
|
|
});
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
setRedrawId((id) => id + 1);
|
|
}}
|
|
>
|
|
<Toolbar
|
|
canUndo={model.canUndo()}
|
|
canRedo={model.canRedo()}
|
|
onRedo={onRedo}
|
|
onUndo={onUndo}
|
|
onToggleUnderline={onToggleUnderline}
|
|
onToggleBold={onToggleBold}
|
|
onToggleItalic={onToggleItalic}
|
|
onToggleStrike={onToggleStrike}
|
|
onToggleHorizontalAlign={onToggleHorizontalAlign}
|
|
onToggleVerticalAlign={onToggleVerticalAlign}
|
|
onToggleWrapText={onToggleWrapText}
|
|
onCopyStyles={onCopyStyles}
|
|
onTextColorPicked={onTextColorPicked}
|
|
onFillColorPicked={onFillColorPicked}
|
|
onNumberFormatPicked={onNumberFormatPicked}
|
|
onClearFormatting={() => {
|
|
const {
|
|
sheet,
|
|
range: [rowStart, columnStart, rowEnd, columnEnd],
|
|
} = model.getSelectedView();
|
|
model.rangeClearFormatting(
|
|
sheet,
|
|
rowStart,
|
|
columnStart,
|
|
rowEnd,
|
|
columnEnd,
|
|
);
|
|
setRedrawId((id) => id + 1);
|
|
}}
|
|
onIncreaseFontSize={(delta: number) => {
|
|
onIncreaseFontSize(delta);
|
|
}}
|
|
onDownloadPNG={() => {
|
|
// creates a new canvas element in the visible part of the the selected area
|
|
const worksheetCanvas = worksheetRef.current?.getCanvas();
|
|
if (!worksheetCanvas) {
|
|
return;
|
|
}
|
|
const {
|
|
range: [rowStart, columnStart, rowEnd, columnEnd],
|
|
} = model.getSelectedView();
|
|
// NB: cells outside of the displayed area are not rendered
|
|
// I think the only reasonable way to do this would be server side.
|
|
let [x, y] = worksheetCanvas.getCoordinatesByCell(
|
|
rowStart,
|
|
columnStart,
|
|
);
|
|
const [x1, y1] = worksheetCanvas.getCoordinatesByCell(
|
|
rowEnd + 1,
|
|
columnEnd + 1,
|
|
);
|
|
const width = (x1 - x) * devicePixelRatio;
|
|
const height = (y1 - y) * devicePixelRatio;
|
|
x *= devicePixelRatio;
|
|
y *= devicePixelRatio;
|
|
|
|
const capturedCanvas = document.createElement("canvas");
|
|
capturedCanvas.width = width;
|
|
capturedCanvas.height = height;
|
|
const ctx = capturedCanvas.getContext("2d");
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
ctx.drawImage(
|
|
worksheetCanvas.canvas,
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
0,
|
|
0,
|
|
width,
|
|
height,
|
|
);
|
|
|
|
const downloadLink = document.createElement("a");
|
|
downloadLink.href = capturedCanvas.toDataURL("image/png");
|
|
downloadLink.download = "ironcalc.png";
|
|
downloadLink.click();
|
|
}}
|
|
onBorderChanged={(border: BorderOptions): 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) + 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 || "#FFFFFF"}
|
|
fontColor={style.font.color}
|
|
fontSize={style.font.sz}
|
|
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?.vertical ? style.alignment.vertical : "bottom"
|
|
}
|
|
wrapText={style.alignment?.wrap_text || false}
|
|
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);
|
|
}}
|
|
/>
|
|
<WorksheetAreaLeft $drawerWidth={isDrawerOpen ? drawerWidth : 0}>
|
|
<FormulaBar
|
|
cellAddress={cellAddress()}
|
|
formulaValue={formulaValue()}
|
|
onChange={() => {
|
|
setRedrawId((id) => id + 1);
|
|
focusWorkbook();
|
|
}}
|
|
onTextUpdated={() => {
|
|
setRedrawId((id) => id + 1);
|
|
}}
|
|
model={model}
|
|
workbookState={workbookState}
|
|
openDrawer={() => {
|
|
setDrawerOpen(true);
|
|
}}
|
|
canEdit={true}
|
|
/>
|
|
<Worksheet
|
|
model={model}
|
|
workbookState={workbookState}
|
|
refresh={(): void => {
|
|
setRedrawId((id) => id + 1);
|
|
}}
|
|
ref={worksheetRef}
|
|
/>
|
|
|
|
<SheetTabBar
|
|
sheets={info}
|
|
selectedIndex={model.getSelectedSheet()}
|
|
workbookState={workbookState}
|
|
onSheetSelected={(sheet: number): void => {
|
|
if (info[sheet].state !== "visible") {
|
|
model.unhideSheet(sheet);
|
|
}
|
|
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);
|
|
}}
|
|
onHideSheet={(): void => {
|
|
const selectedSheet = model.getSelectedSheet();
|
|
model.hideSheet(selectedSheet);
|
|
setRedrawId((value) => value + 1);
|
|
}}
|
|
/>
|
|
</WorksheetAreaLeft>
|
|
<RightDrawer
|
|
isOpen={isDrawerOpen}
|
|
onClose={() => setDrawerOpen(false)}
|
|
width={drawerWidth}
|
|
onWidthChange={setDrawerWidth}
|
|
model={model}
|
|
onUpdate={() => {
|
|
setRedrawId((id) => id + 1);
|
|
}}
|
|
getSelectedArea={() => {
|
|
const worksheetNames = model
|
|
.getWorksheetsProperties()
|
|
.map((s) => s.name);
|
|
const selectedView = model.getSelectedView();
|
|
return getFullRangeToString(selectedView, worksheetNames);
|
|
}}
|
|
/>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
type WorksheetAreaLeftProps = { $drawerWidth: number };
|
|
const WorksheetAreaLeft = styled("div")<WorksheetAreaLeftProps>(
|
|
({ $drawerWidth }) => ({
|
|
position: "absolute",
|
|
top: `${TOOLBAR_HEIGHT + 1}px`,
|
|
width: `calc(100% - ${$drawerWidth}px)`,
|
|
height: `calc(100% - ${TOOLBAR_HEIGHT}px)`,
|
|
}),
|
|
);
|
|
|
|
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;
|