excalidraw/src/components/PasteChartDialog.tsx

178 lines
4.8 KiB
TypeScript

import oc from "open-color";
import React, { useLayoutEffect, useRef, useState } from "react";
import { trackEvent } from "../analytics";
import {
ChartElements,
renderSpreadsheet,
sortSpreadsheet,
Spreadsheet,
tryParseNumber,
} from "../charts";
import { ChartType } from "../element/types";
import { t } from "../i18n";
import { exportToSvg } from "../scene/export";
import { AppState, LibraryItem } from "../types";
import { Dialog } from "./Dialog";
import "./PasteChartDialog.scss";
import { ensureSubtypesLoaded } from "../subtypes";
import { isTextElement } from "../element";
import {
getContainerElement,
redrawTextBoundingBox,
} from "../element/textElement";
import { CheckboxItem } from "./CheckboxItem";
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
const ChartPreviewBtn = (props: {
spreadsheet: Spreadsheet | null;
chartType: ChartType;
selected: boolean;
onClick: OnInsertChart;
sortChartLabels: boolean;
}) => {
const previewRef = useRef<HTMLDivElement | null>(null);
const [chartElements, setChartElements] = useState<ChartElements | null>(
null,
);
useLayoutEffect(() => {
let svg: SVGSVGElement;
const previewNode = previewRef.current!;
(async () => {
(async () => {
let elements: ChartElements;
await ensureSubtypesLoaded(
props.spreadsheet?.activeSubtypes ?? [],
() => {
if (!props.spreadsheet) {
return;
}
const spreadsheet = props.sortChartLabels
? sortSpreadsheet(props.spreadsheet)
: props.spreadsheet;
elements = renderSpreadsheet(props.chartType, spreadsheet, 0, 0);
elements.forEach(
(el) =>
isTextElement(el) &&
redrawTextBoundingBox(el, getContainerElement(el)),
);
setChartElements(elements);
},
).then(async () => {
svg = await exportToSvg(
elements,
{
exportBackground: false,
viewBackgroundColor: oc.white,
},
null, // files
);
svg.querySelector(".style-fonts")?.remove();
previewNode.replaceChildren();
previewNode.appendChild(svg);
if (props.selected) {
(previewNode.parentNode as HTMLDivElement).focus();
}
});
})();
return () => {
previewNode.replaceChildren();
};
})();
}, [
props.spreadsheet,
props.chartType,
props.selected,
props.sortChartLabels,
]);
return (
<button
className="ChartPreview"
onClick={() => {
if (chartElements) {
props.onClick(props.chartType, chartElements);
}
}}
>
<div ref={previewRef} />
</button>
);
};
export const PasteChartDialog = ({
setAppState,
appState,
onClose,
onInsertChart,
}: {
appState: AppState;
onClose: () => void;
setAppState: React.Component<any, AppState>["setState"];
onInsertChart: (elements: LibraryItem["elements"]) => void;
}) => {
const handleClose = React.useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
onInsertChart(elements);
trackEvent("magic", "chart", chartType);
setAppState({
currentChartType: chartType,
pasteDialog: {
shown: false,
data: null,
},
});
};
const showSortChartLabels = appState.pasteDialog.data?.labels?.every((val) =>
tryParseNumber(val),
);
const [sortChartLabels, setSortChartLabels] = useState<boolean>(false);
return (
<Dialog
small
onCloseRequest={handleClose}
title={t("labels.pasteCharts")}
className={"PasteChartDialog"}
autofocus={false}
>
<div className={"container"}>
<ChartPreviewBtn
chartType="bar"
spreadsheet={appState.pasteDialog.data}
selected={appState.currentChartType === "bar"}
onClick={handleChartClick}
sortChartLabels={(showSortChartLabels && sortChartLabels) ?? false}
/>
<ChartPreviewBtn
chartType="line"
spreadsheet={appState.pasteDialog.data}
selected={appState.currentChartType === "line"}
onClick={handleChartClick}
sortChartLabels={(showSortChartLabels && sortChartLabels) ?? false}
/>
</div>
{showSortChartLabels && (
<div className={"container"}>
<CheckboxItem
checked={sortChartLabels}
onChange={(checked: boolean) => {
setSortChartLabels(checked);
}}
>
{t("labels.sortChartLabels")}
</CheckboxItem>
</div>
)}
</Dialog>
);
};