rshinyshinyjsshinyjqui

Implement drag-and-drop functionality using an observeEvent triggered by an actionButton in shinyjqui


This is a follow-up question to this Drag and drop with shinyjqui to a grid-table

This is the code:

library(shiny)
library(shinyjqui)

connections <- paste0("droppable_cell_", 1:7) # id of the grid cells

ui <- fluidPage(
    tags$head(tags$script(
        JS(
            "
                       $(function() {
                           $('[id^=droppable_cell]').sortable({
                               connectWith: '#letters',
                               drop: function(event, ui) {
                                         $(this).append(ui.draggable);
                                     }
                           })
                        });
                        "
        )
    ),
    # some styling
    tags$style(
        HTML(
            "
            .grid-table {
                width: 150px;
                border-collapse: collapse;
            }
            .grid-cell {
                width: 100%;
                height: 50px;
                border: 1px solid black;
                background-color: white;
                text-align: center;
                margin: 0;
                padding: 5px;
            }
            .grid-cell-text {
                display: flex;
                align-items: center;
                justify-content: center;
                height: 100%;
                background-color: steelblue;
                color: white;
                font-size: 18px;
            }
            .droppable-cell {
                background-color: lightgray;
            }
            .table-container {
                display: flex;
                position: absolute;
                left: 550px;
                top: 30px;
                margin-top: 0px;
                overflow: hidden;
            }
            "
        )
    )),
    
    div(
        class = "table-container",
        div(
            class = "grid-table",
            id = "my_grid",
            div(
                class = "grid-row",
                div(class = "grid-cell grid-cell-text", "my_grid"),
                div(id = "droppable_cell_1", class = "grid-cell droppable-cell", ""),
                div(id = "droppable_cell_2", class = "grid-cell droppable-cell", ""),
                div(id = "droppable_cell_3", class = "grid-cell droppable-cell", ""),
                div(id = "droppable_cell_4", class = "grid-cell droppable-cell", ""),
                div(id = "droppable_cell_5", class = "grid-cell droppable-cell", ""),
                div(id = "droppable_cell_6", class = "grid-cell droppable-cell", ""),
                div(id = "droppable_cell_7", class = "grid-cell droppable-cell", "")
            )
        ),
        
        orderInput('letters', 'Letters', items = LETTERS[1:7],
                   connect = connections) # defined above
    )
)

server <- function(input, output, session) {
    
}

shinyApp(ui, server)

I am attempting to implement drag-and-drop functionality using an observeEvent triggered by an actionButton. My objective is to enable dragging and dropping of a vector, let's say vec <- c(A, B, C), onto the my_grid table through a button click.

Context: The underlying concept involves preselecting and positioning items in specific locations within the my_grid. It's important to note that when items are moved to the my_grid, they should also vanish from the letters section.


Solution

  • As discussed in the comments, we define two suggestions for the placement of the letters at the beginning, e.g. here as given by you

    connections <- paste0("droppable_cell_", 1:7) # id of the grid cells
    
    vec_suggestion1 <- c("A", NA, "G", NA, "B", "C", "D")
    vec_suggestion2 <- c("A", "B", "C", "D", "E", "F", "G")
    
    df <- data.frame(
        connections = connections,
        vec_suggestion1 = vec_suggestion1,
        vec_suggestion2 = vec_suggestion2
    )
    

    , and we would like to implement three buttons. Two of them are used for filling the grid as given by the two suggestions and one of them shall have a functionality for resetting the Drag and Drop. Also, the original Drag and Drop functionality has to stay as it is. This should look like this:

    enter image description here

    We use a shinyjs approach here which adds some custom JS functions to the buttons. I explain the important changes in the following and include the full minimal working example at the end.

    The buttons are defined as usual inside the ui, e.g.

    actionButton("btn_suggestion1", "Suggestion 1")
    

    Inside the server, we use

    shinyjs::js$pageCol(df)
        
    observeEvent(input$btn_suggestion1, {
        shinyjs::disable("btn_suggestion1")
        shinyjs::js$setSuggestion(1)
        shinyjs::enable("btn_suggestion1")
    })
    

    As requested by you, the observeEvent is triggered by the button and will basically call a function setSuggestion with parameter 1. The 1 is the column index of the choices in df (since JS indices start with 0). The function is defined inside pageCol, which is the important part here. pageCol gets df as a parameter and contains

    1. The original definition of the sortable as given in the question.
    2. Some variable declarations which are needed for the buttons.
      var dataArray = Object.values(params[0]);
      dataArray = dataArray[0].map((col, i) => dataArray.map(row => row[i]));
      
      var cacheLetters = $('#letters').html();
      var cacheGridCells = $('[id^=droppable_cell]').html();
    

    dataArray is just df converted into JS, and we transpose it for convenience. cacheLetters and cacheGridCells are used for saving the original state of the letters and the grid (this will be used for the reset button later).

    1. The setSuggestion function
      shinyjs.setSuggestion = function (idxSuggestion) {
        
        // loop over the array rows
        $.each(dataArray, function (index, value) {
    
            // define the selector for the grid cell using the first array column
            var cellSelector = '#' + dataArray[index][0];
    
            // define the new innerHTML of the grid cell such that it will
            // contain the shinyjqui sortable element
            var cellHTML = '<div data-value=\"' 
                            + dataArray[index][idxSuggestion] 
                            + '\" class=\"btn btn-default ui-sortable-handle\" style=\"margin: 1px;\" jqui_sortable_idx=\"letters__' 
                            + (index + 1).toString() 
                            + '\">' 
                            + dataArray[index][idxSuggestion] 
                            + '</div>';
            
            // if the current value is na, next
            if (dataArray[index][idxSuggestion] === null) {
                return true;
            }
            
            // change the innerHTML of the grid cell such that it gets the letter attached
            $(cellSelector).html(cellHTML);
            
            // drop the current letter from the original list
            $('#letters').find(`[data-value='${dataArray[index][idxSuggestion]}']`)[0].remove()
        })
      }
    
    1. The resetDnD function.
      shinyjs.resetDnD = function (params){
        $('#letters').html(cacheLetters).sortable('refresh');
        $('[id^=droppable_cell]').html(cacheGridCells).sortable('refresh');
      }
    

    It just refreshes the letters and the droppable cells using the initially cached HTML of the elements.

    Here is the complete example:

    library(shiny)
    library(shinyjqui)
    library(shinyjs)
    
    connections <- paste0("droppable_cell_", 1:7) # id of the grid cells
    
    vec_suggestion1 <- c("A", NA, "G", NA, "B", "C", "D")
    vec_suggestion2 <- c("A", "B", "C", "D", "E", "F", "G")
    
    df <- data.frame(
        connections = connections,
        vec_suggestion1 = vec_suggestion1,
        vec_suggestion2 = vec_suggestion2
    )
    
    js <- "shinyjs.pageCol = function(params){
    
      $('[id^=droppable_cell]').sortable({
         connectWith: '#letters',
         drop: function(event, ui) {
                   $(this).append(ui.draggable);
                }
      })
      
      var dataArray = Object.values(params[0]);
      dataArray = dataArray[0].map((col, i) => dataArray.map(row => row[i]));
      
      var cacheLetters = $('#letters').html();
      var cacheGridCells = $('[id^=droppable_cell]').html();
      
      shinyjs.setSuggestion = function (idxSuggestion) {
        
        $.each(dataArray, function (index, value) {
            var cellSelector = '#' + dataArray[index][0];
            var cellHTML = '<div data-value=\"' 
                            + dataArray[index][idxSuggestion] 
                            + '\" class=\"btn btn-default ui-sortable-handle\" style=\"margin: 1px;\" jqui_sortable_idx=\"letters__' 
                            + (index + 1).toString() 
                            + '\">' 
                            + dataArray[index][idxSuggestion] 
                            + '</div>';
            
            if (dataArray[index][idxSuggestion] === null) {
                return true;
            }
            
            $(cellSelector).html(cellHTML);
            
            $('#letters').find(`[data-value='${dataArray[index][idxSuggestion]}']`)[0].remove()
        })
      }
      
      shinyjs.resetDnD = function (params){
        $('#letters').html(cacheLetters).sortable('refresh');
        $('[id^=droppable_cell]').html(cacheGridCells).sortable('refresh');
      }
    };
    "
    
    ui <- fluidPage(
        useShinyjs(),
        extendShinyjs(text = js, functions = c("pageCol", "resetDnD", "setSuggestion")),
        tags$head(
        # some styling
        tags$style(
            HTML(
                "
                .grid-table {
                    width: 150px;
                    border-collapse: collapse;
                }
                .grid-cell {
                    width: 100%;
                    height: 50px;
                    border: 1px solid black;
                    background-color: white;
                    text-align: center;
                    margin: 0;
                    padding: 5px;
                }
                .grid-cell-text {
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    height: 100%;
                    background-color: steelblue;
                    color: white;
                    font-size: 18px;
                }
                .droppable-cell {
                    background-color: lightgray;
                }
                .table-container {
                    display: flex;
                    position: absolute;
                    left: 400px;
                    top: 30px;
                    margin-top: 0px;
                    overflow: hidden;
                }
                
                #btn_suggestion1, #btn_suggestion2 {
                    background-color: lightblue;
                }
                
                #btn_resetDnD {
                    background-color: pink;
                }
                "
            )
        )),
        
        actionButton("btn_suggestion1", "Suggestion 1"),
        actionButton("btn_suggestion2", "Suggestion 2"),
        actionButton("btn_resetDnD", "Reset Drag and Drop"),
        
        div(
            class = "table-container",
            div(
                class = "grid-table",
                id = "my_grid",
                div(
                    class = "grid-row",
                    div(class = "grid-cell grid-cell-text", "my_grid"),
                    div(id = "droppable_cell_1", class = "grid-cell droppable-cell", ""),
                    div(id = "droppable_cell_2", class = "grid-cell droppable-cell", ""),
                    div(id = "droppable_cell_3", class = "grid-cell droppable-cell", ""),
                    div(id = "droppable_cell_4", class = "grid-cell droppable-cell", ""),
                    div(id = "droppable_cell_5", class = "grid-cell droppable-cell", ""),
                    div(id = "droppable_cell_6", class = "grid-cell droppable-cell", ""),
                    div(id = "droppable_cell_7", class = "grid-cell droppable-cell", "")
                )
            ),
            
            orderInput('letters', 'Letters', items = LETTERS[1:7],
                       connect = connections) # defined above
        )
    )
    
    server <- function(input, output, session) {
        shinyjs::js$pageCol(df)
        
        observeEvent(input$btn_suggestion1, {
            shinyjs::disable("btn_suggestion1")
            shinyjs::js$setSuggestion(1)
            shinyjs::enable("btn_suggestion1")
        })
        
        observeEvent(input$btn_suggestion2, {
            shinyjs::disable("btn_suggestion2")
            shinyjs::js$setSuggestion(2)
            shinyjs::enable("btn_suggestion2")
        })
        
        observeEvent(input$btn_resetDnD, {
            shinyjs::disable("btn_resetDnD")
            shinyjs::js$resetDnD()
            shinyjs::enable("btn_resetDnD")
        })
    }
    
    shinyApp(ui, server)