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.
567 lines
17 KiB
Plaintext
567 lines
17 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.acceptDrop = function(draggable), 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.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.ignore = ignore children that make element draggable
|
|
//| 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
|
|
//| def.onFinalize = function() - do something in the end
|
|
|
|
const X_THRESHOLD = 5;
|
|
const Y_THRESHOLD = 5;
|
|
const PING_THRESHOLD = 400ms;
|
|
const ANI_DURATION = def.animationDuration || 200ms;
|
|
|
|
var dd_x, dd_y, old_x, old_y;
|
|
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_source_index = 0;
|
|
var dd_placeholder_dst = null;
|
|
var dd_container = null; // DD happens inside this 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 doDrop;
|
|
var doCancelDrop;
|
|
var setupDstPlaceholderAt;
|
|
var setupSrcPlaceholderAt;
|
|
|
|
function ddFinalize() {
|
|
if (def.onFinalize) def.onFinalize();
|
|
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_target = dd_placeholder_src = dd_placeholder_dst = dd_source = null;
|
|
}
|
|
|
|
function doDD(el, tg, vx, vy) {
|
|
// 1) ask initiator about our draggable:
|
|
if (def.acceptDrag) {
|
|
dd_op = def.acceptDrag(el, tg);
|
|
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);
|
|
var (m1, m2, m3, m4) = el.box(#rect, #margin, #border);
|
|
var (p1, p2, p3, p4) = el.box(#rect, #padding, #inner);
|
|
|
|
// 3) create placeholder of the draggable, it will hold its place:
|
|
if (dd_op == #moving)
|
|
dd_placeholder_src = dd_source.tag == "tr"? dd_source.clone(): new Element(dd_source.tag, "");
|
|
else
|
|
dd_placeholder_src = dd_source.clone();
|
|
dd_placeholder_src.@.addClass("placeholder", "src");
|
|
// 3.a) append placeholder to the end of dd_source.parent:
|
|
dd_source.parent.insert(dd_placeholder_src);
|
|
|
|
dd_placeholder_src.style.set {
|
|
width: px(dd_width), height: px(dd_height),
|
|
margin-left: px(m1), margin-top: px(m2), margin-right: px(m3), margin-bottom: px(m4),
|
|
padding-left: px(p1), padding-top: px(p2), padding-right: px(p3), padding-bottom: px(p4)
|
|
};
|
|
|
|
// 3.b) exchange positions of dd_source and dd_placeholder_src so dd_source
|
|
// that we move will always be at the end so it will not conflict with findByPos
|
|
dd_source.swap(dd_placeholder_src);
|
|
dd_source_index = dd_placeholder_src.index;
|
|
// 3.c) call def.setupPlaceholder for dd_placeholder_src so caller can do something special with it.
|
|
if (def.setupPlaceholder)
|
|
def.setupPlaceholder(dd_placeholder_src);
|
|
|
|
// 4) mark the draggable and take it off:
|
|
dd_source.state[dd_op] = true;
|
|
|
|
assert dd_width && dd_height;
|
|
|
|
dd_source.move(vx - dd_x, vy - dd_y, dd_width, dd_height/*, #view, #detached-window*/);
|
|
|
|
// 4a) call user's preparation code
|
|
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 findRowRange(vy) {
|
|
var nrows = dd_target.rows;
|
|
var top = dd_target.box(#top, #inner, #view);
|
|
vy -= top;
|
|
var firstIdx = 0, lastIdx = 0;
|
|
for (var r = 0; r < nrows; ++r) {
|
|
var els = dd_target.row(r);
|
|
firstIdx = els.first.index;
|
|
lastIdx = els.last.index;
|
|
var (ry, rh) = dd_target.rowY(r);
|
|
if (vy < ry + rh)
|
|
break;
|
|
}
|
|
return (firstIdx, lastIdx);
|
|
}
|
|
|
|
function findColRange(vx) {
|
|
var ncols = dd_target.columns;
|
|
var left = dd_target.box(#left, #inner, #view);
|
|
vx -= left;
|
|
var firstIdx = 0, lastIdx = 0;
|
|
for (var c = 0; c < ncols; ++c) {
|
|
var els = dd_target.column(c);
|
|
firstIdx = els.first.index;
|
|
lastIdx = els.last.index;
|
|
var (cx, cw) = dd_target.columnX(c);
|
|
if (vx < cx + cw)
|
|
break;
|
|
}
|
|
return (firstIdx, lastIdx);
|
|
}
|
|
|
|
function findPosHorz(vx, vy, multiRow = false, dir = 0) {
|
|
var notb = def.notBefore;
|
|
var firstIdx = 0;
|
|
var lastIdx = dd_target.length - 1;
|
|
|
|
if (multiRow)
|
|
(firstIdx, lastIdx) = findRowRange(vy);
|
|
|
|
if (dd_target == dd_source.parent) //
|
|
--lastIdx; // exclude current source element
|
|
|
|
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 ((dir > 0 && vx < x1 - (x2 - x1) / 2) || (dir < 0 && vx < x1 + (x2 - x1) / 2)) {
|
|
if (!notb || !tc.match(notb))
|
|
return i;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
function findPosVert(vx, vy, multiCol = false) {
|
|
var notb = def.notBefore;
|
|
var firstIdx = 0;
|
|
var lastIdx = dd_target.length - 1;
|
|
|
|
if (multiCol)
|
|
(firstIdx, lastIdx) = findColRange(vx);
|
|
|
|
if (dd_target == dd_source.parent) //
|
|
--lastIdx; // exclude current source element
|
|
|
|
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 (vy < ((y1 + y2) / 2)) {
|
|
if (!notb || !tc.match(notb))
|
|
return i;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
function validPosition(index) {
|
|
if (!def.notBefore) return index;
|
|
if (index >= dd_target.length) return index;
|
|
if (!dd_target[index].match(def.notBefore)) return index;
|
|
return 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;
|
|
|
|
//var tc = view.root.find(vx, vy);
|
|
|
|
if (tc && tc.parent === dd_target) {
|
|
if (tc.index <= dd_placeholder_src.index || dd_target !== dd_source.parent)
|
|
return validPosition(tc.index);
|
|
else
|
|
return validPosition(tc.index + 1);
|
|
}
|
|
else if (dd_source.parent !== dd_target)
|
|
return dd_target.length;
|
|
else
|
|
return dd_source_index;
|
|
}
|
|
|
|
function doMove(vx, vy) {
|
|
if (!dd_source) return;
|
|
|
|
var dir = 1;
|
|
if (vx < old_x)
|
|
dir = -1;
|
|
old_x = vx;
|
|
|
|
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, #padding, #inner); // actual padding sizes of the draggable
|
|
var (w, h) = dd_source.box(#dimension, #inner); // actual dimensions of the draggable
|
|
// inflate container rect:
|
|
x1 += mx2; x2 -= mx2;
|
|
y1 += my2; y2 -= my1;
|
|
// 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_source.move(x, y, dd_width, dd_height /*, #view, #detached-window*/);
|
|
|
|
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
|
|
if (dd_placeholder_dst) {
|
|
dd_placeholder_dst.detach(); dd_placeholder_dst = null;
|
|
}
|
|
}
|
|
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, false, dir); break;
|
|
case "table-body":
|
|
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)
|
|
return;
|
|
else if (pos >= dd_target.length) { // after last pos
|
|
var tc = dd_target.last;
|
|
if (tc === dd_source) tc = tc.prior;
|
|
if (tc && tc.$is(.placeholder))
|
|
return; // not allowing to insert next to placeholder
|
|
} else {
|
|
var tc = dd_target[pos];
|
|
//if ( tc.$is(.placeholder) || (tc.prior && tc.prior.$is(.placeholder)))
|
|
if (tc.$is(.placeholder))
|
|
return;
|
|
}
|
|
// finally setup it:
|
|
if (dd_source.parent === dd_target) // if elements is moved inside its continer
|
|
setupSrcPlaceholderAt(pos, horz);
|
|
else
|
|
setupDstPlaceholderAt(pos, horz);
|
|
}
|
|
|
|
function easeOutQuad(t, b, c, d) {
|
|
return -c *(t /= d)*(t-2) + b;
|
|
}
|
|
|
|
function moveIt(what, where, whenDone) {
|
|
var easef = def.easeDrop || easeOutQuad;
|
|
if (!easef) {
|
|
whenDone(what, where); return;
|
|
}
|
|
|
|
var (fromx, fromy, fromw, fromh) = what.box(#rectw, #inner, #view);
|
|
var (tox, toy, tow, toh) = where.box(#rectw, #inner, #view);
|
|
var toshift = what.toPixels(where.style#margin-left);
|
|
if (where.index == 0) tox -= toshift;
|
|
|
|
function anim(progress) {
|
|
if (!dd_source || progress >= 1.0) {
|
|
is_animating = false;
|
|
what.move();
|
|
whenDone(what, where);
|
|
return false;
|
|
}
|
|
var x = easef(progress, fromx, tox - fromx, 1.0).toInteger();
|
|
var y = easef(progress, fromy, toy - fromy, 1.0).toInteger();
|
|
var w = easef(progress, fromw, tow - fromw, 1.0).toInteger();
|
|
var h = easef(progress, fromh, toh - fromh, 1.0).toInteger();
|
|
what.move(x, y, w, h);
|
|
return true;
|
|
}
|
|
is_animating = true;
|
|
what.animate(anim, ANI_DURATION);
|
|
}
|
|
|
|
doDrop = function() {
|
|
assert dd_source && dd_target;
|
|
var dst = dd_placeholder_dst || dd_placeholder_src;
|
|
if (!def.acceptDrop || def.acceptDrop(dd_source, dd_target, dst.index)) {
|
|
// OK to drop it here, do it:
|
|
moveIt(dd_source, dst, function() {
|
|
var idx = dst.index;
|
|
|
|
if (dd_source) {
|
|
dd_source.move();
|
|
var from = dd_source.parent;
|
|
dd_target.insert(dd_source, idx); // insert our element in place of dd_placeholder_dst
|
|
|
|
if (dd_placeholder_dst) {
|
|
dd_placeholder_dst.remove(); // delete it from the DOM
|
|
dd_placeholder_dst = null;
|
|
}
|
|
|
|
if (dd_placeholder_src) {
|
|
if (dd_op == #moving)
|
|
dd_placeholder_src.remove();
|
|
else if (dd_op == #copying) {
|
|
// cvt our placeholder to normal moveable thing;
|
|
dd_placeholder_src.@.removeClass("placeholder", "src");
|
|
}
|
|
dd_placeholder_src.style.clear();
|
|
dd_placeholder_src = null;
|
|
}
|
|
if (def.dropped) def.dropped(dd_source, from);
|
|
}
|
|
|
|
ddFinalize();
|
|
}
|
|
);
|
|
} else doCancelDrop();
|
|
}
|
|
doCancelDrop = function() {
|
|
moveIt(dd_source, dd_placeholder_src, function() {
|
|
if (dd_source) {
|
|
dd_source.swap(dd_placeholder_src);
|
|
dd_placeholder_src.remove(); // delete it from the DOM
|
|
dd_placeholder_src = null;
|
|
dd_source.move();
|
|
}
|
|
ddFinalize();
|
|
}
|
|
);
|
|
}
|
|
|
|
setupDstPlaceholderAt = function(pos, horz) {
|
|
if (!dd_placeholder_dst) { // if there was no dd_placeholder_dst before create it:
|
|
dd_placeholder_dst = new Element(dd_source.tag);
|
|
dd_placeholder_dst.@#class = "placeholder dst";
|
|
dd_target.insert(dd_placeholder_dst, pos);
|
|
if (horz) dd_placeholder_dst.style#width = px(dd_source.box(#width, #inner, #self));
|
|
else dd_placeholder_dst.style#height = px(dd_source.box(#height, #inner, #self));
|
|
} else dd_target.insert(dd_placeholder_dst, pos);
|
|
view.update();
|
|
}
|
|
|
|
setupSrcPlaceholderAt = function(pos, horz) {
|
|
dd_target.insert(dd_placeholder_src, pos); // just move dd_placeholder_src here
|
|
view.update();
|
|
// just in case it is inside scrollable container make next/previous element visible
|
|
if (dd_autoScroll) {
|
|
if (dd_placeholder_src.prior)
|
|
dd_placeholder_src.prior.scrollToView(false, false);
|
|
if (dd_placeholder_src.next)
|
|
dd_placeholder_src.next.scrollToView(false, false);
|
|
}
|
|
}
|
|
|
|
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 scroll, etc.
|
|
}
|
|
|
|
function draggableMouseHandler(evt) {
|
|
switch(evt.type) {
|
|
case Event.MOUSE_DOWN | Event.SINKING:
|
|
(dd_x, dd_y) = localCoord(this, evt);
|
|
return false;
|
|
case Event.MOUSE_UP | Event.SINKING:
|
|
dd_x = dd_y = null;
|
|
this.timer(0, ping);
|
|
return false;
|
|
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:
|
|
// stdout.println("Event.MOUSE_TICK");
|
|
// 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 (typeof dd_x == #integer) {
|
|
var (x, y) = localCoord(this, evt);
|
|
var deltax = dd_x - x;
|
|
var deltay = dd_y - y;
|
|
if (deltax < -X_THRESHOLD || deltax > X_THRESHOLD ||
|
|
deltay < -Y_THRESHOLD || deltay > Y_THRESHOLD) {
|
|
dd_x = x;
|
|
dd_y = y;
|
|
doDD(this, evt.target, evt.xView, evt.yView);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
var ignores = draggable.selectAll(def.ignore);
|
|
if (ignores.indexOf(evt.target) === -1)
|
|
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
|
|
};
|
|
} |