diff --git a/apps/app/components/tiptap/extensions/updated-image.tsx b/apps/app/components/tiptap/extensions/updated-image.tsx index b24ff2cc8..01648dcd7 100644 --- a/apps/app/components/tiptap/extensions/updated-image.tsx +++ b/apps/app/components/tiptap/extensions/updated-image.tsx @@ -1,9 +1,10 @@ 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()]; + return [UploadImagesPlugin(), TrackImageDeletionPlugin()]; }, addAttributes() { return { diff --git a/apps/app/components/tiptap/index.tsx b/apps/app/components/tiptap/index.tsx index 3f1c3be0e..a96b9c971 100644 --- a/apps/app/components/tiptap/index.tsx +++ b/apps/app/components/tiptap/index.tsx @@ -1,14 +1,9 @@ -// @ts-nocheck import { useEditor, EditorContent, Editor } from "@tiptap/react"; import { useDebouncedCallback } from "use-debounce"; import { EditorBubbleMenu } from "./bubble-menu"; import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; -import { Node } from "@tiptap/pm/model"; -import { Editor as CoreEditor } from "@tiptap/core"; -import { useCallback, useImperativeHandle, useRef } from "react"; -import { EditorState } from "@tiptap/pm/state"; -import fileService from "services/file.service"; +import { useImperativeHandle, useRef } from "react"; import { ImageResizer } from "./extensions/image-resize"; export interface ITiptapRichTextEditor { @@ -51,7 +46,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => { // for instant feedback loop setIsSubmitting?.("submitting"); setShouldShowAlert?.(true); - checkForNodeDeletions(editor); if (debouncedUpdatesEnabled) { debouncedUpdates({ onChange, editor }); } else { @@ -71,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => { }, })); - const previousState = useRef(); - - const onNodeDeleted = useCallback(async (node: Node) => { - if (node.type.name === "image") { - const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1); - const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); - if (resStatus === 204) { - console.log("file deleted successfully"); - } - } - }, []); - - const checkForNodeDeletions = useCallback( - (editor: CoreEditor) => { - const prevNodesById: Record = {}; - previousState.current?.doc.forEach((node) => { - if (node.attrs.id) { - prevNodesById[node.attrs.id] = node; - } - }); - - const nodesById: Record = {}; - editor.state?.doc.forEach((node) => { - if (node.attrs.id) { - nodesById[node.attrs.id] = node; - } - }); - - previousState.current = editor.state; - - for (const [id, node] of Object.entries(prevNodesById)) { - if (nodesById[id] === undefined) { - onNodeDeleted(node); - } - } - }, - [onNodeDeleted] - ); - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { setTimeout(async () => { if (onChange) { diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx new file mode 100644 index 000000000..df189ee3a --- /dev/null +++ b/apps/app/components/tiptap/plugins/delete-image.tsx @@ -0,0 +1,57 @@ +import { Plugin, PluginKey } 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 TrackImageDeletionPlugin = () => + new Plugin({ + key: deleteKey, + appendTransaction: (transactions, oldState, newState) => { + transactions.forEach((transaction) => { + if (!transaction.docChanged) return; + + const removedImages: ProseMirrorNode[] = []; + + oldState.doc.descendants((oldNode, oldPos) => { + console.log(oldNode.type.name) + if (oldNode.type.name !== 'image') 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); + } + } + }); + + removedImages.forEach((node) => { + const src = node.attrs.src; + onNodeDeleted(src); + }); + }); + + return null; + }, + }); + +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"); + } +} diff --git a/apps/app/components/tiptap/plugins/upload-image.tsx b/apps/app/components/tiptap/plugins/upload-image.tsx index 32cdd6731..5897e9ffb 100644 --- a/apps/app/components/tiptap/plugins/upload-image.tsx +++ b/apps/app/components/tiptap/plugins/upload-image.tsx @@ -24,7 +24,7 @@ const UploadImagesPlugin = () => const image = document.createElement("img"); image.setAttribute( "class", - "w-[35%] opacity-10 rounded-lg border border-custom-border-300", + "opacity-10 rounded-lg border border-custom-border-300", ); image.src = src; placeholder.appendChild(image); diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css index 05817c0dc..65e947eeb 100644 --- a/apps/app/styles/editor.css +++ b/apps/app/styles/editor.css @@ -126,3 +126,27 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { transition: opacity 0.2s ease-out; } +.img-placeholder { + position: relative; + width: 35%; + + &:before { + content: ""; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + border-radius: 50%; + border: 3px solid rgba(var(--color-text-200)); + border-top-color: rgba(var(--color-text-800)); + animation: spinning 0.6s linear infinite; + } +} + +@keyframes spinning { + to { + transform: rotate(360deg); + } +}