Content Editable Pasting

I’ve just updated the paste handling in SCEditor which has been needing updating for a while. When it was first implemented the Clipboard API wasn’t really supported by any browsers.

These days most browsers have added support which makes dealing with pasting much easier. Previously you had to detect and redirect the paste (hard to do without bugs) or you had to clear the editable, wait for the paste to happen and then restore the contents.

This is what I came up with:

/**
 * Adds a paste event handler to a contentEditable node
 * and calls the passed callback with an object that has
 * html and/or text properties along with any other mime
 * types that were pasted.
 *
 * @author Sam Clarke <samclarke.com>
 * @license MIT
 */
function detectPaste(editable, callback) {
  var IMAGE_MIME_REGEX = /^image\/(p?jpe?g|gif|png|bmp)$/i;
  var isIE = !!document.documentMode;
  var isEdge = "-ms-ime-align" in document.documentElement.style;
  var prePasteContent;

  function loadImage(file) {
    var reader = new FileReader();
    reader.onload = function (e) {
      callback({ html: '<img src="' + e.target.result + '" />' });
    };
    reader.readAsDataURL(file);
  }

  editable.addEventListener("paste", function (e) {
    var clipboard = e.clipboardData;

    // Modern browsers with clipboard API - everything other than
    // _very_ old android web views and UC browser which doesn't
    // support the paste event at all.
    // Need to also ignore IE as only has text and URL support
    if (clipboard && !(isIE || isEdge)) {
      var data = {};
      var types = clipboard.types;
      var items = clipboard.items;

      e.preventDefault();

      for (var i = 0; i < types.length; i++) {
        // Normalise image pasting to paste as a data-uri
        if (
          window.FileReader &&
          items &&
          IMAGE_MIME_REGEX.test(items[i].type)
        ) {
          return loadImage(clipboard.items[i].getAsFile());
        }

        data[types[i]] = clipboard.getData(types[i]);
      }

      data.text = data["text/plain"];
      data.html = data["text/html"];

      callback(data);
      // If fragment exists then we're already waiting for a
      // previous paste so let that handler for that handle this one
    } else if (!prePasteContent) {
      // Save the scroll position so can restore it with contents
      var scrollTop = editable.scrollTop;

      prePasteContent = document.createDocumentFragment();
      while (editable.firstChild) {
        prePasteContent.appendChild(editable.firstChild);
      }

      setTimeout(function () {
        var html = editable.innerHTML;

        editable.innerHTML = "";
        editable.appendChild(prePasteContent);
        editable.scrollTop = scrollTop;
        prePasteContent = false;

        callback({ html: html });
      }, 0);
    }
  });
}

It uses the clipboard API if supported (basically all modern browsers). If not, it moves the contents into a fragment, waits for the paste to happen, extracts the pasted HTML and then restores the contents.

Removing and restoring the contents can cause a small flash in IE/Edge. To fix it you could detect and redirect the paste to another editable but as you can’t detect all methods of pasting reliably it would still flash sometimes. IE/Edge does have a beforepaste event which I had hoped to use for redirecting the paste. It does work but it also fires when right clicking and sometimes when clicking on the editable.

For the sake of stopping a small flash in IE/Edge that doesn’t always happen I decided it just wasn’t worth it. It’s not a massive UX problem. Better to wait for MS to support the Clipboard API instead.

Demo

Paste here...




  
  
  
  
  

Comments