From eecf6f3c3b57c4c34765597fc755ad2030b5cc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Sun, 23 Feb 2025 14:17:06 +0100 Subject: [PATCH] UPDATE: Download to PNG the visible part of the selected area This downloads only the visible part of the selected area. To download the full selected area we would need to work a bit more --- .../src/components/Toolbar/Toolbar.tsx | 13 + .../src/components/Workbook/Workbook.tsx | 59 ++ .../src/components/Worksheet/Worksheet.tsx | 935 +++++++++--------- webapp/IronCalc/src/locale/en_us.json | 1 + .../frontend/package-lock.json | 18 +- 5 files changed, 558 insertions(+), 468 deletions(-) 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",