Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 121 additions & 4 deletions packages/excalidraw/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ import { EraserTrail } from "../eraser";
import { getShortcutKey } from "../shortcut";

import { tryParseSpreadsheet } from "../charts";
import { parseTableData, createTableHtml, getTableDimensions } from "../table";

import ConvertElementTypePopup, {
getConversionTypeFromElements,
Expand Down Expand Up @@ -1637,6 +1638,19 @@ class App extends React.Component<AppProps, AppState> {
},
} as const;
}
} else if (
isEmbeddableElement(el) &&
el.customData?.tableData &&
Array.isArray(el.customData.tableData)
) {
const cells = el.customData.tableData as string[][];
src = {
intrinsicSize: { w: el.width, h: el.height },
type: "document",
srcdoc: () =>
createTableHtml(cells, this.state.theme as "light" | "dark"),
sandbox: { allowSameOrigin: true },
} as const;
} else {
src = getEmbedLink(toValidURL(el.link || ""));
}
Expand Down Expand Up @@ -3546,15 +3560,37 @@ class App extends React.Component<AppProps, AppState> {
return;
}

// ------------------- Spreadsheet -------------------
// ------------------- Spreadsheet / Table -------------------

if (!isPlainPaste && data.text) {
const result = tryParseSpreadsheet(data.text);
if (result.ok) {
const tableCells = parseTableData(data.text);
const chartResult = tryParseSpreadsheet(data.text);

if (tableCells && tableCells.length > 0) {
if (chartResult.ok) {
this.setState({
openDialog: {
name: "charts",
data: chartResult.data,
rawText: data.text,
tableCells,
},
});
} else {
this.setState({
openDialog: {
name: "table",
cells: tableCells,
rawText: data.text,
},
});
}
return;
} else if (chartResult.ok) {
this.setState({
openDialog: {
name: "charts",
data: result.data,
data: chartResult.data,
rawText: data.text,
},
});
Expand Down Expand Up @@ -8552,6 +8588,61 @@ class App extends React.Component<AppProps, AppState> {
return element;
};

public insertTableElement = ({
sceneX,
sceneY,
cells,
}: {
sceneX?: number;
sceneY?: number;
cells: string[][];
}) => {
const { x: cursorX, y: cursorY } = viewportCoordsToSceneCoords(
{
clientX: this.lastViewportPosition.x,
clientY: this.lastViewportPosition.y,
},
this.state,
);
const x = sceneX ?? cursorX;
const y = sceneY ?? cursorY;
const [gridX, gridY] = getGridPoint(
x,
y,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
? null
: this.getEffectiveGridSize(),
);

const { width, height } = getTableDimensions(
cells.length,
cells[0]?.length ?? 0,
);

const element = newEmbeddableElement({
type: "embeddable",
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
roundness: this.getCurrentItemRoundness("embeddable"),
opacity: this.state.currentItemOpacity,
locked: false,
width,
height,
link: "excalidraw-table:",
customData: { tableData: cells },
});

this.scene.insertElement(element);

return element;
};

//create rectangle element with youtube top left on nearest grid point width / hight 640/360
public insertEmbeddableElement = ({
sceneX,
Expand Down Expand Up @@ -11563,6 +11654,32 @@ class App extends React.Component<AppProps, AppState> {
if (fileItems.length > 0) {
const { file, fileHandle } = fileItems[0];
if (file) {
const isCsvFile =
file.name?.toLowerCase().endsWith(".csv") ||
file.type === "text/csv" ||
file.type === "application/csv";
if (isCsvFile) {
try {
const text = await file.text();
const cells = parseTableData(text);
if (cells && cells.length > 0) {
const tableElement = this.insertTableElement({
sceneX,
sceneY,
cells,
});
if (tableElement) {
this.store.scheduleCapture();
this.setState({
selectedElementIds: { [tableElement.id]: true },
});
}
return;
}
} catch (error: any) {
console.warn("Failed to parse CSV file:", error);
}
}
// Attempt to parse an excalidraw/excalidrawlib file
await this.loadFileToCanvas(file, fileHandle);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/excalidraw/components/LayerUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { LoadingMessage } from "./LoadingMessage";
import { LockButton } from "./LockButton";
import { MobileMenu } from "./MobileMenu";
import { PasteChartDialog } from "./PasteChartDialog";
import { PasteTableDialog } from "./PasteTableDialog";
import { Section } from "./Section";
import Stack from "./Stack";
import { UserList } from "./UserList";
Expand Down Expand Up @@ -560,6 +561,18 @@ const LayerUI = ({
<PasteChartDialog
data={appState.openDialog.data}
rawText={appState.openDialog.rawText}
tableCells={appState.openDialog.tableCells}
onClose={() =>
setAppState({
openDialog: null,
})
}
/>
)}
{appState.openDialog?.name === "table" && (
<PasteTableDialog
cells={appState.openDialog.cells}
rawText={appState.openDialog.rawText}
onClose={() =>
setAppState({
openDialog: null,
Expand Down
27 changes: 27 additions & 0 deletions packages/excalidraw/components/PasteChartDialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,32 @@
min-height: 200px;
}
}
.ChartPreview__table-preview,
.ChartPreview__plaintext-preview {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
padding: 0.5rem;
overflow: hidden;
}
.ChartPreview__table-preview {
table {
border-collapse: collapse;
font-size: 0.7rem;
}
td {
border: 1px solid $color-gray-4;
padding: 2px 4px;
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
}
.ChartPreview__plaintext-preview {
white-space: pre-wrap;
word-break: break-word;
text-align: left;
}
}
}
38 changes: 37 additions & 1 deletion packages/excalidraw/components/PasteChartDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,15 @@ const PlainTextPreviewBtn = (props: {
export const PasteChartDialog = ({
data,
rawText,
tableCells,
onClose,
}: {
data: Spreadsheet;
rawText: string;
tableCells?: string[][];
onClose: () => void;
}) => {
const { onInsertElements, focusContainer } = useApp();
const { onInsertElements, insertTableElement, focusContainer } = useApp();
const [colorSeed, setColorSeed] = useState(Math.random());

const handleReshuffleColors = React.useCallback(() => {
Expand Down Expand Up @@ -214,6 +216,15 @@ export const PasteChartDialog = ({
focusContainer();
};

const handleTableClick = () => {
if (tableCells && tableCells.length > 0) {
insertTableElement({ cells: tableCells });
trackEvent("paste", "chart", "table");
onClose();
focusContainer();
}
};

return (
<Dialog
size="regular"
Expand Down Expand Up @@ -258,6 +269,31 @@ export const PasteChartDialog = ({
/>
);
})}
{tableCells && tableCells.length > 0 && (
<button
type="button"
className="ChartPreview"
aria-label={t("labels.chartType_table")}
onClick={handleTableClick}
>
<div className="ChartPreview__canvas ChartPreview__table-preview">
<table>
<tbody>
{tableCells.slice(0, 4).map((row, i) => (
<tr key={i}>
{row.slice(0, 3).map((cell, j) => (
<td key={j}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="ChartPreview__label">
{t("labels.chartType_table")}
</div>
</button>
)}
{rawText && (
<PlainTextPreviewBtn
rawText={rawText}
Expand Down
98 changes: 98 additions & 0 deletions packages/excalidraw/components/PasteTableDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from "react";

import { newTextElement } from "@excalidraw/element";

import { trackEvent } from "../analytics";
import { t } from "../i18n";

import { useApp } from "./App";
import { Dialog } from "./Dialog";

import "./PasteChartDialog.scss";

export const PasteTableDialog = ({
cells,
rawText,
onClose,
}: {
cells: string[][];
rawText: string;
onClose: () => void;
}) => {
const { insertTableElement, onInsertElements, focusContainer } = useApp();

const handleClose = React.useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);

const handleTableClick = () => {
insertTableElement({ cells });
trackEvent("paste", "table", "table");
onClose();
focusContainer();
};

const handlePlainTextClick = () => {
const textElement = newTextElement({
text: rawText,
x: 0,
y: 0,
});
onInsertElements([textElement]);
trackEvent("paste", "table", "plaintext");
onClose();
focusContainer();
};

return (
<Dialog
size="regular"
onCloseRequest={handleClose}
title={t("labels.pasteCharts")}
className="PasteChartDialog"
autofocus={false}
>
<div className="container">
<button
type="button"
className="ChartPreview"
aria-label={t("labels.chartType_table")}
onClick={handleTableClick}
>
<div className="ChartPreview__canvas ChartPreview__table-preview">
<table>
<tbody>
{cells.slice(0, 4).map((row, i) => (
<tr key={i}>
{row.slice(0, 3).map((cell, j) => (
<td key={j}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="ChartPreview__label">
{t("labels.chartType_table")}
</div>
</button>
<button
type="button"
className="ChartPreview"
aria-label={t("labels.chartType_plaintext")}
onClick={handlePlainTextClick}
>
<div className="ChartPreview__canvas ChartPreview__plaintext-preview">
{rawText.slice(0, 80)}
{rawText.length > 80 ? "…" : ""}
</div>
<div className="ChartPreview__label">
{t("labels.chartType_plaintext")}
</div>
</button>
</div>
</Dialog>
);
};
1 change: 1 addition & 0 deletions packages/excalidraw/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"chartType_line": "Line chart",
"chartType_radar": "Radar chart",
"chartType_plaintext": "Plain text",
"chartType_table": "Table",
"selectAll": "Select all",
"multiSelect": "Add element to selection",
"moveCanvas": "Move canvas",
Expand Down
Loading