Search code examples
rshinydownloadshinyapps

Display a folder structure in shiny app body as a box not a pop-up


I would like to have a box in my shiny app, which user can browse to a folder structure and select a file to download. I have tried the shinyFiles but the file selection is a pop-up window and I could just download a single file :

library(shiny)
library(shinyFiles)


ui <- fluidPage( 
  shinyFilesButton('files', label='File select', title='Please select a file', multiple=T) ,
  verbatimTextOutput('rawInputValue'),
  verbatimTextOutput('filepaths') ,
  downloadButton("downloadFiles", "Download Files")
)

server <- function(input, output) {
  
  roots =  c(wd = 'H:/')
  
  shinyFileChoose(input, 'files', 
                  roots =  roots, 
                  filetypes=c('', 'txt' , 'gz' , 'md5' , 'pdf' , 'fasta' , 'fastq' , 'aln'))
  
  output$rawInputValue <- renderPrint({str(input$files)})
  
  output$filepaths <- renderPrint({parseFilePaths(roots, input$files)})
  
  output$downloadFiles <- downloadHandler(
    filename = function() {
      as.character(parseFilePaths(roots, input$files)$name)
    },
    content = function(file) {
      fullName <- as.character(parseFilePaths(roots, input$files)$datapath)
      file.copy(fullName, file)
    }
  )
}

shinyApp(ui = ui , server = server)

What I would like is to have the file selection option like this

within the ui, not as new (pop-up) window !


Solution

  • Here is a first version of the app I talked about in my comment. Its advantage is that the contents of a folder are loaded only when the user selects this folder, and only the first descendants are loaded, no recursion.

    enter image description here

    App folder structure:

    C:\PATH\TO\MYAPP
    |   global.R
    |   server.R
    |   ui.R
    |
    \---www
            navigator.css
            navigator.js
    

    File global.R:

    library(shiny)
    library(jsTreeR)
    library(htmlwidgets)
    library(magrittr)
    library(shinyFiles)
    
    roots <- c(wd = "C:/SL/MyPackages/", getVolumes()())
    

    File server.R:

    shinyServer(function(input, output, session){
    
      shinyDirChoose(
        input, "rootfolder", roots = roots,
        allowDirCreate = FALSE, defaultRoot = "wd"
      )
    
      RootFolder <- eventReactive(input[["rootfolder"]], {
        parseDirPath(roots, input[["rootfolder"]])
      })
    
      output[["choice"]] <- reactive({
        isTruthy(RootFolder())
      })
      outputOptions(output, "choice", suspendWhenHidden = FALSE)
    
      output[["navigator"]] <- renderJstree({
        req(isTruthy(RootFolder()))
        jstree(
          nodes = list(
            list(
              text = RootFolder(),
              type = "folder",
              children = FALSE,
              li_attr = list(
                class = "jstree-x"
              )
            )
          ),
          types = list(
            folder = list(
              icon = "fa fa-folder gold"
            ),
            file = list(
              icon = "far fa-file red"
            )
          ),
          checkCallback = TRUE,
          theme = "default",
          checkboxes = TRUE,
          search = TRUE,
          selectLeavesOnly = TRUE
        ) %>% onRender("function(el, x){tree = $(el).jstree(true);}")
      })
    
      observeEvent(input[["path"]], {
        lf <- list.files(input[["path"]], full.names = TRUE)
        fi <- file.info(lf, extra_cols = FALSE)
        x <- list(
          elem = as.list(basename(lf)),
          folder = as.list(fi[["isdir"]])
        )
        session$sendCustomMessage("getChildren", x)
      })
    
      Paths <- reactive({
        vapply(
          input[["navigator_selected_paths"]], `[[`,
          character(1L), "path"
        )
      })
    
      output[["selections"]] <- renderPrint({
        cat(Paths(), sep = "\n")
      })
    
      output[["dwnld"]] <- downloadHandler(
        filename = "myfiles.zip",
        content = function(file){
          zip(file, files = Paths())
        }
      )
    
    })
    

    File ui.R:

    shinyUI(fluidPage(
      tags$head(
        tags$link(rel = "stylesheet", href = "navigator.css"),
        tags$script(src = "navigator.js")
      ),
      br(),
      conditionalPanel(
        condition = "!output.choice",
        fluidRow(
          column(
            width = 12,
            shinyDirButton(
              "rootfolder",
              label = "Browse to choose a root folder",
              title = "Choose a folder",
              buttonType = "primary",
              class = "btn-block"
            )
          )
        )
      ),
      conditionalPanel(
        condition = "output.choice",
        style = "display: none;",
        fluidRow(
          column(
            width = 6,
            jstreeOutput("navigator")
          ),
          column(
            width = 6,
            tags$fieldset(
              tags$legend(
                tags$h1("Selections:", style = "float: left;"),
                downloadButton(
                  "dwnld",
                  class = "btn-primary btn-lg",
                  icon = icon("save"),
                  style = "float: right;"
                )
              ),
              verbatimTextOutput("selections")
            )
          )
        )
      )
    ))
    

    File navigator.css:

    .jstree-default .jstree-x.jstree-closed > .jstree-icon.jstree-ocl,
    .jstree-default .jstree-x.jstree-leaf > .jstree-icon.jstree-ocl {
      background-position: -100px -4px;
    }
    
    .red {
      color: red;
    }
    .gold {
      color: gold;
    }
    .jstree-proton {
      font-weight: bold;
    }
    .jstree-anchor {
      font-size: medium;
    }
    

    File navigator.js:

    var tree;
    
    $(document).ready(function () {
      var Children = null;
    
      Shiny.addCustomMessageHandler("getChildren", function (x) {
        Children = x;
      });
    
      $("#navigator").on("click", "li.jstree-x > i", function (e) {
        var $li = $(this).parent();
        if (!$li.hasClass("jstree-x")) {
          alert("that should not happen...");
          return;
        }
        var id = $li.attr("id");
        var node = tree.get_node(id);
        if (tree.is_leaf(node) && node.original.type === "folder") {
          var path = tree.get_path(node, "/");
          Shiny.setInputValue("path", path);
          var interval = setInterval(function () {
            if (Children !== null) {
              clearInterval(interval);
              for (var i = 0; i < Children.elem.length; i++) {
                var isdir = Children.folder[i];
                var newnode = tree.create_node(id, {
                  text: Children.elem[i],
                  type: isdir ? "folder" : "file",
                  children: false,
                  li_attr: isdir ? { class: "jstree-x" } : null
                });
              }
              Children = null;
              setTimeout(function () {
                tree.open_node(id);
              }, 10);
            }
          }, 100);
        }
      });
    });
    

    (I am the author of jsTreeR and I think I will do a Shiny module for this folder navigator and include it in the package.)


    EDIT

    I improved the app and it uses the proton theme now, which looks more pretty to me:

    enter image description here

    To use this app, you first need the updated version of the package:

    remotes::install_github("stla/jsTreeR")
    

    There are some changes in three files:

    • server.R:
    shinyServer(function(input, output, session){
    
      shinyDirChoose(
        input, "rootfolder", roots = roots,
        allowDirCreate = FALSE, defaultRoot = "wd"
      )
    
      RootFolder <- eventReactive(input[["rootfolder"]], {
        parseDirPath(roots, input[["rootfolder"]])
      })
    
      output[["choice"]] <- reactive({
        isTruthy(RootFolder())
      })
      outputOptions(output, "choice", suspendWhenHidden = FALSE)
    
      output[["navigator"]] <- renderJstree({
        req(isTruthy(RootFolder()))
        jstree(
          nodes = list(
            list(
              text = RootFolder(),
              type = "folder",
              children = FALSE,
              li_attr = list(
                class = "jstree-x"
              )
            )
          ),
          types = list(
            folder = list(
              icon = "fa fa-folder gold"
            ),
            file = list(
              icon = "far fa-file red"
            )
          ),
          checkCallback = TRUE,
          theme = "proton",
          checkboxes = TRUE,
          search = TRUE,
          selectLeavesOnly = TRUE
        )
      })
    
      observeEvent(input[["path"]], {
        lf <- list.files(input[["path"]], full.names = TRUE)
        fi <- file.info(lf, extra_cols = FALSE)
        x <- list(
          elem = as.list(basename(lf)),
          folder = as.list(fi[["isdir"]])
        )
        session$sendCustomMessage("getChildren", x)
      })
    
      Paths <- reactive({
        vapply(
          input[["navigator_selected_paths"]], `[[`,
          character(1L), "path"
        )
      })
    
      output[["selections"]] <- renderPrint({
        cat(Paths(), sep = "\n")
      })
    
      output[["dwnld"]] <- downloadHandler(
        filename = "myfiles.zip",
        content = function(file){
          zip(file, files = Paths())
        }
      )
    
    })
    
    • navigator.css:
    .jstree-proton {
      font-weight: bold;
    }
    
    .jstree-anchor {
      font-size: medium;
    }
    
    .jstree-proton .jstree-x.jstree-closed > .jstree-icon.jstree-ocl,
    .jstree-proton .jstree-x.jstree-leaf > .jstree-icon.jstree-ocl {
      background-position: -101px -5px;
    }
    
    .jstree-proton .jstree-checkbox.jstree-checkbox-disabled {
      background-position: -37px -69px;
    }
    
    .red {
      color: red;
    }
    
    .gold {
      color: gold;
    }
    
    • navigator.js:
    $(document).ready(function () {
      var tree;
    
      var Children = null;
    
      Shiny.addCustomMessageHandler("getChildren", function (x) {
        Children = x;
      });
    
      $navigator = $("#navigator");
    
      $navigator.one("ready.jstree", function (e, data) {
        tree = data.instance;
        tree.disable_checkbox("j1_1");
        tree.disable_node("j1_1");
      });
    
      $navigator.on("after_open.jstree", function (e, data) {
        tree.enable_checkbox(data.node);
        tree.enable_node(data.node);
      });
    
      $navigator.on("after_close.jstree", function (e, data) {
        tree.disable_checkbox(data.node);
        tree.disable_node(data.node);
      });
    
      $navigator.on("click", "li.jstree-x > i", function (e) {
        var $li = $(this).parent();
        if (!$li.hasClass("jstree-x")) {
          alert("that should not happen...");
          return;
        }
        var id = $li.attr("id");
        var node = tree.get_node(id);
        if (tree.is_leaf(node) && node.original.type === "folder") {
          var path = tree.get_path(node, "/");
          Shiny.setInputValue("path", path);
          var interval = setInterval(function () {
            if (Children !== null) {
              clearInterval(interval);
              for (var i = 0; i < Children.elem.length; i++) {
                var isdir = Children.folder[i];
                var newnode = tree.create_node(id, {
                  text: Children.elem[i],
                  type: isdir ? "folder" : "file",
                  children: false,
                  li_attr: isdir ? { class: "jstree-x" } : null
                });
                if (isdir) {
                  tree.disable_checkbox(newnode);
                  tree.disable_node(newnode);
                }
              }
              Children = null;
              setTimeout(function () {
                tree.open_node(id);
              }, 10);
            }
          }, 100);
        }
      });
    });
    

    EDIT 2

    The new version of the package provides a Shiny module allowing to conveniently renders such a 'tree navigator' (or even several ones). This is the example given in the package:

    library(shiny)
    library(jsTreeR)
    
    css <- HTML("
      .flexcol {
        display: flex;
        flex-direction: column;
        width: 100%;
        margin: 0;
      }
      .stretch {
        flex-grow: 1;
        height: 1px;
      }
      .bottomright {
        position: fixed;
        bottom: 0;
        right: 15px;
        min-width: calc(50% - 15px);
      }
    ")
    
    ui <- fixedPage(
      tags$head(
        tags$style(css)
      ),
      class = "flexcol",
    
      br(),
    
      fixedRow(
        column(
          width = 6,
          treeNavigatorUI("explorer")
        ),
        column(
          width = 6,
          tags$div(class = "stretch"),
          tags$fieldset(
            class = "bottomright",
            tags$legend(
              tags$h1("Selections:", style = "float: left;"),
              downloadButton(
                "dwnld",
                class = "btn-primary btn-lg",
                style = "float: right;",
                icon  = icon("save")
              )
            ),
            verbatimTextOutput("selections")
          )
        )
      )
    )
    
    server <- function(input, output, session){
    
      Paths <- treeNavigatorServer(
        "explorer", rootFolder = getwd(),
        search = list( # (search in the visited folders only)
          show_only_matches  = TRUE,
          case_sensitive     = TRUE,
          search_leaves_only = TRUE
        )
      )
    
      output[["selections"]] <- renderPrint({
        cat(Paths(), sep = "\n")
      })
    
    }
    
    shinyApp(ui, server)