Development

HTML5 Drag & Drop

In this article, we're gonna see one of the awesome new features of HTML 5 : moving elements in the page and tracking it in javascript.

A demonstration of what we're gonna do is available online.

Why HTML 5 ?

Indeed ! We could very well be using javascript frameworks such as jQuery which already implements drag & drop There are several problems to that however :

  • You become dependant of the jQuery framework.
  • The thing's ergonomy could be discussed whenever you have a bit complex objects (in my case, nested arrays, every element of the array should be moveable from one parent to one of the childs and the opposite).
  • It's quite heavy.
  • It's not accessible at all when you don't have a mouse.

That's why we're gonna look into implementing this drag & drop thing in HTML5.

The HTML tag

We will create, in our page, two simple elements which will be the containers.

<div class="box"></div>
<div class="box></div>

Then, in the first one, we'll add an element which will be moveable to the second.

<div class="box"><div class="element" id="first">Hey !</div></div>

In order to see them better, we'll add a bit of style.

<style type="text/css">
    .box {
        border: 1px solid #CCC;
        width: 150px;
        height: 150px;
        float: left;
        margin: 10px;
    }
    .element {
        width: 50px;
        height: 50px;
        background-color: #CCCCCC;
        margin: 5px;
        padding: 3px
    }
</style>

We know have all the needed elements to make a simple drag & drop in our page. Let's move it.

The events

We have access to several new events which allows us to activate that drag & drop.

Moving the element

Only one event will be needed for now : ondragstart. This event is triggered whenever you start the drag & drop on an object which allows it.

To make an element draggable, we must add it the draggable=true attribute. Then in the box which contains it, we add the appropriate event.

<script type="text/javascript">
    function dragStart(event) {
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData("Text", event.target.getAttribute('id'));
    }
</script>

<div class="box" ondragstart="dragStart(event);">
    <div id="first" class="element" draggable="true">Hey !</div>
</div>

Let's analyse that code.

The dataTransfer object

The two methods used are part of the dataTransfer object. It's a new object of HTML5. It allows you to keep track of some datas between events.

So, when in an event, we update a data in dataTransfer, it'll be accessible to all the other events. Like that, we can save datas and get them back in an other event. This will be very useful to move the appropriate object.

effectAllowed allows us to define the type of move that we're allowing. The moved are the following :

  • all - The event can be copied, moved and linked.
  • copy - The event can be copied.
  • copyLink - The event can be copied and linked.
  • copyMove - The event can be copied and moved.
  • link - The event can be linked.
  • linkMove - The event can be moved and linked.
  • move - The event can be moved.
  • none - The event can't be copied, moved nor linked.
  • uninitialized - Default value, which means "move" for editable elements, "link" for anchors and "copy" for anything else.

setData allows us to define a value to our dataTransfer. Here, we define the element's id. But you could define whatever you want and which will allow you to get back that element thereafter.

The "Text" key isn't definable as you wish. It means the format of the string we transmis (here, some text). Some browsers, like Firefox, accepts any value. Chrome, however, will require you to use the appropriate one.

Our object is now moveable in the page. Let's drop it in the second box.

Dropping the element

Just being able to move the elemnt won't bring us very far. In order to drop it, we'll add two events : ondragover and ondrop.

ondragover is executed whenever our mouse goes over an element when dragging. It'll allow us to allow or not dropping the element in the second box.

By default, javascript doesn't accepts that we drop the element. Consequently, we must return false to stop propagating the event and don't have the default behavior.

ondrop is executed when we drop the element in the second box. That's when we effectively move the element in the dom.

Here's what we get.

<script type="text/javascript">
    function dragStart(event) {
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData("Text", event.target.getAttribute('id'));
    }

    function dragOver(event) {
        return false;
    }

    function drop(event) {
        var element = event.dataTransfer.getData("Text");
        event.target.appendChild(document.getElementById(element));
        event.stopPropagation();

        return false;
    }
</script>

<div class="box" ondragstart="dragStart(event);" ondragover="return dragOver(event);" ondrop="return drop(event);">
    <div id="first" class="element" draggable="true">Hey !</div>
</div>
<div class="box" ondragstart="dragStart(event);" ondragover="return dragOver(event);" ondrop="return drop(event);">

Try it yourself. You can drag/drop the element from a box to an other ! :-)

You can use the online demonstration to try it.

All the available events

We've seen here how to something very simple. However it's possible to make it more complex depending of your needs. Here's all the events you can use.

  • dragstart - Represents the beginning of the move. It's executed whenever you start moving an element.
  • drag - Event executed at regular intervals whenever you're moving (the HTML5 documentation says every 350ms). If it returns false, the move it stopped and canceled.
  • dragenter - Event executed whenever the event you're moving is coming into an other element.
  • dragleave - Event executed whenever the event you're moving leaves an other element.
  • dragover - Event executed whenever you're coming above an other element. If you let it's propagation continue (if you don't return false), it won't be possible to drop the object you're moving in that element.
  • drop - Event executed when you drop the object in an other element.
  • dragend - Event executed when the move ends.

Conclusion

Right now, only Opera doesn't supports drag & drop. All other recent browsers have it. This solution is, of course, to be prefered to any full javascript one which will only simulate something which can be natively managed by the browser.

How and why we've moved from CouchDB to MongoDB

After quite some hesitations and discussions, we've decided to migrate our application from CouchDB to MongoDB.

I'll try, in that article, to explain why and how we've decided to do so.

The why

The image at the left is the search interface for the application on which I'm working.

In that application, we have several records (each one of them representing a borehole). Each record has several attributes such as those displayed here (name; length and date).

The higher part of the image is the filters. We can add as many filters as we want on the attributes. Here, for example, we have two filters : one on the serial of the machine which did the borehole. And an other one on a date interval.

The displayed records are the one which correspond to all the provided filters.

How it works with CouchDB

In order to search for the appropriate record wich CouchDB, we dynamicaly create several views.

For example, the search above is calling the view by_boreholename_creation_date, which returns all the records wich, as key, an array of all the provided attributes. When calling that view, we search with startkey and endkey.

However the way startkey and endkey works is a bit special. It does an equality comparison for all the array elements, except the last one.

So let's guess the following search :

  • An interval of date.
  • The borehole name.

The second elemnt will be appropriately searched. However for the interval, the search will be a ==. Consequently, we won't have the results we want. We will only have the results for which the date is exactly the one we've entered.

At the WebWorkersCamp, when I asked for this, someone suggested I should do one request for every filter.

Knowing that some of our clients have more than 700 records and it's growing exponentially, that wasn't possible.

Let's see MongoDB

So we've decided to migrate to MongoDB. I started to work on that migration the 26th or july and it's been deployed in production this week (30th of august).

MongoDB queries are way easier. Mostly because we don't need to care about javascript views. They're generated automatically by the engine. The operators allows us to manage all of our needs.

We're using mongoid (with rails 2.3). And a simple call to the where method with the appropriate arguments hash is enough to make our search.

We don't have the same problems with the filters order. Our searches are always done on an interval when there's one or on a regular expression when it's a string.

The how

With CouchDB, we were using CouchRest. With MongoDB, we're using Mongoid.

The "how" problem was the data migration. I didn't wish to do too low level.

So I've moved the CouchDB models to the lib/ folder (so they don't get automatically loaded when launching the application in production) and I gave them the following content :

module Couchdb  
    class Record < CouchRest::ExtendedDocument
        def self.to_s
            "Record"
        end
    end
end

So my model gets the datas from the CouchRest namespace "Record" and not "Couchdb::Record"

Than in a rake task, I loop through all the CouchDB records and add them to MongoDB.

Couchdb::Record.all.each do |record|
    record.delete '_rev'
    record.delete '_attachments'
    record.delete 'couchrest-type'
    r = Record.new record     
    puts "Record saved"
  end
  puts "There are now #{Record.count} records in the mongo db"

As CouchRest's models extends from Hashes (which is a quite weird decision), it's much easier to get all the attributes and just remove the internal ones.

You'll note that we don't remove the _id from CouchRest. Like that, our documents keep the same id.

That rake task is manually executed when we've been deploying in production.

Conclusion

Don't give up on CouchDB because of that article ! This engine is really great, even though it didn't fit with our specific search needs. I wouldn't hesitate to recommand it.

Moreover a good news is that, as MongoDB stores it's datas in binary json, accessing them is much faster. We don't have real production statistics yet. But the execution time of our tests has reduced of 20% !

Add TinyMCE to a Django admin field

The Django's automatic administration generator is nice. With three lines, you have an interface to add, update and delete datas. However with textareas, it might be useful to have a more advanced editor.

So we're going to see here, how to implement TinyMCE to the textareas in a specific administration page. But this technic can also work with FCKEditor or any other WYSIWYG editor. All you have to do is put the appropriate javascript.

Let's our application's admin.py

from django.contrib import admin
from project.application.models import Page
class PageAdmin(admin.ModelAdmin):
    list_display = ('name', 'url')
    fieldsets = [
        (None, {'fields': ['name', 'url']}),
        ('Content', {'fields': ['content']}),
    ]
admin.site.register(Page, PageAdmin)

We define an admin interface for our model Page with the fields name, url and content. It's the last field that will be a TinyMCE field.

Let's start by adding external javascript files for this model. After fieldsets[], add :

class Media:
    js = (
        '/js/tiny_mce/tiny_mce.js',
        '/js/admin_pages.js'
    )
We include those two supplementary documents in the admin page for the model Pages. And only this one.

Install tinymce in the folder /js/. And create the file /js/admin_pages.js in which you'll put :

tinyMCE.init({
    mode : 'textareas',
    theme : "simple"
});
So you'll transform any textarea in this page to a TinyMCE field (in our case, there's only one). Restart your application and look at your nice text editor :)

You can now add any javascript to any admin page this way.