Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Click and drag to select multiple rows #565

Closed
jordimartorell opened this issue Jul 2, 2018 · 22 comments
Closed

Click and drag to select multiple rows #565

jordimartorell opened this issue Jul 2, 2018 · 22 comments

Comments

@jordimartorell
Copy link

Good morning and thank you for this awesome package.

Although it is possible to select multiple rows at a time using shift + click, I think that selecting multiple rows clicking and dragging (similar to Excel) would be a great improvement, given that, for many users, this is more intuitive than using shift + click.

Thanks in advance!

@stla
Copy link
Collaborator

stla commented Jun 3, 2019

If you use Shiny, this is possible with the help of jquery-ui.

@yihui
Copy link
Member

yihui commented Jun 3, 2019

Edit: Sorry, but it was not a duplicate. But I guess we are very unlikely to implement it by ourselves. Pull requests will be welcomed!

@yihui yihui marked this as a duplicate of #305 Jun 3, 2019
@yihui yihui closed this as completed Jun 3, 2019
@yihui yihui added duplicate and removed duplicate labels Jun 3, 2019
@stla
Copy link
Collaborator

stla commented Jun 6, 2019

Here is the Shiny implementation with jquery-ui:

library(shiny)
library(DT)

ui <- fluidPage(
  tags$head(
    tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js")
  ),
  DTOutput("dt")
)

callback <- c(
  "var dt = table.table().node();",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        table.row(i).select();",
  "      } else {",
  "        table.row(i).deselect();",
  "      }",
  "    });",
  "  }",
  "});"
)

server <- function(input, output){
  output[["dt"]] <- renderDT({
    datatable(iris, extensions = "Select", callback = JS(callback), 
              selection = "none")
  })
}

shinyApp(ui, server)

SCREENTOGIF

@yihui Is it possible to attach a dependency to an instance of datatable ? So that we could do that without Shiny, if we were able to attach jquery-ui as a dependency.

@stla
Copy link
Collaborator

stla commented Jun 6, 2019

Better, allows to select several blocks of rows:

callback <- c(
  "var dt = table.table().node();",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        table.row(i).select();",
  "      }",
  "    });",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});"
)

server <- function(input, output){
  output[["dt"]] <- renderDT({
    datatable(iris, extensions = "Select", callback = JS(callback), 
              selection = "multiple")
  })
}

Double-click on the table to remove all selections.

@stla
Copy link
Collaborator

stla commented Jun 6, 2019

@yihui Is it possible to attach a dependency to an instance of datatable ? So that we could do that without Shiny, if we were able to attach jquery-ui as a dependency.

Yes, I've found the way:

library(htmltools)
dtable <- datatable(iris, extensions = "Select", callback = JS(callback), 
                    selection = "multiple")
dep <- htmlDependency("jqueryui", "1.12.1", 
                      system.file("www/shared/jqueryui", package = "shiny"),
                      script = "jquery-ui.min.js")
dtable$dependencies <- c(dtable$dependencies, list(dep))

But selection = "multiple" has no effect outside Shiny. Is it normal ?

@jordimartorell
Copy link
Author

Dear @stla, thank you very much for your help. Your code is really useful.
However, I found a strange behaviour in this output. If you click and drag to select multiple rows and, then, you reorder the table, only the row clicked first remains selected while the other rows are unselected.

If you want to reproduce this, order by Sepal.Lenght column, select the first 5 rows and reorder two more times by this column.

I wonder if you can help me to solve this, I don't have experience with JS.

Thank you again!

@stla
Copy link
Collaborator

stla commented Sep 16, 2019

@jordimartorell Thanks for the feedback. You are right. But sorry, I don't know how to solve this issue.

@stla
Copy link
Collaborator

stla commented Sep 16, 2019

@jordimartorell I've found a solution !

Replace the callback with:

callback <- c(
  "var dt = table.table().node();",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        table.row(':eq('+i+')').select();",
  "      }",
  "    });",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});"
)

Then use the option server = FALSE in renderDT :

  output[["dt"]] <- renderDT({
    dtable <- datatable(
      iris, extensions = "Select", 
      callback = JS(callback), 
      selection = "multiple"
    )
    dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
                                     "www/shared/jqueryui",
                                     script = "jquery-ui.min.js",
                                     package = "shiny")
    dtable$dependencies <- c(dtable$dependencies, list(dep))
    dtable
  }, server = FALSE)

Please give it a try and tell me whether it works.

@jordimartorell
Copy link
Author

@stla it works perfect now!
Thank you again for your effort :-)

@stla
Copy link
Collaborator

stla commented Sep 16, 2019

@jordimartorell You're welcome. Thanks again for the feedback.

FYI this code is on my blog. I have just updated it to include this solution.

@jordimartorell
Copy link
Author

jordimartorell commented Sep 16, 2019

@stla sorry to disturb you again, but I realised that there is a problem when retrieving which rows are selected. It seems that only the first row clicked is stored in the variable "input$dt_rows_selected", so I can't get the rows selected by dragging. Here it is an example, the selected rows are printed in the R console:

library(DT)
library(htmlwidgets)

callback <- c(
	"var dt = table.table().node();",
	"$(dt).selectable({",
	"  distance : 10,",
	"  selecting: function(evt, ui){",
	"    $(this).find('tbody tr').each(function(i){",
	"      if($(this).hasClass('ui-selecting')){",
	"        table.row(':eq('+i+')').select();",
	"      }",
	"    });",
	"  }",
	"}).on('dblclick', function(){table.rows().deselect();});"
)

ui <- fluidPage(
	tags$head(
		tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js")
	),
	DTOutput("dt")
	)

server <- function(input, output){
	output[["dt"]] <- renderDT({
		datatable(iris,  extensions = "Select", 
				  callback = JS(callback),
				  selection = "multiple", 
				  filter = "none", 
				  options = list(scrollY = "200px", "scrollCollapse" = TRUE, paging = FALSE))
	}, server = FALSE)
	observeEvent(input$dt_rows_selected, {
		print(input$dt_rows_selected)
	})
}```

@stla
Copy link
Collaborator

stla commented Sep 16, 2019

@jordimartorell Hmm sorry that sounds hard. input_dt_rows_selected is generated by @yihui 's JavaScript code in the DT package, it is triggered when you select a row by clicking on it. If you select by dragging, it is not triggered. Not sure I'm able to find a solution but I will give a try.

@stla
Copy link
Collaborator

stla commented Sep 16, 2019

This code prints the selected rows but there's an issue: when you unselect a row, it still appears. I dont know how to solve that.

library(shiny)
library(DT)

callback <- c(
  "var dt = table.table().node();",
  "var selected = [];",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        table.row(':eq('+i+')').select();",
  "        var id = table.row(':eq('+i+')').id();",
  "        selected.push(id);",
  "        Shiny.setInputValue('selectedRows', selected);",
  "      }",
  "    });",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});"
)

ui <- fluidPage(
  tags$head(
    tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js")
  ),
  DTOutput("dt")
)

iris$ID <- as.character(1:nrow(iris))

server <- function(input, output){
  output[["dt"]] <- renderDT({
    datatable(iris,  extensions = "Select", 
              callback = JS(callback),
              selection = "multiple", 
              filter = "none", 
              options = list(
                scrollY = "200px", 
                "scrollCollapse" = TRUE, 
                paging = FALSE,
                rowId = JS(sprintf("function(data){return data[%d];}", 
                                   ncol(iris)))
              )
    )
  }, server = FALSE)

  observeEvent(list(input$selectedRows, input$dt_rows_selected), {
    print(unique(c(input$selectedRows, input$dt_rows_selected)))
  })
}

shinyApp(ui, server)

@stla
Copy link
Collaborator

stla commented Sep 16, 2019

@jordimartorell I think I managed (this was hard). Please try this one:

library(shiny)
library(DT)

callback <- c(
  "function distinct(value, index, self) { 
    return self.indexOf(value) === index;
  }",
  "var dt = table.table().node();",
  "var selected = [];",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        var row = table.row(':eq('+i+')')",
  "        row.select();",
  "        selected.push(row.id());",
  "        selected = selected.filter(distinct);",
  "        Shiny.setInputValue('selectedRows', selected);",
  "      }",
  "    });",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});",
  "table.on('click', 'tr', function(){",
  "  var row = table.row(this);",
  "  if(!$(this).hasClass('selected')){",
  "    var rowId = row.id();",
  "    var index = selected.indexOf(rowId);",
  "    if(index > -1){",
  "       selected.splice(index, 1);",
  "    }",
  "  }",
  "  Shiny.setInputValue('selectedRows', selected);",
  "});"
)

ui <- fluidPage(
  tags$head(
    tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js")
  ),
  DTOutput("dt")
)

iris$ID <- as.character(1:nrow(iris))

server <- function(input, output){
  output[["dt"]] <- renderDT({
    datatable(iris,  extensions = "Select", 
              callback = JS(callback),
              selection = "multiple", 
              filter = "none", 
              options = list(
                scrollY = "200px", 
                "scrollCollapse" = TRUE, 
                paging = FALSE,
                rowId = JS(sprintf("function(data){return data[%d];}", 
                                   ncol(iris)))
              )
    )
  }, server = FALSE)

  observeEvent(list(input$selectedRows, input$dt_rows_selected), {
    print(unique(c(input$selectedRows, input$dt_rows_selected)))
  })
}

shinyApp(ui, server)

@stla
Copy link
Collaborator

stla commented Sep 16, 2019

@jordimartorell Here is a cleaner version. Use this one.

library(shiny)
library(DT)

callback <- c(
  "function distinct(value, index, self) { 
    return self.indexOf(value) === index;
  }",
  "var dt = table.table().node();",
  "var selected = [];",
  "$(dt).selectable({",
  "  distance : 10,",
  "  selecting: function(evt, ui){",
  "    $(this).find('tbody tr').each(function(i){",
  "      if($(this).hasClass('ui-selecting')){",
  "        var row = table.row(':eq('+i+')')",
  "        row.select();",
  "        var rowIndex = parseInt(row.id().split('-')[1]);",
  "        selected.push(rowIndex);",
  "        selected = selected.filter(distinct);",
  "        Shiny.setInputValue('selectedRows', selected);",
  "      }",
  "    });",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});",
  "table.on('click', 'tr', function(){",
  "  var row = table.row(this);",
  "  if(!$(this).hasClass('selected')){",
  "    var rowIndex = parseInt(row.id().split('-')[1]);",
  "    var index = selected.indexOf(rowIndex);",
  "    if(index > -1){",
  "       selected.splice(index, 1);",
  "    }",
  "  }",
  "  Shiny.setInputValue('selectedRows', selected);",
  "});"
)

ui <- fluidPage(
  DTOutput("dt")
)

dat <- iris
dat$ID <- paste0("row-", 1:nrow(dat))

rowNames <- TRUE # whether to show row names in the table
colIndex <- as.integer(rowNames)

server <- function(input, output){
  output[["dt"]] <- renderDT({
    dtable <- datatable(
      dat, rownames = rowNames,  
      extensions = "Select", 
      callback = JS(callback),
      selection = "multiple", 
      filter = "none", 
      options = list(
        scrollY = "200px", 
        scrollCollapse = TRUE, 
        paging = FALSE,
        rowId = JS(sprintf("function(data){return data[%d];}", 
                           ncol(dat)-1L+colIndex)),
        columnDefs = list(
          list(visible = FALSE, targets = ncol(dat)-1L+colIndex)
        )
      )
    )
    dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
                                     "www/shared/jqueryui",
                                     script = "jquery-ui.min.js",
                                     package = "shiny")
    dtable$dependencies <- c(dtable$dependencies, list(dep))
    dtable
  }, server = FALSE)
  
  observeEvent(list(input$selectedRows, input$dt_rows_selected), {
    print(unique(c(input$selectedRows, input$dt_rows_selected)))
  })
}

shinyApp(ui, server)

@jordimartorell
Copy link
Author

@stla Awesome, your code works perfectly!

Thank you very much and congratulations for your blog.

@jordimartorell
Copy link
Author

Hi again @stla

I faced another problem. I think that the solution is easy, but I'm not able to handle it.
In your example, the selected rows are stored in variables input$dt_rows_selected and input$selectedRows. The first one's name depends on the name of the table variable. However, the second one will never change on the session. That's a problem if you have more than one DT table in a Shiny application.

For instance, if you have dt1 and dt2, you can access to input$dt1_rows_selected and input$dt2_rows_selected, but only to one input$selectedRows. So the solution would be to generate a name for this variable related to the DT name.

Thanks again!

@stla
Copy link
Collaborator

stla commented Sep 26, 2019

@jordimartorell This is already done on my blog :-). Have a look.

@jordimartorell
Copy link
Author

Wonderful, that's exactly what I needed :-)

@anshpujara14
Copy link

Hi @stla ! I used the above code you mentioned for the click and drag to select the rows and it works great. But now I want to implement the same for the cells. Is there any way you can help with it?
Thank you.

@stla
Copy link
Collaborator

stla commented Apr 14, 2021

Hi @anshpujara14

Here is the first step:

library(shiny)
library(DT)

callback <- c(
  "var dt = table.table().node();",
  "$(dt).selectable({",
  "  distance : 10,",
  "  filter: 'td',",
  "  selecting: function(evt, ui){",
  "    var cell = ui.selecting;",
  "    table.cell(cell).select();",
  "  }",
  "}).on('dblclick', function(){table.rows().deselect();});"
)

ui <- fluidPage(
  DTOutput("dt")
)

server <- function(input, output){
  output[["dt"]] <- renderDT({
    dtable <- datatable(
      iris, extensions = "Select", 
      callback = JS(callback), selection = "none",
      options = list(
        select = list(items = "cell")
      )
    )
    dep <- htmltools::htmlDependency("jqueryui", "1.12.1",
                                     "www/shared/jqueryui",
                                     script = "jquery-ui.min.js",
                                     package = "shiny")
    dtable$dependencies <- c(dtable$dependencies, list(dep))
    dtable
  }, server = FALSE)
}

shinyApp(ui, server)

@anshpujara14
Copy link

Thank You @stla ! The code works perfect. Implemented it and tried for my application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants