Chapter 7. Drag-and-Drop

Drag-and-drop (DnD) can give your application incredible desktop-like functionality and usability that can really differentiate it from the others. This chapter systematically works through this topic, providing plenty of visual examples and source code. You might build off these examples to add some visual flare to your existing application, or perhaps even do something as brave as incorporate the concepts and the machinery that Dojo provides into a DHTML game that people can play online. Either way, this is a fun chapter, so let's get started.

Dragging

While drag-and-drop has been an integral part of desktop applications for more than two decades, web applications have been slow to adopt it. At least part of the reason for the slow adoption is because the DOM machinery provided is quite primitive in and of itself, and the event-driven nature of drag-and-drop makes it especially difficult to construct a unified framework that performs consistently across the board. Fortunately, overcoming these tasks is perfect work for a toolkit, and Dojo provides facilities that spare you from the tedious and time-consuming work of manually developing that boilerplate yourself.

Simple Moveables

Tip

This chapter assumes a minimal working knowledge of CSS. The W3C schools provide a CSS tutorial at http://www.w3schools.com/css/default.asp. Eric Meyer's CSS: The Definitive Guide (O'Reilly) is also a great desktop reference.

As a warm up, let's start out with the most basic example possible: moving an object[16] around on the screen. Example 7-1 shows the basic page structure that gets the work done in markup. Take a look, especially at the emphasized lines that introduce the Moveable class, and then we'll review the specifics.

Example 7-1. Simple Moveable

<html>
    <head>
        <title>Fun with Moveables!</title>
        <style type="text/css">
              .moveable {
                  background: #FFFFBF;
                    border: 1px solid black;
                    width: 100px;
                    height: 100px;
                    cursor: pointer;
            }
        </style>
            <script
              type="text/javascript"
              djConfig="parseOnLoad:true,isDebug:true"
              src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js">
            </script>

           <script type="text/javascript">
            dojo.require("dojo.dnd.Moveable");
            dojo.require("dojo.parser");
        </script>
    </head>
    <body>
        <div class="moveable" dojoType="dojo.dnd.Moveable" ></div>
    </body>
</html>

As you surely noticed, creating a moveable object on the screen is quite trivial. Once the Moveable resource was required into the page, all that's left is to specify an element on the page as being moveable via a dojoType tag and parsing the page on load via an option to djConfig. There's really nothing left except that a bit of style was provided to make the node look a little bit more fun than an ordinary snippet of text—though a snippet of text would have worked just as well.

In general, anything you can do by parsing the page when it loads, you can do programmatically sometime after the page loads. Here's the very same example, but with a programmatically built Moveable:

<!-- ... Snip ... -->

<script type="text/javascript">
    dojo.require("dojo.dnd.Moveable");

    dojo.addOnLoad(function(  ) {
        var e = document.createElement("div");
        dojo.addClass(e, "moveable");
        dojo.body(  ).appendChild(e);
        var m = new dojo.dnd.Moveable(e);
      });
</script>
</head>
    <body></body>
</html>

Table 7-1 lists the methods you need to create and destroy a Moveable.

Table 7-1. Creating and destroying a Moveable

Name

Comment

Moveable(/*DOMNode*/node, /*Object*/params)

The constructor function that identifies the node that should become moveable. params may include the following values:

handle (String | DOMNode)

A node or node's id that should be used as a mouse handle. By default, the node itself is used.

skip (Boolean)

Whether to skip the normal drag-and-drop action associated with text-based form elements that would normally occur when a mouse-down event happens (false by default).

mover (Object)

A constructor for a custom Mover.

delay (Number)

The number of pixels to delay the move by (0 by default).

destroy( )

Used to disassociate the node with moves, deleting all references so that garbage collection can occur.

Tip

A Mover is even lower-level drag-and-drop machinery that Moveable uses internally. Mover objects are not discussed in this chapter, and are only mentioned for your awareness.

Let's build upon our previous example to demonstrate how to ensure text-based form elements are editable by setting the skip parameter by building a simple sticky note on the screen that you can move around and edit. Example 7-2 provides a working example.

Example 7-2. Using Moveable to create a sticky note

<html>
    <head>
        <title>Even More Fun with Moveables! </title>
        <style type="text/css">
            .note {
                background: #FFFFBF;
                border-bottom: 1px solid black;
                border-left: 1px solid black;
                border-right: 1px solid black;
                width: 302px;
                height: 300px;
                margin : 0px;
                padding : 0px;
            }
            .noteHandle {
                border-left: 1px solid black;
                border-right: 1px solid black;
                border-top: 1px solid black;
                cursor :pointer;
                background: #FFFF8F;
                width : 300px;
                height: 10px;
                margin : 0px;
                padding : 0px;
            }
        </style>

        <script
            type="text/javascript"
            djConfig="parseOnLoad:true,isDebug:true"
            src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js">
        </script>

        <script type="text/javascript">
            dojo.require("dojo.dnd.Moveable");
            dojo.require("dojo.parser");
        </script>
    </head>
    <body>
        <div dojoType="dojo.dnd.Moveable" skip=true>
            <div class="noteHandle"></div>
            <textarea class="note">Type some text here</textarea>
     </div>
    </body>
</html>

Tip

The effect of skip isn't necessarily intuitive, and it's quite instructive to remove the skip=true from the outermost DIV element to see for yourself what happens if you do not specify that form elements should be skipped.

Although our sticky note didn't necessarily need to employ drag handles because the innermost div element was only one draggable part of the note, we could have achieved the same effect by using them: limiting a particular portion of the Moveable object to be capable of providing the drag action (the drag handle) implies that any form elements outside of the drag handle may be editable. Replacing the emphasized code from the previous code listing with the following snippet illustrates:

<div id="note" dojoType="dojo.dnd.Moveable"  handle='dragHandle'>
    <div id='dragHandle' class="noteHandle"></div>
    <textarea class="note">This form element can't trigger drag action</textarea>
</div>

Drag Events

It's likely that you'll want to detect when the beginning and end of drag action occurs for triggering special effects such as providing a visual cue as to the drag action. Detecting these events is a snap with dojo.subscribe and dojo.connect. Example 7-3 shows another rendition of Example 7-2.

Example 7-3. Connecting and subscribing to drag Events

<html>
    <head>
        <title>Yet More Fun with Moveable!</title>
        <style type="text/css">
            .note {
                background: #FFFFBF;
                border-bottom: 1px solid black;
                border-left: 1px solid black;
                border-right: 1px solid black;
                width: 302px;
                height: 300px;
                margin : 0px;
                padding : 0px;
            }
            .noteHandle {
                border-left: 1px solid black;
                border-right: 1px solid black;
                border-top: 1px solid black;
                cursor :pointer;
                background: #FFFF8F;
                width : 300px;
                height: 10px;
                margin : 0px;
                padding : 0px;
            }
          .movingNote {
                background : #FFFF3F;
          }
        </style>

        <script
            type="text/javascript"
            src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js">
        </script>

        <script type="text/javascript">
            dojo.require("dojo.dnd.Moveable");

            dojo.addOnLoad(function(  ) {
                //create and keep references to Moveables for connecting later.
                var m1 = new dojo.dnd.Moveable("note1", {handle : "dragHandle1"});
                var m2 = new dojo.dnd.Moveable("note2", {handle : "dragHandle2"});

                // system-wide topics for all moveables.
                dojo.subscribe("/dnd/move/start", function(node){
                        console.log("Start moving", node);
                });
                dojo.subscribe("/dnd/move/stop", function(node){
                        console.log("Stop moving", node);
                });

                // highlight note when it moves...
                //connect to the Moveables, not the raw nodes.
                dojo.connect(m1, "onMoveStart", function(mover){
                        console.log("note1 start moving with mover:", mover);
                        dojo.query("#note1 > textarea").addClass("movingNote");

                });
                dojo.connect(m1, "onMoveStop", function(mover){
                        console.log("note1 stop moving with mover:", mover);
                        dojo.query("#note1 > textarea").removeClass("movingNote");
                });
            });

        </script>
    </head>
    <body>
            <div id="note1">
                <div id='dragHandle1' class="noteHandle"></div>
                <textarea class="note">Note1</textarea>
            </div>
            <div id="note2">
                <div id='dragHandle2' class="noteHandle"></div>
                <textarea class="note">Note2</textarea>
            </div>
    </body>
</html>

Tip

In the dojo.query function calls, you should recall that the parameter "#note1 > textarea" means to return the textarea nodes that are children of the node with an id of "note1". See Table 5-1 for a summary of common CSS3 selectors that can be passed into dojo.query.

Note from the previous code listing that you do not connect to the actual node of interest. Instead, you connect to the Moveable that is returned via a programmatic call to create a new dojo.dnd.Moveable.

As you can see, it is possible to subscribe to global drag events via pub/sub style communication or zero in on specific events by connecting to the particular Moveable nodes of interest. Table 7-2 summarizes the events that you may connect to via dojo.connect.

For pub/sub style communication, you can use dojo.subscribe to subscribe to the "dnd/move/start" and "dnd/move/stop" topics.

Table 7-2. Moveable events

Event

Summary

onMoveStart(/*dojo.dnd.Mover*/mover)

Called before every move.

onMoveStop(/*dojo.dnd.Mover*/mover)

Called after every move.

onFirstMove(/*dojo.dnd.Mover*/mover)

Called during the very first move; handy for performing initialization routines.

onMove(/*dojo.dnd.Mover*/mover),

(/* Object */ leftTop)

Called during every move notification; by default, calls onMoving, moves the Moveable, and then calls onMoved.

onMoving(/*dojo.dnd.Mover*/mover),

(/*Object*/leftTop)

Called just before onMove.

onMoved(/*dojo.dnd.Mover*/mover),

(/*Object */leftTop)

Called just after onMove.

Z-Indexing

Our working example with sticky notes is growing increasingly sophisticated, but one noticeable characteristic that may become an issue is that the initial z-indexes of the notes do not change: one of them is always on top and the other is always on the bottom. It might seem more natural if the note that was last selected became the note that is on top, with the highest z-index. Fortunately, it is quite simple to adjust z-index values in a function that is fired off via a connection to the onMoveStartEvent.

The solution presented below requires modifying the addOnLoad function's logic and is somewhat elegant in that it uses a closure to trap a state variable instead of explicitly using a module-level or global variable:

dojo.addOnLoad(function(  ) {
    //create and keep references to Moveables for connecting later.
    var m1 = new dojo.dnd.Moveable("note1", {handle : "dragHandle1"});
    var m2 = new dojo.dnd.Moveable("note2", {handle : "dragHandle2"});

    var zIdx = 1; // trapped in closure of this anonymous function

    dojo.connect(m1, "onMoveStart", function(mover){
         dojo.style(mover.host.node, "zIndex", zIdx++);
    });
    dojo.connect(m2, "onMoveStart", function(mover){
         dojo.style(mover.host.node, "zIndex", zIdx++);
    });
 });

Warning

Recall from Chapter 2 that dojo.style requires the use of DOM accessor formatted properties, not stylesheet formatted properties. For example, trying to set a style property called "z-index" would not work.

Constrained Moveables

Being able to move a totally unconstrained object around on the screen with what amounts to a trivial amount of effort is all fine and good, but sooner than later, you'll probably find yourself writing up logic to define boundaries, restrict overlap, and define other constraints. Fortunately, the drag-and-drop facilities provide additional help for reducing the boilerplate you'd normally have to write for defining drag-and-drop constraints.

There are three primary facilities included in dojo.dnd that allow you to constrain your moveable objects: writing your own custom constraint function that dynamically computes a bounding box (a constrainedMoveable ), defining a static boundary box when you create the moveable objects (a boxConstrainedMoveable ), and constraining a moveable object within the boundaries defined by another parent node (a parentConstrainedMoveable ). The format for each type of boundary box follows the same conventions as are described in Chapter 2 in the section "The Box Model."

Here's a modification of our previous sticky note example to start out with a constrainedMoveable :

<html>
    <head>
        <title>Moving Around</title>
        <style type="text/css">
            .note {
                background: #FFFFBF;
                border-bottom: 1px solid black;
                border-left: 1px solid black;
                border-right: 1px solid black;
                width: 302px;
                height: 300px;
                margin : 0px;
                padding : 0px;
            }
            .noteHandle {
                border-left: 1px solid black;
                border-right: 1px solid black;
                border-top: 1px solid black;
                cursor :pointer;
                background: #FFFF8F;
                width : 300px;
                height: 10px;
                margin : 0px;
                padding : 0px;
            }
            .movingNote {
                background : #FFFF3F;
            }
            #note1, #note2 {
                   width : 302px
            }
        </style>
        <script
            type="text/javascript"
            src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js">
        </script>

        <script type="text/javascript">
            dojo.require("dojo.dnd.Moveable");
            dojo.require("dojo.dnd.move");

            dojo.addOnLoad(function(  ) {
                var f1 = function(  ) {
                    //clever calculations to define a bounding box.
                    //keep note1 within 50 pixels to the right/bottom of note2
                    var mb2 = dojo.marginBox("note2");
                    b = {};
                    b["t"] = 0;
                    b["l"] = 0;
                    b["w"] = mb2.l + mb2.w + 50;
                    b["h"] = mb2.h + mb2.t + 50;
                    return b;
                }

                var m1 = new dojo.dnd.move.constrainedMoveable("note1",
                   {handle : "dragHandle1", constraints : f1, within : true});

                var m2 = new dojo.dnd.Moveable("note2", {handle : "dragHandle2"});

                var zIdx = 1;

                dojo.connect(m1, "onMoveStart", function(mover){
                    dojo.style(mover.host.node, "zIndex", zIdx++);
                });
                dojo.connect(m2, "onMoveStart", function(mover){
                    dojo.style(mover.host.node, "zIndex", zIdx++);
                });
            });

        </script>
    </head>
    <body>
            <div id="note1">
                <div id='dragHandle1' class="noteHandle"></div>
                <textarea class="note">Note1</textarea>
            </div>
            <div id="note2">
                <div id='dragHandle2' class="noteHandle"></div>
                <textarea class="note">Note2</textarea>
            </div>
    </body>
</html>

Warning

When computing bounding boxes for Moveable objects, ensure that you have explicitly defined a height and width for the outermost container of what is being moved around on the screen. For example, leaving the outermost div that is the container for our sticky note unconstrained in width produces erratic results because the moveable div is actually much wider than the yellow box that you see on the screen. Thus, attempting to compute constraints using its margin box does not function as expected.

To summarize, an explicit boundary was defined for the note's outermost div so that its margin box could be computed with an accurate width via dojo.marginBox, and a custom constraint function was written that prevents note1 from ever being more than 50 pixels to the right and to the bottom of note2.

Warning

Attempting to use a constrainedMoveable without specifying a constraint function produces a slew of errors, so if you decide not to use a constraint function, you'll need to revert to using a plain old Moveable.

Defining a static boundary for a Moveable is even simpler. Instead of providing a custom function, you simply pass in an explicit boundary. Modify the previous example to make note2 a boxConstrainedMoveable with the following change and see for yourself:

var m2 = new dojo.dnd.move.boxConstrainedMoveable("note2",
{
    handle : "dragHandle2",
    box : {l : 20, t : 20, w : 500, h : 300}
});

As you can see, the example works as before, with the exception that note2 cannot move outside of the constraint box defined.

Finally, a parentConstrainedMoveable works in a similar fashion. You simply define the Moveable s and ensure that the parent node is of sufficient stature to provide a workspace. No additional work is required to make the parent node a special kind of Dojo class. Here's another revision of our working example to illustrate:

<!-- ... snip ... -->
.parent {
    background: #BFECFF;
    border: 10px solid lightblue;
    width: 400px;
    height: 700px;
    padding: 10px;
    margin: 10px;
}
<!-- ... snip ... -->
<script type="text/javascript"> 
    dojo.require("dojo.dnd.move"); 
    dojo.addOnLoad(function() { 

        new dojo.dnd.move.parentConstrainedMoveable("note1", 
        { 
            handle : "dragHandle1", area: "margin", within: true 
         }); 
        new dojo.dnd.move.parentConstrainedMoveable("note2", 
        { 
            handle : "dragHandle2", area: "padding", within: true 
        }); 
    }); 
</script>

    </head>
    <body>
        <div class="parent" >
            <div id="note1">
                <div id='dragHandle1' class="noteHandle"></div>
                <textarea class="note">Note1</textarea>
            </div>
            <div id="note2">
                <div id='dragHandle2' class="noteHandle"></div>
                <textarea class="note">Note2</textarea>
            </div>
        </div>
    </body>
</html>

The area parameter for parentConstrainedMoveable s is of particular interest. You may provide "margin", "padding", "content", and "border" to confine the Moveable s to the parent's area.

Tip

Like ordinary Moveable s, you can connect to specific objects or use pub/sub style communication to detect global drag-and-drop events. Because constrainedMoveable and boxConstrainedMoveable inherit from Moveable, the event names for dojo.connect and dojo.subscribe are the same as outlined in Table 7-2 for Moveable.



[16] The term object is used in this chapter to generically refer to a moveable DOM node. This usage implies nothing whatsoever about objects from object-oriented programming.

Get Dojo: The Definitive Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.