diff --git a/space/components/tiptap/bubble-menu/index.tsx b/space/components/tiptap/bubble-menu/index.tsx deleted file mode 100644 index 217317ea1..000000000 --- a/space/components/tiptap/bubble-menu/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { BubbleMenu, BubbleMenuProps } from "@tiptap/react"; -import { FC, useState } from "react"; -import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react"; - -import { NodeSelector } from "./node-selector"; -import { LinkSelector } from "./link-selector"; -import { cn } from "../utils"; - -export interface BubbleMenuItem { - name: string; - isActive: () => boolean; - command: () => void; - icon: typeof BoldIcon; -} - -type EditorBubbleMenuProps = Omit; - -export const EditorBubbleMenu: FC = (props: any) => { - const items: BubbleMenuItem[] = [ - { - name: "bold", - isActive: () => props.editor?.isActive("bold"), - command: () => props.editor?.chain().focus().toggleBold().run(), - icon: BoldIcon, - }, - { - name: "italic", - isActive: () => props.editor?.isActive("italic"), - command: () => props.editor?.chain().focus().toggleItalic().run(), - icon: ItalicIcon, - }, - { - name: "underline", - isActive: () => props.editor?.isActive("underline"), - command: () => props.editor?.chain().focus().toggleUnderline().run(), - icon: UnderlineIcon, - }, - { - name: "strike", - isActive: () => props.editor?.isActive("strike"), - command: () => props.editor?.chain().focus().toggleStrike().run(), - icon: StrikethroughIcon, - }, - { - name: "code", - isActive: () => props.editor?.isActive("code"), - command: () => props.editor?.chain().focus().toggleCode().run(), - icon: CodeIcon, - }, - ]; - - const bubbleMenuProps: EditorBubbleMenuProps = { - ...props, - shouldShow: ({ editor }) => { - if (!editor.isEditable) { - return false; - } - if (editor.isActive("image")) { - return false; - } - return editor.view.state.selection.content().size > 0; - }, - tippyOptions: { - moveTransition: "transform 0.15s ease-out", - onHidden: () => { - setIsNodeSelectorOpen(false); - setIsLinkSelectorOpen(false); - }, - }, - }; - - const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); - const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); - - return ( - - {!props.editor.isActive("table") && ( - { - setIsNodeSelectorOpen(!isNodeSelectorOpen); - setIsLinkSelectorOpen(false); - }} - /> - )} - { - setIsLinkSelectorOpen(!isLinkSelectorOpen); - setIsNodeSelectorOpen(false); - }} - /> -
- {items.map((item, index) => ( - - ))} -
-
- ); -}; diff --git a/space/components/tiptap/bubble-menu/link-selector.tsx b/space/components/tiptap/bubble-menu/link-selector.tsx deleted file mode 100644 index 559521db6..000000000 --- a/space/components/tiptap/bubble-menu/link-selector.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { Check, Trash } from "lucide-react"; -import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; -import { cn } from "../utils"; -import isValidHttpUrl from "./utils/link-validator"; -interface LinkSelectorProps { - editor: Editor; - isOpen: boolean; - setIsOpen: Dispatch>; -} - -export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { - const inputRef = useRef(null); - - const onLinkSubmit = useCallback(() => { - const input = inputRef.current; - const url = input?.value; - if (url && isValidHttpUrl(url)) { - editor.chain().focus().setLink({ href: url }).run(); - setIsOpen(false); - } - }, [editor, inputRef, setIsOpen]); - - useEffect(() => { - inputRef.current && inputRef.current?.focus(); - }); - - return ( -
- - {isOpen && ( -
{ - if (e.key === "Enter") { - e.preventDefault(); - onLinkSubmit(); - } - }} - > - - {editor.getAttributes("link").href ? ( - - ) : ( - - )} -
- )} -
- ); -}; diff --git a/space/components/tiptap/bubble-menu/node-selector.tsx b/space/components/tiptap/bubble-menu/node-selector.tsx deleted file mode 100644 index 34d40ec06..000000000 --- a/space/components/tiptap/bubble-menu/node-selector.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { - Check, - ChevronDown, - Heading1, - Heading2, - Heading3, - TextQuote, - ListOrdered, - TextIcon, - Code, - CheckSquare, -} from "lucide-react"; -import { Dispatch, FC, SetStateAction } from "react"; - -import { BubbleMenuItem } from "."; -import { cn } from "../utils"; - -interface NodeSelectorProps { - editor: Editor; - isOpen: boolean; - setIsOpen: Dispatch>; -} - -export const NodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { - const items: BubbleMenuItem[] = [ - { - name: "Text", - icon: TextIcon, - command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(), - isActive: () => - editor.isActive("paragraph") && - !editor.isActive("bulletList") && - !editor.isActive("orderedList"), - }, - { - name: "H1", - icon: Heading1, - command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), - isActive: () => editor.isActive("heading", { level: 1 }), - }, - { - name: "H2", - icon: Heading2, - command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), - isActive: () => editor.isActive("heading", { level: 2 }), - }, - { - name: "H3", - icon: Heading3, - command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), - isActive: () => editor.isActive("heading", { level: 3 }), - }, - { - name: "To-do List", - icon: CheckSquare, - command: () => editor.chain().focus().toggleTaskList().run(), - isActive: () => editor.isActive("taskItem"), - }, - { - name: "Bullet List", - icon: ListOrdered, - command: () => editor.chain().focus().toggleBulletList().run(), - isActive: () => editor.isActive("bulletList"), - }, - { - name: "Numbered List", - icon: ListOrdered, - command: () => editor.chain().focus().toggleOrderedList().run(), - isActive: () => editor.isActive("orderedList"), - }, - { - name: "Quote", - icon: TextQuote, - command: () => - editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(), - isActive: () => editor.isActive("blockquote"), - }, - { - name: "Code", - icon: Code, - command: () => editor.chain().focus().toggleCodeBlock().run(), - isActive: () => editor.isActive("codeBlock"), - }, - ]; - - const activeItem = items.filter((item) => item.isActive()).pop() ?? { - name: "Multiple", - }; - - return ( -
- - - {isOpen && ( -
- {items.map((item, index) => ( - - ))} -
- )} -
- ); -}; diff --git a/space/components/tiptap/bubble-menu/utils/link-validator.tsx b/space/components/tiptap/bubble-menu/utils/link-validator.tsx deleted file mode 100644 index 9af366c02..000000000 --- a/space/components/tiptap/bubble-menu/utils/link-validator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export default function isValidHttpUrl(string: string): boolean { - let url; - - try { - url = new URL(string); - } catch (_) { - return false; - } - - return url.protocol === "http:" || url.protocol === "https:"; -} diff --git a/space/components/tiptap/extensions/image-resize.tsx b/space/components/tiptap/extensions/image-resize.tsx deleted file mode 100644 index 448b8811c..000000000 --- a/space/components/tiptap/extensions/image-resize.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Editor } from "@tiptap/react"; -import Moveable from "react-moveable"; - -export const ImageResizer = ({ editor }: { editor: Editor }) => { - const updateMediaSize = () => { - const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement; - if (imageInfo) { - const selection = editor.state.selection; - editor.commands.setImage({ - src: imageInfo.src, - width: Number(imageInfo.style.width.replace("px", "")), - height: Number(imageInfo.style.height.replace("px", "")), - } as any); - editor.commands.setNodeSelection(selection.from); - } - }; - - return ( - <> - { - delta[0] && (target!.style.width = `${width}px`); - delta[1] && (target!.style.height = `${height}px`); - }} - onResizeEnd={() => { - updateMediaSize(); - }} - scalable={true} - renderDirections={["w", "e"]} - onScale={({ target, transform }: any) => { - target!.style.transform = transform; - }} - /> - - ); -}; diff --git a/space/components/tiptap/extensions/index.tsx b/space/components/tiptap/extensions/index.tsx deleted file mode 100644 index 8ad4e07b4..000000000 --- a/space/components/tiptap/extensions/index.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import StarterKit from "@tiptap/starter-kit"; -import HorizontalRule from "@tiptap/extension-horizontal-rule"; -import TiptapLink from "@tiptap/extension-link"; -import Placeholder from "@tiptap/extension-placeholder"; -import TiptapUnderline from "@tiptap/extension-underline"; -import TextStyle from "@tiptap/extension-text-style"; -import { Color } from "@tiptap/extension-color"; -import TaskItem from "@tiptap/extension-task-item"; -import TaskList from "@tiptap/extension-task-list"; -import { Markdown } from "tiptap-markdown"; -import Highlight from "@tiptap/extension-highlight"; -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"; - -import "highlight.js/styles/github-dark.css"; -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); - -export const TiptapExtensions = ( - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -) => [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", - }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", - }, - }, - listItem: { - HTMLAttributes: { - class: "leading-normal -mb-2", - }, - }, - 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", - }, - }, - 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", - }, - }), - 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, - }), - 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 deleted file mode 100644 index 643cb8c64..000000000 --- a/space/components/tiptap/extensions/table/table-cell.ts +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index f23aa93ef..000000000 --- a/space/components/tiptap/extensions/table/table-header.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 9b727bb51..000000000 --- a/space/components/tiptap/extensions/table/table.ts +++ /dev/null @@ -1,9 +0,0 @@ -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/extensions/updated-image.tsx b/space/components/tiptap/extensions/updated-image.tsx deleted file mode 100644 index b62050953..000000000 --- a/space/components/tiptap/extensions/updated-image.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Image from "@tiptap/extension-image"; -import TrackImageDeletionPlugin from "../plugins/delete-image"; -import UploadImagesPlugin from "../plugins/upload-image"; - -const UpdatedImage = Image.extend({ - addProseMirrorPlugins() { - return [UploadImagesPlugin(), TrackImageDeletionPlugin()]; - }, - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: null, - }, - }; - }, -}); - -export default UpdatedImage; diff --git a/space/components/tiptap/index.tsx b/space/components/tiptap/index.tsx deleted file mode 100644 index 84f691c35..000000000 --- a/space/components/tiptap/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -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 { ImageResizer } from "./extensions/image-resize"; -import { TableMenu } from "./table-menu"; - -export interface ITipTapRichTextEditor { - value: string; - noBorder?: boolean; - borderOnFocus?: boolean; - customClassName?: string; - editorContentCustomClassNames?: string; - onChange?: (json: any, html: string) => void; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; - setShouldShowAlert?: (showAlert: boolean) => void; - workspaceSlug: string; - editable?: boolean; - forwardedRef?: any; - debouncedUpdatesEnabled?: boolean; -} - -const Tiptap = (props: ITipTapRichTextEditor) => { - const { - onChange, - debouncedUpdatesEnabled, - forwardedRef, - editable, - setIsSubmitting, - setShouldShowAlert, - editorContentCustomClassNames, - value, - noBorder, - workspaceSlug, - borderOnFocus, - customClassName, - } = props; - - const editor = useEditor({ - editable: editable ?? true, - editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting), - extensions: TiptapExtensions(workspaceSlug, setIsSubmitting), - content: value, - onUpdate: async ({ editor }) => { - // for instant feedback loop - setIsSubmitting?.("submitting"); - setShouldShowAlert?.(true); - if (debouncedUpdatesEnabled) { - debouncedUpdates({ onChange, editor }); - } else { - onChange?.(editor.getJSON(), editor.getHTML()); - } - }, - }); - - const editorRef: React.MutableRefObject = useRef(null); - - useImperativeHandle(forwardedRef, () => ({ - clearEditor: () => { - editorRef.current?.commands.clearContent(); - }, - setEditorValue: (content: string) => { - editorRef.current?.commands.setContent(content); - }, - })); - - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { - setTimeout(async () => { - if (onChange) { - onChange(editor.getJSON(), editor.getHTML()); - } - }, 500); - }, 1000); - - 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}`; - - if (!editor) return null; - editorRef.current = editor; - - return ( -
{ - editor?.chain().focus().run(); - }} - className={`tiptap-editor-container cursor-text ${editorClassNames}`} - > - {editor && } -
- - - {editor?.isActive("image") && } -
-
- ); -}; - -const TipTapEditor = forwardRef((props, ref) => ( - -)); - -TipTapEditor.displayName = "TipTapEditor"; - -export { TipTapEditor }; diff --git a/space/components/tiptap/plugins/delete-image.tsx b/space/components/tiptap/plugins/delete-image.tsx deleted file mode 100644 index fdf515ccc..000000000 --- a/space/components/tiptap/plugins/delete-image.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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"; - -interface ImageNode extends ProseMirrorNode { - attrs: { - src: string; - id: string; - }; -} - -const TrackImageDeletionPlugin = (): Plugin => - new Plugin({ - key: deleteKey, - 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: ImageNode[] = []; - - oldState.doc.descendants((oldNode, oldPos) => { - 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_NODE_TYPE) { - if (!newImageSources.has(oldNode.attrs.src)) { - removedImages.push(oldNode as ImageNode); - } - } - }); - - removedImages.forEach(async (node) => { - const src = node.attrs.src; - await onNodeDeleted(src); - }); - }); - - return null; - }, - }); - -export default TrackImageDeletionPlugin; - -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 deleted file mode 100644 index bc0acdc54..000000000 --- a/space/components/tiptap/plugins/upload-image.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; -import fileService from "services/file.service"; - -const uploadKey = new PluginKey("upload-image"); - -const UploadImagesPlugin = () => - new Plugin({ - key: uploadKey, - state: { - init() { - return DecorationSet.empty; - }, - apply(tr, set) { - set = set.map(tr.mapping, tr.doc); - // See if the transaction adds or removes any placeholders - const action = tr.getMeta(uploadKey); - if (action && action.add) { - const { id, pos, src } = action.add; - - const placeholder = document.createElement("div"); - placeholder.setAttribute("class", "img-placeholder"); - const image = document.createElement("img"); - image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300"); - image.src = src; - placeholder.appendChild(image); - const deco = Decoration.widget(pos + 1, placeholder, { - id, - }); - set = set.add(tr.doc, [deco]); - } else if (action && action.remove) { - set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id)); - } - return set; - }, - }, - props: { - decorations(state) { - return this.getState(state); - }, - }, - }); - -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 - ); - return found.length ? found[0].from : null; -} - -export async function startImageUpload( - file: File, - view: EditorView, - pos: number, - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -) { - if (!file.type.includes("image/")) { - return; - } - - const id = {}; - - const tr = view.state.tr; - if (!tr.selection.empty) tr.deleteSelection(); - - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - tr.setMeta(uploadKey, { - add: { - id, - pos, - src: reader.result, - }, - }); - view.dispatch(tr); - }; - - if (!workspaceSlug) { - return; - } - setIsSubmitting?.("submitting"); - const src = await UploadImageHandler(file, workspaceSlug); - const { schema } = view.state; - pos = findPlaceholder(view.state, id); - - if (pos == null) return; - 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 } }); - view.dispatch(transaction); -} - -const UploadImageHandler = (file: File, workspaceSlug: string): Promise => { - if (!workspaceSlug) { - return Promise.reject("Workspace slug is missing"); - } - try { - const formData = new FormData(); - formData.append("asset", file); - formData.append("attributes", JSON.stringify({})); - - return new Promise(async (resolve, reject) => { - const imageUrl = await fileService - .uploadFile(workspaceSlug, formData) - .then((response) => response.asset); - - const image = new Image(); - image.src = imageUrl; - image.onload = () => { - resolve(imageUrl); - }; - }); - } catch (error) { - console.log(error); - return Promise.reject(error); - } -}; diff --git a/space/components/tiptap/props.tsx b/space/components/tiptap/props.tsx deleted file mode 100644 index 8233e3ab4..000000000 --- a/space/components/tiptap/props.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { EditorProps } from "@tiptap/pm/view"; -import { startImageUpload } from "./plugins/upload-image"; -import { findTableAncestor } from "./table-menu"; - -export function TiptapEditorProps( - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -): EditorProps { - return { - attributes: { - class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, - }, - handleDOMEvents: { - keydown: (_view, event) => { - // prevent default event listeners from firing when slash command is active - if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { - const slashCommand = document.querySelector("#slash-command"); - if (slashCommand) { - return true; - } - } - }, - }, - 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]; - const pos = view.state.selection.from; - startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting); - return true; - } - 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]; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - // here we deduct 1 from the pos or else the image will create an extra node - if (coordinates) { - startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting); - } - return true; - } - return false; - }, - }; -} diff --git a/space/components/tiptap/slash-command/index.tsx b/space/components/tiptap/slash-command/index.tsx deleted file mode 100644 index 46bf5ea5a..000000000 --- a/space/components/tiptap/slash-command/index.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import React, { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react"; -import { Editor, Range, Extension } from "@tiptap/core"; -import Suggestion from "@tiptap/suggestion"; -import { ReactRenderer } from "@tiptap/react"; -import tippy from "tippy.js"; -import { - Heading1, - Heading2, - Heading3, - List, - ListOrdered, - Text, - TextQuote, - Code, - MinusSquare, - CheckSquare, - ImageIcon, - Table, -} from "lucide-react"; -import { startImageUpload } from "../plugins/upload-image"; -import { cn } from "../utils"; - -interface CommandItemProps { - title: string; - description: string; - icon: ReactNode; -} - -interface CommandProps { - editor: Editor; - range: Range; -} - -const Command = Extension.create({ - name: "slash-command", - addOptions() { - return { - suggestion: { - char: "/", - command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { - props.command({ editor, range }); - }, - }, - }; - }, - addProseMirrorPlugins() { - return [ - Suggestion({ - editor: this.editor, - allow({ editor }) { - return !editor.isActive("table"); - }, - ...this.options.suggestion, - }), - ]; - }, -}); - -const getSuggestionItems = - ( - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void - ) => - ({ query }: { query: string }) => - [ - { - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); - }, - }, - { - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); - }, - }, - { - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - }, - }, - { - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - }, - }, - { - title: "To-do List", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleTaskList().run(); - }, - }, - { - title: "Bullet List", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleBulletList().run(); - }, - }, - { - title: "Divider", - description: "Visually divide blocks", - searchTerms: ["line", "divider", "horizontal", "rule", "separate"], - icon: , - command: ({ editor, range }: CommandProps) => { - 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.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleOrderedList().run(); - }, - }, - { - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }: CommandProps) => - 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(), - }, - { - title: "Image", - description: "Upload an image from your computer.", - searchTerms: ["photo", "picture", "media"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).run(); - // upload image - const input = document.createElement("input"); - input.type = "file"; - input.accept = "image/*"; - input.onchange = async () => { - if (input.files?.length) { - const file = input.files[0]; - const pos = editor.view.state.selection.from; - startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting); - } - }; - input.click(); - }, - }, - ].filter((item) => { - if (typeof query === "string" && query.length > 0) { - const search = query.toLowerCase(); - return ( - item.title.toLowerCase().includes(search) || - item.description.toLowerCase().includes(search) || - (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search))) - ); - } - return true; - }); - -export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { - const containerHeight = container.offsetHeight; - const itemHeight = item ? item.offsetHeight : 0; - - const top = item.offsetTop; - const bottom = top + itemHeight; - - if (top < container.scrollTop) { - container.scrollTop -= container.scrollTop - top + 5; - } else if (bottom > containerHeight + container.scrollTop) { - container.scrollTop += bottom - containerHeight - container.scrollTop + 5; - } -}; - -const CommandList = ({ - items, - command, -}: { - items: CommandItemProps[]; - command: any; - editor: any; - range: any; -}) => { - const [selectedIndex, setSelectedIndex] = useState(0); - - const selectItem = useCallback( - (index: number) => { - const item = items[index]; - if (item) { - command(item); - } - }, - [command, items] - ); - - useEffect(() => { - const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"]; - const onKeyDown = (e: KeyboardEvent) => { - if (navigationKeys.includes(e.key)) { - e.preventDefault(); - if (e.key === "ArrowUp") { - setSelectedIndex((selectedIndex + items.length - 1) % items.length); - return true; - } - if (e.key === "ArrowDown") { - setSelectedIndex((selectedIndex + 1) % items.length); - return true; - } - if (e.key === "Enter") { - selectItem(selectedIndex); - return true; - } - return false; - } - }; - document.addEventListener("keydown", onKeyDown); - return () => { - document.removeEventListener("keydown", onKeyDown); - }; - }, [items, selectedIndex, setSelectedIndex, selectItem]); - - useEffect(() => { - setSelectedIndex(0); - }, [items]); - - const commandListContainer = useRef(null); - - useLayoutEffect(() => { - const container = commandListContainer?.current; - - const item = container?.children[selectedIndex] as HTMLElement; - - if (item && container) updateScrollView(container, item); - }, [selectedIndex]); - - return items.length > 0 ? ( -
- {items.map((item: CommandItemProps, index: number) => ( - - ))} -
- ) : null; -}; - -const renderItems = () => { - let component: ReactRenderer | null = null; - let popup: any | null = null; - - return { - onStart: (props: { editor: Editor; clientRect: DOMRect }) => { - component = new ReactRenderer(CommandList, { - props, - editor: props.editor, - }); - - // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.querySelector("#tiptap-container"), - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); - }, - onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => { - component?.updateProps(props); - - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); - }, - onKeyDown: (props: { event: KeyboardEvent }) => { - if (props.event.key === "Escape") { - popup?.[0].hide(); - - return true; - } - - // @ts-ignore - return component?.ref?.onKeyDown(props); - }, - onExit: () => { - popup?.[0].destroy(); - component?.destroy(); - }, - }; -}; - -export const SlashCommand = ( - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -) => - Command.configure({ - suggestion: { - items: getSuggestionItems(workspaceSlug, setIsSubmitting), - render: renderItems, - }, - }); - -export default SlashCommand; diff --git a/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx b/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx deleted file mode 100644 index 0e42ba648..000000000 --- a/space/components/tiptap/table-menu/InsertBottomTableIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 1fd75fe87..000000000 --- a/space/components/tiptap/table-menu/InsertLeftTableIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 1a6570969..000000000 --- a/space/components/tiptap/table-menu/InsertRightTableIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 8f04f4f61..000000000 --- a/space/components/tiptap/table-menu/InsertTopTableIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 94f9c0f8d..000000000 --- a/space/components/tiptap/table-menu/index.tsx +++ /dev/null @@ -1,143 +0,0 @@ -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/space/components/tiptap/utils.ts b/space/components/tiptap/utils.ts deleted file mode 100644 index a5ef19350..000000000 --- a/space/components/tiptap/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/space/package.json b/space/package.json index dc6257376..da7152640 100644 --- a/space/package.json +++ b/space/package.json @@ -17,26 +17,6 @@ "@heroicons/react": "^2.0.12", "@mui/icons-material": "^5.14.1", "@mui/material": "^5.14.1", - "@tiptap/extension-code-block-lowlight": "^2.0.4", - "@tiptap/extension-color": "^2.0.4", - "@tiptap/extension-gapcursor": "^2.1.7", - "@tiptap/extension-highlight": "^2.0.4", - "@tiptap/extension-horizontal-rule": "^2.0.4", - "@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", - "@tiptap/extension-underline": "^2.0.4", - "@tiptap/pm": "^2.0.4", - "@tiptap/react": "^2.0.4", - "@tiptap/starter-kit": "^2.0.4", - "@tiptap/suggestion": "^2.0.4", "axios": "^1.3.4", "clsx": "^2.0.0", "js-cookie": "^3.0.1", @@ -51,12 +31,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.38.0", - "react-moveable": "^0.54.1", "swr": "^2.2.2", "tailwind-merge": "^1.14.0", - "tiptap-markdown": "^0.8.2", "typescript": "4.9.5", - "use-debounce": "^9.0.4", "uuid": "^9.0.0", "@plane/editor": "*" }, diff --git a/web/components/tiptap/bubble-menu/index.tsx b/web/components/tiptap/bubble-menu/index.tsx deleted file mode 100644 index 217317ea1..000000000 --- a/web/components/tiptap/bubble-menu/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { BubbleMenu, BubbleMenuProps } from "@tiptap/react"; -import { FC, useState } from "react"; -import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, CodeIcon } from "lucide-react"; - -import { NodeSelector } from "./node-selector"; -import { LinkSelector } from "./link-selector"; -import { cn } from "../utils"; - -export interface BubbleMenuItem { - name: string; - isActive: () => boolean; - command: () => void; - icon: typeof BoldIcon; -} - -type EditorBubbleMenuProps = Omit; - -export const EditorBubbleMenu: FC = (props: any) => { - const items: BubbleMenuItem[] = [ - { - name: "bold", - isActive: () => props.editor?.isActive("bold"), - command: () => props.editor?.chain().focus().toggleBold().run(), - icon: BoldIcon, - }, - { - name: "italic", - isActive: () => props.editor?.isActive("italic"), - command: () => props.editor?.chain().focus().toggleItalic().run(), - icon: ItalicIcon, - }, - { - name: "underline", - isActive: () => props.editor?.isActive("underline"), - command: () => props.editor?.chain().focus().toggleUnderline().run(), - icon: UnderlineIcon, - }, - { - name: "strike", - isActive: () => props.editor?.isActive("strike"), - command: () => props.editor?.chain().focus().toggleStrike().run(), - icon: StrikethroughIcon, - }, - { - name: "code", - isActive: () => props.editor?.isActive("code"), - command: () => props.editor?.chain().focus().toggleCode().run(), - icon: CodeIcon, - }, - ]; - - const bubbleMenuProps: EditorBubbleMenuProps = { - ...props, - shouldShow: ({ editor }) => { - if (!editor.isEditable) { - return false; - } - if (editor.isActive("image")) { - return false; - } - return editor.view.state.selection.content().size > 0; - }, - tippyOptions: { - moveTransition: "transform 0.15s ease-out", - onHidden: () => { - setIsNodeSelectorOpen(false); - setIsLinkSelectorOpen(false); - }, - }, - }; - - const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); - const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); - - return ( - - {!props.editor.isActive("table") && ( - { - setIsNodeSelectorOpen(!isNodeSelectorOpen); - setIsLinkSelectorOpen(false); - }} - /> - )} - { - setIsLinkSelectorOpen(!isLinkSelectorOpen); - setIsNodeSelectorOpen(false); - }} - /> -
- {items.map((item, index) => ( - - ))} -
-
- ); -}; diff --git a/web/components/tiptap/bubble-menu/link-selector.tsx b/web/components/tiptap/bubble-menu/link-selector.tsx deleted file mode 100644 index 559521db6..000000000 --- a/web/components/tiptap/bubble-menu/link-selector.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { Check, Trash } from "lucide-react"; -import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; -import { cn } from "../utils"; -import isValidHttpUrl from "./utils/link-validator"; -interface LinkSelectorProps { - editor: Editor; - isOpen: boolean; - setIsOpen: Dispatch>; -} - -export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { - const inputRef = useRef(null); - - const onLinkSubmit = useCallback(() => { - const input = inputRef.current; - const url = input?.value; - if (url && isValidHttpUrl(url)) { - editor.chain().focus().setLink({ href: url }).run(); - setIsOpen(false); - } - }, [editor, inputRef, setIsOpen]); - - useEffect(() => { - inputRef.current && inputRef.current?.focus(); - }); - - return ( -
- - {isOpen && ( -
{ - if (e.key === "Enter") { - e.preventDefault(); - onLinkSubmit(); - } - }} - > - - {editor.getAttributes("link").href ? ( - - ) : ( - - )} -
- )} -
- ); -}; diff --git a/web/components/tiptap/bubble-menu/node-selector.tsx b/web/components/tiptap/bubble-menu/node-selector.tsx deleted file mode 100644 index 34d40ec06..000000000 --- a/web/components/tiptap/bubble-menu/node-selector.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { - Check, - ChevronDown, - Heading1, - Heading2, - Heading3, - TextQuote, - ListOrdered, - TextIcon, - Code, - CheckSquare, -} from "lucide-react"; -import { Dispatch, FC, SetStateAction } from "react"; - -import { BubbleMenuItem } from "."; -import { cn } from "../utils"; - -interface NodeSelectorProps { - editor: Editor; - isOpen: boolean; - setIsOpen: Dispatch>; -} - -export const NodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { - const items: BubbleMenuItem[] = [ - { - name: "Text", - icon: TextIcon, - command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(), - isActive: () => - editor.isActive("paragraph") && - !editor.isActive("bulletList") && - !editor.isActive("orderedList"), - }, - { - name: "H1", - icon: Heading1, - command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), - isActive: () => editor.isActive("heading", { level: 1 }), - }, - { - name: "H2", - icon: Heading2, - command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), - isActive: () => editor.isActive("heading", { level: 2 }), - }, - { - name: "H3", - icon: Heading3, - command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), - isActive: () => editor.isActive("heading", { level: 3 }), - }, - { - name: "To-do List", - icon: CheckSquare, - command: () => editor.chain().focus().toggleTaskList().run(), - isActive: () => editor.isActive("taskItem"), - }, - { - name: "Bullet List", - icon: ListOrdered, - command: () => editor.chain().focus().toggleBulletList().run(), - isActive: () => editor.isActive("bulletList"), - }, - { - name: "Numbered List", - icon: ListOrdered, - command: () => editor.chain().focus().toggleOrderedList().run(), - isActive: () => editor.isActive("orderedList"), - }, - { - name: "Quote", - icon: TextQuote, - command: () => - editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(), - isActive: () => editor.isActive("blockquote"), - }, - { - name: "Code", - icon: Code, - command: () => editor.chain().focus().toggleCodeBlock().run(), - isActive: () => editor.isActive("codeBlock"), - }, - ]; - - const activeItem = items.filter((item) => item.isActive()).pop() ?? { - name: "Multiple", - }; - - return ( -
- - - {isOpen && ( -
- {items.map((item, index) => ( - - ))} -
- )} -
- ); -}; diff --git a/web/components/tiptap/bubble-menu/utils/link-validator.tsx b/web/components/tiptap/bubble-menu/utils/link-validator.tsx deleted file mode 100644 index 9af366c02..000000000 --- a/web/components/tiptap/bubble-menu/utils/link-validator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export default function isValidHttpUrl(string: string): boolean { - let url; - - try { - url = new URL(string); - } catch (_) { - return false; - } - - return url.protocol === "http:" || url.protocol === "https:"; -} diff --git a/web/components/tiptap/extensions/image-resize.tsx b/web/components/tiptap/extensions/image-resize.tsx deleted file mode 100644 index 448b8811c..000000000 --- a/web/components/tiptap/extensions/image-resize.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Editor } from "@tiptap/react"; -import Moveable from "react-moveable"; - -export const ImageResizer = ({ editor }: { editor: Editor }) => { - const updateMediaSize = () => { - const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement; - if (imageInfo) { - const selection = editor.state.selection; - editor.commands.setImage({ - src: imageInfo.src, - width: Number(imageInfo.style.width.replace("px", "")), - height: Number(imageInfo.style.height.replace("px", "")), - } as any); - editor.commands.setNodeSelection(selection.from); - } - }; - - return ( - <> - { - delta[0] && (target!.style.width = `${width}px`); - delta[1] && (target!.style.height = `${height}px`); - }} - onResizeEnd={() => { - updateMediaSize(); - }} - scalable={true} - renderDirections={["w", "e"]} - onScale={({ target, transform }: any) => { - target!.style.transform = transform; - }} - /> - - ); -}; diff --git a/web/components/tiptap/extensions/index.tsx b/web/components/tiptap/extensions/index.tsx deleted file mode 100644 index 8ad4e07b4..000000000 --- a/web/components/tiptap/extensions/index.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import StarterKit from "@tiptap/starter-kit"; -import HorizontalRule from "@tiptap/extension-horizontal-rule"; -import TiptapLink from "@tiptap/extension-link"; -import Placeholder from "@tiptap/extension-placeholder"; -import TiptapUnderline from "@tiptap/extension-underline"; -import TextStyle from "@tiptap/extension-text-style"; -import { Color } from "@tiptap/extension-color"; -import TaskItem from "@tiptap/extension-task-item"; -import TaskList from "@tiptap/extension-task-list"; -import { Markdown } from "tiptap-markdown"; -import Highlight from "@tiptap/extension-highlight"; -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"; - -import "highlight.js/styles/github-dark.css"; -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); - -export const TiptapExtensions = ( - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -) => [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", - }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", - }, - }, - listItem: { - HTMLAttributes: { - class: "leading-normal -mb-2", - }, - }, - 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", - }, - }, - 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", - }, - }), - 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, - }), - 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/web/components/tiptap/extensions/table/table-cell.ts b/web/components/tiptap/extensions/table/table-cell.ts deleted file mode 100644 index 643cb8c64..000000000 --- a/web/components/tiptap/extensions/table/table-cell.ts +++ /dev/null @@ -1,32 +0,0 @@ -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/web/components/tiptap/extensions/table/table-header.ts b/web/components/tiptap/extensions/table/table-header.ts deleted file mode 100644 index f23aa93ef..000000000 --- a/web/components/tiptap/extensions/table/table-header.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header"; - -const TableHeader = BaseTableHeader.extend({ - content: "paragraph", -}); - -export { TableHeader }; diff --git a/web/components/tiptap/extensions/table/table.ts b/web/components/tiptap/extensions/table/table.ts deleted file mode 100644 index 9b727bb51..000000000 --- a/web/components/tiptap/extensions/table/table.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Table as BaseTable } from "@tiptap/extension-table"; - -const Table = BaseTable.configure({ - resizable: true, - cellMinWidth: 100, - allowTableNodeSelection: true, -}); - -export { Table }; diff --git a/web/components/tiptap/extensions/updated-image.tsx b/web/components/tiptap/extensions/updated-image.tsx deleted file mode 100644 index b62050953..000000000 --- a/web/components/tiptap/extensions/updated-image.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Image from "@tiptap/extension-image"; -import TrackImageDeletionPlugin from "../plugins/delete-image"; -import UploadImagesPlugin from "../plugins/upload-image"; - -const UpdatedImage = Image.extend({ - addProseMirrorPlugins() { - return [UploadImagesPlugin(), TrackImageDeletionPlugin()]; - }, - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: null, - }, - }; - }, -}); - -export default UpdatedImage; diff --git a/web/components/tiptap/index.tsx b/web/components/tiptap/index.tsx deleted file mode 100644 index 39a4edbb7..000000000 --- a/web/components/tiptap/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useImperativeHandle, useRef, forwardRef } 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 { ImageResizer } from "./extensions/image-resize"; -import { TableMenu } from "./table-menu"; - -export interface ITipTapRichTextEditor { - value: string; - noBorder?: boolean; - borderOnFocus?: boolean; - customClassName?: string; - editorContentCustomClassNames?: string; - onChange?: (json: any, html: string) => void; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; - setShouldShowAlert?: (showAlert: boolean) => void; - workspaceSlug: string; - editable?: boolean; - forwardedRef?: any; - debouncedUpdatesEnabled?: boolean; -} - -const Tiptap = (props: ITipTapRichTextEditor) => { - const { - onChange, - debouncedUpdatesEnabled, - forwardedRef, - editable, - setIsSubmitting, - setShouldShowAlert, - editorContentCustomClassNames, - value, - noBorder, - workspaceSlug, - borderOnFocus, - customClassName, - } = props; - - const editor = useEditor({ - editable: editable ?? true, - editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting), - extensions: TiptapExtensions(workspaceSlug, setIsSubmitting), - content: (typeof value === "string" && value.trim() !== "") ? value : "

", - onUpdate: async ({ editor }) => { - // for instant feedback loop - setIsSubmitting?.("submitting"); - setShouldShowAlert?.(true); - if (debouncedUpdatesEnabled) { - debouncedUpdates({ onChange, editor }); - } else { - onChange?.(editor.getJSON(), editor.getHTML()); - } - }, - }); - - const editorRef: React.MutableRefObject = useRef(null); - - useImperativeHandle(forwardedRef, () => ({ - clearEditor: () => { - editorRef.current?.commands.clearContent(); - }, - setEditorValue: (content: string) => { - editorRef.current?.commands.setContent(content); - }, - })); - - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { - setTimeout(async () => { - if (onChange) { - onChange(editor.getJSON(), editor.getHTML()); - } - }, 500); - }, 1000); - - 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}`; - - if (!editor) return null; - editorRef.current = editor; - - return ( -
{ - editor?.chain().focus().run(); - }} - className={`tiptap-editor-container cursor-text ${editorClassNames}`} - > - {editor && } -
- - - {editor?.isActive("image") && } -
-
- ); -}; - -const TipTapEditor = forwardRef((props, ref) => ( - -)); - -TipTapEditor.displayName = "TipTapEditor"; - -export { TipTapEditor }; diff --git a/web/components/tiptap/plugins/delete-image.tsx b/web/components/tiptap/plugins/delete-image.tsx deleted file mode 100644 index fdf515ccc..000000000 --- a/web/components/tiptap/plugins/delete-image.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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"; - -interface ImageNode extends ProseMirrorNode { - attrs: { - src: string; - id: string; - }; -} - -const TrackImageDeletionPlugin = (): Plugin => - new Plugin({ - key: deleteKey, - 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: ImageNode[] = []; - - oldState.doc.descendants((oldNode, oldPos) => { - 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_NODE_TYPE) { - if (!newImageSources.has(oldNode.attrs.src)) { - removedImages.push(oldNode as ImageNode); - } - } - }); - - removedImages.forEach(async (node) => { - const src = node.attrs.src; - await onNodeDeleted(src); - }); - }); - - return null; - }, - }); - -export default TrackImageDeletionPlugin; - -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/web/components/tiptap/plugins/upload-image.tsx b/web/components/tiptap/plugins/upload-image.tsx deleted file mode 100644 index bc0acdc54..000000000 --- a/web/components/tiptap/plugins/upload-image.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; -import fileService from "services/file.service"; - -const uploadKey = new PluginKey("upload-image"); - -const UploadImagesPlugin = () => - new Plugin({ - key: uploadKey, - state: { - init() { - return DecorationSet.empty; - }, - apply(tr, set) { - set = set.map(tr.mapping, tr.doc); - // See if the transaction adds or removes any placeholders - const action = tr.getMeta(uploadKey); - if (action && action.add) { - const { id, pos, src } = action.add; - - const placeholder = document.createElement("div"); - placeholder.setAttribute("class", "img-placeholder"); - const image = document.createElement("img"); - image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300"); - image.src = src; - placeholder.appendChild(image); - const deco = Decoration.widget(pos + 1, placeholder, { - id, - }); - set = set.add(tr.doc, [deco]); - } else if (action && action.remove) { - set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id)); - } - return set; - }, - }, - props: { - decorations(state) { - return this.getState(state); - }, - }, - }); - -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 - ); - return found.length ? found[0].from : null; -} - -export async function startImageUpload( - file: File, - view: EditorView, - pos: number, - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -) { - if (!file.type.includes("image/")) { - return; - } - - const id = {}; - - const tr = view.state.tr; - if (!tr.selection.empty) tr.deleteSelection(); - - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - tr.setMeta(uploadKey, { - add: { - id, - pos, - src: reader.result, - }, - }); - view.dispatch(tr); - }; - - if (!workspaceSlug) { - return; - } - setIsSubmitting?.("submitting"); - const src = await UploadImageHandler(file, workspaceSlug); - const { schema } = view.state; - pos = findPlaceholder(view.state, id); - - if (pos == null) return; - 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 } }); - view.dispatch(transaction); -} - -const UploadImageHandler = (file: File, workspaceSlug: string): Promise => { - if (!workspaceSlug) { - return Promise.reject("Workspace slug is missing"); - } - try { - const formData = new FormData(); - formData.append("asset", file); - formData.append("attributes", JSON.stringify({})); - - return new Promise(async (resolve, reject) => { - const imageUrl = await fileService - .uploadFile(workspaceSlug, formData) - .then((response) => response.asset); - - const image = new Image(); - image.src = imageUrl; - image.onload = () => { - resolve(imageUrl); - }; - }); - } catch (error) { - console.log(error); - return Promise.reject(error); - } -}; diff --git a/web/components/tiptap/props.tsx b/web/components/tiptap/props.tsx deleted file mode 100644 index 8233e3ab4..000000000 --- a/web/components/tiptap/props.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { EditorProps } from "@tiptap/pm/view"; -import { startImageUpload } from "./plugins/upload-image"; -import { findTableAncestor } from "./table-menu"; - -export function TiptapEditorProps( - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -): EditorProps { - return { - attributes: { - class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, - }, - handleDOMEvents: { - keydown: (_view, event) => { - // prevent default event listeners from firing when slash command is active - if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { - const slashCommand = document.querySelector("#slash-command"); - if (slashCommand) { - return true; - } - } - }, - }, - 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]; - const pos = view.state.selection.from; - startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting); - return true; - } - 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]; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - // here we deduct 1 from the pos or else the image will create an extra node - if (coordinates) { - startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting); - } - return true; - } - return false; - }, - }; -} diff --git a/web/components/tiptap/slash-command/index.tsx b/web/components/tiptap/slash-command/index.tsx deleted file mode 100644 index 46bf5ea5a..000000000 --- a/web/components/tiptap/slash-command/index.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import React, { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react"; -import { Editor, Range, Extension } from "@tiptap/core"; -import Suggestion from "@tiptap/suggestion"; -import { ReactRenderer } from "@tiptap/react"; -import tippy from "tippy.js"; -import { - Heading1, - Heading2, - Heading3, - List, - ListOrdered, - Text, - TextQuote, - Code, - MinusSquare, - CheckSquare, - ImageIcon, - Table, -} from "lucide-react"; -import { startImageUpload } from "../plugins/upload-image"; -import { cn } from "../utils"; - -interface CommandItemProps { - title: string; - description: string; - icon: ReactNode; -} - -interface CommandProps { - editor: Editor; - range: Range; -} - -const Command = Extension.create({ - name: "slash-command", - addOptions() { - return { - suggestion: { - char: "/", - command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { - props.command({ editor, range }); - }, - }, - }; - }, - addProseMirrorPlugins() { - return [ - Suggestion({ - editor: this.editor, - allow({ editor }) { - return !editor.isActive("table"); - }, - ...this.options.suggestion, - }), - ]; - }, -}); - -const getSuggestionItems = - ( - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void - ) => - ({ query }: { query: string }) => - [ - { - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); - }, - }, - { - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); - }, - }, - { - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - }, - }, - { - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - }, - }, - { - title: "To-do List", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleTaskList().run(); - }, - }, - { - title: "Bullet List", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleBulletList().run(); - }, - }, - { - title: "Divider", - description: "Visually divide blocks", - searchTerms: ["line", "divider", "horizontal", "rule", "separate"], - icon: , - command: ({ editor, range }: CommandProps) => { - 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.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleOrderedList().run(); - }, - }, - { - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }: CommandProps) => - 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(), - }, - { - title: "Image", - description: "Upload an image from your computer.", - searchTerms: ["photo", "picture", "media"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).run(); - // upload image - const input = document.createElement("input"); - input.type = "file"; - input.accept = "image/*"; - input.onchange = async () => { - if (input.files?.length) { - const file = input.files[0]; - const pos = editor.view.state.selection.from; - startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting); - } - }; - input.click(); - }, - }, - ].filter((item) => { - if (typeof query === "string" && query.length > 0) { - const search = query.toLowerCase(); - return ( - item.title.toLowerCase().includes(search) || - item.description.toLowerCase().includes(search) || - (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search))) - ); - } - return true; - }); - -export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { - const containerHeight = container.offsetHeight; - const itemHeight = item ? item.offsetHeight : 0; - - const top = item.offsetTop; - const bottom = top + itemHeight; - - if (top < container.scrollTop) { - container.scrollTop -= container.scrollTop - top + 5; - } else if (bottom > containerHeight + container.scrollTop) { - container.scrollTop += bottom - containerHeight - container.scrollTop + 5; - } -}; - -const CommandList = ({ - items, - command, -}: { - items: CommandItemProps[]; - command: any; - editor: any; - range: any; -}) => { - const [selectedIndex, setSelectedIndex] = useState(0); - - const selectItem = useCallback( - (index: number) => { - const item = items[index]; - if (item) { - command(item); - } - }, - [command, items] - ); - - useEffect(() => { - const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"]; - const onKeyDown = (e: KeyboardEvent) => { - if (navigationKeys.includes(e.key)) { - e.preventDefault(); - if (e.key === "ArrowUp") { - setSelectedIndex((selectedIndex + items.length - 1) % items.length); - return true; - } - if (e.key === "ArrowDown") { - setSelectedIndex((selectedIndex + 1) % items.length); - return true; - } - if (e.key === "Enter") { - selectItem(selectedIndex); - return true; - } - return false; - } - }; - document.addEventListener("keydown", onKeyDown); - return () => { - document.removeEventListener("keydown", onKeyDown); - }; - }, [items, selectedIndex, setSelectedIndex, selectItem]); - - useEffect(() => { - setSelectedIndex(0); - }, [items]); - - const commandListContainer = useRef(null); - - useLayoutEffect(() => { - const container = commandListContainer?.current; - - const item = container?.children[selectedIndex] as HTMLElement; - - if (item && container) updateScrollView(container, item); - }, [selectedIndex]); - - return items.length > 0 ? ( -
- {items.map((item: CommandItemProps, index: number) => ( - - ))} -
- ) : null; -}; - -const renderItems = () => { - let component: ReactRenderer | null = null; - let popup: any | null = null; - - return { - onStart: (props: { editor: Editor; clientRect: DOMRect }) => { - component = new ReactRenderer(CommandList, { - props, - editor: props.editor, - }); - - // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.querySelector("#tiptap-container"), - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); - }, - onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => { - component?.updateProps(props); - - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); - }, - onKeyDown: (props: { event: KeyboardEvent }) => { - if (props.event.key === "Escape") { - popup?.[0].hide(); - - return true; - } - - // @ts-ignore - return component?.ref?.onKeyDown(props); - }, - onExit: () => { - popup?.[0].destroy(); - component?.destroy(); - }, - }; -}; - -export const SlashCommand = ( - workspaceSlug: string, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void -) => - Command.configure({ - suggestion: { - items: getSuggestionItems(workspaceSlug, setIsSubmitting), - render: renderItems, - }, - }); - -export default SlashCommand; diff --git a/web/components/tiptap/table-menu/InsertBottomTableIcon.tsx b/web/components/tiptap/table-menu/InsertBottomTableIcon.tsx deleted file mode 100644 index 0e42ba648..000000000 --- a/web/components/tiptap/table-menu/InsertBottomTableIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -const InsertBottomTableIcon = (props: any) => ( - - - -); - -export default InsertBottomTableIcon; diff --git a/web/components/tiptap/table-menu/InsertLeftTableIcon.tsx b/web/components/tiptap/table-menu/InsertLeftTableIcon.tsx deleted file mode 100644 index 1fd75fe87..000000000 --- a/web/components/tiptap/table-menu/InsertLeftTableIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const InsertLeftTableIcon = (props: any) => ( - - - -); -export default InsertLeftTableIcon; diff --git a/web/components/tiptap/table-menu/InsertRightTableIcon.tsx b/web/components/tiptap/table-menu/InsertRightTableIcon.tsx deleted file mode 100644 index 1a6570969..000000000 --- a/web/components/tiptap/table-menu/InsertRightTableIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -const InsertRightTableIcon = (props: any) => ( - - - -); - -export default InsertRightTableIcon; diff --git a/web/components/tiptap/table-menu/InsertTopTableIcon.tsx b/web/components/tiptap/table-menu/InsertTopTableIcon.tsx deleted file mode 100644 index 8f04f4f61..000000000 --- a/web/components/tiptap/table-menu/InsertTopTableIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const InsertTopTableIcon = (props: any) => ( - - - -); -export default InsertTopTableIcon; diff --git a/web/components/tiptap/table-menu/index.tsx b/web/components/tiptap/table-menu/index.tsx deleted file mode 100644 index 94f9c0f8d..000000000 --- a/web/components/tiptap/table-menu/index.tsx +++ /dev/null @@ -1,143 +0,0 @@ -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/utils.ts b/web/components/tiptap/utils.ts deleted file mode 100644 index a5ef19350..000000000 --- a/web/components/tiptap/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/web/package.json b/web/package.json index 4682e0f3d..eab756c18 100644 --- a/web/package.json +++ b/web/package.json @@ -27,26 +27,6 @@ "@nivo/scatterplot": "0.80.0", "@sentry/nextjs": "^7.36.0", "@plane/editor": "*", - "@tiptap/extension-code-block-lowlight": "^2.0.4", - "@tiptap/extension-color": "^2.0.4", - "@tiptap/extension-gapcursor": "^2.1.7", - "@tiptap/extension-highlight": "^2.0.4", - "@tiptap/extension-horizontal-rule": "^2.0.4", - "@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", - "@tiptap/extension-underline": "^2.0.4", - "@tiptap/pm": "^2.0.4", - "@tiptap/react": "^2.0.4", - "@tiptap/starter-kit": "^2.0.4", - "@tiptap/suggestion": "^2.0.4", "@types/lodash.debounce": "^4.0.7", "@types/react-datepicker": "^4.8.0", "axios": "^1.1.3", @@ -74,12 +54,9 @@ "react-markdown": "^8.0.7", "react-moveable": "^0.54.1", "sharp": "^0.32.1", - "sonner": "^0.6.2", "swr": "^2.1.3", "tailwind-merge": "^1.14.0", - "tiptap-markdown": "^0.8.2", "tlds": "^1.238.0", - "use-debounce": "^9.0.4", "uuid": "^9.0.0" }, "devDependencies": {