Getting that Ghost Drag and Drop effect

Tagged , and

While I am mostly meh when it comes to Ghost, which as I have said before is just a node.js version of Habari, they did come up with some ideas that are novel, and dare I say it, useful.

My favorite of these is the custom work they did with markdown to give you the drag and drop media interface in the editor. I am working on a Ghost inspired Admin plugin for Habari, and one of the first things I tackled was recreating this novel experience.

Here is a video of it from the fine folks at Ghost. The drag and drop bit starts at around 1:25.

Okay, so that is what we are recreating. Here is our manifest of items:

  1. jQuery — For obvious reasons.
  2. Dropzone.js — Handles creating dropzones... I probably didn't have to explain that.
  3. Showdown.js — A JS port of Markdown I happen to like.
  4. WMD.js — Original version of Stackoverflow's Markdown based RTE.

I haven't linked to the individual items since we are going to need specific versions of each one. In few cases they aren't maintained anymore and newer versions aren't quite as easy to use. I have a github repo with my custom versions of all this for you to fork.

No worries boo. I got you.

So the majority of the magic will be happening in showdown.js, since we need to add a new pattern to match. I will show you how to use the other items in conjunction with our custom version of showdown though, for completeness sake.

So, what are we hoping to accomplish?

Currently showdown.js supports the following syntax to add an image that is already available via the interwebs:

!['google logo'](http://google.com/logo.gif)

We want to add a new syntax that will allow us to drag and drop images from our local machine. Something like this:

!dnd['a kitten']

Which will give us something like this:

Our new drag and drop interface

Once that is popped into the edit view you can drag and drop an image on to it, and if everything has gone according to plan, it will hand off the file to Dropzone.js which handles all the uploading.

Updates to Showdown.js

First let's talk a little bit about the workflow of this whole process.

  1. We add a drag and drop zone to our preview pane via !dnd[].
  2. We drop an image on the pane.
  3. That image is picked up by Dropzone.js and passed off to a method we have built to upload images.
  4. That method returns the bit we need to find the !dnd[] code in the editor pane and replace it with !['our file'](http://path.to/our/file) which we can then save to the DB.

Pretty simple right? Okay let's take a look at the first piece in our puzzle: Adding our new syntax to Showdown.js. This consists of two blocks.

First is a method to identify that you have added !dnd[..] to the editor. Using a Regular Expression, the method parses through the editor to find our pattern.

For each instance it finds, placeholderTag is called which handles all the magic of dropping in the drag and drop zone HTML.

var _DoPlaceholder = function (text) {
   text = text.replace(/(!dnd\[(.*?)\]?(?:\n[ ]*)?\])()()()()/g, placeholderTag);
   return text;
};

Speaking of magic, here is the code for placeholderTag:

var placeholderTag = function (wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
   html = '
'; html += '
Add an image of ' + m2 + '
'; return html; }

So we have found a match, and broken it into pieces, so we now grab the second bit, in this case the text we added to the region, and make it the data-id of the new div we are creating. We are doing this since there could be any number of drag and drop targets in play, and need something to differentiate them.

I am working on a more extensive version that adds uniquely generated IDs for each region but for now this works. Just make sure the text you pass to each one is unique. We add some niceties like the camera icon, drop the m2 text in and then return the newly formatted HTML so it can be ran up the chain.

At this point the region is prepared and dropped into your preview pane ready for dragging and dropping. Next let's look at how we hook in dropzone.js.

Making the zones droppy.

So now that we are successfully dropping those fancy drag and drop zone into our editor and preview div, we need to attach dropzone.js to each one, both in real time and on page load, in case you added a few and saved without uploading.

First we will take a look at how to handle when new zones are added. Here is the code in question:

   $('#preview-div').bind('DOMNodeInserted DOMNodeRemoved', function() {
      $('.placeholder_drop').each( function() {
         var dpz = $(this).dropzone({ 
            url: ADMIN.url + '/auth_ajax/upload_files', 
            previewTemplate: '',
            success: showMe
         });
     });
   });

So let's break it down. First we bind DOMNodeInserted and DOMNodeRemoved to our preview div, to cover whether a zone has been added or removed. Then we find and loop through all the instances of the dropzone HTML we created with our Showdown.js additions.

When we find one, we attach the drag and drop behavior provided by Dropzone.js to it. This consists of:

  1. An ajax URL to post the dropped file to.
  2. A preview template. This is an important bit, since by default Dropzone.js applies a template to every region it creates, that doesn't really mesh well with what we are doing. Adding the custom template, set to hidden fixes this problem. And yes, you have to provide a template of some kind for everything to work.
  3. A callback function reference upon successful capture of the file via the drop event. In our case showMe(). I have some imaginative method names, don't I?

Next, let's look at the showMe(), where we handle updating the editor and preview divs with the results of our upload.

function showMe(file, r) {
      var which = dpz.data('id');
      var str = '!dnd[' + which + ']';
      var new_str = $('#editor_area').val();
      var parsed = new_str.replace(str, '![](' + ADMIN.url + '/' + r.data + ')');
      $('#editor_area').val( parsed );
}

This one is pretty easy. Let's step through it. Our function automagically gets the file object from Dropzone.js, as well as the results returned from our upload method. In this case it is a JSON struct that looks like this:

{
   "response_code":200,
   "message":null,
   "data":"user\/files\/uploads\/1\/quick-tips.png"
}

With this struct passed to showMe() we now have everything we need to update the editor, replacing the drag and drop markdown with the normal image syntax, which will also update our preview with the image, instead of the drag and drop zone. To do this we first find the dropzone in question, which luckily we have a handle to since the dpz object is still available to us.

Once we have isolated that piece of the DOM, we create a string from the our zone, grab the contents of the editor, and then call the replace() method on it passing the zone string, and a constructed version of the normal image markdown syntax. You can see here we are grabbing the data member from the JSON struct we got earlier. Combine it with the root URL (in this case ADMIN.url) and we have the absolute path to the newly upload image.

We then take that newly updated markdown and replace the editors val() attribute with it, that handily also fires a refresh of the preview area. At this point we have recreated the core of one of the more interesting aspects of the Ghost editor interface. The following code block is a variation on the above theme, this time to make sure we activate each dropzone when you access the page.

function activateDrops() {
   $('.placeholder_drop').each( function() {
      var dpz = $(this).dropzone({ 
         url: ADMIN.url + '/auth_ajax/upload_files', 
         previewTemplate: '',
         success: showMe
      });
  function showMe(file, r) {
     var which = dpz.data('id');
     var str = '!dnd[' + which + ']';
     var new_str = $('#editor_area').html().replace(str, '![](' + ADMIN.url + '/' + r.data + ')');
     $('#editor_area').html( new_str );
  }

});
}

Right, so pretty awesome right? Using a couple of off the shelf open source projects, and a little elbow grease we can add a new paradigm to how we write on the web. As I promised earlier I have added my custom versions of the JS in question to a repo on Github, so feel free to fork away.

Also if you want to just test out how this all works, as well as get a look at a Ghost like interface, feel free to download the latest Habari release, and then grab my Haunted plugin.

My solution isn't perfect yet, but I am continually working on it, and would welcome pull requests. So, grab a copy of Habari, install the plugin and go to town. If you use my solution for another platform, let me know. I would love to link to it.

Thanks!