DnD Target
Implement target
While source delegate has one mandatory method dndModel(), target delegate has two: dndCanDrop(model) and dndDrop(location).
dndCanDrop(model)decides whether this target can be dropped onto.dndDrop(location)is called when drop happened on this target. This is where you should finally mutate the state. This callback will not be called ifdndCanDrop(model)returned falsy result.
dndDrop(location)doesn’t havemodelin the arguments, you get current DnD model fromthis.dnd.model. More explanation onthis.dndafter following code snippet.
export class Container {
// ... add/removeTarget in attached/detached
// ... hook up dndElement in container.html
dndCanDrop(model) {
return model.type === 'moveItem';
}
dndDrop(location) {
const {item} = this.dnd.model;
const {previewElementRect, targetElementRect} = location;
const newLoc = {
x: previewElementRect.x - targetElementRect.x,
y: previewElementRect.y - targetElementRect.y
};
item.x = newLoc.x;
item.y = newLoc.y;
// move the item to end of array, in order to show it above others
const idx = this.items.indexOf(item);
if (idx >= 0) {
this.items.splice(idx, 1);
this.items.push(item);
}
}
}
Injected “dnd” property
As start of a DnD session, DndService got a model from the source element (dndModel()), then use the model to ask (only once) every targets’ dndCanDrop(model), it also injects a special property dnd onto target delegate.
- dnd.isProcessing,
trueduring a DnD session, no matter whether can drop on this target or not. - dnd.canDrop, a boolean, it’s the cached result of
dndCanDrop(model). - dnd.model, the model of the DnD session, no matter whether can drop on this target or not.
- dnd.isHoveringShallowly,
truewhen mouse is hovering directly over target element. - dnd.isHovering,
truewhen mouse is hovering within target element region. - all of above have value
undefinedwhen not in a DnD session.
Only when canDrop is true, the target delegate has a chance of receiving dndDrop(location) callback.
“location” argument
There are few properties in the location argument for dndDrop(location).
- location.mouseStartAt, drag start mouse location
{x, y}(not{left, top}). - location.mouseEndAt, drop end mouse location
{x, y}fordndDrop(), or current mouse location fordndHover(). - location.sourceElementRect, source element location and size
{x, y, width, height}. - location.previewElementRect, preview element location and size
{x, y, width, height}. - location.targetElementRect, target element location and size
{x, y, width, height}.
All x and y are page offset (relative to the HTML body), not client offset (relative to browser current view-port).
Page offset is stabler to use than client offset, especially when there is scrolling or browser zooming.
For convenience, previewElementRect always presents. Even if you turn off the preview (you will see that in customize preview), it still reports location and size as if you were using default preview.
Beware, sourceElementRect is not current location of source element. It is a cached location for the source element at the time when DnD session started.
The reason behind this, is that DndService doesn’t retain the source delegate/element during a DnD session. Even when the source was removed by removeSource(...) during a DnD session, DndService would not care. DndService gets the model, and caches source element location at start of a DnD session, that’s the only time it ever uses that source delegate/element.
In fact,
addSource(),removeSource(),addTarget(),removeTarget()are all allowed any time, within or out of a DnD session.DndServicethrives on dynamic behaviour. This will likely happen in your app without your notice, we will elaborate this topic when examiningdndHover.
With that much code, we got movable boxes.
There is one more thing we want to fix. During dragging a box, the original source box still presents. How to hide the original box while dragging?
Style DnD Source Element
While DnD target delegate got special injected dnd property, DnD source delegate got similar dnd injection onto source delegate (since v1.6.0+).
- dnd.isProcessing,
trueduring a DnD session. - dnd.isStartingSource, a boolean. During a DnD session, it’s
truefor the DnD source that the DnD session was started with, false for every other DnD sources.
Here we use them to identify this box (DnD source) is being dragged.
Hide the element when dragging, so user only sees the preview element during DnD session.
<template>
<require from="./box.css"></require>
<div
ref="dndElement"
class="box"
style.bind="positionCss"
show.bind="!dnd.isStartingSource"
>
${item.name}
</div>
</template>
Note we use
show.bind, notif.bind. Aureliaif.bindadds/removes the element from DOM tree, whileshow.bindsimply toggles cssvisibility. Because we haveref="dndElement"on this DOM node, we really don’t wantif.binddynamically adds/removes it.
In Aurelia, it’s a common practice to not put
refbehindiforrepeat.for.
In addition, you can directly observe two properties directly on dndService instance: isProcessing and model.
Following has same effect as the dnd.isStartingSource showed above. Actually this is the example documented for bcx-aurelia-dnd before v1.6.0.
export class Box {
//...
@computedFrom('dndService.isProcessing', 'dndService.model')
get draggingMe() {
return this.dndService.isProcessing &&
this.dndService.model.item === this.item;
}
}
<template>
<require from="./box.css"></require>
<div
ref="dndElement"
class="box"
style.bind="positionCss"
show.bind="!draggingMe"
>
${item.name}
</div>
</template>
dndService.isProcessinganddndService.modelare exactly same as DnD targetdelegate’sdnd.isProcessinganddnd.model. In fact, you can use them as well in DnD target code.
Now we got the full version of the first example.
Crosstalk
If you have two DnD targets on same screen, and did not design dndCanDrop() defensively, you can actually drag item from one target to the other, that generates cross-talk and unexpected result.
To avoid cross-talk in your app, design dndModel() and dndCanDrop() defensively for target to only respond to the interested model. For instance, if we distinguish the type of two groups of sources in two target area, set type to move-box and move-box2 respectively, we can eliminate crosstalk.
Let’s look at how to turn off preview, use dndHover.