Search code examples
cssrshinyjstreejstreer

How to add sequential numbering to elements dragged into a hierarchy node using jsTreeR?


I am evaluating different options for allowing users to build a hierarchy of a series of mathematical operations, and am trying out different packages such as jsTreeR, shinyTree, shinyDND, sortable, and last resort (giving up on a user-friendly visual approach) rhandsontable. As explained in the related post How to build a drag and drop hierarchical tree with user inputs using shinyTree, jsTreeR, or similar package?

Using the below reproducible code, I'm trying to add sequential numbering to the elements dragged (or copied) into the "Drag here" node from the "Menu" node. Any ideas how to do this? As better explained in the image at the very bottom of this post.

In using the sortable and HTMLwidgets packages for running something similar (drag and drop), I ran the following code to successfully number dragged elements but am unsure how to implement it in jsTreeR:

tags$head(
    tags$style(HTML('
      #dragTo {list-style-type: none;  counter-reset: css-counter 0;}
      #dragTo > div {counter-increment: css-counter 1;}
      #dragTo > div:before {content: counter(css-counter) ". ";}
      ')
    )
  ),

where the "dragTo" id was set up in a series of div() HTML functions:

div(
      div(
        class = ...,
        div(class = "panel-heading", "Drag to here"),
        div(
          class = "...",
          id = "dragTo"
        )
      )
    )

Reproducible code for this question:

library(jsTreeR)
library(shiny)

nodes <- list(
  list(
    text = "Menu",
    state = list(opened = TRUE),
    children = list(
      list(text = "Dog", type = "moveable", state = list(disabled = TRUE)),
      list(text = "Cat", type = "moveable", state = list(disabled = TRUE)),
      list(text = "Rat", type = "moveable", state = list(disabled = TRUE)),
      list(text = "Bat", type = "moveable", state = list(disabled = TRUE))
    )
  ),
  list(text = "Drag here:", type = "target", state = list(opened = TRUE))
)

checkCallback <- JS(
  "function(operation, node, parent, position, more) { console.log(node);",
  "  if(operation === 'copy_node') {",
  "    if(parent.id === '#' || node.parent !== 'j1_1' || parent.type !== 'target') {",
  "      return false;", # prevent moving an item above or below the root
  "    }",               # and moving inside an item except a 'target' item
  "  }",
  "  return true;",      # allow everything else
  "}"
)
  
dnd <- list(
  always_copy = TRUE,
  is_draggable = JS(
    "function(node) {",
    "  return node[0].type === 'moveable';",
    "}"
  )
)

customMenu <- JS(
  "function customMenu(node) {",
  "  var tree = $('#mytree').jstree(true);", 
  "  var items = {",
  "    'delete' : {",
  "      'label'  : 'Delete',",
  "      'action' : function (obj) { tree.delete_node(node); },",
  "      'icon'   : 'glyphicon glyphicon-trash'",
  "     }",
  "  }",
  "  return items;",
  "}")

  
ui <- fluidPage(jstreeOutput("mytree"))  

server <- function(input, output){
  output[["mytree"]] <- renderJstree({
    jstree(
      nodes, 
      dragAndDrop = TRUE, 
      dnd = dnd, 
      checkCallback = checkCallback,
      types = list(moveable = list(), 
                   target = list()),
      contextMenu = list(items = customMenu),
    )
  })

}  

shinyApp(ui, server)

Illustration:

enter image description here


Solution

  • Add this script in the UI:

    script <- '
    $(document).ready(function(){
      $("#mytree").on("copy_node.jstree", function(e, data){
        var instance = data.new_instance;
        var node = data.node;
        var id = node.id;
        var text = node.text;
        var index = $("#"+id).index() + 1;
        instance.rename_node(node, index + ". " + text);
      })
    });
    '
    

    i.e. tags$head(tags$script(HTML(script))) at the beginning of the UI. This works but you have to carefully target the last position; as you can see I did a mistake here:

    enter image description here