/**
* ### Drag'n'drop plugin
*
* Enables dragging and dropping of nodes in the tree, resulting in a move or copy operations.
*/
/*globals jQuery, define, exports, require, document */
(function (factory) {
"use strict";
if (typeof define === 'function' && define.amd) {
define('jstree.dnd', ['jquery','jstree'], factory);
}
else if(typeof exports === 'object') {
factory(require('jquery'), require('jstree'));
}
else {
factory(jQuery, jQuery.jstree);
}
}(function ($, jstree, undefined) {
"use strict";
if($.jstree.plugins.dnd) { return; }
/**
* stores all defaults for the drag'n'drop plugin
* @name $.jstree.defaults.dnd
* @plugin dnd
*/
$.jstree.defaults.dnd = {
/**
* a boolean indicating if a copy should be possible while dragging (by pressint the meta key or Ctrl). Defaults to `true`.
* @name $.jstree.defaults.dnd.copy
* @plugin dnd
*/
copy : true,
/**
* a number indicating how long a node should remain hovered while dragging to be opened. Defaults to `500`.
* @name $.jstree.defaults.dnd.open_timeout
* @plugin dnd
*/
open_timeout : 500,
/**
* a function invoked each time a node is about to be dragged, invoked in the tree's scope and receives the nodes about to be dragged as an argument (array) - return `false` to prevent dragging
* @name $.jstree.defaults.dnd.is_draggable
* @plugin dnd
*/
is_draggable : true,
/**
* a boolean indicating if checks should constantly be made while the user is dragging the node (as opposed to checking only on drop), default is `true`
* @name $.jstree.defaults.dnd.check_while_dragging
* @plugin dnd
*/
check_while_dragging : true,
/**
* a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is `false`
* @name $.jstree.defaults.dnd.always_copy
* @plugin dnd
*/
always_copy : false,
/**
* when dropping a node "inside", this setting indicates the position the node should go to - it can be an integer or a string: "first" (same as 0) or "last", default is `0`
* @name $.jstree.defaults.dnd.inside_pos
* @plugin dnd
*/
inside_pos : 0
};
// TODO: now check works by checking for each node individually, how about max_children, unique, etc?
// TODO: drop somewhere else - maybe demo only?
$.jstree.plugins.dnd = function (options, parent) {
this.bind = function () {
parent.bind.call(this);
this.element
.on('mousedown.jstree touchstart.jstree', '.jstree-anchor', $.proxy(function (e) {
var obj = this.get_node(e.target),
mlt = this.is_selected(obj) ? this.get_selected().length : 1;
if(obj && obj.id && obj.id !== "#" && (e.which === 1 || e.type === "touchstart") &&
(this.settings.dnd.is_draggable === true || ($.isFunction(this.settings.dnd.is_draggable) && this.settings.dnd.is_draggable.call(this, (mlt > 1 ? this.get_selected(true) : [obj]))))
) {
this.element.trigger('mousedown.jstree');
return $.vakata.dnd.start(e, { 'jstree' : true, 'origin' : this, 'obj' : this.get_node(obj,true), 'nodes' : mlt > 1 ? this.get_selected() : [obj.id] }, '
' + (mlt > 1 ? mlt + ' ' + this.get_string('nodes') : this.get_text(e.currentTarget, true)) + '+
');
}
}, this));
};
};
$(function() {
// bind only once for all instances
var lastmv = false,
laster = false,
opento = false,
marker = $('
').hide().appendTo('body');
$(document)
.bind('dnd_start.vakata', function (e, data) {
lastmv = false;
})
.bind('dnd_move.vakata', function (e, data) {
if(opento) { clearTimeout(opento); }
if(!data.data.jstree) { return; }
// if we are hovering the marker image do nothing (can happen on "inside" drags)
if(data.event.target.id && data.event.target.id === 'jstree-marker') {
return;
}
var ins = $.jstree.reference(data.event.target),
ref = false,
off = false,
rel = false,
l, t, h, p, i, o, ok, t1, t2, op, ps, pr, ip, tm;
// if we are over an instance
if(ins && ins._data && ins._data.dnd) {
marker.attr('class', 'jstree-' + ins.get_theme() + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ));
data.helper
.children().attr('class', 'jstree-' + ins.get_theme() + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ))
.find('.jstree-copy:eq(0)')[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? 'show' : 'hide' ]();
// if are hovering the container itself add a new root node
if( (data.event.target === ins.element[0] || data.event.target === ins.get_container_ul()[0]) && ins.get_container_ul().children().length === 0) {
ok = true;
for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) {
ok = ok && ins.check( (data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)) ) ? "copy_node" : "move_node"), (data.data.origin && data.data.origin !== ins ? data.data.origin.get_node(data.data.nodes[t1]) : data.data.nodes[t1]), '#', 'last', { 'dnd' : true, 'ref' : ins.get_node('#'), 'pos' : 'i', 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) });
if(!ok) { break; }
}
if(ok) {
lastmv = { 'ins' : ins, 'par' : '#', 'pos' : 'last' };
marker.hide();
data.helper.find('.jstree-icon:eq(0)').removeClass('jstree-er').addClass('jstree-ok');
return;
}
}
else {
// if we are hovering a tree node
ref = $(data.event.target).closest('.jstree-anchor');
if(ref && ref.length && ref.parent().is('.jstree-closed, .jstree-open, .jstree-leaf')) {
off = ref.offset();
rel = data.event.pageY - off.top;
h = ref.height();
if(rel < h / 3) {
o = ['b', 'i', 'a'];
}
else if(rel > h - h / 3) {
o = ['a', 'i', 'b'];
}
else {
o = rel > h / 2 ? ['i', 'a', 'b'] : ['i', 'b', 'a'];
}
$.each(o, function (j, v) {
switch(v) {
case 'b':
l = off.left - 6;
t = off.top;
p = ins.get_parent(ref);
i = ref.parent().index();
break;
case 'i':
ip = ins.settings.dnd.inside_pos;
tm = ins.get_node(ref.parent());
l = off.left - 2;
t = off.top + h / 2 + 1;
p = tm.id;
i = ip === 'first' ? 0 : (ip === 'last' ? tm.children.length : Math.min(ip, tm.children.length));
break;
case 'a':
l = off.left - 6;
t = off.top + h;
p = ins.get_parent(ref);
i = ref.parent().index() + 1;
break;
}
/*!
// TODO: moving inside, but the node is not yet loaded?
// the check will work anyway, as when moving the node will be loaded first and checked again
if(v === 'i' && !ins.is_loaded(p)) { }
*/
ok = true;
for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) {
op = data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? "copy_node" : "move_node";
ps = i;
if(op === "move_node" && v === 'a' && (data.data.origin && data.data.origin === ins) && p === ins.get_parent(data.data.nodes[t1])) {
pr = ins.get_node(p);
if(ps > $.inArray(data.data.nodes[t1], pr.children)) {
ps -= 1;
}
}
ok = ok && ( (ins && ins.settings && ins.settings.dnd && ins.settings.dnd.check_while_dragging === false) || ins.check(op, (data.data.origin && data.data.origin !== ins ? data.data.origin.get_node(data.data.nodes[t1]) : data.data.nodes[t1]), p, ps, { 'dnd' : true, 'ref' : ins.get_node(ref.parent()), 'pos' : v, 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) }) );
if(!ok) {
if(ins && ins.last_error) { laster = ins.last_error(); }
break;
}
}
if(ok) {
if(v === 'i' && ref.parent().is('.jstree-closed') && ins.settings.dnd.open_timeout) {
opento = setTimeout((function (x, z) { return function () { x.open_node(z); }; }(ins, ref)), ins.settings.dnd.open_timeout);
}
lastmv = { 'ins' : ins, 'par' : p, 'pos' : v === 'i' && ip === 'last' && i === 0 && !ins.is_loaded(tm) ? 'last' : i };
marker.css({ 'left' : l + 'px', 'top' : t + 'px' }).show();
data.helper.find('.jstree-icon:eq(0)').removeClass('jstree-er').addClass('jstree-ok');
laster = {};
o = true;
return false;
}
});
if(o === true) { return; }
}
}
}
lastmv = false;
data.helper.find('.jstree-icon').removeClass('jstree-ok').addClass('jstree-er');
marker.hide();
})
.bind('dnd_scroll.vakata', function (e, data) {
if(!data.data.jstree) { return; }
marker.hide();
lastmv = false;
data.helper.find('.jstree-icon:eq(0)').removeClass('jstree-ok').addClass('jstree-er');
})
.bind('dnd_stop.vakata', function (e, data) {
if(opento) { clearTimeout(opento); }
if(!data.data.jstree) { return; }
marker.hide();
var i, j, nodes = [];
if(lastmv) {
for(i = 0, j = data.data.nodes.length; i < j; i++) {
nodes[i] = data.data.origin ? data.data.origin.get_node(data.data.nodes[i]) : data.data.nodes[i];
if(data.data.origin) {
nodes[i].instance = data.data.origin;
}
}
lastmv.ins[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? 'copy_node' : 'move_node' ](nodes, lastmv.par, lastmv.pos);
}
else {
i = $(data.event.target).closest('.jstree');
if(i.length && laster && laster.error && laster.error === 'check') {
i = i.jstree(true);
if(i) {
i.settings.core.error.call(this, laster);
}
}
}
})
.bind('keyup keydown', function (e, data) {
data = $.vakata.dnd._get();
if(data.data && data.data.jstree) {
data.helper.find('.jstree-copy:eq(0)')[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (e.metaKey || e.ctrlKey))) ? 'show' : 'hide' ]();
}
});
});
// helpers
(function ($) {
// private variable
var vakata_dnd = {
element : false,
target : false,
is_down : false,
is_drag : false,
helper : false,
helper_w: 0,
data : false,
init_x : 0,
init_y : 0,
scroll_l: 0,
scroll_t: 0,
scroll_e: false,
scroll_i: false,
is_touch: false
};
$.vakata.dnd = {
settings : {
scroll_speed : 10,
scroll_proximity : 20,
helper_left : 5,
helper_top : 10,
threshold : 5,
threshold_touch : 50
},
_trigger : function (event_name, e) {
var data = $.vakata.dnd._get();
data.event = e;
$(document).triggerHandler("dnd_" + event_name + ".vakata", data);
},
_get : function () {
return {
"data" : vakata_dnd.data,
"element" : vakata_dnd.element,
"helper" : vakata_dnd.helper
};
},
_clean : function () {
if(vakata_dnd.helper) { vakata_dnd.helper.remove(); }
if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; }
vakata_dnd = {
element : false,
target : false,
is_down : false,
is_drag : false,
helper : false,
helper_w: 0,
data : false,
init_x : 0,
init_y : 0,
scroll_l: 0,
scroll_t: 0,
scroll_e: false,
scroll_i: false,
is_touch: false
};
$(document).off("mousemove touchmove", $.vakata.dnd.drag);
$(document).off("mouseup touchend", $.vakata.dnd.stop);
},
_scroll : function (init_only) {
if(!vakata_dnd.scroll_e || (!vakata_dnd.scroll_l && !vakata_dnd.scroll_t)) {
if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; }
return false;
}
if(!vakata_dnd.scroll_i) {
vakata_dnd.scroll_i = setInterval($.vakata.dnd._scroll, 100);
return false;
}
if(init_only === true) { return false; }
var i = vakata_dnd.scroll_e.scrollTop(),
j = vakata_dnd.scroll_e.scrollLeft();
vakata_dnd.scroll_e.scrollTop(i + vakata_dnd.scroll_t * $.vakata.dnd.settings.scroll_speed);
vakata_dnd.scroll_e.scrollLeft(j + vakata_dnd.scroll_l * $.vakata.dnd.settings.scroll_speed);
if(i !== vakata_dnd.scroll_e.scrollTop() || j !== vakata_dnd.scroll_e.scrollLeft()) {
/**
* triggered on the document when a drag causes an element to scroll
* @event
* @plugin dnd
* @name dnd_scroll.vakata
* @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
* @param {DOM} element the DOM element being dragged
* @param {jQuery} helper the helper shown next to the mouse
* @param {jQuery} event the element that is scrolling
*/
$.vakata.dnd._trigger("scroll", vakata_dnd.scroll_e);
}
},
start : function (e, data, html) {
if(e.type === "touchstart" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
e.pageX = e.originalEvent.changedTouches[0].pageX;
e.pageY = e.originalEvent.changedTouches[0].pageY;
e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
}
if(vakata_dnd.is_drag) { $.vakata.dnd.stop({}); }
try {
e.currentTarget.unselectable = "on";
e.currentTarget.onselectstart = function() { return false; };
if(e.currentTarget.style) { e.currentTarget.style.MozUserSelect = "none"; }
} catch(ignore) { }
vakata_dnd.init_x = e.pageX;
vakata_dnd.init_y = e.pageY;
vakata_dnd.data = data;
vakata_dnd.is_down = true;
vakata_dnd.element = e.currentTarget;
vakata_dnd.target = e.target;
vakata_dnd.is_touch = e.type === "touchstart";
if(html !== false) {
vakata_dnd.helper = $("").html(html).css({
"display" : "block",
"margin" : "0",
"padding" : "0",
"position" : "absolute",
"top" : "-2000px",
"lineHeight" : "16px",
"zIndex" : "10000"
});
}
$(document).bind("mousemove touchmove", $.vakata.dnd.drag);
$(document).bind("mouseup touchend", $.vakata.dnd.stop);
return false;
},
drag : function (e) {
if(e.type === "touchmove" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
e.pageX = e.originalEvent.changedTouches[0].pageX;
e.pageY = e.originalEvent.changedTouches[0].pageY;
e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
}
if(!vakata_dnd.is_down) { return; }
if(!vakata_dnd.is_drag) {
if(
Math.abs(e.pageX - vakata_dnd.init_x) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold) ||
Math.abs(e.pageY - vakata_dnd.init_y) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold)
) {
if(vakata_dnd.helper) {
vakata_dnd.helper.appendTo("body");
vakata_dnd.helper_w = vakata_dnd.helper.outerWidth();
}
vakata_dnd.is_drag = true;
/**
* triggered on the document when a drag starts
* @event
* @plugin dnd
* @name dnd_start.vakata
* @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
* @param {DOM} element the DOM element being dragged
* @param {jQuery} helper the helper shown next to the mouse
* @param {Object} event the event that caused the start (probably mousemove)
*/
$.vakata.dnd._trigger("start", e);
}
else { return; }
}
var d = false, w = false,
dh = false, wh = false,
dw = false, ww = false,
dt = false, dl = false,
ht = false, hl = false;
vakata_dnd.scroll_t = 0;
vakata_dnd.scroll_l = 0;
vakata_dnd.scroll_e = false;
$($(e.target).parentsUntil("body").addBack().get().reverse())
.filter(function () {
return (/^auto|scroll$/).test($(this).css("overflow")) &&
(this.scrollHeight > this.offsetHeight || this.scrollWidth > this.offsetWidth);
})
.each(function () {
var t = $(this), o = t.offset();
if(this.scrollHeight > this.offsetHeight) {
if(o.top + t.height() - e.pageY < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; }
if(e.pageY - o.top < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; }
}
if(this.scrollWidth > this.offsetWidth) {
if(o.left + t.width() - e.pageX < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; }
if(e.pageX - o.left < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; }
}
if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) {
vakata_dnd.scroll_e = $(this);
return false;
}
});
if(!vakata_dnd.scroll_e) {
d = $(document); w = $(window);
dh = d.height(); wh = w.height();
dw = d.width(); ww = w.width();
dt = d.scrollTop(); dl = d.scrollLeft();
if(dh > wh && e.pageY - dt < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; }
if(dh > wh && wh - (e.pageY - dt) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; }
if(dw > ww && e.pageX - dl < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; }
if(dw > ww && ww - (e.pageX - dl) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; }
if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) {
vakata_dnd.scroll_e = d;
}
}
if(vakata_dnd.scroll_e) { $.vakata.dnd._scroll(true); }
if(vakata_dnd.helper) {
ht = parseInt(e.pageY + $.vakata.dnd.settings.helper_top, 10);
hl = parseInt(e.pageX + $.vakata.dnd.settings.helper_left, 10);
if(dh && ht + 25 > dh) { ht = dh - 50; }
if(dw && hl + vakata_dnd.helper_w > dw) { hl = dw - (vakata_dnd.helper_w + 2); }
vakata_dnd.helper.css({
left : hl + "px",
top : ht + "px"
});
}
/**
* triggered on the document when a drag is in progress
* @event
* @plugin dnd
* @name dnd_move.vakata
* @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
* @param {DOM} element the DOM element being dragged
* @param {jQuery} helper the helper shown next to the mouse
* @param {Object} event the event that caused this to trigger (most likely mousemove)
*/
$.vakata.dnd._trigger("move", e);
return false;
},
stop : function (e) {
if(e.type === "touchend" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
e.pageX = e.originalEvent.changedTouches[0].pageX;
e.pageY = e.originalEvent.changedTouches[0].pageY;
e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
}
if(vakata_dnd.is_drag) {
/**
* triggered on the document when a drag stops (the dragged element is dropped)
* @event
* @plugin dnd
* @name dnd_stop.vakata
* @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
* @param {DOM} element the DOM element being dragged
* @param {jQuery} helper the helper shown next to the mouse
* @param {Object} event the event that caused the stop
*/
$.vakata.dnd._trigger("stop", e);
}
else {
if(e.type === "touchend" && e.target === vakata_dnd.target) {
var to = setTimeout(function () { $(e.target).click(); }, 100);
$(e.target).one('click', function() { if(to) { clearTimeout(to); } });
}
}
$.vakata.dnd._clean();
return false;
}
};
}(jQuery));
// include the dnd plugin by default
// $.jstree.defaults.plugins.push("dnd");
}));