Reacting to Node Adding/Removing/Changing

I have a case where I need to display a dom element outside of the editor for each instance of a specific dom element inside of the editor in real time. I plan on linking them together with an id that is set on the node’s attribute within the editor. What I am not sure of is the best way to go about handling changes in the editor.

Lets call the node in the editor target. I need to know when a target is added, when a target is removed, and when a target’s coordinates change. Here are two methods that have come to mind:

1. I believe one way I can do this is to scan the doc after each transformation to see if any target’s have been added, removed, or changed. However, this sounds like it would not scale well and be too slow on large documents.

2. Another method might be using filterTransaction in a plugin to react to certain transformations to the document and smartly scan only portions of the doc. However, I am not sure which transformations (or steps) exist or how to react correctly to them. The docs mention a ReplaceStep and ReplaceAroundStep so maybe those are the only steps to handle? I think this method would scale well but without knowing the inner workings of ProseMirror better I am afraid I’ll miss the doc changes that I am looking for.

Does anyone know of a better approach? Or if I do go with the second method how I can make sure I properly handle the different transformations?

2 Likes

This is probably fine – unless your documents are humongous, scanning them is cheap.

You don’t need to use filterTransaction – you can simply react to transactions as they are dispatched. To see the extent of the document ‘touched’ by a given transaction, you can call forEach on each step map in its mapping property, but doing this is kind of subtle as each step will have its positions expressed relative to the document as it was when that step was applied, and later steps might move those positions again.

So I’d say go with the first approach, and when you somehow find that that is too slow, implement the second.

Thanks @marijn, I’ll give the first approach a try. It looks like Node.forEach and Node.descendants would be the ways to scan.

Node.forEach simply iterates over child arrays within the content. And Node.descendants uses nodesBetween which appears to do more than just iterate over an array. Am I correct to assume forEach will give better performance than descendants?

forEach will only iterate over direct children, not grandchildren and further descendants, so you probably do need descendants.

Right, I was planning on making a function that would go through all the descendants using forEach. I may just use the descendants function to start with though.

Diffing tree with all descendents seems kind of expensive to me since you’ll have to do a lot of deep comparisons. It seems like there should be an easier way leveraging the persistent data-structure. But even that is tricky because its not going to detect moves as easily…

I’m interested in implementing the second approach if you wouldn’t mind giving some pointers. It looks like transaction.steps doesn’t necessarily have from/to positions…

Hmm. Perhaps it does make more sense to diff the whole document… Here’s how I’m doing it.

		dispatchTransaction(transaction) {
			const prevState = view.state
			const nextState = view.state.apply(transaction)

			if (prevState.doc !== nextState.doc) {
				const prevNodesById: Record<string, ProsemirrorNode<ISchema>> = {}
				prevState.doc.descendants((node) => {
					if (node.attrs.id) {
						prevNodesById[node.attrs.id] = node
					}
				})

				const nextNodesById: Record<string, ProsemirrorNode<ISchema>> = {}
				nextState.doc.descendants((node) => {
					if (node.attrs.id) {
						nextNodesById[node.attrs.id] = node
					}
				})

				const deletedIds = new Set<string>()
				const changedIds = new Set<string>()
				const addedIds = new Set<string>()

				for (const [id, node] of Object.entries(prevNodesById)) {
					if (nextNodesById[id] === undefined) {
						deletedIds.add(id)
					} else if (node !== nextNodesById[id]) {
						changedIds.add(id)
					}
				}

				for (const [id, node] of Object.entries(nextNodesById)) {
					if (prevNodesById[id] === undefined) {
						addedIds.add(id)
					} else if (node !== prevNodesById[id]) {
						changedIds.add(id)
					}
				}
				console.log({
					deletedIds,
					changedIds,
					addedIds,
				})
			}

			view.updateState(nextState)
1 Like