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
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
ce7318840d
commit
eecf6f3c3b
@@ -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) {
|
||||
>
|
||||
<RemoveFormatting />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={!canEdit}
|
||||
onClick={() => {
|
||||
properties.onDownloadPNG();
|
||||
}}
|
||||
title={t("toolbar.selected_png")}
|
||||
>
|
||||
<ImageDown />
|
||||
</StyledButton>
|
||||
|
||||
<ColorPicker
|
||||
color={properties.fontColor}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
LAST_COLUMN,
|
||||
ROW_HEIGH_SCALE,
|
||||
} from "../WorksheetCanvas/constants";
|
||||
import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
|
||||
import { devicePixelRatio } from "../WorksheetCanvas/worksheetCanvas";
|
||||
import {
|
||||
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
||||
getNewClipboardId,
|
||||
@@ -30,6 +32,9 @@ 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
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<SheetTabBar
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { type Model, columnNameFromNumber } from "@ironcalc/wasm";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import Editor from "../Editor/Editor";
|
||||
import {
|
||||
COLUMN_WIDTH_SCALE,
|
||||
@@ -34,11 +41,15 @@ function useWindowSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
function Worksheet(props: {
|
||||
const Worksheet = forwardRef(
|
||||
(
|
||||
props: {
|
||||
model: Model;
|
||||
workbookState: WorkbookState;
|
||||
refresh: () => void;
|
||||
}) {
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const worksheetElement = useRef<HTMLDivElement>(null);
|
||||
@@ -62,6 +73,10 @@ function Worksheet(props: {
|
||||
const { model, workbookState, refresh } = props;
|
||||
const [clientWidth, clientHeight] = useWindowSize();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getCanvas: () => worksheetCanvas.current,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const canvasRef = canvasElement.current;
|
||||
const columnGuideRef = columnResizeGuide.current;
|
||||
@@ -409,7 +424,8 @@ function Worksheet(props: {
|
||||
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;
|
||||
const editorHeight =
|
||||
model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
|
||||
workbookState.setEditingCell({
|
||||
sheet,
|
||||
row,
|
||||
@@ -514,7 +530,8 @@ function Worksheet(props: {
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const Spacer = styled("div")`
|
||||
position: absolute;
|
||||
|
||||
@@ -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",
|
||||
|
||||
18
webapp/app.ironcalc.com/frontend/package-lock.json
generated
18
webapp/app.ironcalc.com/frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user