diff --git a/ipytree/tree.py b/ipytree/tree.py index 2216c26..64622bd 100644 --- a/ipytree/tree.py +++ b/ipytree/tree.py @@ -86,18 +86,20 @@ class Tree(DOMWidget): nodes = Tuple().tag(trait=Instance(Node), sync=True, **widget_serialization) multiple_selection = Bool(True, read_only=True).tag(sync=True) + drag_and_drop = Bool(True, read_only=True).tag(sync=True) animation = Int(200, read_only=True).tag(sync=True) selected_nodes = Tuple(read_only=True).tag(trait=Instance(Node),sync=True, **widget_serialization) _id = Unicode('#', read_only=True).tag(sync=True) def __init__( - self, nodes=[], multiple_selection=True, animation=200, + self, nodes=[], multiple_selection=True, animation=200, drag_and_drop=True, **kwargs): super(Tree, self).__init__(**kwargs) self.nodes = nodes self.set_trait('multiple_selection', multiple_selection) + self.set_trait('drag_and_drop', drag_and_drop) self.set_trait('animation', animation) def add_node(self, node, position=None): diff --git a/js/lib/tree.js b/js/lib/tree.js index 512f22a..ec3fb4e 100644 --- a/js/lib/tree.js +++ b/js/lib/tree.js @@ -32,6 +32,22 @@ var NodeModel = widgets.WidgetModel.extend({ NodeModel.__super__.initialize.apply(this, arguments); nodesRegistry[this.get('_id')] = this; + }, + + contains: function(node) { + // Recursively search for node + // params: + // node : id of the node to search for + // Returns true if found + if (node == this.get("_id")) return true; + var arr = this.get('nodes'); + for (var i=0; i { + var plugins = ['wholerow'] + if (this.model.get('drag_and_drop')) { + plugins.push('dnd'); + } $(this.el).jstree({ 'core': { check_callback: true, multiple: this.model.get('multiple_selection'), animation: this.model.get('animation'), }, - 'plugins': [ - 'wholerow' - ] + 'plugins': plugins }).on('ready.jstree', () => { this.tree = $(this.el).jstree(true); @@ -320,6 +340,55 @@ var TreeView = widgets.DOMWidgetView.extend({ this.model.set('selected_nodes', selected_nodes); this.model.save_changes(); } + ).bind( + "move_node.jstree", (evt, data) => { + + var new_parent = ((data.parent == "#") ? this.model : nodesRegistry[data.parent]); + var old_parent = ((data.old_parent == "#") ? this.model : nodesRegistry[data.old_parent]); + + var np_children = []; // new children of new_parent + var op_children = []; // new children of old_parent + + var update_np_quietly = false; + // If updating the old_parent's children changes the index of a new ancestor + // of the moved node, then the change event will propagate through the ancestor + // and eventually update the new parent. + // If we then explicitly update the new parent, then it's children will be duplicated + // on the front end, which is bad. + // This happens if the moved node was above a new ancestor. For example, + // | - node1 -> | - node2 + // | - node2 | - node1 + // root.indexof(node2) changes from 1 to 0, so the change event is propagated to node2 + + + // Construct the new list of children for the old_parent + var i = -1; + data.instance._model.data[data.old_parent].children.slice().forEach((id) => { + i++; + op_children.push(nodesRegistry[id]); + + // Once the old index of the moved node has been reached + // it is necessary to start searching for the new parent + // in order to catch the double change event issue. + if (i >= data.old_position) { + if (nodesRegistry[id].contains(data.parent)) { + update_np_quietly = true; + } + } + }); + + // Construct the new list of children for the new_parent + data.instance._model.data[data.parent].children.slice().forEach((id) => { + np_children.push(nodesRegistry[id]); + }); + + // Set and propagate the change events + old_parent.set('nodes', op_children); + old_parent.save_changes(); + + new_parent.set('nodes', np_children, {silent: update_np_quietly}); + new_parent.save_changes(); + } ); },