diff --git a/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx b/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx
index e82877d..f572eae 100644
--- a/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx
+++ b/webapp/IronCalc/src/components/Toolbar/Toolbar.tsx
@@ -20,6 +20,7 @@ import {
Grid2X2,
Grid2x2Check,
Grid2x2X,
+ ImageDown,
Italic,
PaintBucket,
PaintRoller,
@@ -70,6 +71,7 @@ type ToolbarProperties = {
onBorderChanged: (border: BorderOptions) => void;
onClearFormatting: () => void;
onIncreaseFontSize: (delta: number) => void;
+ onDownloadPNG: () => void;
fillColor: string;
fontColor: string;
bold: boolean;
@@ -399,6 +401,17 @@ function Toolbar(properties: ToolbarProperties) {
>
+ {
+ properties.onDownloadPNG();
+ }}
+ title={t("toolbar.selected_png")}
+ >
+
+
{
const { model, workbookState } = props;
const rootRef = useRef(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
@@ -548,6 +553,59 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
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();
+ const { topLeftCell, bottomRightCell } =
+ worksheetCanvas.getVisibleCells();
+ const firstRow = Math.max(rowStart, topLeftCell.row);
+ const firstColumn = Math.max(columnStart, topLeftCell.column);
+ const lastRow = Math.min(rowEnd, bottomRightCell.row);
+ const lastColumn = Math.min(columnEnd, bottomRightCell.column);
+ let [x, y] = worksheetCanvas.getCoordinatesByCell(
+ firstRow,
+ firstColumn,
+ );
+ const [x1, y1] = worksheetCanvas.getCoordinatesByCell(
+ lastRow + 1,
+ lastColumn + 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,
@@ -640,6 +698,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
refresh={(): void => {
setRedrawId((id) => id + 1);
}}
+ ref={worksheetRef}
/>
void;
-}) {
- const canvasElement = useRef(null);
+const Worksheet = forwardRef(
+ (
+ props: {
+ model: Model;
+ workbookState: WorkbookState;
+ refresh: () => void;
+ },
+ ref,
+ ) => {
+ const canvasElement = useRef(null);
- const worksheetElement = useRef(null);
- const scrollElement = useRef(null);
+ const worksheetElement = useRef(null);
+ const scrollElement = useRef(null);
- const editorElement = useRef(null);
- const spacerElement = useRef(null);
- const cellOutline = useRef(null);
- const areaOutline = useRef(null);
- const cellOutlineHandle = useRef(null);
- const extendToOutline = useRef(null);
- const columnResizeGuide = useRef(null);
- const rowResizeGuide = useRef(null);
- const columnHeaders = useRef(null);
- const worksheetCanvas = useRef(null);
+ const editorElement = useRef(null);
+ const spacerElement = useRef(null);
+ const cellOutline = useRef(null);
+ const areaOutline = useRef(null);
+ const cellOutlineHandle = useRef(null);
+ const extendToOutline = useRef(null);
+ const columnResizeGuide = useRef(null);
+ const rowResizeGuide = useRef(null);
+ const columnHeaders = useRef(null);
+ const worksheetCanvas = useRef(null);
- const [contextMenuOpen, setContextMenuOpen] = useState(false);
+ const [contextMenuOpen, setContextMenuOpen] = useState(false);
- const ignoreScrollEventRef = useRef(false);
+ const ignoreScrollEventRef = useRef(false);
- const { model, workbookState, refresh } = props;
- const [clientWidth, clientHeight] = useWindowSize();
+ const { model, workbookState, refresh } = props;
+ const [clientWidth, clientHeight] = useWindowSize();
- useEffect(() => {
- const canvasRef = canvasElement.current;
- const columnGuideRef = columnResizeGuide.current;
- const rowGuideRef = rowResizeGuide.current;
- const columnHeadersRef = columnHeaders.current;
- const worksheetRef = worksheetElement.current;
+ useImperativeHandle(ref, () => ({
+ getCanvas: () => worksheetCanvas.current,
+ }));
- const outline = cellOutline.current;
- const handle = cellOutlineHandle.current;
- const area = areaOutline.current;
- const extendTo = extendToOutline.current;
- const editor = editorElement.current;
+ useEffect(() => {
+ const canvasRef = canvasElement.current;
+ const columnGuideRef = columnResizeGuide.current;
+ const rowGuideRef = rowResizeGuide.current;
+ const columnHeadersRef = columnHeaders.current;
+ const worksheetRef = worksheetElement.current;
- if (
- !canvasRef ||
- !columnGuideRef ||
- !rowGuideRef ||
- !columnHeadersRef ||
- !worksheetRef ||
- !outline ||
- !handle ||
- !area ||
- !extendTo ||
- !scrollElement.current ||
- !editor
- )
- return;
- // FIXME: This two need to be computed.
- model.setWindowWidth(clientWidth - 37);
- model.setWindowHeight(clientHeight - 190);
- const canvas = new WorksheetCanvas({
- width: worksheetRef.clientWidth,
- height: worksheetRef.clientHeight,
- model,
- workbookState,
- elements: {
- canvas: canvasRef,
- columnGuide: columnGuideRef,
- rowGuide: rowGuideRef,
- columnHeaders: columnHeadersRef,
- cellOutline: outline,
- cellOutlineHandle: handle,
- areaOutline: area,
- extendToOutline: extendTo,
- editor: editor,
- },
- onColumnWidthChanges(sheet, column, width) {
- if (width < 0) {
- return;
- }
- const { range } = model.getSelectedView();
- let columnStart = column;
- let columnEnd = column;
- const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
- const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
- if (
- fullColumn &&
- column >= range[1] &&
- column <= range[3] &&
- !fullRow
- ) {
- columnStart = Math.min(range[1], column, range[3]);
- columnEnd = Math.max(range[1], column, range[3]);
- }
- model.setColumnsWidth(sheet, columnStart, columnEnd, width);
- worksheetCanvas.current?.renderSheet();
- },
- onRowHeightChanges(sheet, row, height) {
- if (height < 0) {
- return;
- }
- const { range } = model.getSelectedView();
- let rowStart = row;
- let rowEnd = row;
- const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
- const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
- if (fullRow && row >= range[0] && row <= range[2] && !fullColumn) {
- rowStart = Math.min(range[0], row, range[2]);
- rowEnd = Math.max(range[0], row, range[2]);
- }
- model.setRowsHeight(sheet, rowStart, rowEnd, height);
- worksheetCanvas.current?.renderSheet();
- },
- refresh,
+ const outline = cellOutline.current;
+ const handle = cellOutlineHandle.current;
+ const area = areaOutline.current;
+ const extendTo = extendToOutline.current;
+ const editor = editorElement.current;
+
+ if (
+ !canvasRef ||
+ !columnGuideRef ||
+ !rowGuideRef ||
+ !columnHeadersRef ||
+ !worksheetRef ||
+ !outline ||
+ !handle ||
+ !area ||
+ !extendTo ||
+ !scrollElement.current ||
+ !editor
+ )
+ return;
+ // FIXME: This two need to be computed.
+ model.setWindowWidth(clientWidth - 37);
+ model.setWindowHeight(clientHeight - 190);
+ const canvas = new WorksheetCanvas({
+ width: worksheetRef.clientWidth,
+ height: worksheetRef.clientHeight,
+ model,
+ workbookState,
+ elements: {
+ canvas: canvasRef,
+ columnGuide: columnGuideRef,
+ rowGuide: rowGuideRef,
+ columnHeaders: columnHeadersRef,
+ cellOutline: outline,
+ cellOutlineHandle: handle,
+ areaOutline: area,
+ extendToOutline: extendTo,
+ editor: editor,
+ },
+ onColumnWidthChanges(sheet, column, width) {
+ if (width < 0) {
+ return;
+ }
+ const { range } = model.getSelectedView();
+ let columnStart = column;
+ let columnEnd = column;
+ const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
+ const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
+ if (
+ fullColumn &&
+ column >= range[1] &&
+ column <= range[3] &&
+ !fullRow
+ ) {
+ columnStart = Math.min(range[1], column, range[3]);
+ columnEnd = Math.max(range[1], column, range[3]);
+ }
+ model.setColumnsWidth(sheet, columnStart, columnEnd, width);
+ worksheetCanvas.current?.renderSheet();
+ },
+ onRowHeightChanges(sheet, row, height) {
+ if (height < 0) {
+ return;
+ }
+ const { range } = model.getSelectedView();
+ let rowStart = row;
+ let rowEnd = row;
+ const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
+ const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
+ if (fullRow && row >= range[0] && row <= range[2] && !fullColumn) {
+ rowStart = Math.min(range[0], row, range[2]);
+ rowEnd = Math.max(range[0], row, range[2]);
+ }
+ model.setRowsHeight(sheet, rowStart, rowEnd, height);
+ worksheetCanvas.current?.renderSheet();
+ },
+ refresh,
+ });
+ const scrollX = model.getScrollX();
+ const scrollY = model.getScrollY();
+ const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000];
+ if (spacerElement.current) {
+ spacerElement.current.style.height = `${sheetHeight}px`;
+ spacerElement.current.style.width = `${sheetWidth}px`;
+ }
+ const left = scrollElement.current.scrollLeft;
+ const top = scrollElement.current.scrollTop;
+ if (scrollX !== left) {
+ ignoreScrollEventRef.current = true;
+ scrollElement.current.scrollLeft = scrollX;
+ setTimeout(() => {
+ ignoreScrollEventRef.current = false;
+ }, 0);
+ }
+
+ if (scrollY !== top) {
+ ignoreScrollEventRef.current = true;
+ scrollElement.current.scrollTop = scrollY;
+ setTimeout(() => {
+ ignoreScrollEventRef.current = false;
+ }, 0);
+ }
+
+ canvas.renderSheet();
+ worksheetCanvas.current = canvas;
});
- const scrollX = model.getScrollX();
- const scrollY = model.getScrollY();
- const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000];
- if (spacerElement.current) {
- spacerElement.current.style.height = `${sheetHeight}px`;
- spacerElement.current.style.width = `${sheetWidth}px`;
- }
- const left = scrollElement.current.scrollLeft;
- const top = scrollElement.current.scrollTop;
- if (scrollX !== left) {
- ignoreScrollEventRef.current = true;
- scrollElement.current.scrollLeft = scrollX;
- setTimeout(() => {
- ignoreScrollEventRef.current = false;
- }, 0);
- }
- if (scrollY !== top) {
- ignoreScrollEventRef.current = true;
- scrollElement.current.scrollTop = scrollY;
- setTimeout(() => {
- ignoreScrollEventRef.current = false;
- }, 0);
- }
-
- canvas.renderSheet();
- worksheetCanvas.current = canvas;
- });
-
- const { onPointerMove, onPointerDown, onPointerHandleDown, onPointerUp } =
- usePointer({
- model,
- workbookState,
- refresh,
- onColumnSelected: (column: number, shift: boolean) => {
- let firstColumn = column;
- let lastColumn = column;
- if (shift) {
- const { range } = model.getSelectedView();
- firstColumn = Math.min(range[1], column, range[3]);
- lastColumn = Math.max(range[3], column, range[1]);
- }
- model.setSelectedCell(1, firstColumn);
- model.setSelectedRange(1, firstColumn, LAST_ROW, lastColumn);
- refresh();
- },
- onRowSelected: (row: number, shift: boolean) => {
- let firstRow = row;
- let lastRow = row;
- if (shift) {
- const { range } = model.getSelectedView();
- firstRow = Math.min(range[0], row, range[2]);
- lastRow = Math.max(range[2], row, range[0]);
- }
- model.setSelectedCell(firstRow, 1);
- model.setSelectedRange(firstRow, 1, lastRow, LAST_COLUMN);
- refresh();
- },
- onAllSheetSelected: () => {
- model.setSelectedCell(1, 1);
- model.setSelectedRange(1, 1, LAST_ROW, LAST_COLUMN);
- },
- onCellSelected: (cell: Cell, event: React.MouseEvent) => {
- event.preventDefault();
- event.stopPropagation();
- model.setSelectedCell(cell.row, cell.column);
- refresh();
- },
- onAreaSelecting: (cell: Cell) => {
- const canvas = worksheetCanvas.current;
- if (!canvas) {
- return;
- }
- const { row, column } = cell;
- model.onAreaSelecting(row, column);
- canvas.renderSheet();
- refresh();
- },
- onAreaSelected: () => {
- const styles = workbookState.getCopyStyles();
- if (styles?.length) {
- model.onPasteStyles(styles);
+ const { onPointerMove, onPointerDown, onPointerHandleDown, onPointerUp } =
+ usePointer({
+ model,
+ workbookState,
+ refresh,
+ onColumnSelected: (column: number, shift: boolean) => {
+ let firstColumn = column;
+ let lastColumn = column;
+ if (shift) {
+ const { range } = model.getSelectedView();
+ firstColumn = Math.min(range[1], column, range[3]);
+ lastColumn = Math.max(range[3], column, range[1]);
+ }
+ model.setSelectedCell(1, firstColumn);
+ model.setSelectedRange(1, firstColumn, LAST_ROW, lastColumn);
+ refresh();
+ },
+ onRowSelected: (row: number, shift: boolean) => {
+ let firstRow = row;
+ let lastRow = row;
+ if (shift) {
+ const { range } = model.getSelectedView();
+ firstRow = Math.min(range[0], row, range[2]);
+ lastRow = Math.max(range[2], row, range[0]);
+ }
+ model.setSelectedCell(firstRow, 1);
+ model.setSelectedRange(firstRow, 1, lastRow, LAST_COLUMN);
+ refresh();
+ },
+ onAllSheetSelected: () => {
+ model.setSelectedCell(1, 1);
+ model.setSelectedRange(1, 1, LAST_ROW, LAST_COLUMN);
+ },
+ onCellSelected: (cell: Cell, event: React.MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ model.setSelectedCell(cell.row, cell.column);
+ refresh();
+ },
+ onAreaSelecting: (cell: Cell) => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
+ const { row, column } = cell;
+ model.onAreaSelecting(row, column);
canvas.renderSheet();
- }
- workbookState.setCopyStyles(null);
- if (worksheetElement.current) {
- worksheetElement.current.style.cursor = "auto";
- }
- refresh();
- },
- onExtendToCell: (cell) => {
- const canvas = worksheetCanvas.current;
- if (!canvas) {
- return;
- }
- const { row, column } = cell;
- const {
- range: [rowStart, columnStart, rowEnd, columnEnd],
- } = model.getSelectedView();
- // We are either extending by rows or by columns
- // And we could be doing it in the positive direction (downwards or right)
- // or the negative direction (upwards or left)
-
- if (
- row > rowEnd &&
- ((column <= columnEnd && column >= columnStart) ||
- (column < columnStart && columnStart - column < row - rowEnd) ||
- (column > columnEnd && column - columnEnd < row - rowEnd))
- ) {
- // rows downwards
- const area = {
- type: AreaType.rowsDown,
- rowStart: rowEnd + 1,
- rowEnd: row,
- columnStart,
- columnEnd,
- };
- workbookState.setExtendToArea(area);
- canvas.renderSheet();
- } else if (
- row < rowStart &&
- ((column <= columnEnd && column >= columnStart) ||
- (column < columnStart && columnStart - column < rowStart - row) ||
- (column > columnEnd && column - columnEnd < rowStart - row))
- ) {
- // rows upwards
- const area = {
- type: AreaType.rowsUp,
- rowStart: row,
- rowEnd: rowStart,
- columnStart,
- columnEnd,
- };
- workbookState.setExtendToArea(area);
- canvas.renderSheet();
- } else if (
- column > columnEnd &&
- ((row <= rowEnd && row >= rowStart) ||
- (row < rowStart && rowStart - row < column - columnEnd) ||
- (row > rowEnd && row - rowEnd < column - columnEnd))
- ) {
- // columns right
- const area = {
- type: AreaType.columnsRight,
- rowStart,
- rowEnd,
- columnStart: columnEnd + 1,
- columnEnd: column,
- };
- workbookState.setExtendToArea(area);
- canvas.renderSheet();
- } else if (
- column < columnStart &&
- ((row <= rowEnd && row >= rowStart) ||
- (row < rowStart && rowStart - row < columnStart - column) ||
- (row > rowEnd && row - rowEnd < columnStart - column))
- ) {
- // columns left
- const area = {
- type: AreaType.columnsLeft,
- rowStart,
- rowEnd,
- columnStart: column,
- columnEnd: columnStart,
- };
- workbookState.setExtendToArea(area);
- canvas.renderSheet();
- }
- },
- onExtendToEnd: () => {
- const canvas = worksheetCanvas.current;
- if (!canvas) {
- return;
- }
- const { sheet, range } = model.getSelectedView();
- const extendedArea = workbookState.getExtendToArea();
- if (!extendedArea) {
- return;
- }
- const rowStart = Math.min(range[0], range[2]);
- const height = Math.abs(range[2] - range[0]) + 1;
- const width = Math.abs(range[3] - range[1]) + 1;
- const columnStart = Math.min(range[1], range[3]);
-
- const area = {
- sheet,
- row: rowStart,
- column: columnStart,
- width,
- height,
- };
-
- switch (extendedArea.type) {
- case AreaType.rowsDown:
- model.autoFillRows(area, extendedArea.rowEnd);
- break;
- case AreaType.rowsUp: {
- model.autoFillRows(area, extendedArea.rowStart);
- break;
+ refresh();
+ },
+ onAreaSelected: () => {
+ const styles = workbookState.getCopyStyles();
+ if (styles?.length) {
+ model.onPasteStyles(styles);
+ const canvas = worksheetCanvas.current;
+ if (!canvas) {
+ return;
+ }
+ canvas.renderSheet();
}
- case AreaType.columnsRight: {
- model.autoFillColumns(area, extendedArea.columnEnd);
- break;
+ workbookState.setCopyStyles(null);
+ if (worksheetElement.current) {
+ worksheetElement.current.style.cursor = "auto";
}
- case AreaType.columnsLeft: {
- model.autoFillColumns(area, extendedArea.columnStart);
- break;
+ refresh();
+ },
+ onExtendToCell: (cell) => {
+ const canvas = worksheetCanvas.current;
+ if (!canvas) {
+ return;
}
- }
- model.setSelectedRange(
- Math.min(rowStart, extendedArea.rowStart),
- Math.min(columnStart, extendedArea.columnStart),
- Math.max(rowStart + height - 1, extendedArea.rowEnd),
- Math.max(columnStart + width - 1, extendedArea.columnEnd),
- );
- workbookState.clearExtendToArea();
- canvas.renderSheet();
- },
- canvasElement,
- worksheetElement,
- worksheetCanvas,
- });
+ const { row, column } = cell;
+ const {
+ range: [rowStart, columnStart, rowEnd, columnEnd],
+ } = model.getSelectedView();
+ // We are either extending by rows or by columns
+ // And we could be doing it in the positive direction (downwards or right)
+ // or the negative direction (upwards or left)
- const onScroll = (): void => {
- if (!scrollElement.current || !worksheetCanvas.current) {
- return;
- }
- if (ignoreScrollEventRef.current) {
- // Programmatic scroll ignored
- return;
- }
- const left = scrollElement.current.scrollLeft;
- const top = scrollElement.current.scrollTop;
+ if (
+ row > rowEnd &&
+ ((column <= columnEnd && column >= columnStart) ||
+ (column < columnStart && columnStart - column < row - rowEnd) ||
+ (column > columnEnd && column - columnEnd < row - rowEnd))
+ ) {
+ // rows downwards
+ const area = {
+ type: AreaType.rowsDown,
+ rowStart: rowEnd + 1,
+ rowEnd: row,
+ columnStart,
+ columnEnd,
+ };
+ workbookState.setExtendToArea(area);
+ canvas.renderSheet();
+ } else if (
+ row < rowStart &&
+ ((column <= columnEnd && column >= columnStart) ||
+ (column < columnStart && columnStart - column < rowStart - row) ||
+ (column > columnEnd && column - columnEnd < rowStart - row))
+ ) {
+ // rows upwards
+ const area = {
+ type: AreaType.rowsUp,
+ rowStart: row,
+ rowEnd: rowStart,
+ columnStart,
+ columnEnd,
+ };
+ workbookState.setExtendToArea(area);
+ canvas.renderSheet();
+ } else if (
+ column > columnEnd &&
+ ((row <= rowEnd && row >= rowStart) ||
+ (row < rowStart && rowStart - row < column - columnEnd) ||
+ (row > rowEnd && row - rowEnd < column - columnEnd))
+ ) {
+ // columns right
+ const area = {
+ type: AreaType.columnsRight,
+ rowStart,
+ rowEnd,
+ columnStart: columnEnd + 1,
+ columnEnd: column,
+ };
+ workbookState.setExtendToArea(area);
+ canvas.renderSheet();
+ } else if (
+ column < columnStart &&
+ ((row <= rowEnd && row >= rowStart) ||
+ (row < rowStart && rowStart - row < columnStart - column) ||
+ (row > rowEnd && row - rowEnd < columnStart - column))
+ ) {
+ // columns left
+ const area = {
+ type: AreaType.columnsLeft,
+ rowStart,
+ rowEnd,
+ columnStart: column,
+ columnEnd: columnStart,
+ };
+ workbookState.setExtendToArea(area);
+ canvas.renderSheet();
+ }
+ },
+ onExtendToEnd: () => {
+ const canvas = worksheetCanvas.current;
+ if (!canvas) {
+ return;
+ }
+ const { sheet, range } = model.getSelectedView();
+ const extendedArea = workbookState.getExtendToArea();
+ if (!extendedArea) {
+ return;
+ }
+ const rowStart = Math.min(range[0], range[2]);
+ const height = Math.abs(range[2] - range[0]) + 1;
+ const width = Math.abs(range[3] - range[1]) + 1;
+ const columnStart = Math.min(range[1], range[3]);
- worksheetCanvas.current.setScrollPosition({ left, top });
- worksheetCanvas.current.renderSheet();
- };
-
- return (
-
-
- {
- event.preventDefault();
- event.stopPropagation();
- setContextMenuOpen(true);
- }}
- onDoubleClick={(event) => {
- // Starts editing cell
- 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({
+ const area = {
sheet,
- row,
- column,
- text,
- cursorStart: text.length,
- cursorEnd: text.length,
- focus: "cell",
- referencedRange: null,
- activeRanges: [],
- mode: "accept",
- editorWidth,
- editorHeight,
- });
- event.stopPropagation();
- // event.preventDefault();
- props.refresh();
- }}
- >
-
-
-
- {
- props.refresh();
- }}
- onTextUpdated={(): void => {
- props.refresh();
- }}
- model={model}
- workbookState={workbookState}
- type={"cell"}
+ row: rowStart,
+ column: columnStart,
+ width,
+ height,
+ };
+
+ switch (extendedArea.type) {
+ case AreaType.rowsDown:
+ model.autoFillRows(area, extendedArea.rowEnd);
+ break;
+ case AreaType.rowsUp: {
+ model.autoFillRows(area, extendedArea.rowStart);
+ break;
+ }
+ case AreaType.columnsRight: {
+ model.autoFillColumns(area, extendedArea.columnEnd);
+ break;
+ }
+ case AreaType.columnsLeft: {
+ model.autoFillColumns(area, extendedArea.columnStart);
+ break;
+ }
+ }
+ model.setSelectedRange(
+ Math.min(rowStart, extendedArea.rowStart),
+ Math.min(columnStart, extendedArea.columnStart),
+ Math.max(rowStart + height - 1, extendedArea.rowEnd),
+ Math.max(columnStart + width - 1, extendedArea.columnEnd),
+ );
+ workbookState.clearExtendToArea();
+ canvas.renderSheet();
+ },
+ canvasElement,
+ worksheetElement,
+ worksheetCanvas,
+ });
+
+ const onScroll = (): void => {
+ if (!scrollElement.current || !worksheetCanvas.current) {
+ return;
+ }
+ if (ignoreScrollEventRef.current) {
+ // Programmatic scroll ignored
+ return;
+ }
+ const left = scrollElement.current.scrollLeft;
+ const top = scrollElement.current.scrollTop;
+
+ worksheetCanvas.current.setScrollPosition({ left, top });
+ worksheetCanvas.current.renderSheet();
+ };
+
+ return (
+
+
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ setContextMenuOpen(true);
+ }}
+ onDoubleClick={(event) => {
+ // Starts editing cell
+ 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,
+ focus: "cell",
+ referencedRange: null,
+ activeRanges: [],
+ mode: "accept",
+ editorWidth,
+ editorHeight,
+ });
+ event.stopPropagation();
+ // event.preventDefault();
+ props.refresh();
+ }}
+ >
+
+
+
+ {
+ props.refresh();
+ }}
+ onTextUpdated={(): void => {
+ props.refresh();
+ }}
+ model={model}
+ workbookState={workbookState}
+ type={"cell"}
+ />
+
+
+
+
-
-
-
-
+
+
+
+ setContextMenuOpen(false)}
+ anchorEl={cellOutline.current}
+ onInsertRowAbove={(): void => {
+ const view = model.getSelectedView();
+ model.insertRow(view.sheet, view.row);
+ setContextMenuOpen(false);
+ }}
+ onInsertRowBelow={(): void => {
+ const view = model.getSelectedView();
+ model.insertRow(view.sheet, view.row + 1);
+ setContextMenuOpen(false);
+ }}
+ onInsertColumnLeft={(): void => {
+ const view = model.getSelectedView();
+ model.insertColumn(view.sheet, view.column);
+ setContextMenuOpen(false);
+ }}
+ onInsertColumnRight={(): void => {
+ const view = model.getSelectedView();
+ model.insertColumn(view.sheet, view.column + 1);
+ setContextMenuOpen(false);
+ }}
+ onFreezeColumns={(): void => {
+ const view = model.getSelectedView();
+ model.setFrozenColumnsCount(view.sheet, view.column);
+ setContextMenuOpen(false);
+ }}
+ onFreezeRows={(): void => {
+ const view = model.getSelectedView();
+ model.setFrozenRowsCount(view.sheet, view.row);
+ setContextMenuOpen(false);
+ }}
+ onUnfreezeColumns={(): void => {
+ const sheet = model.getSelectedSheet();
+ model.setFrozenColumnsCount(sheet, 0);
+ setContextMenuOpen(false);
+ }}
+ onUnfreezeRows={(): void => {
+ const sheet = model.getSelectedSheet();
+ model.setFrozenRowsCount(sheet, 0);
+ setContextMenuOpen(false);
+ }}
+ onDeleteRow={(): void => {
+ const view = model.getSelectedView();
+ model.deleteRow(view.sheet, view.row);
+ setContextMenuOpen(false);
+ }}
+ onDeleteColumn={(): void => {
+ const view = model.getSelectedView();
+ model.deleteColumn(view.sheet, view.column);
+ setContextMenuOpen(false);
+ }}
+ row={model.getSelectedView().row}
+ column={columnNameFromNumber(model.getSelectedView().column)}
/>
-
-
-
-
- setContextMenuOpen(false)}
- anchorEl={cellOutline.current}
- onInsertRowAbove={(): void => {
- const view = model.getSelectedView();
- model.insertRow(view.sheet, view.row);
- setContextMenuOpen(false);
- }}
- onInsertRowBelow={(): void => {
- const view = model.getSelectedView();
- model.insertRow(view.sheet, view.row + 1);
- setContextMenuOpen(false);
- }}
- onInsertColumnLeft={(): void => {
- const view = model.getSelectedView();
- model.insertColumn(view.sheet, view.column);
- setContextMenuOpen(false);
- }}
- onInsertColumnRight={(): void => {
- const view = model.getSelectedView();
- model.insertColumn(view.sheet, view.column + 1);
- setContextMenuOpen(false);
- }}
- onFreezeColumns={(): void => {
- const view = model.getSelectedView();
- model.setFrozenColumnsCount(view.sheet, view.column);
- setContextMenuOpen(false);
- }}
- onFreezeRows={(): void => {
- const view = model.getSelectedView();
- model.setFrozenRowsCount(view.sheet, view.row);
- setContextMenuOpen(false);
- }}
- onUnfreezeColumns={(): void => {
- const sheet = model.getSelectedSheet();
- model.setFrozenColumnsCount(sheet, 0);
- setContextMenuOpen(false);
- }}
- onUnfreezeRows={(): void => {
- const sheet = model.getSelectedSheet();
- model.setFrozenRowsCount(sheet, 0);
- setContextMenuOpen(false);
- }}
- onDeleteRow={(): void => {
- const view = model.getSelectedView();
- model.deleteRow(view.sheet, view.row);
- setContextMenuOpen(false);
- }}
- onDeleteColumn={(): void => {
- const view = model.getSelectedView();
- model.deleteColumn(view.sheet, view.column);
- setContextMenuOpen(false);
- }}
- row={model.getSelectedView().row}
- column={columnNameFromNumber(model.getSelectedView().column)}
- />
-
- );
-}
+
+ );
+ },
+);
const Spacer = styled("div")`
position: absolute;
diff --git a/webapp/IronCalc/src/locale/en_us.json b/webapp/IronCalc/src/locale/en_us.json
index dc35972..60ae6b9 100644
--- a/webapp/IronCalc/src/locale/en_us.json
+++ b/webapp/IronCalc/src/locale/en_us.json
@@ -25,6 +25,7 @@
"vertical_align_bottom": "Align bottom",
"vertical_align_middle": " Align middle",
"vertical_align_top": "Align top",
+ "selected_png": "Export Selected area as PNG",
"format_menu": {
"auto": "Auto",
"number": "Number",
diff --git a/webapp/app.ironcalc.com/frontend/package-lock.json b/webapp/app.ironcalc.com/frontend/package-lock.json
index 4b12df0..ebeb880 100644
--- a/webapp/app.ironcalc.com/frontend/package-lock.json
+++ b/webapp/app.ironcalc.com/frontend/package-lock.json
@@ -43,21 +43,21 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "^3.2.4",
- "@storybook/addon-essentials": "^8.5.3",
- "@storybook/addon-interactions": "^8.5.3",
- "@storybook/blocks": "^8.5.3",
- "@storybook/react": "^8.5.3",
- "@storybook/react-vite": "^8.5.3",
- "@storybook/test": "^8.5.3",
+ "@storybook/addon-essentials": "^8.6.0",
+ "@storybook/addon-interactions": "^8.6.0",
+ "@storybook/blocks": "^8.6.0",
+ "@storybook/react": "^8.6.0",
+ "@storybook/react-vite": "^8.6.0",
+ "@storybook/test": "^8.6.0",
"@vitejs/plugin-react": "^4.2.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "storybook": "^8.5.3",
+ "storybook": "^8.6.0",
"ts-node": "^10.9.2",
"typescript": "~5.6.2",
- "vite": "^6.0.5",
+ "vite": "^6.2.0",
"vite-plugin-svgr": "^4.2.0",
- "vitest": "^2.0.5"
+ "vitest": "^3.0.7"
},
"peerDependencies": {
"@types/react": "^18.0.0 || ^19.0.0",