Drag and Drop: A Design Specification
I. A High-Level “Talk-Through” of the System
DragEntity
A DragEntity is the most basic utility object in the drag and drop system. The simplest implementation of drag/drop is to create a new DragEntity:
dde = new DragEntity(node)
As such, the DragEntity will now listen to onMousedown events, it will begin drag-moving when appropriate, and it will be draggable anywhere in the document. When dropped it will remain where it was placed, ready for future drag/drop sequences.
Every DragEntity has methods and properties it receives from DragObject and DragTrigger. DragObject provides many of the core methods and properties used throughout drag/drop, and DragTrigger provides the functionality which listens for the start of drag operations.
Because our DragEntity was created without passing in any group membership information, it gets assigned to the default group. Another member of the default group is dojo.body(), which has a DragDropTarget automatically assigned to it through addOnLoad. Hence, any DragEntity created without group memberships can be dragged anywhere inside the body at any time. (More on group memberships later.)
DragTrigger
DragEntity offers full control over when and how drag begins. This is because each DragEntity has a DragTrigger object assigned to it. DragTriggers are responsible for listening to mouse events and determining when a drag is appropriate. Drag is started when the user has either moved a minimum number of pixels with the mouse button held down, or when the user has held the mouse button down for a minimum amount of time while within the trigger region. Both are configurable. By default, the user needs to move 3 pixels or hold the mouse button down 1 second before drag begins. Other types of objects will be introduced later which also use DragTrigger, and it is easily reusable as the trigger component for custom drag classes.
DragSelect
A DragEntity can also take advantage of different states of selection through its DragSelect object. Though not selectable by default, any entity can very easily be defined to be selectable, and its appearance will be altered subtly to reflect its selected state. Selection and drag are not mutually-exclusive occurrences. Selection is by design easier to invoke, because a click or a shift-click can happen more quickly than it usually takes for a default-configuration drag event to be recognized. Therefore, it’s trivial if desired to have objects which always get selection state immediately prior to drag activity. Picture the operating system model for selection and movement of “folders”; this typifies the basic relationship between selection and drag in the drag/drop system.
Controllable options for selection state can include being multi-selectable and being toggle-like in their deselectability. Mutually exclusive collections of selectable objects can also be defined. Multi-selectable entities can be dragged together, which affords complex application drag functionality.
DragResizer
There are times when we want to interpret the user’s drag actions as resizing instructions instead of movement instructions. For this we use a sibling class to DragEntity called DragResizer. DragResizer is almost identical in structure to DragEntity, but it differs in two key ways. First, its dragging logic resizes instead of moves, which is to be expected. And second, the node on which the listener is bound is often not the same node upon which the resizing occurs. Picture an iconic “drag chip” like that one found in the bottom-right corner of Mac OS X window frames, and you have the basic model: the DragResizer’s listener node is comprised only of the small chip, but the node it is resizing is the larger window frame. The direction of resizing can be set to either VERTICAL or HORIZONTAL, and a special value called SCALING will resize both the horizontal and the vertical together, keeping the initial aspect ration of the resizable node fixed.
More Sophisticated Drag Control
As was just mentioned, DragEntity is almost identical in structure to DragResizer. As such, we might also want to have one node for listening to drag events while moving another node. This is how we would implement the “titlebar” functionality present in most windowing systems. The DragEntity’s listener would be bound to a thin, wide block at the top of the window frame, and the window frame itself would be the target of movement. Because the movement handle node is a child contained within the frame it moves faithfully in tandem with it.
Continuing with the example of how windows can be implemented, titlebar buttons can be achieved by adding clickable buttons as children. The only real requirement to making them work as expected is that the onMousedown for the buttons needs to be ignored by the DragTrigger on the containing titlebar, because we aren’t expecting that a user will want to drag a window frame at the same time they are expanding it or closing it. Our instinct would be to simply stop the propagation of the event bubbling for these constructs. But in reality that may prove limiting, since something outside the parent titlebar or grandparent window frame may be interested in the event. So instead of brute force cancellation of the event, we simply invalidate the onMousedown event for drag purposes by calling DragDropMgr.invalidateDragEvent(e) on the event inside our button element event handler code. The event will continue to bubble through the event system, but it will be tainted for drag purposes and ignored. (We jumped ahead in mentioning eventing and the DragDropMgr: both are discussed later.)
Customization of Drag Appearance
Customizing the appearance of the object being dragged is not difficult. By default the DragEntity’s node itself is dragged. But DragEntity defines both an avatarModel and a getAvatarNode() method which gets called at the time dragging begins. By default, avatarModel is null, and the default logic for getAvatarNode() is usually just to return the dragNode for the entity, after first ensuring that the node is absolutely positioned. Appearance can be customized just by providing a DOM node reference to avatarModel. Once assigned, any need to display the entity’s avatar will result in this node being cloned, added to the document, made visible if necessary, and properly prepared for movement. The original node can be hidden at the time of avatar display, if desired. For further customization of avatars, the getAvatarNode() method of any instance can be reassigned to one of the other pre-defined avatar provider methods, allowing from a range of canned avatar functionality. It can also be completely customized by redefining the method or by intercepting it using AOP.
When a drag operation “drops” successfully, the default behavior is to reposition the original dragNode to the new location and to destroy any avatarNode, if needed. However, the DragEntity provides easy hooks to customize what exactly gets rendered in the new location. Much like the paired functionality of avatarModel and getAvatarNode(), the DragEntity exposes a dropElementModel and insertDropElementNode() property/method pair that makes simple modifications simple and still allows for highly customized appearance transitions.
Implicit Parent-Child Relationships
It is perfectly legal for a one draggable entity to contain another one. Enabling this in the drag/drop system needs to be as simple and foolproof as possible. If this behavior were always modeled by using DOM parent-child relationships, life would be simple. But this is often not the case. Sometimes a “child” drag entity is actually implemented as an absolutely positioned node which is higher in z-index and visually “sitting on top” of the “parent” drag entity, yet in the DOM there is no ancestral relationship which can be inferred between the two. The drag/drop system has robust computational logic for determining z-index containment and intersection even when this state cannot be gleaned from DOM tree relationships. The drag/drop system uses an object’s positioning relative to the viewport to also determine how “stacked” elements should interrelate.
Each time a DragEntity is created, moved, resized, or retired, the DOM is compared against all known entities to see how the relationships between them have changed. By default, moving a parent-like entity which contains child-like entity nodes will cause the implied children to move in tandem, relative to the changes of the parent. But flexibility requires that parent-child relationships be malleable, and simplicity requires them to be auto-configured. Hence any time a drag event occurs, the parent-child relationships of all drag/drop systems objects within the event horizon are re-examined and adjusted as needed. (If it sounds like There Be Dragons Here, yes in fact there be. But they are known dragons, and they can be made to cooperate efficiently.)
DragFactory
Dragging an object from one place to another is not the only use case for drag/drop. Let’s say instead we want to implement a WYSIWYG form designer. The concept is to provide a palette where an icon “chip” representing a form control can be dragged onto a workspace, thereby creating a new instance of the form control. The original chip in the palette remains unchanged, because we are expecting to be able to reuse it at will, and so with every drag operation, we are in reality creating a new draggable object. And because the new object is by design a virtual representation of a form control, we also expect that newly spawned object to remain draggable.
This use case calls for an object called a DragFactory. A DragFactory doesn’t actually get dragged; all it does is listen for drag events being handled by its DragTrigger. When drag is initiated it creates a new DragEntity on the fly and reassigns drag control to this new entity. A DragFactory is quite different from a DragEntity or a DragResizer, because it never actually does any node manipulation — all it does is create new instances of other object which themselves can be moved. Customizing a DragFactory to create pre-configured DragEntity objects is simple: the DragFactory has a modelEntity property which is actually a full prototype object whose constructor is DragEntity. All the DragFactory needs to do at drag time is call “new this.modelEntity()” within its scope, forge a relationship between the global drag handler and the newly created entity, and its work is done.
DragDropTarget
Some nodes will respond differently to drag-time events than others. It may be legal to drag a “kayak” into a “lake” or onto a “car”, but illegal to drop it onto a “wife”. For modeling this type of behavior we rely on the DragDropTarget. By default a DragDropTarget provides a legal destination for any DragEntity in its group (more on groups later). When a DragEntity is over a compatible DragDropTarget, it shows a cursor suggesting “droppability” and any subsequent drops follow a path of positive resolution. This is because the default targetType for any given DragDropTarget is VALID. But this target can easily be made invalid by specifying INVALID for its targetType. This will cause a different “no-drop” cursor to be displayed for that target when a compatible DragEntity passes over it, and any attempt to drop the entity on the target will proceed down a codepath of negative resolution. (Glossing over some details here, but hopefully you get the point.) And a DragDropTarget can also be made inert, simulating the same behavior it would if it weren’t even a target at all, by specifying INERT for the targetType.
DragScroller
In some applications, there is often a usability need during drag operations to scroll underlying objects. In the browser bookmark manager for instance, when dragging a bookmark from one place to another in a list which is taller than the viewport, there are usually hover targets at the top and the bottom of the clipped list which scroll the list up and down, respectively. This functionality is encapsulated by the DragScroller object.
The DragScroller is simple. It has a node (or list of nodes) it will listen to, it has a node (or list of nodes) it will scroll, and it has a pre-configured direction it will scroll in. Any time a compatible DragEntity hovers over the DragScroller’s nodes during a drag/drop operation, the DragScroller will scroll the nodes under its control, moving them in the desired direction at a comfortable rate. A DragScroller can be instructed to scroll horizontally to either the left or the right and vertically either up or down. Ergo, a diagonal scroller can be created. But a scroller which moves in opposite directions at the same time cannot. The scroll rate is configurable. And the rate of scroll can either be uniform, or the scroller can be instructed to vary the rate of scroll on a linear scale based upon how far “into” the hover element the DragEntity has been positioned. Rates for delay before scroll are also adjustable.
DragGroup
In a more complex use case, not every DragEntity should be draggable everywhere throughout the dojo.body(). The best way to customize what can be dragged where is by taking advantage of DragGroup functionality. Simply described, a DragGroup is a named collection of drag/drop objects which are free to interoperate among themselves, but they are not allowed to interoperate with members which are not in their group. DragGroups essentially sub-divide the drag/drop system into namespaced areas of functionality. A drag/drop system object can belong to more than one group.
DragGroups are implicitly, dynamically, and lazily created upon first mention of a group by name. Group names can be set on an object instance when the object is constructed, or group names can be set later by method call. DragGroup objects are also dynamically destroyed when the last remaining member of the group is unassigned. (The only exception to this rule is the default group: it is never destroyed, and dojo.body() is always a member. The default group can be disabled if desired, but there is no practical difference between disabling the default group, assigning the targetType of dojo.body() to be INERT, or simply not using it.)
For most applications, it is not necessary to manipulate DragGroups directly. Every object constructor in the drag/drop system has an argument for assigning groupMembership at construction time. Objects in a group act in concordance without explicit instruction. A DragEntity for group “plover” will automatically be made aware of any DragDropTarget for “plover”. Drag/drop objects in different groups are essentially invisible to each other, unless and until there is an intersection in their memberships. The DragGroup objects do provide convenience, however. The simplest way to enable and disable the operations for an entire group is through control of its DragGroup object.
Objects which do not specify groupMembership are assigned to the default group (the one which has dojo.body() as a DragDropTarget). Explicit assigning of an object to the default group can be achieved by passing the DragGroup.DEFAULT_GROUP constant as the name of the group.
DragGroup and Widgets
The DragGroup object framework is aware of the distinction between custom user drag functionality and that which is being used by the natively provided dojo widgets. By default, groups being used by widgets are not visible to custom user code. It will not be easy for users to accidentally conflict with widget-specific drag/drop functionality. And though it is still possible, it will require explicit intervention. For example, a call to the static class method DragGroup.getGroups() will not return system widget groups by default, it will only return groups named by the user. Getting access to widget groups will require passing a flag.
DragDropGrid
In some applications it is useful to force dropped objects to be pixel-positioned at precise mathematical intervals. This “snap-to-grid” behavior is supported by the DragDropGrid class of objects. A DragDropGrid defines a gridNode, and each instance of DragDropGrid will automatically inform any DragDropTarget objects contained within this node that their drop event locations must conform to the specifications of the grid. The relationships between DragDropGrid objects and DragDropTarget objects is maintained automatically, and relative changes in positioning between the objects causes these relationships to be re-evaluated. Grid size is configurable, and any positive integer is a legal value. (Any value of one or less causes the grid behavior to become inactive.) A transparent grid pattern appears by default for many common values of grid size, and it is easy to create and implement new custom background patterns. Disabling the visual grid altogether is also supported. The grid behavior can be toggled on and off using the enable() and disable() methods of the DragDropGrid instance.
The Event Lifecycle
It is important to discuss the event lifecycle of drag/drop. A drag operation begins with onMousedown. If an entity is selectable and eligible then an onSelect occurs. (A second mousedown can trigger an onDeselect depending upon the settings for the entity.) When the minimum amount of mouse movement or time is satisfied, an onStartDrag occurs, and we are off to the races. (And when less than the minimum occurs an onAbortDrag occurs.) Once we are dragging, every mouse movement reported by the browser creates an onDrag occurrence. When a compatible and eligible target is entered by the drag entity, an onDragEnter occurs. During all mouse movements within this target an onDragOver is reported, and this sequence is bookended by a onDragOut event when the entity exits. If the user tries to drop on an invalid target an onInvalidDrop occurs. And a drop on a valid target generates an onValidDrop event. All drag sequences which began with an onStartDrag will also end with an onEndDrag. And finally, to complete the event cycle an onMouseUp is reported.
For more extensive customizations, event listeners can easily be connected between node and controller object for onMouseover, onMouseout, and any other standard DOM events.
onMouseDown, onSelect, onDeselect, onStartDrag, onAbortDrag, onValidDrop, onInvalidDrop, onEndDrag, and onMouseUp are all unique within a drag sequence: they will never occur more than once each, and some of them are mutually exclusive.
onDragEnter and onDragOut will fire once each for each time a valid drop target is passed over, and so they can occur numerous times in the same drag sequence.
In contrast, onDragOver and onDrag are both extremely prolific. They will both fire at a rate in direct proportion to amount of movement made by the mouse. Any code executed in response to these events should run as briefly as possible; optimizations are critical here. (In its design, the drag/drop system will be throttling calls through these codepaths at a reasonable and adjustable rate, so some sanity-checking is already in place. But the takeaway here is that keeping ops in these loops to a minimum is critical.)
DragDropMgr
There remains one unmentioned object in the drag/drop system: the manager object called DragDropMgr. This object is a singleton, and it holds references to many of the object definition internals as well as most of the critical state data for drag/drop. DragDropMgr can be queried to see what the current state of drag/drop is, what object if any is being manipulated, what groups have been defined, which groups are active and inactive, and much more. This manager object is also responsible for the actual movement of any entity from one position to another. And because the mathematics surrounding detecting which node is over which other node is not a simple operation in the node hierarchy of the document, the DragDropMgr is also responsible for pre-calculating and optimizing the computations required to make this operation performant.
Odds and Ends
Customizing the cursor which are displayed for the various objects in the drag/drop system is easy. They can be assigned at the class prototype level, or they can be set on the instances themselves.
II. Object Class Overview
In Progress….
martin said,
May 11, 2007 at 7:08 am
I’m trying to get one div to snap to another div like menus do in photoshop as an example and split apart. Do you have any suggestions on how you might do this or any examples of pieces I can play with?
Any help would be much appreciated as there isn’t many docs on dojo.
Best Regards
Martin
tudor_in_britishcolumbia@hotmail.com