You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
531 lines
15 KiB
Plaintext
531 lines
15 KiB
Plaintext
function DragDrop(def) {
|
|
//|
|
|
//| def here is an object that has following fields:
|
|
//|
|
|
//| def.what = selector [string], defines group of draggable elements.
|
|
//| def.where = selector [string], defines group of target elements where dragables can be dropped.
|
|
//| def.notBefore = selector [string], defines positions where drop is not allowed.
|
|
//| def.notOn = selector [string], defines sub element(s) that cannot trigger D&D, item rlative
|
|
//| def.acceptDrop = function(draggable, target, indexAtTarget), function to be called before the drop, if it is defined and returns true operation ends successfully.
|
|
//| def.acceptDrag = function(draggable), function to be called before the drag starts, if it is defined and returns either #copying or #moving operation starts successfully.
|
|
//| def.arrivedTo = function(draggable, target), function to be called when draggable enters the target.
|
|
//| def.dropped = function(draggable, from), function to be called when draggable dropped on the target.
|
|
//| target is new draggable.parent and 'from' is a previous parent of the draggable.
|
|
//| def.container = parent-selector [string], selector of the nearest parent of def.what elements where DD operation is allowed.
|
|
//| def.easeDrop = function(t, b, c, d) - ease function used for the drop animation, one of Animation.Ease.*** functions.
|
|
//| def.setupPlaceholder = function(placeholderElement) - do something special with created placeholder.
|
|
//| def.animationDuration = milliseconds, duration of "docking" animation
|
|
//| def.before = function(), called before entering DD loop
|
|
//| def.after = function(), called after finshing DD loop
|
|
//| def.autoScroll = true | false , if autoScroll of container is required
|
|
|
|
const MOVE_THRESHOLD = 4dip;
|
|
const STEP_DELAY = 8;
|
|
const PING_THRESHOLD = 400ms;
|
|
const ANI_DURATION = def.animationDuration || 200ms;
|
|
|
|
const AUTO_SCROLL_DELTA = 1; // pixels
|
|
|
|
const dd_marker_color = color(200, 200, 200);
|
|
const dd_marker_width = 2;
|
|
|
|
var dd_x, dd_y;
|
|
var dd_cx, dd_cy; // drag position inside the container
|
|
var dd_op = #moving;
|
|
var dd_source = null; // the draggable
|
|
var dd_target = null; // current target, mouse is over it.
|
|
var dd_targets = def.where instanceof Element?[def.where]: self.selectAll(def.where);
|
|
//var dd_placeholder_src = null;
|
|
//var dd_placeholder_dst = null;
|
|
var dd_container = self; // DD happens inside this element only
|
|
var dd_width, dd_height; // dims of the draggable
|
|
var is_animating;
|
|
var requested_cancel = false;
|
|
var dd_dragging = null;
|
|
var dd_autoScroll = def.autoScroll !== undefined? def.autoScroll: true;
|
|
|
|
var dd_draggable_image = null; // Image instance - holds snapshot of the draggable
|
|
var dd_marker_x1 = null, dd_marker_x2, dd_marker_y1, dd_marker_y2;
|
|
var dd_marker_position; // index of marker position (insertion point)
|
|
|
|
// forward declaration of functions:
|
|
var doDrop;
|
|
var doCancelDrop;
|
|
var doMove;
|
|
//var onMouseHandler;
|
|
|
|
// do cleanup
|
|
function ddFinalize() {
|
|
// clean all this up
|
|
for (var tel in dd_targets)
|
|
tel.state.droptarget = false;
|
|
if (dd_target) dd_target.state.dragover = false;
|
|
//if (dd_placeholder_dst) dd_placeholder_dst.remove();
|
|
//if (dd_placeholder_src) dd_placeholder_src.remove();
|
|
if (dd_source) {
|
|
dd_source.state[dd_op] = false;
|
|
dd_source.state.dragsource = false;
|
|
}
|
|
|
|
// Be polite with the GC:
|
|
dd_target = dd_source = null;
|
|
|
|
if (dd_draggable_image) {
|
|
dd_draggable_image.destroy();
|
|
dd_draggable_image = null;
|
|
}
|
|
dd_container.paintForeground = null;
|
|
dd_marker_x1 = null;
|
|
}
|
|
|
|
function ddPainter(gfx) {
|
|
gfx.save();
|
|
gfx.blendImage(dd_draggable_image, dd_cx, dd_cy, dd_width, dd_height, 0.75);
|
|
gfx.strokeColor(dd_marker_color);
|
|
gfx.strokeWidth(dd_marker_width);
|
|
if (dd_marker_x1 !== null)
|
|
gfx.line(dd_marker_x1, dd_marker_y1, dd_marker_x2, dd_marker_y2);
|
|
gfx.restore();
|
|
}
|
|
|
|
// init-loop-commit:
|
|
function doDD(el, vx, vy) { // DD loop
|
|
// 1) ask initiator about our draggable:
|
|
if (def.acceptDrag) {
|
|
dd_op = def.acceptDrag(el);
|
|
if (dd_op != #copying && dd_op != #moving)
|
|
return false; // not this time, sigh.
|
|
}
|
|
// 1-bis) setup container, if any:
|
|
if (def.container) {
|
|
dd_container = el.parent.selectParent(def.container);
|
|
assert dd_container;
|
|
}
|
|
|
|
// 2) find and mark all allowed targets:
|
|
dd_targets = def.where instanceof Element?[def.where]: self.selectAll(def.where);
|
|
|
|
// sort all dd_targets by depth, so child options can be found before the whole |
|
dd_targets.sort(:e1, e2 {
|
|
function depth(e) {
|
|
var depth = 0;
|
|
do {
|
|
e = e.parent; depth++;
|
|
} while(e.parent);
|
|
return depth;
|
|
}
|
|
var d1 = depth(e1);
|
|
var d2 = depth(e2);
|
|
if (d1 < d2) return 1;
|
|
if (d1 === d2) return 0;
|
|
return -1;
|
|
}
|
|
);
|
|
|
|
assert dd_targets.length > 0;
|
|
for (var tel in dd_targets)
|
|
tel.state.droptarget = true; // to give CSS a chance to highlight them somehow using :drop-target
|
|
|
|
dd_source = el;
|
|
(dd_width, dd_height) = el.box(#dimension, #border);
|
|
|
|
dd_source.state[dd_op] = true;
|
|
|
|
dd_draggable_image = new Image(dd_width, dd_height, dd_source);
|
|
|
|
// 3) setup paintHandler on container
|
|
|
|
doMove(vx, vy);
|
|
|
|
dd_container.paintForeground = ddPainter;
|
|
|
|
// 4) mark the draggable as dragsource...
|
|
|
|
dd_source.state.dragsource = true;
|
|
|
|
if (def.before)
|
|
def.before();
|
|
|
|
// 5) commit screen updates:
|
|
view.update();
|
|
|
|
// 6) DD events until mouse up is received
|
|
requested_cancel = false;
|
|
dd_dragging = el;
|
|
el.capture(#strict);
|
|
|
|
if (!view.doEvent(#untilMouseUp))
|
|
requested_cancel = true;
|
|
|
|
el.capture(false);
|
|
dd_dragging = null;
|
|
|
|
// 7) Loop finished, do either drop or cancel it:
|
|
|
|
if (!requested_cancel && dd_target && dd_source)
|
|
doDrop();
|
|
else if (dd_source)
|
|
doCancelDrop();
|
|
|
|
// 7) run user's finalizer
|
|
if (def.after)
|
|
el.post(def.after);
|
|
|
|
return true;
|
|
}
|
|
|
|
function validPosition(index) {
|
|
//if (!def.notBefore && def.notAfter) return index;
|
|
|
|
if (index < dd_target.length) {
|
|
if (dd_target[index] === dd_source) {
|
|
return null;
|
|
}
|
|
if (dd_target[index] === dd_source.next) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (def.notBefore && index < dd_target.length && dd_target[index].match(def.notBefore)) {
|
|
return null;
|
|
}
|
|
if (def.notAfter && index > 0 && dd_target[index-1].match(def.notAfter)) {
|
|
return null;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
function findPosHorz(vx, vy) {
|
|
var notb = def.notBefore;
|
|
var firstIdx = 0;
|
|
var lastIdx = dd_target.length - 1; /*non inclusive*/
|
|
|
|
if (firstIdx > lastIdx)
|
|
return firstIdx;
|
|
|
|
var i;
|
|
for (i = firstIdx; i <= lastIdx; ++i) {
|
|
var tc = dd_target[i];
|
|
var (x1, y1, x2, y2) = tc.box(#rect, #margin, #view);
|
|
if (vx < ((x1 + x2) / 2)) {
|
|
if (!notb || !tc.match(notb))
|
|
return i;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
function findPosVert(vx, vy) {
|
|
var notb = def.notBefore;
|
|
var firstIdx = 0;
|
|
var lastIdx = dd_target.length - 1; /*non inclusive*/
|
|
|
|
if (firstIdx > lastIdx)
|
|
return firstIdx;
|
|
|
|
var i;
|
|
var tc;
|
|
for (i = firstIdx; i <= lastIdx; ++i) {
|
|
tc = dd_target[i];
|
|
var (x1, y1, x2, y2) = tc.box(#rect, #margin, #view);
|
|
var my = (y1 + y2) / 2;
|
|
if (vy < my)
|
|
return validPosition(i);
|
|
if (vy < y2)
|
|
return validPosition(i+1);
|
|
}
|
|
return tc? validPosition(tc.index + 1): null;
|
|
}
|
|
|
|
function findPosWrap(vx, vy, vert) {
|
|
var notb = def.notBefore;
|
|
|
|
var (tvx, tvy) = dd_target.box(#position, #inner, #view);
|
|
var tc = dd_target.find(vx - tvx, vy - tvy);
|
|
|
|
while(tc && tc.parent !== dd_target)
|
|
tc = tc.parent;
|
|
|
|
if (tc)
|
|
return validPosition(tc.index);
|
|
|
|
return null;
|
|
}
|
|
|
|
doMove = function(vx, vy) {
|
|
// stdout.$n({vx} {vy});
|
|
if (!dd_source) return;
|
|
|
|
var x = vx - dd_x;
|
|
var y = vy - dd_y;
|
|
// move the draggable:
|
|
if (dd_container) {
|
|
var (x1, y1, x2, y2) = dd_container.box(#rect, #inner, #view);
|
|
var (mx1, my1, mx2, my2) = dd_source.box(#rect, #margin, #inner); // actual margin sizes of the draggable
|
|
var (w, h) = dd_source.box(#dimension, #inner); // actual dimensions of the draggable
|
|
// inflate container rect:
|
|
x1 += mx1; x2 -= mx2;
|
|
y1 += my1; y2 -= my2;
|
|
// apply positioning constraints we've got:
|
|
if (x < x1) x = x1; else if (x + w > x2) x = x2 - w + 1;
|
|
if (y < y1) y = y1; else if (y + h > y2) y = y2 - h + 1;
|
|
vy = y; vx = x;
|
|
dd_cx = vx - x1;
|
|
dd_cy = vy - y1;
|
|
}
|
|
|
|
//dd_source.move(x, y, dd_width, dd_height /*, #view, #detached-window*/);
|
|
dd_container.refresh();
|
|
|
|
var found = null;
|
|
for (var tel in dd_targets) {
|
|
var (x1, y1, x2, y2) = tel.box(#rect, #inner, #view);
|
|
if (vx >= x1 && vy >= y1 && vx <= x2 && vy <= y2) {
|
|
found = tel; break;
|
|
}
|
|
}
|
|
//stdout.$n({found.tag});
|
|
if (dd_target !== found) {
|
|
if (dd_target) // we have left it
|
|
dd_target.state.dragover = false; // CSS: :drag-over
|
|
dd_target = found;
|
|
if (dd_target) dd_target.state.dragover = true;
|
|
}
|
|
|
|
if (!dd_target)
|
|
return;
|
|
|
|
// ok, we are on dd_target, find insert position on it
|
|
var flow = dd_target.style#flow;
|
|
var horz = false;
|
|
var pos = 0;
|
|
|
|
switch(flow) {
|
|
case "horizontal-wrap":
|
|
case "horizontal-flow": horz = true; pos = findPosWrap(vx, vy, false); break;
|
|
case "horizontal": horz = true; pos = findPosHorz(vx, vy); break;
|
|
|
|
case "table-body": horz = false; pos = findPosVert(vx, vy); break;
|
|
case "vertical-wrap":
|
|
case "vertical-flow": horz = false; pos = findPosWrap(vx, vy, true); break;
|
|
default: horz = false; pos = findPosVert(vx, vy); break;
|
|
}
|
|
|
|
// check for positions that are not allowed in DD:
|
|
if (typeof pos != #integer) {
|
|
dd_marker_position = null;
|
|
dd_marker_x1 = dd_marker_x2 = dd_marker_y1 = dd_marker_y2 = null;
|
|
return;
|
|
}
|
|
|
|
var after = false;
|
|
var marker;
|
|
if (pos < dd_target.length)
|
|
marker = dd_target[pos];
|
|
else {
|
|
marker = dd_target.last;
|
|
after = true;
|
|
}
|
|
|
|
dd_marker_position = pos;
|
|
|
|
var (x1, y1, x2, y2) = marker.box(#rect, #border, #view);
|
|
var (cx, cy) = dd_container.box(#position, #inner, #view);
|
|
|
|
if (horz) {
|
|
dd_marker_y1 = y1 - cy; dd_marker_y2 = y2 - cy;
|
|
if (after)
|
|
dd_marker_x1 = dd_marker_x2 = x2 - cx;
|
|
else
|
|
dd_marker_x1 = dd_marker_x2 = x1 - cx;
|
|
} else {
|
|
dd_marker_x1 = x1 - cx; dd_marker_x2 = x2 - cx;
|
|
if (after)
|
|
dd_marker_y1 = dd_marker_y2 = y2 - cy;
|
|
else
|
|
dd_marker_y1 = dd_marker_y2 = y1 - cy;
|
|
}
|
|
};
|
|
|
|
function easeOutQuad(t, b, c, d) {
|
|
return -c *(t /= d)*(t-2) + b;
|
|
}
|
|
|
|
function moveIt(whenDone)
|
|
{
|
|
var easef = def.easeDrop || easeOutQuad;
|
|
if (!easef) {
|
|
whenDone(); return;
|
|
} // just return
|
|
|
|
var (fromx, fromy) = (dd_cx, dd_cy);
|
|
var (tox, toy) = (dd_marker_x1, dd_marker_y1);
|
|
|
|
function anim(progress) {
|
|
if (!dd_source || progress >= 1.0) {
|
|
is_animating = false;
|
|
whenDone();
|
|
return false;
|
|
}
|
|
dd_cx = easef(progress, fromx, tox - fromx, 1.0).toInteger();
|
|
dd_cy = easef(progress, fromy, toy - fromy, 1.0).toInteger();
|
|
dd_container.refresh();
|
|
return true;
|
|
}
|
|
is_animating = true;
|
|
dd_container.animate(anim, ANI_DURATION);
|
|
}
|
|
|
|
doDrop = function() {
|
|
assert dd_source && dd_target;
|
|
|
|
if (typeof dd_marker_position != #integer || typeof dd_marker_x1 != #integer)
|
|
doCancelDrop();
|
|
else if (!def.acceptDrop || def.acceptDrop(dd_source, dd_target, dst.index)) {
|
|
moveIt(function() {
|
|
if (dd_source == null) return;
|
|
var from = dd_source.parent;
|
|
dd_target.insert(dd_source, dd_marker_position);
|
|
if (def.dropped) def.dropped(dd_source, from);
|
|
ddFinalize();
|
|
});
|
|
} else doCancelDrop();
|
|
}
|
|
doCancelDrop = function() {
|
|
ddFinalize();
|
|
}
|
|
|
|
function offset(parent, child) {
|
|
var (px, py) = parent.box(#position, #inner, #view);
|
|
var (cx, cy) = child.box(#position, #inner, #view);
|
|
return (cx - px, cy - py);
|
|
}
|
|
|
|
function localCoord(el, evt) {
|
|
var (tx, ty) = el.box(#position, #inner, #view);
|
|
tx = evt.xView - tx;
|
|
ty = evt.yView - ty;
|
|
return (tx, ty);
|
|
}
|
|
|
|
var xViewPos, yViewPos;
|
|
|
|
function ping() {
|
|
var el = view.root.find(xViewPos, yViewPos);
|
|
if (el) {
|
|
el.postEvent("drag-n-drop-ping"); // generate "ping" event in case of UI need to scrolling, etc.
|
|
}
|
|
}
|
|
|
|
function doScroll(direction, distance) {
|
|
if (!def.autoScroll) return;
|
|
var x = dd_container.scroll(#left);
|
|
var y = dd_container.scroll(#top);
|
|
switch(direction) {
|
|
case 8: dd_container.scrollTo(x, y - distance * AUTO_SCROLL_DELTA); break; // top
|
|
case 2: dd_container.scrollTo(x, y + distance * AUTO_SCROLL_DELTA); break; // bottom
|
|
case 4: dd_container.scrollTo(x - distance * AUTO_SCROLL_DELTA, y); break; // left
|
|
case 2: dd_container.scrollTo(x + distance * AUTO_SCROLL_DELTA, y); break; // right
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function distance(x1,y1,x2,y2) {
|
|
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
|
|
}
|
|
|
|
var el_mousedown = null; // mouse down was detected on that element
|
|
|
|
function draggableMouseHandler(evt) {
|
|
switch(evt.type) {
|
|
case Event.MOUSE_DOWN | Event.SINKING:
|
|
if (def.notOn && evt.target.selectParent(def.notOn)) break;
|
|
if (el_mousedown = evt.target.selectParent(def.what)) (dd_x, dd_y) = localCoord(this, evt);
|
|
return false;
|
|
case Event.MOUSE_UP | Event.SINKING:
|
|
dd_x = dd_y = null;
|
|
if (dd_source) {
|
|
this.timer(0, ping);
|
|
return true;
|
|
}
|
|
break;
|
|
// case Event.MOUSE_ENTER | Event.SINKING:
|
|
// case Event.MOUSE_LEAVE | Event.SINKING:
|
|
// if (!dd_source)
|
|
// dd_x = dd_y = null;
|
|
// break;
|
|
|
|
case Event.MOUSE_TICK | Event.SINKING:
|
|
if (dd_source) {
|
|
var (x1, y1, x2, y2) = dd_container.box(#rect, #inner, #view);
|
|
var xv = evt.xView;
|
|
var yv = evt.yView;
|
|
if (yv < y1) dd_container.sendEvent("drag-n-drop-ping", 8) || doScroll(8, y1 - yv); // generate "ping" event in case of UI need to scroll, etc.
|
|
else if (yv > y2) dd_container.sendEvent("drag-n-drop-ping", 2) || doScroll(2, yv - y2); // evt.data -> direction
|
|
else if (xv < x1) dd_container.sendEvent("drag-n-drop-ping", 4) || doScroll(4, x1 - xv);
|
|
else if (xv > x2) dd_container.sendEvent("drag-n-drop-ping", 6) || doScroll(6, xv > x2);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case Event.MOUSE_MOVE | Event.SINKING:
|
|
if (!evt.mainButton)
|
|
return;
|
|
if (is_animating)
|
|
return;
|
|
if (dd_source) {
|
|
xViewPos = evt.xView;
|
|
yViewPos = evt.yView;
|
|
this.timer(PING_THRESHOLD, ping);
|
|
return doMove(xViewPos, yViewPos);
|
|
} else if (el_mousedown && evt.target.belongsTo(el_mousedown, false, true)) {
|
|
var (x, y) = localCoord(this, evt);
|
|
//stdout.$n({dd_x} {dd_y});
|
|
if (distance(dd_x, dd_y, x, y) >= self.toPixels(MOVE_THRESHOLD)) {
|
|
dd_x = x;
|
|
dd_y = y;
|
|
el_mousedown = null;
|
|
doDD(this, evt.xView, evt.yView);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// ready to go, attach onMouseHandler to the draggables
|
|
|
|
function validDraggable(draggable) {
|
|
for (var t in dd_targets)
|
|
if (draggable.belongsTo(t, true, true))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
function mouseEventMonitor(evt) {
|
|
if (!evt.target)
|
|
return false;
|
|
var draggable = evt.target.selectParent(def.what);
|
|
if (draggable && validDraggable(draggable))
|
|
return draggableMouseHandler.call(draggable, evt);
|
|
}
|
|
|
|
function ddCancel() {
|
|
// cancel DD loop
|
|
requested_cancel = true;
|
|
if (dd_dragging)
|
|
dd_dragging.capture(false); // remove capture, stop view.doEvent(#untilMouseUp) loop
|
|
}
|
|
|
|
function ddShutdown() {
|
|
// cancel DD loop and remove traces of this DragDrop call.
|
|
ddCancel();
|
|
self.unsubscribe(mouseEventMonitor);
|
|
}
|
|
|
|
self.subscribe(mouseEventMonitor, Event.MOUSE);
|
|
|
|
return {
|
|
cancel: ddCancel,
|
|
remove: ddShutdown
|
|
};
|
|
} |