Skip to content

DecorationSet.map deletes all decorations #1497

@rChaoz

Description

@rChaoz

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
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
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions