Search code examples
jquerydrag-and-dropfancytree

Fancytree selectmode 3 and drag and drop challenges


I prefer to work with selectmode 3 with Fancytree as I believe this makes for the most intuitive experience for users (where selecting a parent node auto selects the children). However, I noticed that when I use that mode with the dnd5 extension, that the hierarchy of the folders is lost when drag and dropping them (with one exception).

I have set up a codepen to demonstrate the issue. If you select the checkbox next to Folder 4 and drag it to Folder 3, when you drop it, all hierarchy is lost. When selectmode 2 is used, this is not the case. Interestingly, if you do not check the box, but just grab Folder 4 and move it, the hierarchy remains.

I looked at the debugging and the console (specifically at data.otherNodeList) and I observed that when (in selectmode 3) if I check the checkbox, the parent folder node and the children nodes are all treated is individual leaf nodes and in fact, if you examine the folder node it says it has no children. But if (still in selectmode 3) I just grab the folder without checking the checkbox, the child nodes are nested beneath the folder and the folder indicates that it has children.

All that is to say, does anyone know a work around to maintain the hierarchy within a folder that is being moved? I really prefer to stay with selectmode 3 since it is used in other areas of my code for downloading and deleting. Plus, I feel like it is more auto-selection is what an end user expects.

 $("#tree").fancytree({
  checkbox: true,
  selectMode: 3,
  extensions: ["dnd5", "multi"],
  debugLevel: 0,
  source: [
    // Typically we would load using ajax instead...
    { title: "Node 1" },
    { title: "Node 2" },
    {
      title: "Folder 3",
      folder: true,
      expanded: true,
      children: [
        { title: "Node 3.1", key: "id3.1" },
        { title: "Node 3.2" }
      ]
    },
    {
      title: "Folder 4",
      folder: true,
      expanded: true,
      children: [{ title: "Node 4.1" }, { title: "Node 4.2" }]
    }
  ],
  activate: function(event, data) {
    $("#statusLine").text(event.type + ": " + data.node);
  },
  select: function(event, data) {
    $("#statusLine").text(
      event.type + ": " + data.node.isSelected() + " " + data.node
    );
  },
  dnd5: {
    preventVoidMoves: true, // Prevent moving nodes 'before self', etc.
    preventRecursion: true, // Prevent dropping nodes on own descendants
    preventSameParent: false, // Prevent dropping nodes under the same direct parent
    autoExpandMS: 1000,
    multiSource: true,  // drag all selected nodes (plus current node)
    // focusOnClick: true,
    // refreshPositions: true,
    dragStart: function(node, data) {
      // allow dragging `node`:
      data.effectAllowed = "all";
      data.dropEffect = data.dropEffectSuggested;  //"link";
      // data.dropEffect = "move";
      return true;
    },
    // dragDrag: function(node, data) {
    //   data.node.info("dragDrag", data);
    //   data.dropEffect = "copy";
    //   return true;
    // },
    dragEnter: function(node, data) {
      data.node.info("dragEnter", data);
      // data.dropEffect = "link";
      return true;
    },
    dragOver: function(node, data) {
      // data.node.info("dragOver", data);
      data.dropEffect = data.dropEffectSuggested;  //"link";
      return true;
    },
    dragEnd: function(node, data) {
      data.node.info("dragEnd", data);
    },
    dragDrop: function(node, data) {
      // This function MUST be defined to enable dropping of items on the tree.
      //
      // The source data is provided in several formats:
      //   `data.otherNode` (null if it's not a FancytreeNode from the same page)
      //   `data.otherNodeData` (Json object; null if it's not a FancytreeNode)
      //   `data.dataTransfer.getData()`
      //
      // We may access some meta data to decide what to do:
      //   `data.hitMode` ("before", "after", or "over").
      //   `data.dropEffect`, `.effectAllowed`
      //   `data.originalEvent.shiftKey`, ...
      //
      // Example:

      var sourceNodes = data.otherNodeList,
          copyMode = data.dropEffect !== "move";
      console.log("sourceNodes below");
      console.log(sourceNodes);

      if( data.hitMode === "after" ){
        // If node are inserted directly after tagrget node one-by-one,
        // this would reverse them. So we compensate:
        sourceNodes.reverse();
      }
      if( copyMode ) {
        $.each(sourceNodes, function(i, o){
          o.info("copy to " + node + ": " + data.hitMode);
          o.copyTo(node, data.hitMode, function(n){
            delete n.key;
            n.selected = false;
            n.title = "Copy of " + n.title;
          });
        });
      } else {
        $.each(sourceNodes, function(i, o){
          o.info("move to " + node + ": " + data.hitMode);
          o.moveTo(node, data.hitMode);
        });
      }
      node.debug("drop", data);
      node.setExpanded();
    }
  }
});

Solution

  • This code moves all selected nodes as children of the target node:

    var sourceNodes = data.otherNodeList;
    ...
    $.each(sourceNodes, function(i, o){
      o.info("move to " + node + ": " + data.hitMode);
      o.moveTo(node, data.hitMode);
    });
    

    I guess in your case you want to only move the drag source, which is the top node otherNode:

    data.otherNode.moveTo(node, data.hitMode);
    

    If you need to move more then one top node, another option would be to implement the dnd5. multiSource callback and return a modified list only containing those.