Skip to main content

Getting that Ghost Drag and Drop effect

0
<h2>While I am mostly <strong>meh</strong> when it comes to <a href="http://ghost.org" title="Ghost, simple blogging. AKA know as a node version of Habari">Ghost</a>, 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, <em>useful</em>.</h2>

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 <a href="https://github.com/leagueofbeards/Haunted" title="Haunted, a Ghostly Admin Experience.">Admin plugin for Habari</a>, 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.

<div class="video-container"> <iframe class="video" style="margin-bottom:20px;" src="//www.youtube.com/embed/n1HChHB0kj0?rel=0&amp;vq=hd720&amp;showinfo=0" frameborder="0" allowfullscreen></iframe> </div>

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

<ol> <li><code>jQuery</code> &mdash; For obvious reasons.</li> <li><code>Dropzone.js</code> &mdash; Handles creating dropzones... I probably didn't have to explain that. </li> <li><code>Showdown.js</code> &mdash; A JS port of Markdown I happen to like.</li> <li><code>WMD.js</code> &mdash; Original version of Stackoverflow's Markdown based RTE.</li> </ol>

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 <code>showdown.js</code>, 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.

<h4>So, what are we hoping to accomplish?</h4>

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

<pre class="prettyprint"> !['google logo'](http://google.com/logo.gif) </pre>

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

<pre class="prettyprint"> !dnd['a kitten'] </pre>

Which will give us something like this:

<img src="/user/files/in-post/draganddrop.png" class="image" style="margin-bottom:20px;" alt="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 <code>Dropzone.js</code> which handles all the uploading.

<h4>Updates to Showdown.js</h4>

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

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

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

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

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

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

Speaking of magic, here is the code for <code>placeholderTag</code>:

<pre class="prettyprint"> var placeholderTag = function (wholeMatch, m1, m2, m3, m4, m5, m6, m7) { html = '&lt;div data-id="' + m2 + '" class="placeholder_drop">'; html += '&lt;i class="icon-camera-retro icon-4x">&lt;/i>&lt;br> Add an image of &lt;strong>' + m2 + '&lt;/strong>&lt;/div>'; return html; } </pre>

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 <code>dropzone.js</code>.

<h4>Making the zones droppy.</h4>

So now that we are successfully dropping those fancy drag and drop zone into our editor and preview div, we need to attach <code>dropzone.js</code> 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:

<pre class="prettyprint"> $('#preview-div').bind('DOMNodeInserted DOMNodeRemoved', function() { $('.placeholder_drop').each( function() { var dpz = $(this).dropzone({ url: ADMIN.url + '/auth_ajax/upload_files', previewTemplate: '&lt;div id="dndPreview" class="dz-preview dz-file-preview" style="display:none;">&lt;div class="dz-details">&lt;div class="dz-filename">&lt;span data-dz-name>&lt;/span>&lt;/div>&lt;div class="dz-size" data-dz-size>&lt;/div>&lt;img data-dz-thumbnail />&lt;/div>&lt;div class="dz-progress">&lt;span class="dz-upload" data-dz-uploadprogress>&lt;/span>&lt;/div>&lt;/div>', success: showMe }); }); }); </pre>

So let's break it down. First we bind <code>DOMNodeInserted</code> and <code>DOMNodeRemoved</code> 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 <code>Showdown.js</code> additions.

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

<ol> <li>An ajax URL to post the dropped file to.</li> <li>A preview template. This is an important bit, since by default <code>Dropzone.js</code> 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.</li> <li>A callback function reference upon successful capture of the file via the drop event. In our case <code>showMe()</code>. I have some imaginative method names, don't I?</li> </ol>

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

<pre class="prettyprint"> 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 ); } </pre>

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

<pre class="prettyprint"> { "response_code":200, "message":null, "data":"user\/files\/uploads\/1\/quick-tips.png" } </pre>

With this struct passed to <code>showMe()</code> 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 <code>dpz</code> 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 <code>replace()</code> 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 <code>val()</code> 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.

<pre class="prettyprint"> function activateDrops() { $('.placeholder_drop').each( function() { var dpz = $(this).dropzone({ url: ADMIN.url + '/auth_ajax/upload_files', previewTemplate: '&lt;div id="dndPreview" class="dz-preview dz-file-preview" style="display:none;">&lt;div class="dz-details">&lt;div class="dz-filename">&lt;span data-dz-name>&lt;/span>&lt;/div>&lt;div class="dz-size" data-dz-size>&lt;/div>&lt;img data-dz-thumbnail />&lt;/div>&lt;div class="dz-progress">&lt;span class="dz-upload" data-dz-uploadprogress>&lt;/span>&lt;/div>&lt;/div>', 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 ); } }); } </pre>

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 <a href="https://github.com/chrisjdavis/Ghost-Drag-and-Drop-Effect" title="Stuff for the thing.">repo on Github</a>, 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 <a href="http://habariproject.org/en/" title="Habari">latest Habari release</a>, and then grab my <a href="https://github.com/leagueofbeards/Haunted" title="Haunted.">Haunted plugin</a>.

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!

Discussion

Login Required

You must be signed in to post to this board.

Registered users only ยท No anonymous posting

No messages in this thread yet.