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.
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.
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 |
---|---|
| The constructor
function that identifies the node that should become
moveable.
|
| 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>
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 |
---|---|
| Called before every move. |
| Called after every move. |
| Called during the very first move; handy for performing initialization routines. |
| Called during every
move notification; by default, calls |
| Called just before
|
| Called just after
|
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.
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.