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.
RnQ/Distro/Template/ddm.tis

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
};
}