How to insert an async uploaded image?

Why not use the current selection?

This is best done using a widget or node decoration.

You can call setNodeType. If you kept a decoration at the image and mapped it forward when new edits came in, you can get the position of the image node from that decoration (and drop the decoration when you’re done).

In the run function of a MenuItem I’m doing the following:

let $image = document.createElement('div')
$image.setAttribute('id', 'file-' + fileMessage.fileId)
$image.setAttribute('class', 'image')
$image.textContent = 'Uploading'
Decoration.widget(view.state.selection.from, $image)

nothing is shown at the position.

I removed

view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs)))

Can’t seem to get the widget in the right position.

Decoration.widget creates a widget object, but doesn’t add it to anything. You have to create a DecorationSet and arrange for it to be provided to the editor view through the decorations prop, probably by writing a plugin.

Well at the moment when the run function of the MenuItem is triggered I need to do something? What is completely unclear to me at the moment. Do I have to execute:

view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs)))

to insert an image node? Or do I need to create a decoration here. At this moment the src is not yet available. Because showing a preview is async. What I want to do is insert a div with some text like Uploading or whatever.

This must be accomplished by a decoration. But how and where do I do this. I really can’t get a grasp on it. I have created a UploadFilesPlugin.

export const uploadFilesPlugin = new Plugin({
    state: {
        init(config, instance) {
            return {
                deco: DecorationSet.empty
            };
        },
        apply(transaction, state, prevEditorState, editorState) {

            return state;
        }
    },
    props: {
        decorations(state) {
            if (state && this.getState(state)) {
                return this.getState(state).deco;
            }
            return null;
        }
    }
});

But really don’t know how to create a domNode here at the right moment and the right position. This domNode also gets some events attached. These events keep track of the progress and state of the file.

Currently in the run function of the MenuItem, I want to do:

view.dispatch(view.state.tr.setMeta('file', fileMessage.fileId))

With this metadata set, I’d think to retrieve the meta info in the plugin. But nothing gets executed. Is that expected behavior and if yes, how can I dispatch an transaction such that nothig gets added or changed.

UPDATED Had it wrong, this is triggering the Plugin.

I have the following Plugin:

export const uploadFilesPlugin = new Plugin({
    state: {
        init(config, instance) {
            return {
                deco: DecorationSet.empty
            };
        },
        apply(transaction, state, prevEditorState, editorState) {

            const fileId = transaction.getMeta('file')

            if (!fileId) {
                return state
            }

            const $dom = document.createElement('div')
            $dom.setAttribute('id', 'file-' + fileId)
            $dom.setAttribute('class', 'image')
            $dom.textContent = 'Uploading'

            const decos = [Decoration.widget(editorState.selection.from, $dom)]
            const deco = DecorationSet.create(editorState.doc, decos)
            return {
                deco
            };
        }
    },
    props: {
        decorations(state) {
            if (state && this.getState(state)) {
                return this.getState(state).deco;
            }
            return null
        }
    }
});

This gives very weird behavior. When I want to insert an image. The widget is added. However when I go with the cursor near the “Uploading” text of the widget and start typing, it keeps on inserting the “Uploading” text on every insert of a character.

It is near to impossible to get this working :cry:. I’ve tried everything, from node views to decorations. I viewed some demos, like the codemirror demo and the commit demo, but it is very difficult to understand what happens and what not.

The multiplication of the widget text was a bug in the way widgets were being drawn. It should be fixed by this patch.

Your plugin mostly looks good, but you aren’t mapping your decoration set to the new document, which will cause issues. You could simplify it to this:

const uploadFilesPlugin = new Plugin({
  state: {
    init() { return DecorationSet.empty },
    apply(tr, deco) {
      const fileId = tr.getMeta('file')
      if (fileId) {
        const $dom = document.createElement('div')
        $dom.setAttribute('id', 'file-' + fileId)
        $dom.setAttribute('class', 'image')
        $dom.textContent = 'Uploading'
        return  DecorationSet.create(tr.doc, [Decoration.widget(tr.selection.from, $dom)])
      }

      // Note that this maps the decorations to the new document when needed
      return tr.docChanged ? deco.map(tr.mapping, tr.doc) : deco
    }
  },
  props: {
    decorations(state) { return this.getState(state) }
  }
})

[Edit: made sure I actually tested the code, fixed mistakes]

The patch did the trick of removing the multiplication of the widget text :slight_smile: However when I now add a decoration and insert another decoration, the other is removed. So if I upload 2 files. The other decoration is removed. I fixed this by replacing:

return DecorationSet.create(tr.doc, [Decoration.widget(tr.selection.from, $dom)])

with

return deco.add(tr.doc, [Decoration.widget(tr.selection.from, $dom)])

Is this correct or should the deco be a completely new object with the old decorations copied?

Currently it also isn’t completely clear to me how and where I remove the decoration and replace it wit the actual image. When the $dom is added as a decoration, the $dom also has an event listener. This event gets triggered by the upload in the background. In the event I know which id is finished. I have to track down the decoration belonging to this $dom. How do I do this?

Also is it possible to let the decoration behave like a block node? Currently it is inserted in a paragraph, but it would be nice if it splits the paragraph or wherever the cursor is and inserts the placeholder div between the split paragraphs.

Is this correct or should the deco be a completely new object with the old decorations copied?

The add method creates a new object. Most of the ProseMirror API is functional-style.

You could put the upload ID in the decoration’s options object (third parameter to Decoration.widget), and then call deco.find() to get the current decorations, loop through them to find the one whose .options.uploadID property matches the ID you’re interested in, and remove that one.

If I do:

$dom.addEventListener('changes', (ev) => {
  const state = window.editor.editor.state // retrieve global reference, because no reference is available???
  let decoset = this.getState(state)
  let decos = decoset.find()
  for (let j = 0; j < decos.length; j++) {
    if (decos[j].type.options.fileId == ev.detail.fileMessage.fileId) {
      decoset = decoset.remove([decos[j]])
    }
  }
  console.log(decoset.find())
  decoset.map(state.tr.mapping, state.tr.doc)
})

inside the apply(tr, deco) method of the plugin, isn’t the reference to deco outdated if more files are upload. So shouldn’t I get a fresh reference to the decorations. For example by doing

let decoset = this.getState(window.editor.editor.state) // Feels kind of hacky

wherein state is a reference to the EditorState object. But if so, how do I get the latest reference to the EditorState object. Because I can’t find a property on the Plugin which has a reference to this state and if I have finally deleted the decoration from the decorationSet, how is the new decorationSet applied to the document.

You could pass a getState callback when you create the plugin, something like this:

let state = EditorState.create({
  ...
  plugins: [myPlugin({getState: () => state}), ...]
})

function myPlugin({getState}) {
  return new Plugin(...)
}

If I implement the getState like this, it returns a very old state when there were no decorations added. When I use:

const state = window.editor.editor.state

as state it has the latest state, or it has a state with decorations.

The following is still not working. When I remove the decoration from the decorationSet:

decoset = decoset.remove([decos[j]])

and map it:

decoset.map(state.tr.mapping, state.tr.doc)

The decoration is still visible. There must be done something that applies the removal of the decoration, but what is it? I thought that the mapping did it, but it isn’t.

I don’t understand what you’re describing. The decoration is still there after you’ve removed it? Why do you think the mapping is related?

What I want to accomplish is the following:

  1. After button click insert decoration with id
  2. After some async event, remove the same decoration based on id

Step 1 is working, step 2 isn’t.

What I do in step 2 is the following:

  1. Get latest state by const state = window.editor.editor.state (window.editor.editor is ref to view)
  2. Retrieve the decorationSet let decoset = this.getState(state)
  3. Get it in an array let decos = decoset.find()
  4. Loop over the array and remove matching decorations from array, if found, remove deco from decoset
  5. As step 5 I have to apply the new decorationSet (but how can this be done?). I thought I had to do it with decoset.map(state.tr.mapping, state.tr.doc), but this just returns a decorationSet. How can I update the editor with the new deocrationSet?

In other words, how do I refresh the editor with the new DecorationSet?

Removing them from the array is not necessary or useful. Removing it from the set is what will cause it to vanish, provided the new set without that decoration is returned the next time the decorations prop is called.

Mapping a decoration set just moves it from one version of the document to another, and has nothing to do with applying it (it’s a pure function that returns a new set)

What you need to do is create and dispatch a transaction that contains some metainfo which tells your decoration plugin to drop the decoration with a given id, and return the new set as updated state.

Finally got it working. Thanks for your patience :slight_smile: The trick is to dispatch a new transaction in the event and set some meta and catch in the Plugin on this meta and do some transformations.

The part that bothers me is to get a reference to the view in the event handler of the $dom in the plugin. Currently I am doing this via the global window object, but this isn’t the nicest solution. How can I get a reference to the view without using the global window object. The plugin is residing in a separate file. The view is instantiated somewhere else. I tried the method as mentioned above for the state. But this also doesn’t seem to work, because the view is instantiated after the state is created.

Also I’d like to know if it is intended behavior, that when I insert a decoration at the end of a document it can’t be removed with the cursor or selected. If I add a decoration in the middle of some text, I can delete the decoration and also select it.

That’s what my suggestion about passing a getState callback to the plugin was about. It’ll involve storing the state somewhere outside of the view (which is easy to do in dispatchTransaction, and a good idea in general), but if you do that, you can pass a function that looks at whichever variable or property you’re using for the state when you create the plugin.

Hmm, really don’t know how to accomplish this. I always thought that the view is just a reference to one and the same object and state is constantly deep copied and returned or isn’t that true?

Yes, view is a mutable object that has its state property updated when you call updateState or update, and state objects are persistent immutable values that are recreated (sharing as much of the old structure as possible) when updated.

I’m not sure how that makes it difficult to create a closure that has access to the current state, though.

1 Like