diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json
index b43325c24..f5fff8a80 100644
--- a/packages/editor/package-lock.json
+++ b/packages/editor/package-lock.json
@@ -35,8 +35,10 @@
"@tiptap/pm": "^2.0.0-beta.218",
"@tiptap/starter-kit": "^2.0.0-beta.218",
"detect-indent": "^7.0.0",
+ "file-saver": "^2.0.5",
"katex": "^0.16.2",
"nanoid": "^4.0.1",
+ "papaparse": "^5.4.0",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.1",
"re-resizable": "^6.9.9",
@@ -52,7 +54,9 @@
"devDependencies": {
"@mdi/js": "^6.9.96",
"@mdi/react": "^1.6.0",
+ "@types/file-saver": "^2.0.5",
"@types/katex": "^0.14.0",
+ "@types/papaparse": "^5.3.7",
"@types/prismjs": "^1.26.0",
"@types/react": "17.0.2",
"@types/react-color": "^3.0.6",
@@ -160,9 +164,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.21.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz",
- "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==",
+ "version": "7.21.3",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz",
+ "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==",
"dependencies": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
@@ -1492,6 +1496,12 @@
"@types/chai": "*"
}
},
+ "node_modules/@types/file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
+ "dev": true
+ },
"node_modules/@types/fs-extra": {
"version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
@@ -1537,6 +1547,15 @@
"resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.2.tgz",
"integrity": "sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg=="
},
+ "node_modules/@types/papaparse": {
+ "version": "5.3.7",
+ "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.7.tgz",
+ "integrity": "sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -2312,6 +2331,11 @@
"node": "^12.20 || >= 14.13"
}
},
+ "node_modules/file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+ },
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -3047,6 +3071,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/papaparse": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.0.tgz",
+ "integrity": "sha512-ZBQABWG09p+u8rFoJVl/GhgxZ5zy9Zh1Lu/LVc7VX5T4nljjC14/YTcpebYwqP218B9X307eBOP7Tuhoqv7v7w=="
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4504,9 +4533,9 @@
}
},
"@babel/types": {
- "version": "7.21.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz",
- "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==",
+ "version": "7.21.3",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz",
+ "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==",
"requires": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
@@ -5329,6 +5358,12 @@
"@types/chai": "*"
}
},
+ "@types/file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
+ "dev": true
+ },
"@types/fs-extra": {
"version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
@@ -5374,6 +5409,15 @@
"resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.2.tgz",
"integrity": "sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg=="
},
+ "@types/papaparse": {
+ "version": "5.3.7",
+ "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.7.tgz",
+ "integrity": "sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -5993,6 +6037,11 @@
"web-streams-polyfill": "^3.0.3"
}
},
+ "file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+ },
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -6536,6 +6585,11 @@
"yocto-queue": "^1.0.0"
}
},
+ "papaparse": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.0.tgz",
+ "integrity": "sha512-ZBQABWG09p+u8rFoJVl/GhgxZ5zy9Zh1Lu/LVc7VX5T4nljjC14/YTcpebYwqP218B9X307eBOP7Tuhoqv7v7w=="
+ },
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
diff --git a/packages/editor/package.json b/packages/editor/package.json
index 6032690a5..51c4ac620 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -31,8 +31,10 @@
"@tiptap/pm": "^2.0.0-beta.218",
"@tiptap/starter-kit": "^2.0.0-beta.218",
"detect-indent": "^7.0.0",
+ "file-saver": "^2.0.5",
"katex": "^0.16.2",
"nanoid": "^4.0.1",
+ "papaparse": "^5.4.0",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.1",
"re-resizable": "^6.9.9",
@@ -48,7 +50,9 @@
"devDependencies": {
"@mdi/js": "^6.9.96",
"@mdi/react": "^1.6.0",
+ "@types/file-saver": "^2.0.5",
"@types/katex": "^0.14.0",
+ "@types/papaparse": "^5.3.7",
"@types/prismjs": "^1.26.0",
"@types/react": "17.0.2",
"@types/react-color": "^3.0.6",
diff --git a/packages/editor/src/extensions/table/actions.ts b/packages/editor/src/extensions/table/actions.ts
index a3aa36fb2..38a9e70de 100644
--- a/packages/editor/src/extensions/table/actions.ts
+++ b/packages/editor/src/extensions/table/actions.ts
@@ -21,13 +21,17 @@ import { Editor } from "@tiptap/core";
import { selectedRect, TableRect } from "@tiptap/pm/tables";
import { Transaction } from "prosemirror-state";
import { Node } from "prosemirror-model";
+import { unparse } from "papaparse";
+import { saveAs } from "file-saver";
type TableCell = {
cell: Node;
pos: number;
};
-function moveColumnRight(editor: Editor) {
+function moveColumnRight(editor?: Editor) {
+ if (!editor) return;
+
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.right === rect.map.width) return;
@@ -38,7 +42,9 @@ function moveColumnRight(editor: Editor) {
editor.view.dispatch(transaction);
}
-function moveColumnLeft(editor: Editor) {
+function moveColumnLeft(editor?: Editor) {
+ if (!editor) return;
+
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.left === 0) return;
@@ -49,7 +55,9 @@ function moveColumnLeft(editor: Editor) {
editor.view.dispatch(transaction);
}
-function moveRowDown(editor: Editor) {
+function moveRowDown(editor?: Editor) {
+ if (!editor) return;
+
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.top + 1 === rect.map.height) return;
@@ -60,7 +68,9 @@ function moveRowDown(editor: Editor) {
editor.view.dispatch(transaction);
}
-function moveRowUp(editor: Editor) {
+function moveRowUp(editor?: Editor) {
+ if (!editor) return;
+
const { tr } = editor.state;
const rect = selectedRect(editor.state);
if (rect.top === 0) return;
@@ -159,4 +169,19 @@ function moveCells(
return tr;
}
-export { moveColumnLeft, moveColumnRight, moveRowDown, moveRowUp };
+function exportToCSV(editor?: Editor) {
+ if (!editor) return;
+
+ const rect = selectedRect(editor.state);
+
+ const rows: string[][] = [];
+ rect.table.forEach((node) => {
+ const row: string[] = [];
+ node.forEach((cell) => row.push(cell.textContent));
+ rows.push(row);
+ });
+
+ saveAs(new Blob([new TextEncoder().encode(unparse(rows))]), "table.csv");
+}
+
+export { moveColumnLeft, moveColumnRight, moveRowDown, moveRowUp, exportToCSV };
diff --git a/packages/editor/src/hooks/use-permission-handler.ts b/packages/editor/src/hooks/use-permission-handler.ts
index 41ebce1b0..04ba476a9 100644
--- a/packages/editor/src/hooks/use-permission-handler.ts
+++ b/packages/editor/src/hooks/use-permission-handler.ts
@@ -17,18 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { UnionCommands } from "@tiptap/core";
import { useEffect } from "react";
-import { PermissionRequestEvent } from "../types";
+import { PermissionRequestEvent, Commands } from "../types";
export type Claims = "premium";
export type PermissionHandlerOptions = {
claims: Record;
- onPermissionDenied: (claim: Claims, id: keyof UnionCommands) => void;
+ onPermissionDenied: (claim: Claims, id: Commands) => void;
};
-const ClaimsMap: Record = {
- premium: ["insertImage"]
+const ClaimsMap: Record = {
+ premium: ["insertImage", "exportToCSV"]
};
export function usePermissionHandler(options: PermissionHandlerOptions) {
diff --git a/packages/editor/src/toolbar/icons.ts b/packages/editor/src/toolbar/icons.ts
index 7614ca104..7521dd5b6 100644
--- a/packages/editor/src/toolbar/icons.ts
+++ b/packages/editor/src/toolbar/icons.ts
@@ -224,6 +224,7 @@ export const Icons = {
heading: mdiFormatHeaderPound,
indent: mdiFormatIndentIncrease,
outdent: mdiFormatIndentDecrease,
+ csv: "m22.28572 6-1.714289 11.142849L18.857151 6h-1.714302l2.156582 12h2.544L24 6Zm-8.57144 12H8.571428v-1.71428h5.142852v-3.428577h-3.42856a1.716 1.716 0 0 1-1.714292-1.714286V7.7142849A1.716 1.716 0 0 1 10.28572 6h5.142849v1.7142849H10.28572v3.4285721h3.42856a1.716 1.716 0 0 1 1.714289 1.714286v3.428577A1.716 1.716 0 0 1 13.71428 18Zm-6.8571369 0H1.7142849A1.716 1.716 0 0 1 0 16.28572V7.7142849A1.716 1.716 0 0 1 1.7142849 6h5.1428582v1.7142849H1.7142849V16.28572h5.1428582z",
plus: mdiPlus,
minus: mdiMinus,
diff --git a/packages/editor/src/toolbar/tool-definitions.ts b/packages/editor/src/toolbar/tool-definitions.ts
index e30ad2382..b11eed014 100644
--- a/packages/editor/src/toolbar/tool-definitions.ts
+++ b/packages/editor/src/toolbar/tool-definitions.ts
@@ -217,6 +217,11 @@ const tools: Record = {
title: "Delete table",
conditional: true
},
+ exportToCSV: {
+ icon: "csv",
+ title: "Export to CSV",
+ conditional: true
+ },
cellBackgroundColor: {
icon: "backgroundColor",
title: "Cell background color",
diff --git a/packages/editor/src/toolbar/tools/index.ts b/packages/editor/src/toolbar/tools/index.ts
index 6be37cdea..1743989c2 100644
--- a/packages/editor/src/toolbar/tools/index.ts
+++ b/packages/editor/src/toolbar/tools/index.ts
@@ -59,7 +59,8 @@ import {
CellBackgroundColor,
CellBorderColor,
CellTextColor,
- CellBorderWidth
+ CellBorderWidth,
+ ExportToCSV
} from "./table";
import {
ImageSettings,
@@ -160,6 +161,7 @@ const tools = {
moveRowDown: MoveRowDown,
deleteRow: DeleteRow,
deleteTable: DeleteTable,
+ exportToCSV: ExportToCSV,
outdent: Outdent,
indent: Indent,
diff --git a/packages/editor/src/toolbar/tools/table.tsx b/packages/editor/src/toolbar/tools/table.tsx
index 2a9115f22..f2a591dc4 100644
--- a/packages/editor/src/toolbar/tools/table.tsx
+++ b/packages/editor/src/toolbar/tools/table.tsx
@@ -27,7 +27,8 @@ import {
moveColumnLeft as moveColumnLeftAction,
moveColumnRight as moveColumnRightAction,
moveRowDown as moveRowDownAction,
- moveRowUp as moveRowUpAction
+ moveRowUp as moveRowUpAction,
+ exportToCSV as exportToCSVAction
} from "../../extensions/table/actions";
import { MoreTools } from "../components/more-tools";
import { menuButtonToTool } from "./utils";
@@ -166,6 +167,7 @@ export function TableProperties(props: ToolProps) {
splitCells(editor),
cellProperties(editor),
{ type: "separator", key: "tableSeperator" },
+ exportToCSV(editor),
deleteTable(editor)
],
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -334,14 +336,14 @@ const moveColumnLeft = (editor: Editor): MenuButton => ({
...getToolDefinition("moveColumnLeft"),
key: "moveColumnLeft",
type: "button",
- onClick: () => moveColumnLeftAction(editor)
+ onClick: () => moveColumnLeftAction(editor.current)
});
const moveColumnRight = (editor: Editor): MenuButton => ({
...getToolDefinition("moveColumnRight"),
key: "moveColumnRight",
type: "button",
- onClick: () => moveColumnRightAction(editor)
+ onClick: () => moveColumnRightAction(editor.current)
});
const deleteColumn = (editor: Editor): MenuButton => ({
@@ -383,13 +385,13 @@ const moveRowUp = (editor: Editor): MenuButton => ({
...getToolDefinition("moveRowUp"),
key: "moveRowUp",
type: "button",
- onClick: () => moveRowUpAction(editor)
+ onClick: () => moveRowUpAction(editor.current)
});
const moveRowDown = (editor: Editor): MenuButton => ({
...getToolDefinition("moveRowDown"),
key: "moveRowDown",
type: "button",
- onClick: () => moveRowDownAction(editor)
+ onClick: () => moveRowDownAction(editor.current)
});
const deleteRow = (editor: Editor): MenuButton => ({
@@ -406,6 +408,13 @@ const deleteTable = (editor: Editor): MenuButton => ({
onClick: () => editor.current?.chain().focus().deleteTable().run()
});
+const exportToCSV = (editor: Editor): MenuButton => ({
+ ...getToolDefinition("exportToCSV"),
+ key: "exportToCSV",
+ type: "button",
+ onClick: () => exportToCSVAction(editor.requestPermission("exportToCSV"))
+});
+
const cellProperties = (editor: Editor): MenuButton => ({
...getToolDefinition("cellProperties"),
key: "cellProperties",
@@ -430,3 +439,4 @@ export const MoveRowUp = menuButtonToTool(moveRowUp);
export const MoveRowDown = menuButtonToTool(moveRowDown);
export const DeleteRow = menuButtonToTool(deleteRow);
export const DeleteTable = menuButtonToTool(deleteTable);
+export const ExportToCSV = menuButtonToTool(exportToCSV);
diff --git a/packages/editor/src/types.ts b/packages/editor/src/types.ts
index cfa87dd8e..e086e90e5 100644
--- a/packages/editor/src/types.ts
+++ b/packages/editor/src/types.ts
@@ -19,7 +19,8 @@ along with this program. If not, see .
import { UnionCommands, Editor as TiptapEditor } from "@tiptap/core";
-export type PermissionRequestEvent = CustomEvent<{ id: keyof UnionCommands }>;
+export type Commands = keyof UnionCommands | "exportToCSV";
+export type PermissionRequestEvent = CustomEvent<{ id: Commands }>;
export class Editor extends TiptapEditor {
/**
@@ -35,7 +36,7 @@ export class Editor extends TiptapEditor {
* @param id the command id to get permission for
* @returns latest editor instance
*/
- requestPermission(id: keyof UnionCommands): TiptapEditor | undefined {
+ requestPermission(id: Commands): TiptapEditor | undefined {
const event = new CustomEvent("permissionrequest", {
detail: { id },
cancelable: true