480 lines
18 KiB
JavaScript
480 lines
18 KiB
JavaScript
/**
|
|
* ### 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 node as an argument - 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
|
|
};
|
|
// 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 touchstart', '.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, 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] }, '<div id="jstree-dnd" class="jstree-' + this.get_theme() + '"><i class="jstree-icon jstree-er"></i>' + (mlt > 1 ? mlt + ' ' + this.get_string('nodes') : this.get_text(e.currentTarget, true)) + '<ins class="jstree-copy" style="display:none;">+</ins></div>');
|
|
}
|
|
}, this));
|
|
};
|
|
};
|
|
|
|
$(function() {
|
|
// bind only once for all instances
|
|
var lastmv = false,
|
|
laster = false,
|
|
opento = false,
|
|
marker = $('<div id="jstree-marker"> </div>').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;
|
|
// if we are over an instance
|
|
if(ins && ins._data && ins._data.dnd) {
|
|
marker.attr('class', 'jstree-' + ins.get_theme());
|
|
data.helper
|
|
.children().attr('class', 'jstree-' + ins.get_theme())
|
|
.find('.jstree-copy:eq(0)')[ data.data.origin && 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.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');
|
|
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('a');
|
|
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 - 5;
|
|
p = ins.get_parent(ref);
|
|
i = ref.parent().index();
|
|
break;
|
|
case 'i':
|
|
l = off.left - 2;
|
|
t = off.top - 5 + h / 2 + 1;
|
|
p = ref.parent();
|
|
i = 0;
|
|
break;
|
|
case 'a':
|
|
l = off.left - 6;
|
|
t = off.top - 5 + 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.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) );
|
|
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' : 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];
|
|
}
|
|
lastmv.ins[ data.data.origin && 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.copy && (e.metaKey || e.ctrlKey) ? 'show' : 'hide' ]();
|
|
}
|
|
});
|
|
});
|
|
|
|
// helpers
|
|
(function ($) {
|
|
$.fn.vakata_reverse = [].reverse;
|
|
// private variable
|
|
var vakata_dnd = {
|
|
element : 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
|
|
};
|
|
$.vakata.dnd = {
|
|
settings : {
|
|
scroll_speed : 10,
|
|
scroll_proximity : 20,
|
|
helper_left : 5,
|
|
helper_top : 10,
|
|
threshold : 5
|
|
},
|
|
_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,
|
|
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
|
|
};
|
|
$(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.targetTouches && e.originalEvent.targetTouches[0]) {
|
|
e.pageX = e.originalEvent.targetTouches[0].pageX;
|
|
e.pageY = e.originalEvent.targetTouches[0].pageY;
|
|
e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[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;
|
|
if(html !== false) {
|
|
vakata_dnd.helper = $("<div id='vakata-dnd'></div>").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.targetTouches && e.originalEvent.targetTouches[0]) {
|
|
e.pageX = e.originalEvent.targetTouches[0].pageX;
|
|
e.pageY = e.originalEvent.targetTouches[0].pageY;
|
|
e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[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.settings.threshold ||
|
|
Math.abs(e.pageY - vakata_dnd.init_y) > $.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().vakata_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);
|
|
},
|
|
stop : function (e) {
|
|
if(e.type === "touchend" && e.originalEvent && e.originalEvent.targetTouches && e.originalEvent.targetTouches[0]) {
|
|
e.pageX = e.originalEvent.targetTouches[0].pageX;
|
|
e.pageY = e.originalEvent.targetTouches[0].pageY;
|
|
e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[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);
|
|
}
|
|
$.vakata.dnd._clean();
|
|
}
|
|
};
|
|
}(jQuery));
|
|
|
|
// include the dnd plugin by default
|
|
// $.jstree.defaults.plugins.push("dnd");
|
|
}));
|