ProseMirror, YJS and decorations tip

Hey Everyone, since a ton of people are using YJS now I thought I’d share a workaround for the decorations mapping issue ( when external changes are coming into YJS it replaces the whole document, causing DecorationSet.map to return an empty array, effectively deleting decorations ).

Just use YJS absolute positions in the plugin state, and create decorations from that. In case of prosemirror-image-plugin:

const createDecorations = (state: EditorState) => {
  // @ts-ignore
  const updatedState: Array<{ id: object; pos: any }> = imagePluginKey.getState(state);
  const YState = ySyncPluginKey.getState(state);
  const decors = updatedState.map((i) => {
    const pos = relativePositionToAbsolutePosition(YState.doc, YState.type, i.pos, YState.binding.mapping);
    const widget = document.createElement('placeholder');
    const deco = typeof pos === "number"? Decoration.widget(pos, widget, {
      id: i.id
    }): undefined;
    return deco;
  });
  // @ts-ignore
  return DecorationSet.create(state.doc, decors.filter(i=>i)) || DecorationSet.empty;
};

export const createState = <T extends Schema>() => ({
  init() {
    return [];
  },
  // @ts-ignore
  apply(tr: Transaction<T>, value: StateValue[], oldState: EditorState<T>, newState: EditorState<T>): StateValue[] {
    const action: ImagePluginAction = tr.getMeta(imagePluginKey);
    if (action?.type === 'add') {
      const YState = ySyncPluginKey.getState(newState);
      const relPos = absolutePositionToRelativePosition(action.pos, YState.type, YState.binding.mapping);
      return [...value, { id: action.id as object, pos: relPos }];
    } else if (action?.type === 'remove') {
      return value.filter((i) => i.id !== action.id);
    }
    return value;
  }
});

use the creareState functions in the plugin’s state, and createDecorations in the decorations field. Good luck!

5 Likes

Maybe I’m missing something, but …

YJS absolute positions in the plugin state

It seems that your state apply function actually stores relative positions (by converting the absolute positions to relative using absolutePositionToRelativePosition)?

You’re totally right, I meant relative. I’d rather call them YJS or ProseMirror positions tbh :slight_smile: I get the naming, it’s just easy for me to mix up.

1 Like

This does indeed work, if you don’t have any kind of special interaction with your decorations.

The problem with this approach is that the decorations get updated quite fast. In my case, I’m working on a suggester plugin that adds a widget decoration in which you can type (as to not interfere with other users currently editing the document).

This approach does indeed keep that Decoration in the document, but it can’t really be interacted with this way (not that Decorations were made for that purpose, but still).

If only there was some way to map these using YJS, without having to recreate them… :thinking:

I might be wrong, but I have a feeling that you’d be better off using something outside of PM. You have to store the position in the plugin, but you don’t display any decorations. Then from the outside you use the plugin’s state and the EditorView’s coordsAtPos method to position the overlay.