diff --git a/web/components/tiptap/extensions/index.tsx b/web/components/tiptap/extensions/index.tsx index ea5d421a6..f5dc11384 100644 --- a/web/components/tiptap/extensions/index.tsx +++ b/web/components/tiptap/extensions/index.tsx @@ -32,122 +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: { + 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: - "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", - spellcheck: "false", + "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, - }, - 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(); + }), + 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 ""; + } - 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, - }), - Table, - TableHeader, - CustomTableCell, - TableRow, -]; + 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/web/components/tiptap/plugins/delete-image.tsx b/web/components/tiptap/plugins/delete-image.tsx index 568e4b748..fdf515ccc 100644 --- a/web/components/tiptap/plugins/delete-image.tsx +++ b/web/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/web/components/tiptap/plugins/upload-image.tsx b/web/components/tiptap/plugins/upload-image.tsx index 499a00455..bc0acdc54 100644 --- a/web/components/tiptap/plugins/upload-image.tsx +++ b/web/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";