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

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.

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>

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>

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++);
    });
 });

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>

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.

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.

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.

Dropping

Thus far, this chapter has focused on dragging objects around on the screen. This section wraps up the discussion by focusing in on the dropping part of it all. To get started, let's first take a look at dojo.dnd.Source, a special container class the toolkit provides a drag-and-drop source. A Source can also act as a target for a drop, but as we'll see in a moment, you can also specify a "pure" target with dojo.dnd.Target. While a Source may act as an origin and a destination, a Target may only act as a destination.

Creating a Source is just like creating a Moveable ; you call the constructor function and pass in a node as the first argument and an Object of parameters as the second argument, like so. Table 7-3 lists the relevant methods.

Table 7-3. Creating and destroying a Source

Name

Comment

dojo.dnd.Source(/*DOMNode*/node, /*Object*/params)

Constructor method for creation. Valid values for params are provided in Table 7-1.

destroy( )

Prepares the Object to be garbage-collected.

Table 7-4 summarizes key parameters involved in the creation of a Source object.

Table 7-4. Configuration parameters for Source's params in Table 7-3

Parameter

Type

Comment

isSource

boolean

true by default; if false, prevents drag action from being possible.

horizontal

boolean

false by default; if true, constructs a horizontal layout (inline HTML elements required).

copyOnly

boolean

false by default; if true, always copies items instead of moving them (no Ctrl-key required).

skipform

boolean

false by default; like Moveable, controls whether to make text-based form elements editable.

withHandles

boolean

false by default; when true, allows dragging only by handles.

accept

Array

["text"] by default. Specifies the type of object that can be accepted for a drop.

A very common use for a Source is to eliminate some of the bookkeeping that is involved in dragging and dropping items that are arranged in a list-like format. The following code example illustrates:

<html>
    <head>
        <title>Fun with Source!</title>
        <link rel="stylesheet" type="text/css"
          href="http://o.aolcdn.com/dojo/1.1/dijit/themes/tundra/tundra.css" />
        <link rel="stylesheet" type="text/css"
          href="http://o.aolcdn.com/dojo/1.1/dojo/resources/dojo.css" />
        <link rel="stylesheet" type="text/css"
          href="http://o.aolcdn.com/dojo/1.1/dojo/resources/dnd.css" />
        <link rel="stylesheet" type="text/css"
          href="dndDefault.css" />
        <script
            type="text/javascript"
            djConfig="parseOnLoad:true"
            src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js">
        </script>

        <script type="text/javascript">
            dojo.require("dojo.dnd.Source");
            dojo.require("dojo.parser");
        </script>
    </head>
    <body>
        <div dojoType="dojo.dnd.Source" class="container">
            <div class="dojoDndItem">foo</div>
            <div class="dojoDndItem">bar</div>
            <div class="dojoDndItem">baz</div>
            <div class="dojoDndItem">quux</div>
        </div>
    </body>
</html>

Although this initial example may not look like much, there is a tremendous amount of functionality packed into it. For starters, notice that the only Dojo class that is directly involved is Source, and to create a container of items that are drag-and-droppable within that container, you simply provide the token dojoType tag and ensure that the element is parsed; like most other examples, the parseOnLoad parameter passed to djConfig takes care of this task.

Next, take a few moments to tinker around with the example. It might be obvious that you can drag-and-drop single items at a time, but it's important to also note the full gamut of functionality that is offered:

  • Clicking selects a single element and results in all other elements becoming unselected.

  • Ctrl-clicking toggles the selection state of an item and allows you to build up multiple items at a time; you can also deselect individual items from a multiple selection situation.

  • Shift-clicking selects a range of elements from the previous-most selection to the current element being clicked. Any selections before the previous-most selection become unselected.

  • Ctrl-Shift-clicking selects a range of elements from the previous-most selection to the current element being clicked, but preserves any selections before the previous-most selection.

  • Holding down the Ctrl key while performing a drop results in the selection(s) being copied. Figure 7-1 illustrates some of these actions.

dnd1 shows an initial selection using Ctrl-click; dnd2 is the result of performing a Shift-click on quux; dnd3 is the result of performing a Shift-Ctrl-click on quux; dnd4 depicts a move operation by dragging without the Ctrl key; and dnd5 shows a copy operation by dragging with the Ctrl key applied
Figure 7-1. dnd1 shows an initial selection using Ctrl-click; dnd2 is the result of performing a Shift-click on quux; dnd3 is the result of performing a Shift-Ctrl-click on quux; dnd4 depicts a move operation by dragging without the Ctrl key; and dnd5 shows a copy operation by dragging with the Ctrl key applied

Pure Targets

As mentioned earlier in the chapter, there are bound to be plenty of times when you'll need to employ a Target that can only act as a destination; once items are placed in it, they may not be moved or reordered. Make the following trivial modification to the previous code listing to see a Target in action.

<body>
    <div dojoType="dojo.dnd.Source" class="container">
        <div class="dojoDndItem">foo</div>
        <div class="dojoDndItem">bar</div>
        <div class="dojoDndItem">baz</div>
        <div class="dojoDndItem">quux</div>
    </div>
    <!-- Items added to targets cannot be removed or reordered -->
    <div dojoType="dojo.dnd.Target" class="container"></div>
</body>

As you may be able to tell by now, a tremendous amount of functionality is wrapped up into just a few lines of code, and although div elements were used for the example, note that other types of standard HTML elements work equally well. Unordered lists via the ul and li elements are a common choice.

Custom Avatars

The small icon that temporarily appears when an item from a Source is being moved around is called an avatar. Although the standard avatar is quite nice, you may want to construct your own at some point. The following code adjustment illustrates how to define custom text for an avatar by overriding the creator method because this method is used to create an avatar representation of one or more nodes. In this particular circumstance, we'll choose to override creator in markup. The layout is also adjusted to a horizontal specification to simultaneously demonstrate how to adjust a layout:

<body>
    <div dojoType="dojo.dnd.Source" horizontal=true class="container">
        <span class="dojoDndItem ">foo</span>
        <span class="dojoDndItem ">bar</span>
        <span class="dojoDndItem ">baz</span>
        <span class="dojoDndItem ">quux</span>

        <script type="dojo/method" event="creator" args="item,hint">
            // override the creator function and return the appropriate type
            var node = dojo.doc.createElement("span");
            node.id = dojo.dnd.getUniqueId(  );
            node.className = "dojoDndItem";
            node.innerHTML = "<strong style='color: red'>Custom</strong>  "+item;
            return {node: node, data: item, type: ["text"]};
        </script>

    </div>
    <div dojoType="dojo.dnd.Target" horizontal=true class="container"></div>
</body>

Note that the arguments passed into creator are item and hint, the actual item being moved and a value specifying a "hint" for the kind of representation of that should be created. Unless you implement your own low-level machinery, hint will always be "avatar". The creator function is expected to return an object representation of item with keys for an actual DOM node, a data representation, and the type of representation. Recall that "text" is the default representation accepted by a Source object.

Drop Events

Subscribing and connecting to events via dojo.subscribe and dojo.connect works just as easy as with Moveable objects. Table 7-5 summarizes public events for pub/sub and connection-style communications, and a code example follows.

Table 7-5. Drop events

Type

Event

Parameters

Summary

subscribe

"/dnd/source/over"

/* Node */ source

Published when the mouse moves over a Source container; the source parameter specifies the container. When the mouse leaves the Source container, another topic is published with null as Source.

subscribe

"/dnd/start"

/* Node */ source

/* Array */ nodes

/* Boolean */ copy

Published when a drag beings. Parameter source specifies the Source container that provides the origin of the drop operations. Parameter copy is true for a copy operation and false for a move operation. Parameter nodes is an array of items involved in the drop operation.

subscribe

"/dnd/drop"

/* Node */ source

/* Array */ nodes

/* Boolean */ copy

Published when a drop occurs (and a drag officially) ends. Parameter source specifies the Source container that provides the origin and destination of the drop operations. Parameter copy is true for a copy operation and false for a move operation. Parameter nodes is an array of items involved in the drop operation.

subscribe

"/dnd/cancel"

N/A

Published when a drop operation is cancelled (for example, when the Esc key is pressed).

connect

onDndSourceOver

/* Node */ source

Called when a mouse moves over a Source container; parameter source specifies the container. When the mouse leaves the Source container, another onDndSourceOver is called again with null as Source.

connect

onDndStart

/* Node */ source

/* Array */ nodes

/* Boolean */ copy

Called when a drag begins. Parameter source specifies the Source container that provides the origin of the drop operations. Parameter copy is true for a copy operation and false for a move operation. Parameter nodes is an array of items involved in the drop operation.

connect

onDndDrop

/* Node */ source

/* Array */ nodes

/* Boolean */ copy

Called when a drop occurs (and a drag officially) ends. Parameter source specifies the Source container that provides the origin and destination of the drop operations. Parameter copy is true if the operation is a copy operation and false for a move operation. Parameter nodes is an array of items involved in the drop operation.

connect

onDndCancel

N/A

Called when a drop operation is cancelled (for example, when the Esc key is pressed).

Go ahead and load up the following full-blown example and use Firebug to inspect the output that occurs from the various topics that we subscribe to and log to the console, and remember that you can drag-and-drop from different Source containers. Figure 7-2 shows the result. Good stuff!

Firebug is great for learning the ropes of drag-and-drop
Figure 7-2. Firebug is great for learning the ropes of drag-and-drop
<html>
    <head>
        <title>More Fun with Drop!</title>
        <link rel="stylesheet" type="text/css"
          href="http://o.aolcdn.com/dojo/1.1/dijit/themes/tundra/tundra.css" />
        <link rel="stylesheet" type="text/css"
          href="http://o.aolcdn.com/dojo/1.1/dojo/resources/dojo.css" />
        <link rel="stylesheet" type="text/css"
          href="dndDefault.css" />
        <script
            type="text/javascript"
            djConfig="parseOnLoad:true"
            src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js">
        </script>

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

            dojo.addOnLoad(function(  ) {
                dojo.subscribe("/dnd/source/over", function(source) {
                    console.log("/dnd/source/over", source);
                });
                dojo.subscribe("/dnd/start", function(source, nodes, copy) {
                    console.log("/dnd/start", source, nodes, copy);
                });
                dojo.subscribe("/dnd/drop", function(source, nodes, copy) {
                    console.log("/dnd/drop", source, nodes, copy);
                });
                dojo.subscribe("/dnd/cancel", function(  ) {
                    console.log("/dnd/cancel");
                });
            });
        </script>
    </head>
    <body>
        <div id="source1" dojoType="dojo.dnd.Source" class="container">
            <div class="dojoDndItem">foo</div>
            <div class="dojoDndItem">bar</div>
            <div class="dojoDndItem">baz</div>
            <div class="dojoDndItem">quux</div>
        </div>
        <div id="source2" dojoType="dojo.dnd.Source" class="container">
            <div class="dojoDndItem">FOO</div>
            <div class="dojoDndItem">BAR</div>
            <div class="dojoDndItem">BAZ</div>
            <div class="dojoDndItem">QUUX</div>
        </div>
    </body>
</html>

All it takes to demonstrate some connections is a different addOnLoad function. Note that because we need to have a reference to the Source that is created (not the DOM node), we need to programmatically create the Source instead of relying on the parser to instantiate widgets that are defined in markup. Substitute the following, turn off djConfig's parseOnLoad flag, and take a look at the Firebug console once again:

dojo.addOnLoad(function(  ) {
        //keep a reference to the Source to use for connecting.
        var s1 = new dojo.dnd.Source("source1");

        dojo.connect(s1, "onDndSourceOver", function(source) {
            console.log("onDndSourceOver for", s1, source);
        });
        dojo.connect(s1, "onDndStart", function(source, nodes, copy) {
            console.log("onDndStart for ", s1, source, nodes, copy);
        });
        dojo.connect(s1, "onDndStop", function(source, nodes, copy, target) {
            console.log("onDndStop for",  s1, source, nodes, copy, target);
        });
        dojo.connect(s1, "onDndCancel", function(  ) {
            console.log("onDndCancel for ", s1);
         });
});

Scripting Droppables

While the previous example demonstrated that you could use the Source constructor function to make a node droppable, there is considerably more functionality you can achieve via scripting. Table 7-6 summarizes the functionality that Selector, a lower level class in dojo.dnd, offers. Because Source inherits from Selector, these functions are directly available to you though Source, although you might very well find uses for Selector in and of itself.

Table 7-6. Selector API

Method

Comment

getSelectedNodes( )

Returns an Array of the selected nodes.

selectNone( )

Deselects all of the nodes.

selectAll( )

Selects all of the nodes.

deleteSelectedNodes( )

Deletes all selected nodes.

insertNodes(/* Boolean */ addSelected, /* Array */ data, /* Boolean */ before, /* Node */ anchor)

Inserts an Array of nodes, optionally allowing them to be selected via addSelected. If no anchor is supplied, nodes are inserted before the first child of the Selector. Otherwise, they are inserted either before or after the anchor node according to the value of before.

destroy( )

Prepares the object for garbage collection.

onMouseDown(/* Object */ event)

Can be connected to via dojo.connect for detecting onmousedown events, although higher-level onDnd methods should first be considered. Parameter event provides standard event info.

onMouseUp(/* Object */ event)

Can be connected to via dojo.connect for detecting onmouseup, although higher-level onDnd methods should first be considered. Parameter event provides standard event info.

onMouseMove(/* Object */ event)

Can be connected to via dojo.connect for detecting mouse motion, although higher-level onDnd methods should first be considered. Parameter event provides standard event info.

onOverEvent(/* Object */ event)

Can be connected to via dojo.connect for detecting when the mouse enters the area, although higher-level onDnd methods should first be considered. Parameter event provides standard event info.

onOutEvent(/* Object */ event)

Can be connected to via dojo.connect for detecting when the mouse leaves the area, although higher-level onDnd methods should first be considered. Parameter event provides standard event info.

Summary

For a really practical example of drag-and-drop at work, be sure to check out ""Drag-and-Drop with the Tree" in Chapter 15, where drag-and-drop is applied to the problem of manipulating the Tree dijit. The Tree is a phenomenal piece of engineering and dnd only makes it better!

After reading this chapter, you should be able to:

  • Construct unconstrained Moveable objects and drag them around on the screen

  • Define constraints for Moveable objects to control their behavior

  • Be able to implement Source and Target containers to create collections of items that can be dragged and dropped to/from/within one another

  • Create custom avatars to communicate with the user when a drop operation is about to occur

  • Use dojo.connect and dojo.subscribe to receive event notifications for drag-and-drop objects

Next, we'll cover animation and special effects.



[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.