diff --git a/space/components/tiptap/bubble-menu/index.tsx b/space/components/tiptap/bubble-menu/index.tsx index e68900782..217317ea1 100644 --- a/space/components/tiptap/bubble-menu/index.tsx +++ b/space/components/tiptap/bubble-menu/index.tsx @@ -77,14 +77,16 @@ 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); - }} - /> + {!props.editor.isActive("table") && ( + { + setIsNodeSelectorOpen(!isNodeSelectorOpen); + setIsLinkSelectorOpen(false); + }} + /> + )} = ({ editor, isOpen, setIsOpen name: "Text", icon: TextIcon, command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(), - isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"), + isActive: () => + editor.isActive("paragraph") && + !editor.isActive("bulletList") && + !editor.isActive("orderedList"), }, { name: "H1", @@ -69,7 +72,8 @@ export const NodeSelector: FC = ({ editor, isOpen, setIsOpen { name: "Quote", icon: TextQuote, - command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(), + command: () => + editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(), isActive: () => editor.isActive("blockquote"), }, { diff --git a/space/components/tiptap/extensions/index.tsx b/space/components/tiptap/extensions/index.tsx index 1aa0e2685..f5dc11384 100644 --- a/space/components/tiptap/extensions/index.tsx +++ b/space/components/tiptap/extensions/index.tsx @@ -13,6 +13,7 @@ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import { lowlight } from "lowlight/lib/core"; import SlashCommand from "../slash-command"; import { InputRule } from "@tiptap/core"; +import Gapcursor from "@tiptap/extension-gapcursor"; import ts from "highlight.js/lib/languages/typescript"; @@ -20,6 +21,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); @@ -27,113 +32,122 @@ export const TiptapExtensions = ( workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void ) => [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", + StarterKit.configure({ + bulletList: { + HTMLAttributes: { + class: "list-disc list-outside leading-3 -mt-2", + }, }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", + orderedList: { + HTMLAttributes: { + class: "list-decimal list-outside leading-3 -mt-2", + }, }, - }, - listItem: { - HTMLAttributes: { - class: "leading-normal -mb-2", + listItem: { + HTMLAttributes: { + class: "leading-normal -mb-2", + }, }, - }, - blockquote: { - HTMLAttributes: { - class: "border-l-4 border-custom-border-300", + blockquote: { + HTMLAttributes: { + class: "border-l-4 border-custom-border-300", + }, }, - }, - code: { - HTMLAttributes: { - class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", - spellcheck: "false", + code: { + HTMLAttributes: { + class: + "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", + spellcheck: "false", + }, }, - }, - codeBlock: false, - horizontalRule: false, - dropcursor: { - color: "#DBEAFE", - width: 2, - }, - gapcursor: false, - }), - CodeBlockLowlight.configure({ - lowlight, - }), - HorizontalRule.extend({ - addInputRules() { - return [ - new InputRule({ - find: /^(?:---|—-|___\s|\*\*\*\s)$/, - handler: ({ state, range, commands }) => { - commands.splitBlock(); + codeBlock: false, + horizontalRule: false, + dropcursor: { + color: "rgba(var(--color-text-100))", + width: 2, + }, + gapcursor: false, + }), + CodeBlockLowlight.configure({ + lowlight, + }), + HorizontalRule.extend({ + addInputRules() { + return [ + new InputRule({ + find: /^(?:---|—-|___\s|\*\*\*\s)$/, + handler: ({ state, range, commands }) => { + commands.splitBlock(); - const attributes = {}; - const { tr } = state; - const start = range.from; - const end = range.to; - // @ts-ignore - tr.replaceWith(start - 1, end, this.type.create(attributes)); - }, - }), - ]; - }, - }).configure({ - HTMLAttributes: { - class: "mb-6 border-t border-custom-border-300", - }, - }), - TiptapLink.configure({ - protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), - HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", - }, - }), - UpdatedImage.configure({ - HTMLAttributes: { - class: "rounded-lg border border-custom-border-300", - }, - }), - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; - } + const attributes = {}; + const { tr } = state; + const start = range.from; + const end = range.to; + // @ts-ignore + tr.replaceWith(start - 1, end, this.type.create(attributes)); + }, + }), + ]; + }, + }).configure({ + HTMLAttributes: { + class: "mb-6 border-t border-custom-border-300", + }, + }), + Gapcursor, + TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), + HTMLAttributes: { + class: + "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", + }, + }), + UpdatedImage.configure({ + HTMLAttributes: { + class: "rounded-lg border border-custom-border-300", + }, + }), + Placeholder.configure({ + placeholder: ({ node }) => { + if (node.type.name === "heading") { + return `Heading ${node.attrs.level}`; + } + if (node.type.name === "image" || node.type.name === "table") { + return ""; + } - return "Press '/' for commands..."; - }, - includeChildren: true, - }), - UniqueID.configure({ - types: ["image"], - }), - SlashCommand(workspaceSlug, setIsSubmitting), - TiptapUnderline, - TextStyle, - Color, - Highlight.configure({ - multicolor: true, - }), - TaskList.configure({ - HTMLAttributes: { - class: "not-prose pl-2", - }, - }), - TaskItem.configure({ - HTMLAttributes: { - class: "flex items-start my-4", - }, - nested: true, - }), - Markdown.configure({ - html: true, - transformCopiedText: true, - }), -]; + return "Press '/' for commands..."; + }, + includeChildren: true, + }), + UniqueID.configure({ + types: ["image"], + }), + SlashCommand(workspaceSlug, setIsSubmitting), + TiptapUnderline, + TextStyle, + Color, + Highlight.configure({ + multicolor: true, + }), + TaskList.configure({ + HTMLAttributes: { + class: "not-prose pl-2", + }, + }), + TaskItem.configure({ + HTMLAttributes: { + class: "flex items-start my-4", + }, + nested: true, + }), + Markdown.configure({ + html: true, + transformCopiedText: true, + }), + Table, + TableHeader, + CustomTableCell, + TableRow, + ]; diff --git a/space/components/tiptap/extensions/table/table-cell.ts b/space/components/tiptap/extensions/table/table-cell.ts new file mode 100644 index 000000000..643cb8c64 --- /dev/null +++ b/space/components/tiptap/extensions/table/table-cell.ts @@ -0,0 +1,32 @@ +import { TableCell } from "@tiptap/extension-table-cell"; + +export const CustomTableCell = TableCell.extend({ + addAttributes() { + return { + ...this.parent?.(), + isHeader: { + default: false, + parseHTML: (element) => { + isHeader: element.tagName === "TD"; + }, + renderHTML: (attributes) => { + tag: attributes.isHeader ? "th" : "td"; + }, + }, + }; + }, + renderHTML({ HTMLAttributes }) { + if (HTMLAttributes.isHeader) { + return [ + "th", + { + ...HTMLAttributes, + class: `relative ${HTMLAttributes.class}`, + }, + ["span", { class: "absolute top-0 right-0" }], + 0, + ]; + } + return ["td", HTMLAttributes, 0]; + }, +}); diff --git a/space/components/tiptap/extensions/table/table-header.ts b/space/components/tiptap/extensions/table/table-header.ts new file mode 100644 index 000000000..f23aa93ef --- /dev/null +++ b/space/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/space/components/tiptap/extensions/table/table.ts b/space/components/tiptap/extensions/table/table.ts new file mode 100644 index 000000000..9b727bb51 --- /dev/null +++ b/space/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/space/components/tiptap/index.tsx b/space/components/tiptap/index.tsx index 3fb44fbf7..28c8b1691 100644 --- a/space/components/tiptap/index.tsx +++ b/space/components/tiptap/index.tsx @@ -6,6 +6,7 @@ import { EditorBubbleMenu } from "./bubble-menu"; import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; import { ImageResizer } from "./extensions/image-resize"; +import { TableMenu } from "./table-menu"; export interface ITipTapRichTextEditor { value: string; @@ -37,6 +38,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => { borderOnFocus, customClassName, } = props; + const editor = useEditor({ editable: editable ?? true, editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting), @@ -81,8 +83,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => { const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md ${noBorder ? "" : "border border-custom-border-200"} ${ - borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" - } ${customClassName}`; + borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" + } ${customClassName}`; if (!editor) return null; editorRef.current = editor; @@ -98,6 +100,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => { {editor && }
+ {editor?.isActive("image") && }
diff --git a/space/components/tiptap/plugins/delete-image.tsx b/space/components/tiptap/plugins/delete-image.tsx index a1b90fe57..fdf515ccc 100644 --- a/space/components/tiptap/plugins/delete-image.tsx +++ b/space/components/tiptap/plugins/delete-image.tsx @@ -1,43 +1,51 @@ -import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import fileService from "services/file.service"; const deleteKey = new PluginKey("delete-image"); +const IMAGE_NODE_TYPE = "image"; -const TrackImageDeletionPlugin = () => +interface ImageNode extends ProseMirrorNode { + attrs: { + src: string; + id: string; + }; +} + +const TrackImageDeletionPlugin = (): Plugin => new Plugin({ key: deleteKey, - appendTransaction: (transactions, oldState, newState) => { + appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + const newImageSources = new Set(); + newState.doc.descendants((node) => { + if (node.type.name === IMAGE_NODE_TYPE) { + newImageSources.add(node.attrs.src); + } + }); + transactions.forEach((transaction) => { if (!transaction.docChanged) return; - const removedImages: ProseMirrorNode[] = []; + const removedImages: ImageNode[] = []; oldState.doc.descendants((oldNode, oldPos) => { - if (oldNode.type.name !== "image") return; - + if (oldNode.type.name !== IMAGE_NODE_TYPE) return; + if (oldPos < 0 || oldPos > newState.doc.content.size) return; if (!newState.doc.resolve(oldPos).parent) return; + const newNode = newState.doc.nodeAt(oldPos); // Check if the node has been deleted or replaced - if (!newNode || newNode.type.name !== "image") { - // Check if the node still exists elsewhere in the document - let nodeExists = false; - newState.doc.descendants((node) => { - if (node.attrs.id === oldNode.attrs.id) { - nodeExists = true; - } - }); - - if (!nodeExists) { - removedImages.push(oldNode as ProseMirrorNode); + if (!newNode || newNode.type.name !== IMAGE_NODE_TYPE) { + if (!newImageSources.has(oldNode.attrs.src)) { + removedImages.push(oldNode as ImageNode); } } }); - removedImages.forEach((node) => { + removedImages.forEach(async (node) => { const src = node.attrs.src; - onNodeDeleted(src); + await onNodeDeleted(src); }); }); @@ -47,10 +55,14 @@ const TrackImageDeletionPlugin = () => export default TrackImageDeletionPlugin; -async function onNodeDeleted(src: string) { - const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); - const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); - if (resStatus === 204) { - console.log("Image deleted successfully"); +async function onNodeDeleted(src: string): Promise { + try { + const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); + const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); + if (resStatus === 204) { + console.log("Image deleted successfully"); + } + } catch (error) { + console.error("Error deleting image: ", error); } } diff --git a/space/components/tiptap/plugins/upload-image.tsx b/space/components/tiptap/plugins/upload-image.tsx index 8b14793b1..bc0acdc54 100644 --- a/space/components/tiptap/plugins/upload-image.tsx +++ b/space/components/tiptap/plugins/upload-image.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; import fileService from "services/file.service"; @@ -46,7 +45,11 @@ export default UploadImagesPlugin; function findPlaceholder(state: EditorState, id: {}) { const decos = uploadKey.getState(state); - const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id); + const found = decos.find( + undefined, + undefined, + (spec: { id: number | undefined }) => spec.id == id + ); return found.length ? found[0].from : null; } @@ -59,8 +62,6 @@ export async function startImageUpload( ) { if (!file.type.includes("image/")) { return; - } else if (file.size / 1024 / 1024 > 20) { - return; } const id = {}; @@ -93,7 +94,9 @@ export async function startImageUpload( const imageSrc = typeof src === "object" ? reader.result : src; const node = schema.nodes.image.create({ src: imageSrc }); - const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } }); + const transaction = view.state.tr + .replaceWith(pos, pos, node) + .setMeta(uploadKey, { remove: { id } }); view.dispatch(transaction); } @@ -107,7 +110,9 @@ const UploadImageHandler = (file: File, workspaceSlug: string): Promise formData.append("attributes", JSON.stringify({})); return new Promise(async (resolve, reject) => { - const imageUrl = await fileService.uploadFile(workspaceSlug, formData).then((response) => response.asset); + const imageUrl = await fileService + .uploadFile(workspaceSlug, formData) + .then((response) => response.asset); const image = new Image(); image.src = imageUrl; diff --git a/space/components/tiptap/props.tsx b/space/components/tiptap/props.tsx index 1af40d623..8233e3ab4 100644 --- a/space/components/tiptap/props.tsx +++ b/space/components/tiptap/props.tsx @@ -1,5 +1,6 @@ import { EditorProps } from "@tiptap/pm/view"; import { startImageUpload } from "./plugins/upload-image"; +import { findTableAncestor } from "./table-menu"; export function TiptapEditorProps( workspaceSlug: string, @@ -21,6 +22,15 @@ export function TiptapEditorProps( }, }, handlePaste: (view, event) => { + if (typeof window !== "undefined") { + const selection: any = window?.getSelection(); + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + if (findTableAncestor(range.startContainer)) { + return; + } + } + } if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { event.preventDefault(); const file = event.clipboardData.files[0]; @@ -31,6 +41,15 @@ export function TiptapEditorProps( return false; }, handleDrop: (view, event, _slice, moved) => { + if (typeof window !== "undefined") { + const selection: any = window?.getSelection(); + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + if (findTableAncestor(range.startContainer)) { + return; + } + } + } if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { event.preventDefault(); const file = event.dataTransfer.files[0]; diff --git a/space/components/tiptap/slash-command/index.tsx b/space/components/tiptap/slash-command/index.tsx index 90287404f..46bf5ea5a 100644 --- a/space/components/tiptap/slash-command/index.tsx +++ b/space/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"; @@ -46,6 +47,9 @@ const Command = Extension.create({ return [ Suggestion({ editor: this.editor, + allow({ editor }) { + return !editor.isActive("table"); + }, ...this.options.suggestion, }), ]; @@ -53,7 +57,10 @@ const Command = Extension.create({ }); const getSuggestionItems = - (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => + ( + workspaceSlug: string, + setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + ) => ({ query }: { query: string }) => [ { @@ -119,6 +126,20 @@ const getSuggestionItems = 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.", @@ -134,14 +155,21 @@ const getSuggestionItems = searchTerms: ["blockquote"], icon: , command: ({ editor, range }: CommandProps) => - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(), + editor + .chain() + .focus() + .deleteRange(range) + .toggleNode("paragraph", "paragraph") + .toggleBlockquote() + .run(), }, { title: "Code", description: "Capture a code snippet.", searchTerms: ["codeblock"], icon: , - command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), + command: ({ editor, range }: CommandProps) => + editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), }, { title: "Image", @@ -190,7 +218,15 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { } }; -const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => { +const CommandList = ({ + items, + command, +}: { + items: CommandItemProps[]; + command: any; + editor: any; + range: any; +}) => { const [selectedIndex, setSelectedIndex] = useState(0); const selectItem = useCallback( diff --git a/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx b/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx new file mode 100644 index 000000000..0e42ba648 --- /dev/null +++ b/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx @@ -0,0 +1,16 @@ +const InsertBottomTableIcon = (props: any) => ( + + + +); + +export default InsertBottomTableIcon; diff --git a/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx b/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx new file mode 100644 index 000000000..1fd75fe87 --- /dev/null +++ b/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx @@ -0,0 +1,15 @@ +const InsertLeftTableIcon = (props: any) => ( + + + +); +export default InsertLeftTableIcon; diff --git a/space/components/tiptap/table-menu/InsertRightTableIcon.tsx b/space/components/tiptap/table-menu/InsertRightTableIcon.tsx new file mode 100644 index 000000000..1a6570969 --- /dev/null +++ b/space/components/tiptap/table-menu/InsertRightTableIcon.tsx @@ -0,0 +1,16 @@ +const InsertRightTableIcon = (props: any) => ( + + + +); + +export default InsertRightTableIcon; diff --git a/space/components/tiptap/table-menu/InsertTopTableIcon.tsx b/space/components/tiptap/table-menu/InsertTopTableIcon.tsx new file mode 100644 index 000000000..8f04f4f61 --- /dev/null +++ b/space/components/tiptap/table-menu/InsertTopTableIcon.tsx @@ -0,0 +1,15 @@ +const InsertTopTableIcon = (props: any) => ( + + + +); +export default InsertTopTableIcon; diff --git a/space/components/tiptap/table-menu/index.tsx b/space/components/tiptap/table-menu/index.tsx new file mode 100644 index 000000000..94f9c0f8d --- /dev/null +++ b/space/components/tiptap/table-menu/index.tsx @@ -0,0 +1,143 @@ +import { useState, useEffect } from "react"; +import { Rows, Columns, ToggleRight } from "lucide-react"; +import { cn } from "../utils"; +import { Tooltip } from "components/ui"; +import InsertLeftTableIcon from "./InsertLeftTableIcon"; +import InsertRightTableIcon from "./InsertRightTableIcon"; +import InsertTopTableIcon from "./InsertTopTableIcon"; +import InsertBottomTableIcon from "./InsertBottomTableIcon"; + +interface TableMenuItem { + command: () => void; + icon: any; + key: string; + name: string; +} + +export const findTableAncestor = (node: Node | null): HTMLTableElement | null => { + while (node !== null && node.nodeName !== "TABLE") { + node = node.parentNode; + } + return node as HTMLTableElement; +}; + +export const TableMenu = ({ editor }: { editor: any }) => { + const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 }); + const isOpen = editor?.isActive("table"); + + const items: TableMenuItem[] = [ + { + command: () => editor.chain().focus().addColumnBefore().run(), + icon: InsertLeftTableIcon, + key: "insert-column-left", + name: "Insert 1 column left", + }, + { + command: () => editor.chain().focus().addColumnAfter().run(), + icon: InsertRightTableIcon, + key: "insert-column-right", + name: "Insert 1 column right", + }, + { + command: () => editor.chain().focus().addRowBefore().run(), + icon: InsertTopTableIcon, + key: "insert-row-above", + name: "Insert 1 row above", + }, + { + command: () => editor.chain().focus().addRowAfter().run(), + icon: InsertBottomTableIcon, + key: "insert-row-below", + name: "Insert 1 row below", + }, + { + command: () => editor.chain().focus().deleteColumn().run(), + icon: Columns, + key: "delete-column", + name: "Delete column", + }, + { + command: () => editor.chain().focus().deleteRow().run(), + icon: Rows, + key: "delete-row", + name: "Delete row", + }, + { + command: () => editor.chain().focus().toggleHeaderRow().run(), + icon: ToggleRight, + key: "toggle-header-row", + name: "Toggle header row", + }, + ]; + + useEffect(() => { + if (!window) return; + + const handleWindowClick = () => { + const selection: any = window?.getSelection(); + + if (selection.rangeCount !== 0) { + const range = selection.getRangeAt(0); + const tableNode = findTableAncestor(range.startContainer); + + let parent = tableNode?.parentElement; + + if (tableNode) { + const tableRect = tableNode.getBoundingClientRect(); + const tableCenter = tableRect.left + tableRect.width / 2; + const menuWidth = 45; + const menuLeft = tableCenter - menuWidth / 2; + const tableBottom = tableRect.bottom; + + setTableLocation({ bottom: tableBottom, left: menuLeft }); + + while (parent) { + if (!parent.classList.contains("disable-scroll")) + parent.classList.add("disable-scroll"); + parent = parent.parentElement; + } + } else { + const scrollDisabledContainers = document.querySelectorAll(".disable-scroll"); + + scrollDisabledContainers.forEach((container) => { + container.classList.remove("disable-scroll"); + }); + } + } + }; + + window.addEventListener("click", handleWindowClick); + + return () => { + window.removeEventListener("click", handleWindowClick); + }; + }, [tableLocation, editor]); + + return ( +
+ {items.map((item, index) => ( + + + + ))} +
+ ); +}; diff --git a/web/components/tiptap/index.tsx b/web/components/tiptap/index.tsx index 82304bfb0..28c8b1691 100644 --- a/web/components/tiptap/index.tsx +++ b/web/components/tiptap/index.tsx @@ -1,9 +1,10 @@ +import { useImperativeHandle, useRef, forwardRef, useEffect } from "react"; import { useEditor, EditorContent, Editor } from "@tiptap/react"; import { useDebouncedCallback } from "use-debounce"; +// components import { EditorBubbleMenu } from "./bubble-menu"; import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; -import { useImperativeHandle, useRef, forwardRef } from "react"; import { ImageResizer } from "./extensions/image-resize"; import { TableMenu } from "./table-menu"; @@ -55,6 +56,12 @@ const Tiptap = (props: ITipTapRichTextEditor) => { }, }); + useEffect(() => { + if (editor) { + editor.commands.setContent(value); + } + }, [value]); + const editorRef: React.MutableRefObject = useRef(null); useImperativeHandle(forwardedRef, () => ({ @@ -76,8 +83,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => { const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md ${noBorder ? "" : "border border-custom-border-200"} ${ - borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" - } ${customClassName}`; + borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" + } ${customClassName}`; if (!editor) return null; editorRef.current = editor;