Drag and drop image and upload

Has anyone implemented a drag and drop image functionality with prosemirror?

I am looking to implement a function that allows the user to drag an image file onto the editor, which will then upload the image to a server and insert an image preview into the editor on the current cursor position.

I would be great if you could give me some pointers on where I should look in order to implement this functionality.

Thank you in advance.

1 Like

Well, assuming the drag & drop is handled outside of prosemirror, you can then insert the image by executing the command:

pm.execCommand('image:insert', ["src_goes_here", "alt_text_goes_here", "title_goes_here"]);

That should insert the image at the current cursor position. Not sure how drag&drop cursor position and prosemirror’s cursor interact though. You may have to do something special to get prosemirrors current position to be equal to where the user drops the image.

You can start by getting the execCommand working with a hardcoded url. I’ve used the dev tools console to play with it. Once you get that, try getting a drop handler to exececute a hardcoded image insert. Once you got that working you’ll need to have the drop handler do the upload and set the src on the command.

This is personally a feature I haven’t done yet but plan to. And I’m currently using the https://blueimp.github.io/jQuery-File-Upload/ library to handle direct uploads to S3.

It’s not published anywhere yet, but I’m working on something similar. Here’s the code for your viewing. This implementation does not upload the image, but scales it in the browser and inserts it with a data: URL. On the call to scaleImage might be a good place to insert some kind of upload.

Also, it waits for all images to load before inserting them into the document, this is to at least try to preserve order. Otherwise, you could just pm.tr.insert each image node directly.

@peteb has another take on it in his collection of ProseMirror widgets which is surely helpful to have a look at.

// Usage:
// pm.on("drop", dropHandler(pm))

export function dropHandler (pm) {
  return function (e) {
    if(!(e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0))
      return

    let images = filterImages(e.dataTransfer.files)
    if(images.length === 0) return

    e.preventDefault()
    let insertPos = pm.posAtCoords({left: e.clientX, top: e.clientY})
    let nodes = []
    images.forEach(image => {
      let reader = new FileReader()
      reader.onload = e => {
        scaleImage(e.target.result, getWidth(pm.content), img => {
          nodes.push(pm.schema.node("image", {src: img.src, alt: image.name}))
          if(nodes.length === images.length)
            pm.tr.insert(insertPos, nodes).apply()
        })
      }
      reader.readAsDataURL(image)
    })
  }
}

function filterImages (files) {
  let images = []
  for(let i = 0; i < files.length; i++) {
    let file = files[i]
    if (!(/image/i).test(file.type)) continue
    images.push(file)
  }
  return images
}

function getWidth (el) {
  let width = ('getComputedStyle' in window)
    ? window.getComputedStyle(el).getPropertyValue('width')
    : el.currentStyle.width

  return parseInt(width, 10)
}

function scaleImage (src, targetWidth, callback) {
  let img = document.createElement("img")
  img.src = src
  img.style.visibility = "hidden"
  document.body.appendChild(img)

  let canvas = document.createElement("canvas")
  let ctx = canvas.getContext("2d")

  img.onload = e => {
    if(img.width <= targetWidth) {
      document.body.removeChild(img)
      return callback(img)
    }

    let newWidth = Math.floor(img.width / 2)

    if(newWidth < targetWidth)
      newWidth = targetWidth

    let newHeight = Math.floor(img.height / img.width * newWidth)

    if(newWidth >= targetWidth) {
      canvas.width = newWidth
      canvas.height = newHeight

      ctx.drawImage(img, 0, 0, newWidth, newHeight)

      img.src = canvas.toDataURL()
      img.width = newWidth
    }
  }
}

I have implemented the drag & drop with upload similar to what @linus does. However, the problem I’m having right now is that when the editor replaces the preview image with the actual image, the preview image disappear for a moment before the real image is reloaded, which is not very nice. Does anyone know how I could make it such that the replacement is unnoticeable by the user?

Right now I’m replacing the whole image element. Would it be better to just replace the href?

pm.on('drop', function(e) {
  var files = e.dataTransfer.files;
  if (files.length > 0) {
    e.preventDefault();
    var file = files[0];
    var reader = new FileReader();
    reader.onload = function(e) {
      let imgAttr = {
        class: 'uploading',
        src: e.target.result
      }
      let image = mySchema.node('image', imgAttr);
      pm.tr.insertInline(pm.selection.anchor, image).apply();

      let currentSel = pm.selection;
      let newAnchor = pm.selection.anchor.move(-1);
      let mr = pm.markRange( newAnchor, currentSel.anchor, {className: 'marked'});

      let moveToSelection = new TextSelection(newAnchor);
      pm.setSelection(moveToSelection);
      options.imageUploader(file, function(err, data){
        if (err) return console.error(err);
        let image = mySchema.node('image', {
          src: data.imageUrl
        });
        let newSel = new TextSelection(mr.from, mr.to)
        pm.setSelection(newSel);
        pm.tr.replaceSelection(image, false).apply();
      });
    }
    reader.readAsDataURL(file);
  }
});

Pre-loading the image might help (create a hidden <img> tag pointing at it, and wait for its "load" event)

Am I doing it correctly? Thanks @marijn

function loadAndReplace(imageData, markedRange) {
  let img = new Image();
  img.onload = (e) => {
    let image = jittaSchema.node('image', {
      src: data.imageUrl
    });
    let newSel = new TextSelection(markedRange.from, markedRange.to)
    pm.setSelection(newSel);
    pm.tr.replaceSelection(image, false).apply();
  }
  img.src = data.imageUrl;
}

I don’t know. Does it work?

seems to be working, thanks.