ProseMirror / prosemirror

The ProseMirror WYSIWYM editor
http://prosemirror.net/
MIT License
7.74k stars 336 forks source link

DecorationSet.map deletes all decorations #1497

Open rChaoz opened 3 hours ago

rChaoz commented 3 hours ago

I'm trying to implement image placeholders for uploading images using node decorations. After lots of debugging, however, it seems like this line from the documentation:

decorations = decorations.map(tr.mapping, tr.doc)

has some issues, as decorations.find().length is 2 before it and 0 afterwards. This is rather odd as I expect widget decorations to never be deleted (they are not deleted even when doing Ctrl + ABackspace). However when inserting an image programatically, they are deleted.

As a reproduction, I took dumps of all of the involved variables:

Variable dumps ```js decorations = { "children": [ 0, 11, { "children": [], "local": [ { "from": 9, "to": 9, "type": { "side": 0, "spec": "...", "toDOM": "..." } }, { "from": 9, "to": 9, "type": { "side": 0, "spec": "...", "toDOM": "..." } } ] } ], "local": [] } doc = { "type": "doc", "content": [ { "type": "paragraph", "attrs": {}, "content": [ { "type": "text", "text": "Imagesss:" } ] }, { "type": "image", "attrs": { ... } } ] } mapping = { "maps": [ { "ranges": [10, 1, 2], "inverted": false } ], "from": 0, "to": 1 } tr.steps = [ { "stepType": "replace", "from": 10, "to": 11, "slice": { "content": [ { "type": "paragraph", "attrs": { "textAlign": "left" } }, { "type": "image", "attrs": { ... } } ], "openStart": 1 } } ] ```

This is the code that adds the image:

const pos = (ImageKey.getState(state) as DecorationSet).find(undefined, undefined, (spec) => spec.id === id)[0]?.from
if (pos != null) tr.insert(pos, state.schema.nodeFromJSON({ type: "image", attrs } satisfies JSONContent))

And the code that adds the decoration (inside the apply() function of the plugin state):

if (action.type === "create") {
    const placeholder = document.createElement("div")
    return decorations.add(tr.doc, [
        Decoration.widget(action.pos, placeholder, {
            id: action.id,
            component: new ImagePlaceholder({ target: placeholder, props: { filename: action.filename } }),
        }),
    ])
}

Am I doing something wrong? I tried debugging the map function of DecorationSet with no luck, code too complex for me :(

Workaround

If anyone else has the same issue, this is the workaround I'm using right now instead of decorations = decorations.map(...):

const newDecorations = decorations.map(tr.mapping, tr.doc)
if (newDecorations.find().length === decorations.find().length) {
    decorations = newDecorations
}
marijnh commented 2 hours ago

Mapping will absolutely delete decorations that are covered by a deleted or replaced range. But in your example the decorations appear to be at position 9, not inside the deleted range. But given that their from equals their to position, they don't appear to be valid node decorations, which might explain why they get dropped.