I’m trying to find when an Image node is deleted from the Editor and then trigger a backend call. I’ve made a custom plugin to do so, it basically iterates over the newState/currentState’s descendants and stores the existing image nodes’ src
in the set called newImageSources.
Note: An image node can be resized and moved in the editor.
Now I iterate over the oldState and after some conditions that check if “an image node is possibly deleted/moved from it’s old position”, I do a final check if the deletedNode’s src is in newImageSources set, and if it’s not present in it, I trigger a backend call to delete this node from the s3 bucket.
But this randomly gets triggered sometimes, it happens really randomly and we aren’t able to reproduce it specifically. Can you find any loophole that could possibly cause random triggers (i.e. without it getting triggered by the user by an explicit delete operation on the image node)
Here’s the code snippet for my Custom Plugin
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image";
interface ImageNode extends ProseMirrorNode {
attrs: {
src: string;
id: string;
};
}
const TrackImageDeletionPlugin = (deleteImage): Plugin =>
new Plugin({
key: deleteKey,
appendTransaction: (
transactions: readonly Transaction[],
oldState: EditorState,
newState: EditorState,
) => {
const newImageSources = new Set<string>();
newState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) {
newImageSources.add(node.attrs.src);
}
});
transactions.forEach((transaction) => {
// transaction could be a selection
if (!transaction.docChanged) return;
const removedImages: ImageNode[] = [];
// iterate through all the nodes in the old state
oldState.doc.descendants((oldNode, oldPos) => {
// if the node is not an image, then return as no point in checking
if (oldNode.type.name !== IMAGE_NODE_TYPE) return;
if (oldPos < 0 || oldPos > newState.doc.content.size) return;
const nodeExistsInNewStateAtOldPosition =
newState.doc.resolve(oldPos).parent;
// when image deleted from the end of the document, then document
// closing tag is considered as the final node
if (!nodeExistsInNewStateAtOldPosition) return;
const currentNodeAtOldPosition = newState.doc.nodeAt(oldPos);
// Check if the node has been deleted or replaced
if (
!currentNodeAtOldPosition ||
currentNodeAtOldPosition.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, deleteImage);
});
});
return null;
},
});
export default TrackImageDeletionPlugin;
async function onNodeDeleted(
src: string,
deleteImage: DeleteImage,
): Promise<void> {
try {
console.log("Image delete triggered");
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
const resStatus = await deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("Image deleted successfully");
}
} catch (error) {
console.error("Error deleting image: ", error);
}
}