From 52d48352fd6bab0ad3ab85baca15459f9b2571da Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 18 Mar 2023 11:35:47 +0500 Subject: [PATCH] editor: add export as csv to table --- packages/editor/package-lock.json | 66 +++++++++++++++++-- packages/editor/package.json | 4 ++ .../editor/src/extensions/table/actions.ts | 35 ++++++++-- .../src/hooks/use-permission-handler.ts | 9 ++- packages/editor/src/toolbar/icons.ts | 1 + .../editor/src/toolbar/tool-definitions.ts | 5 ++ packages/editor/src/toolbar/tools/index.ts | 4 +- packages/editor/src/toolbar/tools/table.tsx | 20 ++++-- packages/editor/src/types.ts | 5 +- 9 files changed, 125 insertions(+), 24 deletions(-) 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