From f18bf3ff124923f4f6147a17dd923e50ae1196b6 Mon Sep 17 00:00:00 2001 From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:42:36 +0530 Subject: [PATCH 1/8] added basic table support --- .../components/tiptap/extensions/index.tsx | 12 ++ .../tiptap/extensions/table/table-cell.ts | 39 ++++++ .../tiptap/extensions/table/table-header.ts | 7 + .../tiptap/extensions/table/table-row.ts | 0 .../tiptap/extensions/table/table.ts | 9 ++ .../tiptap/extensions/updated-image.tsx | 2 +- apps/app/components/tiptap/index.tsx | 2 + .../tiptap/plugins/delete-image.tsx | 2 +- .../tiptap/plugins/table-menu-component.tsx | 129 ++++++++++++++++++ .../components/tiptap/plugins/table-menu.tsx | 123 +++++++++++++++++ .../components/tiptap/slash-command/index.tsx | 11 ++ .../components/tiptap/table-menu/index.tsx | 94 +++++++++++++ apps/app/package.json | 5 + apps/app/styles/editor.css | 90 ++++++++++++ 14 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 apps/app/components/tiptap/extensions/table/table-cell.ts create mode 100644 apps/app/components/tiptap/extensions/table/table-header.ts create mode 100644 apps/app/components/tiptap/extensions/table/table-row.ts create mode 100644 apps/app/components/tiptap/extensions/table/table.ts create mode 100644 apps/app/components/tiptap/plugins/table-menu-component.tsx create mode 100644 apps/app/components/tiptap/plugins/table-menu.tsx create mode 100644 apps/app/components/tiptap/table-menu/index.tsx diff --git a/apps/app/components/tiptap/extensions/index.tsx b/apps/app/components/tiptap/extensions/index.tsx index 2c5ffd10a43..e34ea0348b8 100644 --- a/apps/app/components/tiptap/extensions/index.tsx +++ b/apps/app/components/tiptap/extensions/index.tsx @@ -20,6 +20,10 @@ import "highlight.js/styles/github-dark.css"; import UniqueID from "@tiptap-pro/extension-unique-id"; import UpdatedImage from "./updated-image"; import isValidHttpUrl from "../bubble-menu/utils/link-validator"; +import { CustomTableCell } from "./table/table-cell"; +import { Table } from "./table/table"; +import { TableHeader } from "./table/table-header"; +import { TableRow } from "@tiptap/extension-table-row"; lowlight.registerLanguage("ts", ts); @@ -101,9 +105,13 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub }), Placeholder.configure({ placeholder: ({ node }) => { + console.log(node.type.name) if (node.type.name === "heading") { return `Heading ${node.attrs.level}`; } + if (node.type.name === "image" || node.type.name === "table") { + return "" + } return "Press '/' for commands..."; }, @@ -134,4 +142,8 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub html: true, transformCopiedText: true, }), + Table, + TableHeader, + CustomTableCell, + TableRow ]; diff --git a/apps/app/components/tiptap/extensions/table/table-cell.ts b/apps/app/components/tiptap/extensions/table/table-cell.ts new file mode 100644 index 00000000000..f0d5cce42d8 --- /dev/null +++ b/apps/app/components/tiptap/extensions/table/table-cell.ts @@ -0,0 +1,39 @@ +import { TableCell } from "@tiptap/extension-table-cell"; +import { Star } from "lucide-react"; + +export const CustomTableCell = TableCell.extend({ + addAttributes() { + return { + ...this.parent?.(), + isHeader: { + default: false, + parseHTML: (element) => { + console.log("ran inside", element.tagName); + return { isHeader: element.tagName === "TD" }; + }, + renderHTML: (attributes) => { + return { tag: attributes.isHeader ? "th" : "td" }; + }, + }, + }; + }, + renderHTML({ HTMLAttributes }) { + console.log("ran", HTMLAttributes); + if (HTMLAttributes.isHeader) { + return [ + "th", + { + ...HTMLAttributes, + class: `relative ${HTMLAttributes.class}`, + }, + [ + "span", + { class: "absolute top-0 right-0" }, + Star + ], + 0, + ]; + } + return ["td", HTMLAttributes, 0]; + }, +}); diff --git a/apps/app/components/tiptap/extensions/table/table-header.ts b/apps/app/components/tiptap/extensions/table/table-header.ts new file mode 100644 index 00000000000..d04fe85d3fb --- /dev/null +++ b/apps/app/components/tiptap/extensions/table/table-header.ts @@ -0,0 +1,7 @@ +import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header"; + +const TableHeader = BaseTableHeader.extend({ + content: "paragraph" +}); + +export { TableHeader }; diff --git a/apps/app/components/tiptap/extensions/table/table-row.ts b/apps/app/components/tiptap/extensions/table/table-row.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/app/components/tiptap/extensions/table/table.ts b/apps/app/components/tiptap/extensions/table/table.ts new file mode 100644 index 00000000000..b05dedb3b51 --- /dev/null +++ b/apps/app/components/tiptap/extensions/table/table.ts @@ -0,0 +1,9 @@ +import { Table as BaseTable } from "@tiptap/extension-table"; + +const Table = BaseTable.configure({ + resizable: true, + cellMinWidth: 100, + allowTableNodeSelection: true +}); + +export { Table }; diff --git a/apps/app/components/tiptap/extensions/updated-image.tsx b/apps/app/components/tiptap/extensions/updated-image.tsx index 01648dcd701..2f27ffab306 100644 --- a/apps/app/components/tiptap/extensions/updated-image.tsx +++ b/apps/app/components/tiptap/extensions/updated-image.tsx @@ -4,7 +4,7 @@ import UploadImagesPlugin from "../plugins/upload-image"; const UpdatedImage = Image.extend({ addProseMirrorPlugins() { - return [UploadImagesPlugin(), TrackImageDeletionPlugin()]; + return [UploadImagesPlugin()]; }, addAttributes() { return { diff --git a/apps/app/components/tiptap/index.tsx b/apps/app/components/tiptap/index.tsx index 418449c0890..7d04fb09347 100644 --- a/apps/app/components/tiptap/index.tsx +++ b/apps/app/components/tiptap/index.tsx @@ -5,6 +5,7 @@ import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; import { useImperativeHandle, useRef } from "react"; import { ImageResizer } from "./extensions/image-resize"; +import { TableMenu } from "./table-menu"; export interface ITiptapRichTextEditor { value: string; @@ -91,6 +92,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => { {editor && }
+ {editor?.isActive("table") && } {editor?.isActive("image") && }
diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx index 57ab65c6379..2761d7f9f74 100644 --- a/apps/app/components/tiptap/plugins/delete-image.tsx +++ b/apps/app/components/tiptap/plugins/delete-image.tsx @@ -16,7 +16,7 @@ const TrackImageDeletionPlugin = () => oldState.doc.descendants((oldNode, oldPos) => { if (oldNode.type.name !== 'image') return; - if (!newState.doc.resolve(oldPos).parent) return; + // if (!newState.doc.resolve(oldPos).parent) return; const newNode = newState.doc.nodeAt(oldPos); // Check if the node has been deleted or replaced diff --git a/apps/app/components/tiptap/plugins/table-menu-component.tsx b/apps/app/components/tiptap/plugins/table-menu-component.tsx new file mode 100644 index 00000000000..b5d1379fc92 --- /dev/null +++ b/apps/app/components/tiptap/plugins/table-menu-component.tsx @@ -0,0 +1,129 @@ +import { + mdiKeyboardCloseOutline, + mdiTableColumnPlusAfter, + mdiTableColumnPlusBefore, + mdiTableHeadersEye, + mdiTableHeadersEyeOff, + mdiTableRemove, + mdiTableRowPlusAfter, + mdiTableRowPlusBefore +} from "@mdi/js"; +import { ChainedCommands } from "@tiptap/core"; +import clsx from "clsx"; +import { FC, useMemo } from "react"; + +interface TableMenuProps { + state: { + container: HTMLElement | null; + }; +} + +const TableMenu: FC = (props) => { + const hasHeader = useMemo(() => props.state.container?.querySelector("tr:first-child > th") !== null, [props.state.container]); + const runCommand = (fn: (chain: ChainedCommands) => void): void => { + const chain = props.state.editor.chain(); + + if (hasHeader()) { + chain.toggleHeaderRow(); + } + + fn(chain); + + if (hasHeader()) { + chain.toggleHeaderRow(); + } + + chain.fixTables().focus().run(); + }; + + return ( + + + chain.addRowBefore()); + } + }, + { + icon: mdiTableRowPlusAfter, + label: "Insert row below", + onClick() { + runCommand((chain) => chain.addRowAfter()); + } + }, + { + icon: mdiTableColumnPlusBefore, + label: "Insert column left", + onClick() { + runCommand((chain) => chain.addColumnBefore()); + } + }, + { + icon: mdiTableColumnPlusAfter, + label: "Insert column right", + onClick() { + runCommand((chain) => chain.addColumnAfter()); + } + }, + { + label() { + return hasHeader() ? "Remove header row" : "Add header row"; + }, + icon() { + return hasHeader() ? mdiTableHeadersEyeOff : mdiTableHeadersEye; + }, + onClick() { + props.state.editor.chain().focus().toggleHeaderRow().run(); + } + }, + { + icon: mdiTableRemove, + label: "Delete table", + onClick() { + props.state.editor.chain().deleteTable().focus().run(); + } + }, + ...((!breakpoints.md() && [ + { + icon: mdiKeyboardCloseOutline, + label: "Close keyboard", + async onClick() { + props.state.editor.commands.blur(); + } + } + ]) || + []) + ]} + > + {(menuItem) => { + return ( + + + + ); + }} + + + + ); +}; + +export { TableMenu }; diff --git a/apps/app/components/tiptap/plugins/table-menu.tsx b/apps/app/components/tiptap/plugins/table-menu.tsx new file mode 100644 index 00000000000..722a0e724bb --- /dev/null +++ b/apps/app/components/tiptap/plugins/table-menu.tsx @@ -0,0 +1,123 @@ +import { TableMenu } from "./component"; +import { Extension } from "@tiptap/core"; +import { TextSelection } from "@tiptap/pm/state"; +import { CellSelection } from "@tiptap/pm/tables"; +import { Editor } from "@tiptap/react"; + +const generalMenuContainer = document.createElement("div"); + +const getTableParent = (node: Node): HTMLElement | null => { + let currentNode: HTMLElement | null = + node.nodeType === 1 ? (node as HTMLElement) : node.parentElement; + + while (currentNode) { + if (currentNode.tagName === "TABLE") { + return currentNode; + } + + currentNode = currentNode.parentElement; + } + + return null; +}; +const handleUpdate = (editor: Editor): void => { + const { selection } = editor.state; + const isTextSelection = selection instanceof TextSelection; + const isCellSelection = selection instanceof CellSelection; + const selectedNode = selection.$from.node(1) || selection.$from.nodeAfter; + + if ( + !selectedNode || + !editor.isActive("table") || + isCellSelection || + !(isTextSelection && selection.empty) + ) { + generalMenuContainer.style.display = "none"; + + return; + } + + const { view } = editor; + const node = + view.nodeDOM(selection.$from.pos) || + view.nodeDOM(selection.$from.pos - selection.$from.parentOffset) || + view.domAtPos(selection.$from.pos)?.node; + + if (!node) return; + + const blockParent = getTableParent(node); + const parentPos = document.getElementById("pm-container")?.getBoundingClientRect(); + const childPos = blockParent?.getBoundingClientRect(); + const tablePos = blockParent?.querySelector("tbody")?.getBoundingClientRect(); + + if (!parentPos || !childPos) return; + + const relativePos = { + top: childPos.top - parentPos.top, + right: childPos.right - parentPos.right, + bottom: childPos.bottom - parentPos.bottom, + left: childPos.left - parentPos.left + }; + + generalMenuContainer.style.top = `${relativePos.top + (tablePos?.height || 0)}px`; + generalMenuContainer.style.transform = `translate(${(tablePos?.width || 0) > 250 ? "-50%" : "0" + },0.75rem)`; + + if ((tablePos?.width || 0) > 250) { + generalMenuContainer.style.left = `${relativePos.left + Math.min(tablePos?.width || parentPos.width, parentPos.width) / 2 + }px`; + } else { + generalMenuContainer.style.left = "-0.25rem"; + } + + generalMenuContainer.style.display = "block"; + generalMenu?.setState({ + node: selectedNode, + container: blockParent, + editor + }); +}; +const TableMenuPlugin = Extension.create({ + name: "tableMenu", + onCreate() { + generalMenu = new SolidRenderer(TableMenu, { + editor: this.editor as SolidEditor, + state: { + container: null as HTMLElement | null, + editor: this.editor as SolidEditor + } + }); + generalMenuContainer.style.position = "absolute"; + generalMenuContainer.style.top = "-100vh"; + generalMenuContainer.style.left = "-100vw"; + generalMenuContainer.appendChild(generalMenu.element); + document.getElementById("pm-container")?.appendChild(generalMenuContainer); + }, + onBlur() { + const dropdownOpened = document.documentElement.classList.contains("dropdown-opened"); + + if ( + (document.activeElement?.contains(generalMenuContainer) || dropdownOpened) && + breakpoints.md() + ) { + return; + } + + generalMenuContainer.style.display = "none"; + }, + onFocus() { + const isCellSelection = this.editor.state.selection instanceof CellSelection; + + if (this.editor.isActive("table") && !isCellSelection) { + generalMenuContainer.style.display = "block"; + } + }, + onUpdate() { + handleUpdate(this.editor as SolidEditor); + }, + onSelectionUpdate() { + handleUpdate(this.editor as SolidEditor); + } +}); + +export { TableMenuPlugin }; diff --git a/apps/app/components/tiptap/slash-command/index.tsx b/apps/app/components/tiptap/slash-command/index.tsx index 38f5c9c0ad9..13c6e3bc0d6 100644 --- a/apps/app/components/tiptap/slash-command/index.tsx +++ b/apps/app/components/tiptap/slash-command/index.tsx @@ -15,6 +15,7 @@ import { MinusSquare, CheckSquare, ImageIcon, + Table, } from "lucide-react"; import { startImageUpload } from "../plugins/upload-image"; import { cn } from "../utils"; @@ -60,6 +61,7 @@ const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitti searchTerms: ["p", "paragraph"], icon: , command: ({ editor, range }: CommandProps) => { + console.log("focused") editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); }, }, @@ -117,6 +119,15 @@ const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitti editor.chain().focus().deleteRange(range).setHorizontalRule().run(); }, }, + { + title: "Table", + description: "Create a Table", + searchTerms: ["table", "cell", "db", "data", "tabular"], + icon: , + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + }, + }, { title: "Numbered List", description: "Create a list with numbering.", diff --git a/apps/app/components/tiptap/table-menu/index.tsx b/apps/app/components/tiptap/table-menu/index.tsx new file mode 100644 index 00000000000..5b2aa714012 --- /dev/null +++ b/apps/app/components/tiptap/table-menu/index.tsx @@ -0,0 +1,94 @@ +import { BubbleMenu, BubbleMenuProps } from "@tiptap/react"; +import { FC, useState, useEffect } from "react"; +import { Rows, Columns, BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react"; + +import { cn } from "../utils"; +import { ToggleOn } from "@mui/icons-material"; + +export interface TableMenuItem { + name: string; + isActive?: () => boolean; + command: () => void; + icon: typeof Rows; +} + +type EditorTableMenuProps = Omit; + +export const TableMenu: FC = (props: any) => { + const items: TableMenuItem[] = [ + { + name: "Add Column to Right", + command: () => props.editor?.chain().focus().addColumnAfter().run(), + icon: Columns, + }, + { + name: "Toggle table header", + command: () => props.editor?.chain().focus().toggleHeaderRow().run(), + icon: ToggleOn, + }, + { + name: "Add Column to Left", + command: () => props.editor?.chain().focus().addColumnBefore().run(), + icon: Columns, + }, + { + name: "Add Row to Top", + command: () => props.editor?.chain().focus().addRowBefore().run(), + icon: Rows, + }, + { + name: "Add Row Below", + command: () => props.editor?.chain().focus().addRowAfter().run(), + icon: Rows, + }, + { + name: "Delete Column", + command: () => props.editor?.chain().focus().deleteColumn().run(), + icon: Columns, + }, + { + name: "Delete Rows", + command: () => props.editor?.chain().focus().deleteRow().run(), + icon: Rows, + } + ]; + + const tableMenuProps: EditorTableMenuProps = { + ...props, + shouldShow: ({ editor }) => { + if (!editor.isEditable) { + return false; + } + if (editor?.isActive("table")) { + return true; + } + }, + tippyOptions: { + moveTransition: "transform 0.15s ease-out", + }, + }; + + return ( + +
+ {items.map((item, index) => ( + + ))} +
+
+ ); +}; diff --git a/apps/app/package.json b/apps/app/package.json index 578a95716f5..045c02b4399 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -16,6 +16,7 @@ "@headlessui/react": "^1.7.3", "@heroicons/react": "^2.0.12", "@jitsu/nextjs": "^3.1.5", + "@mdi/js": "^7.2.96", "@mui/icons-material": "^5.14.1", "@mui/material": "^5.14.1", "@nivo/bar": "0.80.0", @@ -35,6 +36,10 @@ "@tiptap/extension-image": "^2.0.4", "@tiptap/extension-link": "^2.0.4", "@tiptap/extension-placeholder": "^2.0.4", + "@tiptap/extension-table": "^2.1.6", + "@tiptap/extension-table-cell": "^2.1.6", + "@tiptap/extension-table-header": "^2.1.6", + "@tiptap/extension-table-row": "^2.1.6", "@tiptap/extension-task-item": "^2.0.4", "@tiptap/extension-task-list": "^2.0.4", "@tiptap/extension-text-style": "^2.0.4", diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css index 57c23c911b5..96e3ff84dd2 100644 --- a/apps/app/styles/editor.css +++ b/apps/app/styles/editor.css @@ -150,3 +150,93 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { transform: rotate(360deg); } } + +#tiptap-container { + table { + border-collapse: collapse; + table-layout: fixed; + border: 2px solid rgb(var(--color-border-100)); + border-radius: 10px; + width: 100%; + margin: 0; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + + td, + th { + min-width: 1em; + border: 2px solid rgb(var(--color-border-400)); + padding: 10px 15px; + vertical-align: top; + box-sizing: border-box; + position: relative; + transition: background-color 0.3s ease; + + > * { + margin-bottom: 0; + } + } + + th { + font-weight: bold; + text-align: left; + background-color: rgb(var(--color-primary-300)); + } + + tr:first-child th:first-child { + border-top-left-radius: 10px; + } + + tr:first-child th:last-child { + border-top-right-radius: 10px; + } + + tr:last-child td:first-child { + border-bottom-left-radius: 10px; + } + + tr:last-child td:last-child { + border-bottom-right-radius: 10px; + } + + td:hover { + background-color: rgba(var(--color-primary-300), 0.1); + } + + .selectedCell:after { + z-index: 2; + position: absolute; + content: ""; + left: 0; right: 0; top: 0; bottom: 0; + background-color: rgba(var(--color-primary-300), 0.1); + pointer-events: none; + } + + .column-resize-handle { + position: absolute; + right: -2px; + top: 0; + bottom: -2px; + width: 2px; + background-color: rgb(var(--color-primary-400)); + pointer-events: none; + } + } +} + +.tableWrapper { + overflow-x: auto; +} + +.resize-cursor { + cursor: ew-resize; + cursor: col-resize; +} + +.ProseMirror table * p { + padding: 0px 1px; + margin: 6px 2px; +} + +.ProseMirror table * .is-empty::before { + opacity: 0; +} From 3a34458245d7c0e098036db7f74efefdffcbcb2e Mon Sep 17 00:00:00 2001 From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:06:56 +0530 Subject: [PATCH 2/8] fixed table position at bottom --- .../tiptap/extensions/updated-image.tsx | 2 +- .../tiptap/plugins/delete-image.tsx | 2 +- .../tiptap/plugins/table-menu-component.tsx | 129 ------------------ .../components/tiptap/plugins/table-menu.tsx | 123 ----------------- .../components/tiptap/table-menu/index.tsx | 116 +++++++--------- apps/app/package.json | 1 - apps/app/styles/editor.css | 3 +- yarn.lock | 29 +++- 8 files changed, 84 insertions(+), 321 deletions(-) diff --git a/apps/app/components/tiptap/extensions/updated-image.tsx b/apps/app/components/tiptap/extensions/updated-image.tsx index 2f27ffab306..01648dcd701 100644 --- a/apps/app/components/tiptap/extensions/updated-image.tsx +++ b/apps/app/components/tiptap/extensions/updated-image.tsx @@ -4,7 +4,7 @@ import UploadImagesPlugin from "../plugins/upload-image"; const UpdatedImage = Image.extend({ addProseMirrorPlugins() { - return [UploadImagesPlugin()]; + return [UploadImagesPlugin(), TrackImageDeletionPlugin()]; }, addAttributes() { return { diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx index 2761d7f9f74..57ab65c6379 100644 --- a/apps/app/components/tiptap/plugins/delete-image.tsx +++ b/apps/app/components/tiptap/plugins/delete-image.tsx @@ -16,7 +16,7 @@ const TrackImageDeletionPlugin = () => oldState.doc.descendants((oldNode, oldPos) => { if (oldNode.type.name !== 'image') return; - // if (!newState.doc.resolve(oldPos).parent) return; + if (!newState.doc.resolve(oldPos).parent) return; const newNode = newState.doc.nodeAt(oldPos); // Check if the node has been deleted or replaced diff --git a/apps/app/components/tiptap/plugins/table-menu-component.tsx b/apps/app/components/tiptap/plugins/table-menu-component.tsx index b5d1379fc92..e69de29bb2d 100644 --- a/apps/app/components/tiptap/plugins/table-menu-component.tsx +++ b/apps/app/components/tiptap/plugins/table-menu-component.tsx @@ -1,129 +0,0 @@ -import { - mdiKeyboardCloseOutline, - mdiTableColumnPlusAfter, - mdiTableColumnPlusBefore, - mdiTableHeadersEye, - mdiTableHeadersEyeOff, - mdiTableRemove, - mdiTableRowPlusAfter, - mdiTableRowPlusBefore -} from "@mdi/js"; -import { ChainedCommands } from "@tiptap/core"; -import clsx from "clsx"; -import { FC, useMemo } from "react"; - -interface TableMenuProps { - state: { - container: HTMLElement | null; - }; -} - -const TableMenu: FC = (props) => { - const hasHeader = useMemo(() => props.state.container?.querySelector("tr:first-child > th") !== null, [props.state.container]); - const runCommand = (fn: (chain: ChainedCommands) => void): void => { - const chain = props.state.editor.chain(); - - if (hasHeader()) { - chain.toggleHeaderRow(); - } - - fn(chain); - - if (hasHeader()) { - chain.toggleHeaderRow(); - } - - chain.fixTables().focus().run(); - }; - - return ( - - - chain.addRowBefore()); - } - }, - { - icon: mdiTableRowPlusAfter, - label: "Insert row below", - onClick() { - runCommand((chain) => chain.addRowAfter()); - } - }, - { - icon: mdiTableColumnPlusBefore, - label: "Insert column left", - onClick() { - runCommand((chain) => chain.addColumnBefore()); - } - }, - { - icon: mdiTableColumnPlusAfter, - label: "Insert column right", - onClick() { - runCommand((chain) => chain.addColumnAfter()); - } - }, - { - label() { - return hasHeader() ? "Remove header row" : "Add header row"; - }, - icon() { - return hasHeader() ? mdiTableHeadersEyeOff : mdiTableHeadersEye; - }, - onClick() { - props.state.editor.chain().focus().toggleHeaderRow().run(); - } - }, - { - icon: mdiTableRemove, - label: "Delete table", - onClick() { - props.state.editor.chain().deleteTable().focus().run(); - } - }, - ...((!breakpoints.md() && [ - { - icon: mdiKeyboardCloseOutline, - label: "Close keyboard", - async onClick() { - props.state.editor.commands.blur(); - } - } - ]) || - []) - ]} - > - {(menuItem) => { - return ( - - - - ); - }} - - - - ); -}; - -export { TableMenu }; diff --git a/apps/app/components/tiptap/plugins/table-menu.tsx b/apps/app/components/tiptap/plugins/table-menu.tsx index 722a0e724bb..e69de29bb2d 100644 --- a/apps/app/components/tiptap/plugins/table-menu.tsx +++ b/apps/app/components/tiptap/plugins/table-menu.tsx @@ -1,123 +0,0 @@ -import { TableMenu } from "./component"; -import { Extension } from "@tiptap/core"; -import { TextSelection } from "@tiptap/pm/state"; -import { CellSelection } from "@tiptap/pm/tables"; -import { Editor } from "@tiptap/react"; - -const generalMenuContainer = document.createElement("div"); - -const getTableParent = (node: Node): HTMLElement | null => { - let currentNode: HTMLElement | null = - node.nodeType === 1 ? (node as HTMLElement) : node.parentElement; - - while (currentNode) { - if (currentNode.tagName === "TABLE") { - return currentNode; - } - - currentNode = currentNode.parentElement; - } - - return null; -}; -const handleUpdate = (editor: Editor): void => { - const { selection } = editor.state; - const isTextSelection = selection instanceof TextSelection; - const isCellSelection = selection instanceof CellSelection; - const selectedNode = selection.$from.node(1) || selection.$from.nodeAfter; - - if ( - !selectedNode || - !editor.isActive("table") || - isCellSelection || - !(isTextSelection && selection.empty) - ) { - generalMenuContainer.style.display = "none"; - - return; - } - - const { view } = editor; - const node = - view.nodeDOM(selection.$from.pos) || - view.nodeDOM(selection.$from.pos - selection.$from.parentOffset) || - view.domAtPos(selection.$from.pos)?.node; - - if (!node) return; - - const blockParent = getTableParent(node); - const parentPos = document.getElementById("pm-container")?.getBoundingClientRect(); - const childPos = blockParent?.getBoundingClientRect(); - const tablePos = blockParent?.querySelector("tbody")?.getBoundingClientRect(); - - if (!parentPos || !childPos) return; - - const relativePos = { - top: childPos.top - parentPos.top, - right: childPos.right - parentPos.right, - bottom: childPos.bottom - parentPos.bottom, - left: childPos.left - parentPos.left - }; - - generalMenuContainer.style.top = `${relativePos.top + (tablePos?.height || 0)}px`; - generalMenuContainer.style.transform = `translate(${(tablePos?.width || 0) > 250 ? "-50%" : "0" - },0.75rem)`; - - if ((tablePos?.width || 0) > 250) { - generalMenuContainer.style.left = `${relativePos.left + Math.min(tablePos?.width || parentPos.width, parentPos.width) / 2 - }px`; - } else { - generalMenuContainer.style.left = "-0.25rem"; - } - - generalMenuContainer.style.display = "block"; - generalMenu?.setState({ - node: selectedNode, - container: blockParent, - editor - }); -}; -const TableMenuPlugin = Extension.create({ - name: "tableMenu", - onCreate() { - generalMenu = new SolidRenderer(TableMenu, { - editor: this.editor as SolidEditor, - state: { - container: null as HTMLElement | null, - editor: this.editor as SolidEditor - } - }); - generalMenuContainer.style.position = "absolute"; - generalMenuContainer.style.top = "-100vh"; - generalMenuContainer.style.left = "-100vw"; - generalMenuContainer.appendChild(generalMenu.element); - document.getElementById("pm-container")?.appendChild(generalMenuContainer); - }, - onBlur() { - const dropdownOpened = document.documentElement.classList.contains("dropdown-opened"); - - if ( - (document.activeElement?.contains(generalMenuContainer) || dropdownOpened) && - breakpoints.md() - ) { - return; - } - - generalMenuContainer.style.display = "none"; - }, - onFocus() { - const isCellSelection = this.editor.state.selection instanceof CellSelection; - - if (this.editor.isActive("table") && !isCellSelection) { - generalMenuContainer.style.display = "block"; - } - }, - onUpdate() { - handleUpdate(this.editor as SolidEditor); - }, - onSelectionUpdate() { - handleUpdate(this.editor as SolidEditor); - } -}); - -export { TableMenuPlugin }; diff --git a/apps/app/components/tiptap/table-menu/index.tsx b/apps/app/components/tiptap/table-menu/index.tsx index 5b2aa714012..cf39d3a6b6d 100644 --- a/apps/app/components/tiptap/table-menu/index.tsx +++ b/apps/app/components/tiptap/table-menu/index.tsx @@ -1,94 +1,82 @@ -import { BubbleMenu, BubbleMenuProps } from "@tiptap/react"; -import { FC, useState, useEffect } from "react"; -import { Rows, Columns, BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react"; - +import { useState, useEffect } from "react"; +import { Rows, Columns } from "lucide-react"; import { cn } from "../utils"; -import { ToggleOn } from "@mui/icons-material"; -export interface TableMenuItem { +interface TableMenuItem { name: string; - isActive?: () => boolean; command: () => void; - icon: typeof Rows; + icon: any; } -type EditorTableMenuProps = Omit; +const findTableAncestor = (node: Node | null): HTMLTableElement | null => { + while (node !== null && node.nodeName !== "TABLE") { + node = node.parentNode; + } + return node as HTMLTableElement; +}; -export const TableMenu: FC = (props: any) => { +export const TableMenu = ({ editor }: { editor: any }) => { + const [tableLocation, setTableLocation] = useState(0); const items: TableMenuItem[] = [ { - name: "Add Column to Right", - command: () => props.editor?.chain().focus().addColumnAfter().run(), - icon: Columns, - }, - { - name: "Toggle table header", - command: () => props.editor?.chain().focus().toggleHeaderRow().run(), - icon: ToggleOn, - }, - { - name: "Add Column to Left", - command: () => props.editor?.chain().focus().addColumnBefore().run(), + name: "Insert column right", + command: () => editor.chain().focus().addColumnBefore().run(), icon: Columns, }, { - name: "Add Row to Top", - command: () => props.editor?.chain().focus().addRowBefore().run(), - icon: Rows, - }, - { - name: "Add Row Below", - command: () => props.editor?.chain().focus().addRowAfter().run(), + name: "Insert row below", + command: () => editor.chain().focus().addRowAfter().run(), icon: Rows, }, { name: "Delete Column", - command: () => props.editor?.chain().focus().deleteColumn().run(), + command: () => editor.chain().focus().deleteColumn().run(), icon: Columns, }, { name: "Delete Rows", - command: () => props.editor?.chain().focus().deleteRow().run(), + command: () => editor.chain().focus().deleteRow().run(), icon: Rows, } ]; - const tableMenuProps: EditorTableMenuProps = { - ...props, - shouldShow: ({ editor }) => { - if (!editor.isEditable) { - return false; + useEffect(() => { + const handleWindowClick = () => { + const selection: any = window.getSelection(); + const range = selection.getRangeAt(0); + const tableNode = findTableAncestor(range.startContainer); + if (tableNode) { + const tableBottom = tableNode.getBoundingClientRect().bottom; + tableLocation !== tableBottom && setTableLocation(tableBottom); } - if (editor?.isActive("table")) { - return true; - } - }, - tippyOptions: { - moveTransition: "transform 0.15s ease-out", - }, - }; + }; + + window.addEventListener("click", handleWindowClick); + + return () => { + window.removeEventListener("click", handleWindowClick); + }; + }, [tableLocation]); return ( - -
- {items.map((item, index) => ( - - ))} -
-
+ {items.map((item, index) => ( + + ))} + ); }; diff --git a/apps/app/package.json b/apps/app/package.json index 045c02b4399..b5cb26f6f2b 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -16,7 +16,6 @@ "@headlessui/react": "^1.7.3", "@heroicons/react": "^2.0.12", "@jitsu/nextjs": "^3.1.5", - "@mdi/js": "^7.2.96", "@mui/icons-material": "^5.14.1", "@mui/material": "^5.14.1", "@nivo/bar": "0.80.0", diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css index 96e3ff84dd2..3a721bacf25 100644 --- a/apps/app/styles/editor.css +++ b/apps/app/styles/editor.css @@ -155,10 +155,11 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { table { border-collapse: collapse; table-layout: fixed; + margin: 0; + margin-bottom: 2rem; border: 2px solid rgb(var(--color-border-100)); border-radius: 10px; width: 100%; - margin: 0; box-shadow: 0 0 10px rgba(0,0,0,0.1); td, diff --git a/yarn.lock b/yarn.lock index 8bc1fec304d..36041e2d0ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2349,6 +2349,26 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.4.tgz#13286dcf8780c55610ed65b24238b8395a5be824" integrity sha512-Men7LK6N/Dh3/G4/z2Z9WkDHM2Gxx1XyxYix2ZMf5CnqY37SeDNUnGDqit65pdIN3Y/TQnOZTkKSBilSAtXfJA== +"@tiptap/extension-table-cell@^2.1.6": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.1.7.tgz#87841144b8368c9611ad46f2134b637e2c33c8bc" + integrity sha512-p3e4FNdbKVIjOLHDcXrRtlP6FYPoN6hBUFjq6QZbf5g4+ao2Uq4bQCL+eKbYMxUVERl8g/Qu9X+jG99fVsBDjA== + +"@tiptap/extension-table-header@^2.1.6": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.1.7.tgz#4757834655e2c4edffa65bc6f6807eb59401e0d8" + integrity sha512-rolSUQxFJf/CEj2XBJpeMsLiLHASKrVIzZ2A/AZ9pT6WpFqmECi8r9xyutpJpx21n2Hrk46Y+uGFOKhyvbZ5ug== + +"@tiptap/extension-table-row@^2.1.6": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.1.7.tgz#f736a61035b271423ef18f65a25f8d1e240263a1" + integrity sha512-DBCaEMEuCCoOmr4fdDfp2jnmyWPt672rmCZ5WUuenJ47Cy4Ox2dV+qk5vBZ/yDQcq12WvzLMhdSnAo9pMMMa6Q== + +"@tiptap/extension-table@^2.1.6": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.1.7.tgz#c8a83744f60c76ae1e41438b04d5ac9e984afa66" + integrity sha512-nlKs35vTQOFW9lfw76S7kJvqVJAfHUlz1muQgWT0gNUlKJYINMXjUIg4Wcx8LTaITCCkp0lMGrLETGRNI+RyxA== + "@tiptap/extension-task-item@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.0.4.tgz#71f46d35ac629ca10c5c23d4ad170007338a436e" @@ -6558,13 +6578,20 @@ prosemirror-menu@^1.2.1: prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1: +prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1: version "1.18.1" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== dependencies: orderedmap "^2.0.0" +prosemirror-model@^1.19.0: + version "1.19.3" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006" + integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ== + dependencies: + orderedmap "^2.0.0" + prosemirror-schema-basic@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" From a2a78529ab82c654335b024677d8addf54875567 Mon Sep 17 00:00:00 2001 From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:36:53 +0530 Subject: [PATCH 3/8] fixed image node deletion logic's regression issue --- apps/app/components/tiptap/extensions/index.tsx | 1 - .../tiptap/extensions/table/table-cell.ts | 12 ++---------- .../tiptap/extensions/table/table-row.ts | 0 .../components/tiptap/plugins/delete-image.tsx | 2 +- .../tiptap/plugins/table-menu-component.tsx | 0 .../components/tiptap/plugins/table-menu.tsx | 0 apps/app/components/tiptap/table-menu/index.tsx | 12 +++++++++--- apps/app/styles/editor.css | 17 ----------------- 8 files changed, 12 insertions(+), 32 deletions(-) delete mode 100644 apps/app/components/tiptap/extensions/table/table-row.ts delete mode 100644 apps/app/components/tiptap/plugins/table-menu-component.tsx delete mode 100644 apps/app/components/tiptap/plugins/table-menu.tsx diff --git a/apps/app/components/tiptap/extensions/index.tsx b/apps/app/components/tiptap/extensions/index.tsx index e34ea0348b8..900258d44c8 100644 --- a/apps/app/components/tiptap/extensions/index.tsx +++ b/apps/app/components/tiptap/extensions/index.tsx @@ -105,7 +105,6 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub }), Placeholder.configure({ placeholder: ({ node }) => { - console.log(node.type.name) if (node.type.name === "heading") { return `Heading ${node.attrs.level}`; } diff --git a/apps/app/components/tiptap/extensions/table/table-cell.ts b/apps/app/components/tiptap/extensions/table/table-cell.ts index f0d5cce42d8..94c5aced2d6 100644 --- a/apps/app/components/tiptap/extensions/table/table-cell.ts +++ b/apps/app/components/tiptap/extensions/table/table-cell.ts @@ -1,5 +1,4 @@ import { TableCell } from "@tiptap/extension-table-cell"; -import { Star } from "lucide-react"; export const CustomTableCell = TableCell.extend({ addAttributes() { @@ -7,18 +6,12 @@ export const CustomTableCell = TableCell.extend({ ...this.parent?.(), isHeader: { default: false, - parseHTML: (element) => { - console.log("ran inside", element.tagName); - return { isHeader: element.tagName === "TD" }; - }, - renderHTML: (attributes) => { - return { tag: attributes.isHeader ? "th" : "td" }; - }, + parseHTML: (element) => { isHeader: element.tagName === "TD" }, + renderHTML: (attributes) => { tag: attributes.isHeader ? "th" : "td" } }, }; }, renderHTML({ HTMLAttributes }) { - console.log("ran", HTMLAttributes); if (HTMLAttributes.isHeader) { return [ "th", @@ -29,7 +22,6 @@ export const CustomTableCell = TableCell.extend({ [ "span", { class: "absolute top-0 right-0" }, - Star ], 0, ]; diff --git a/apps/app/components/tiptap/extensions/table/table-row.ts b/apps/app/components/tiptap/extensions/table/table-row.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx index 57ab65c6379..262a3f59145 100644 --- a/apps/app/components/tiptap/plugins/delete-image.tsx +++ b/apps/app/components/tiptap/plugins/delete-image.tsx @@ -16,6 +16,7 @@ const TrackImageDeletionPlugin = () => oldState.doc.descendants((oldNode, oldPos) => { if (oldNode.type.name !== 'image') return; + if (oldPos < 0 || oldPos > newState.doc.content.size) return; if (!newState.doc.resolve(oldPos).parent) return; const newNode = newState.doc.nodeAt(oldPos); @@ -28,7 +29,6 @@ const TrackImageDeletionPlugin = () => nodeExists = true; } }); - if (!nodeExists) { removedImages.push(oldNode as ProseMirrorNode); } diff --git a/apps/app/components/tiptap/plugins/table-menu-component.tsx b/apps/app/components/tiptap/plugins/table-menu-component.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/app/components/tiptap/plugins/table-menu.tsx b/apps/app/components/tiptap/plugins/table-menu.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/app/components/tiptap/table-menu/index.tsx b/apps/app/components/tiptap/table-menu/index.tsx index cf39d3a6b6d..75b301aa4d4 100644 --- a/apps/app/components/tiptap/table-menu/index.tsx +++ b/apps/app/components/tiptap/table-menu/index.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { Rows, Columns } from "lucide-react"; +import { Rows, Columns, ToggleRight } from "lucide-react"; import { cn } from "../utils"; interface TableMenuItem { @@ -19,12 +19,12 @@ export const TableMenu = ({ editor }: { editor: any }) => { const [tableLocation, setTableLocation] = useState(0); const items: TableMenuItem[] = [ { - name: "Insert column right", + name: "Insert Column right", command: () => editor.chain().focus().addColumnBefore().run(), icon: Columns, }, { - name: "Insert row below", + name: "Insert Row below", command: () => editor.chain().focus().addRowAfter().run(), icon: Rows, }, @@ -37,7 +37,13 @@ export const TableMenu = ({ editor }: { editor: any }) => { name: "Delete Rows", command: () => editor.chain().focus().deleteRow().run(), icon: Rows, + }, + { + name: "Toggle Header Row", + command: () => editor.chain().focus().toggleHeaderRow().run(), + icon: ToggleRight, } + ]; useEffect(() => { diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css index 3a721bacf25..8808fb2c4aa 100644 --- a/apps/app/styles/editor.css +++ b/apps/app/styles/editor.css @@ -158,7 +158,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { margin: 0; margin-bottom: 2rem; border: 2px solid rgb(var(--color-border-100)); - border-radius: 10px; width: 100%; box-shadow: 0 0 10px rgba(0,0,0,0.1); @@ -183,22 +182,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { background-color: rgb(var(--color-primary-300)); } - tr:first-child th:first-child { - border-top-left-radius: 10px; - } - - tr:first-child th:last-child { - border-top-right-radius: 10px; - } - - tr:last-child td:first-child { - border-bottom-left-radius: 10px; - } - - tr:last-child td:last-child { - border-bottom-right-radius: 10px; - } - td:hover { background-color: rgba(var(--color-primary-300), 0.1); } From 55d2f4b2aa64ca76392e22cdc7582ca5561f09d6 Mon Sep 17 00:00:00 2001 From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 29 Aug 2023 20:01:48 +0530 Subject: [PATCH 4/8] added compatible styles --- .../components/tiptap/bubble-menu/index.tsx | 20 +++++++++--- .../tiptap/bubble-menu/link-selector.tsx | 7 +++-- .../components/tiptap/slash-command/index.tsx | 31 ++++++++++++++----- .../components/tiptap/table-menu/index.tsx | 8 ++--- apps/app/styles/editor.css | 2 +- 5 files changed, 50 insertions(+), 18 deletions(-) diff --git a/apps/app/components/tiptap/bubble-menu/index.tsx b/apps/app/components/tiptap/bubble-menu/index.tsx index e689007829b..d14c7033c88 100644 --- a/apps/app/components/tiptap/bubble-menu/index.tsx +++ b/apps/app/components/tiptap/bubble-menu/index.tsx @@ -5,6 +5,7 @@ import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from import { NodeSelector } from "./node-selector"; import { LinkSelector } from "./link-selector"; import { cn } from "../utils"; +import { findTableAncestor } from "../table-menu"; export interface BubbleMenuItem { name: string; @@ -16,6 +17,7 @@ export interface BubbleMenuItem { type EditorBubbleMenuProps = Omit; export const EditorBubbleMenu: FC = (props: any) => { + const [isTableSelected, setisTableSelected] = useState(false) const items: BubbleMenuItem[] = [ { name: "bold", @@ -58,6 +60,15 @@ export const EditorBubbleMenu: FC = (props: any) => { if (editor.isActive("image")) { return false; } + const selection: any = window?.getSelection(); + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + if (findTableAncestor(range.startContainer)) { + setisTableSelected(true) + } else { + setisTableSelected(false) + } + } return editor.view.state.selection.content().size > 0; }, tippyOptions: { @@ -77,22 +88,23 @@ export const EditorBubbleMenu: FC = (props: any) => { {...bubbleMenuProps} className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl" > - { setIsNodeSelectorOpen(!isNodeSelectorOpen); setIsLinkSelectorOpen(false); }} - /> - } + {!isTableSelected && { setIsLinkSelectorOpen(!isLinkSelectorOpen); setIsNodeSelectorOpen(false); }} - /> + />}
{items.map((item, index) => (