Question

How to conditionally deactivate or remove selection items in the rhandsontable context menu?

In running the below simplified code, the user can add or remove table rows by right-clicking on a row which, through the rhandsontable package context menu, generates a pop-up of action choices. In the code you can see how I used the onRender(...) function and JavaScript to deactivate row deletion when there is only 1 row in the table. This works.

However, I would also like to either deactivate, or remove (whichever is simpler), the "Insert row above" selection when the user accesses the context menu from the first row of the table. When the user accesses the context menu from any table row other than the first then "Insert row above" should be functional. Basically, I never want a top row that is blank. Any ideas for how to do this?

I went through the handsontable listing of hooks and could not find any equivalent of "beforeAddRow" or the like.

library(shiny)
library(rhandsontable)
library(htmlwidgets)

ui <- fluidPage(
  br(),
  rHandsontableOutput("simple_table")
)

server <- function(input, output) {
  output$simple_table <- renderRHandsontable({
    rhandsontable(data.frame(Value = numeric(1)), contextMenu = TRUE, rowHeaders = TRUE) %>%
      hot_cols(colWidths = 100, type = "numeric") %>%
      # Below prevents row deletion when there is only 1 row in table
      onRender(
        c(
          "function(el, x) {",
          "  var hot = this.hot;",
          "  Handsontable.hooks.add('beforeRemoveRow', function(index, amount){",
          "    var nrows = hot.countRows();",
          "    if(nrows === 1) {",
          "      return false;",
          "    }",
          "  }, hot);",
          "}"
        )
      )
  })
}

shinyApp(ui = ui, server = server)
 2  54  2
1 Jan 1970

Solution

 1

I think that the best option is to remove the not-needed items from the context menu because otherwise it may be confusing for the user why nothing happens when they click on some items. Here is a solution which does not need custom Javascript. I treat here the situation that you would like to have "Insert row above" and "Remove row" not to be visible if the table has only one row.

The idea is that inside the renderRHandsontable we also have an assignment rhot$x$contextMenu <- ContextMenuItems(), where ContextMenuItems is a reactive containing the menu items which we update inside an observeEvent on the table. Inside this observeEvent we distinguish between the nrow() == 1 and nrow() > 1 case and set the items respectively.

enter image description here

library(shiny)
library(rhandsontable)

ui <- fluidPage(
  br(),
  rHandsontableOutput("simple_table")
)

server <- function(input, output) {
  
  ContextMenuItems <- reactiveVal(list(items = c("row_below",
                                                 "---------", "undo", "redo", 
                                                 "---------", "alignment")))
  
  Data <- reactiveVal(data.frame(Value = numeric(1)))
  
  observeEvent(input$simple_table, {
    Data(hot_to_r(input$simple_table))
    nrows_table <- nrow(Data())
    newIt <- ContextMenuItems()
    if (nrows_table == 1) {
      newIt$items <- newIt$items[!(newIt$items %in% c("row_above", "remove_row"))]
    } else if (!(any(c("row_above", "remove_row") %in% newIt$items))) {
      newIt$items <- newIt$items |> append("row_above", 0) |> append("remove_row", 2)
    }
    ContextMenuItems(newIt)
  })
                                  
  output$simple_table <- renderRHandsontable({
    rhot <- rhandsontable(Data(), contextMenu = TRUE, rowHeaders = TRUE) %>%
      hot_cols(colWidths = 100, type = "numeric")
    rhot$x$contextMenu <- ContextMenuItems()
    rhot
  })
}

shinyApp(ui = ui, server = server)
2024-07-21
Jan

Solution

 0

An alternate approach, using only JavScript, simply ignores any attempt by the user to insert a row above the first row of the table. Here is the key modified code from the server section:

rhandsontable(data.frame(Value = numeric(1)), contextMenu = TRUE, rowHeaders = TRUE) %>%
  hot_cols(colWidths = 100, type = "numeric") %>%
  onRender(
    c(
      "function(el, x) {",
      "  var hot = this.hot;",
      "  Handsontable.hooks.add('beforeCreateRow', function(index, amount) {",
      "    if (index === 0) {",
      "      return false;",  
      "    }",
      "  });",
      "}"
    )
  )
2024-07-21
Village.Idyot

Solution

 0

The disabled property for a context menu item can be a JS function that has access to the table as this. As opposed to the hook approach, it’s a bit more involved to work out where rows would be inserted or removed, but this should result in a better user experience.

contextMenuSettings <- list(
  items = list(
    row_above = list(
      # Disable "insert above" if new row would go above the first row.
      disabled = htmlwidgets::JS("function() {
            // Insertion happens above last selection.
            var selected = this.getSelectedRangeLast();
            var topRow = Math.min(selected.from.row, selected.to.row);
            return topRow <= 0;
          }")
    ),
    row_below = list(),
    remove_row = list(
      # Disable "remove row" if all rows would be removed.
      disabled = htmlwidgets::JS("function() {
            // Same row can be selected in multiple ranges.
            var selectedRows = new Set();
            for (var selected of this.getSelectedRange()) {
              var topRow = Math.min(selected.from.row, selected.to.row);
              var bottomRow = Math.max(selected.from.row, selected.to.row);
              for (var row = topRow; row <= bottomRow; row++) {
                selectedRows.add(row);
              }
            }
            return this.countRows() <= selectedRows.size;
          }")
    ),
    sp1 = '---------',
    undo = list(),
    redo = list()
  )
)

The custom settings have to be set after the rhandsontable() has been created.

library(shiny)
library(rhandsontable)

ui <- fluidPage(
  br(),
  rHandsontableOutput("simple_table")
)

server <- function(input, output) {
  output$simple_table <- renderRHandsontable({
    hot <- rhandsontable(
      data.frame(Value = numeric(1), Value2 = numeric(1)),
      contextMenu = TRUE,
      rowHeaders = TRUE
    )
    
    # Customize context menu.
    hot$x$contextMenu <- contextMenuSettings
    
    hot
  })
}

shinyApp(ui = ui, server = server)
2024-07-22
Mikko Marttila