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 havemodel
in the arguments, you get current DnD model fromthis.dnd.model
. More explanation onthis.dnd
after 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,
true
during 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,
true
when mouse is hovering directly over target element. - dnd.isHovering,
true
when mouse is hovering within target element region. - all of above have value
undefined
when 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.DndService
thrives 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 none. But you can observe two properties directly on dndService
instance: isProcessing
and model
.
export class Box {
//...
@computedFrom('dndService.isProcessing', 'dndService.model')
get draggingMe() {
return this.dndService.isProcessing &&
this.dndService.model.item === this.item;
}
}
dndService.isProcessing
anddndService.model
are exactly same as DnD targetdelegate
’sdnd.isProcessing
anddnd.model
. In fact, you can use them as well in DnD target code.
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="!draggingMe"
>
${item.name}
</div>
</template>
Note we use
show.bind
, notif.bind
. Aureliaif.bind
adds/removes the element from DOM tree, whileshow.bind
simply toggles cssvisibility
. Because we haveref="dndElement"
on this DOM node, we really don’t wantif.bind
dynamically adds/removes it.
In Aurelia, it’s a common practice to not put
ref
behindif
orrepeat.for
.
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.