tiptap icon indicating copy to clipboard operation
tiptap copied to clipboard

Event on node deleted

Open gianLuigiL opened this issue 2 years ago • 10 comments

What problem are you facing?

Would it be possibile for tiptap to emit events when a node is deleted altogether? I'm trying to track image deletions but it looks like i have to observe the markup and match tags to discern whether a tag has been deleted by the user.

What’s the solution you would like to see?

it would be cool to have a "deleted" or "removed" event that only takes into account deleted nodes.

What alternatives did you consider?

Matching generated markup by regex, but it's cumbersome.

Anything to add? (optional)

No response

Are you sponsoring us?

  • [ ] Yes, I’m a sponsor. 💖

gianLuigiL avatar Feb 03 '23 17:02 gianLuigiL

I'm also interested in this. I have some interactive node views where I need to perform some cleanup when they're deleted.

Currently planning to track all node ids myself and diff them onUpdate, so I can detect node deletions, but that's a bit cumbersome.

If tiptap had an onNodeDeleted event, that would be perfect. Or if someone finds another way to implement this, please share here!

holdenmatt avatar Feb 27 '23 21:02 holdenmatt

I'm also interested in this. I have some interactive node views where I need to perform some cleanup when they're deleted.

Currently planning to track all node ids myself and diff them onUpdate, so I can detect node deletions, but that's a bit cumbersome.

If tiptap had an onNodeDeleted event, that would be perfect. Or if someone finds another way to implement this, please share here!

Same problem.

And I found a response from this discuss react to node adding/removing. Maybe we can just pick the first approach before tiptap support that event (if they are going to implement this).

Btw, when going with the first approach, remember tiptap export some useful helpers as well, like findChildren, which can save us some time.

Dhalsimzhao avatar Mar 31 '23 07:03 Dhalsimzhao

I came up with a solution that works well for me, happy to share here.

I use an onUpdate handler to diff nodes and emit onNodeDeleted events that can handle whatever cleanup you like. I wrote a separate extension to add unique id attrs to nodes, which are used here. You could probably use the TipTap UniqueId extension to do this, or find some other way to compare nodes across edits.

Hopefully helpful to someone!

image

holdenmatt avatar Mar 31 '23 22:03 holdenmatt

I came up with a solution that works well for me, happy to share here.

https://gist.github.com/stevecastaneda/eb50aed3e5903aac2995f7cc850f71b1

Here's a gist of the above to make it easier to copy/paste, including imports. This works great for me.

stevecastaneda avatar Jul 08 '23 17:07 stevecastaneda

There is an onDestroy event that exists on all node views. I'm wondering how hard it is to tie into that, even without "first class" support from TipTap for this

C-Hess avatar Oct 30 '23 07:10 C-Hess

@stevecastaneda, since Tiptap gives you transaction.before in the onUpdate, I think this approach would be more robust and cleaner:

useEditor({
    ...
    onUpdate: ({ transaction }) => {
      const nodeIds = new Set<string>();
      transaction.doc.forEach((node) => {
        if (node.attrs.id) {
          nodeIds.add(node.attrs.id);
        }
      });
      transaction.before.forEach((node) => {
        if (node.attrs.id && !nodeIds.has(node.attrs.id)) {
          onNodeDeleted(node);
        }
      });
    },
  });

There might be even better ways that avoid looping twice over doc, but I am not very familiar with transactions yet.

goatyeahh avatar Jan 07 '24 10:01 goatyeahh

@stevecastaneda, since Tiptap gives you transaction.before in the onUpdate, I think this approach would be more robust and cleaner:

useEditor({
    ...
    onUpdate: ({ transaction }) => {
      const nodeIds = new Set<string>();
      transaction.doc.forEach((node) => {
        if (node.attrs.id) {
          nodeIds.add(node.attrs.id);
        }
      });
      transaction.before.forEach((node) => {
        if (node.attrs.id && !nodeIds.has(node.attrs.id)) {
          onNodeDeleted(node);
        }
      });
    },
  });

There might be even better ways that avoid looping twice over doc, but I am not very familiar with transactions yet.

Thanks for the nice snippet! For someone struggling with setup, it's probably because you don't have UniqueID extension. Sharing my implementation in case it helps someone.

//install and import unique-id extension
import UniqueID from "@tiptap-pro/extension-unique-id";


  const handleImageDeletes = async (transaction: Transaction) => {
    let current: Node[] = [];
    transaction.doc.content.forEach((node) => {
      if (node.attrs.uid) {
        current.push(node);
      }
    });
    let before: Node[] = [];
    transaction.docs.forEach((doc) =>
      doc.content.forEach((node) => {
        if (node.attrs.uid) {
          before.push(node);
        }
      })
    );

    if (!current || before.length == 0) {
      return;
    }

    const deletedImageNodes = before.filter((node) => {
      const uid = node.attrs.uid;
      return !current.find((curNode) => curNode.attrs.uid == uid);
    });

    if (deletedImageNodes.length > 0) {
    // do stuff
    }
  };


  const editor = useEditor(
    {
      onTransaction({ transaction }) {
        handleImageDeletes(transaction);
      },
      extensions: [
        //... other extensions
        UniqueID.configure({
          attributeName: "uid",
          types: ["imageBlock"],
        }),
        // update node types to be included per your needs, for me, I'm only interested in imageBlock changes
      ],
      //... other configs
    },
    []
  );

ksi9302 avatar Mar 26 '24 22:03 ksi9302

Does anyone have a solution if you do not have tiptap pro?

jiaweing avatar Apr 02 '24 10:04 jiaweing

I think the image node now has a type, so replacing the handleImageDeletes this below is working for me without using the UniqueID plugin.

function handleImageDeletes(transaction) {
    const getImageSrcs = (fragment) => {
      let srcs = new Set();
      fragment.forEach((node) => {
        if (node.type.name === 'image') {
          srcs.add(node.attrs.src);
        }
      });
      return srcs;
    };

    let currentSrcs = getImageSrcs(transaction.doc.content);
    let previousSrcs = getImageSrcs(transaction.before.content);

    if (currentSrcs.size === 0 && previousSrcs.size === 0) {
      return;
    }

    // Determine which images were deleted
    let deletedImageSrcs = [...previousSrcs].filter((src) => !currentSrcs.has(src));

    if (deletedImageSrcs.length > 0) {
     //Handle deleting file on cloud here
    }
  }

d11-kmann avatar Apr 26 '24 15:04 d11-kmann

You can track current and prev images inside array using onUpdate and editor.getJSON( ), and delete images which dosen't match currentImages ex.

onUpdate: async({ editor })=> {
      setContent(editor.getJSON());
      console.log(editor.getJSON())
      const currentImages=[];
      editor.getJSON().content?.forEach((item) => {
        if (item.type === "image") {
          currentImages.push(item.attrs.src);
        }
      });
      const deletedImages = previousImages.filter((url) => !currentImages.includes(url));
      for (const url of deletedImages) {
        console.log("Deleting image from blob storage:", url);
        await fetch(`/api/upload/delete?url=${encodeURIComponent(url)}`, {
          method: "DELETE",
        });
      }

      setPreviousImages(currentImages);
      setImagesList(currentImages);
      console.log(currentImages);
    },

add it inside useEditor({extensions:[...],onUpdate.....given code
I think it should work. It doesn't require TipTap pro.

jyotir-aditya avatar Jul 18 '24 16:07 jyotir-aditya