Single UndoManager Multiple Code/RichText Editors

Hi,

I’m working a tool where I’d like to have many code editors and RichText editors on a canvas. I’d like to try having a single global UndoManager instead of having a per editor undomanager as per the built in ones in most of the example integrations. I’ve banged my head against this for a few days. I stripped out the undo/redo and tried just using an UndoManager on a top level component, but the tiptap integration doesn’t seem to handle changes happening that didn’t come from the sync plugin or the initial indexeddb sync.

TLDR: I came across at some point a comment from I think Kevin mentioning having undo managers that are outside a single editor instance. Does anyone have an example or recommendations for how I’d implement this?

For context:
I’m building a zoomable canvas where you can place/resize as many code editors (Monaco) and RichText (tiptap or prosemirror) on the canvas. I can easily get multiple instances going at least for tiptap editors right now (I’m having issues with embedding a Monaco in next.js with existing example code).

Many thanks in advance!

2 Likes

Hi @selfless,

You can create a single undo-manager to track multiple editors. All editor bindings accept a undoManager option that allows you to define a custom undo manager. You just need to make sure that the shared type of the editor is tracked. In short, define an undo manager like this undoManager = new Y.UndoManager([yquill, yprosemirror]) and then add it to the editor binding.

There is an exhaustive documentation on the inner working of the UndoManager on the documentation website: https://docs.yjs.dev/api/undo-manager If you still have trouble, maybe you can post some source-code here?

Hey @dmonad,

Thanks for the reply. I was able to get my y-codemirror instances working, had missed that you can provide your undomanager. However I can’t get my richtext editors using my global undomanager because y-prosemirror does not support providing an undo manager. It creates it’s own, see:

Inlined for convenience:

export const yUndoPlugin = ({ protectedNodes = new Set(['paragraph']), trackedOrigins = [] } = {}) => new Plugin({

key: yUndoPluginKey,
state: {
init: (initargs, state) => {
// TODO: check if plugin order matches and fix
const ystate = ySyncPluginKey.getState(state)
const undoManager = new UndoManager(ystate.type, {
trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
deleteFilter: item => !(item instanceof Item) ||
!(item.content instanceof ContentType) ||
!(item.content.type instanceof Text ||
(item.content.type instanceof XmlElement && protectedNodes.has(item.content.type.nodeName))) ||
item.content.type._length === 0
})
return {
undoManager,
prevSel: null,
hasUndoOps: undoManager.undoStack.length > 0,
hasRedoOps: undoManager.redoStack.length > 0
}
},

I have a feeling trying to use this less traveled path is a bad idea, looks like there is some nice details that are baked into the undo logic when you let the editor internals have an isolated undomanager. I’d like to try a bit more though, as I prefer a global undo/redo for my tool.

You are right. Feel free to open a PR that adds undoManager as one of the options. If undoManager is set, the other parameters should be ignored.

I think this is the way to go. I implemented something similar with the codemirror editor. The editor bindings are supposed to accept a custom undoManager, and then simply add a origin to the undoManager.

2 Likes

@seflless @dmonad did you find a good solution here? I am facing the same problem.

Tiptap doesn’t take a custom undo manager, so either I fork it and add support for that or try something else. But thought I’d ask here first :slight_smile:

I went with Quill and CodeMirror for my use case. I haven’t worked on this in a few months and am swamped, so I forget exactly the issues I was having. I just reviewed the code quickly to make sure that information was correct.

Code mirror worked like this:
const binding = new CodemirrorBinding(props.text, editor, null, { yUndoManager: yUndoManager })

Quill I am just not having this issue even though I didn’t prove an undo manager, not following what that works exactly. Maybe it’s not.

@nc1 were you able to find a solution for tiptap? im also having the issue setting up a global undo manager to work with multiple editor instances

@ianmcateer For Tiptap I had to add ySyncPluginKey to trackedOrigins to affect all tiptap editors:

import { ySyncPluginKey } from 'y-prosemirror';

let undoManager = new Y.UndoManager(typesInScope,
  { trackedOrigins: new Set([ySyncPluginKey]) },
);

Note: You should have only one version of y-prosemirror installed. I had problems with duplicated y-prosemirror packages.

2 Likes

I have editors with multiple different ydocs. but I want to have single history stack, so user can undo last changes from all editors. How can we achieve this? I am using tiptap editor

Btw, in January 2024 (but probably earlier), undoManager is one of the built-in options for y-prosemirror. See https://github.com/yjs/y-prosemirror/blob/master/src/plugins/undo-plugin.js

1 Like