diff --git a/NEWS.md b/NEWS.md index 910bb9e8..b6136867 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,47 @@ +# CHANGES IN DT VERSION 0.32 + +- Fixed the bug that `replaceData()` failed to work with data that has no column names (thanks, @mmuurr, #1108). + +- `updateSearch()` now sets the slider values based on the new search string for numeric columns (thanks, @mikmart, #1110). + +- Added server-side processing support for the [SearchBuilder](https://datatables.net/extensions/searchbuilder/) extension (thanks, @AhmedKhaled945, @shrektan, @mikmart, #963). + +# CHANGES IN DT VERSION 0.31 + +- Upgraded DataTables version to 1.13.6 (thanks, @stla, #1091). + +- Searching and sorting work now when columns are re-ordered by the `ColReorder` extension (thanks, @ashbaldry #1096, @gergness #534, @nmoraesmunter #921, @isthisthat #1069). + +- Fixed disabling selection on hyperlink clicks (thanks, @guoci, #1093). + +- Fixed an error for R >= 4.3.0 (thanks, @AntoineMichelet, #1095). + +# CHANGES IN DT VERSION 0.30 + +- Fixed a bug that when using `updateSearch()`, the clear button inside the input box doesn't show up, and the table doesn't update when the input is cleared (thanks, @DavidBlairs, #1082). + +- Added support for a list of Booleans as input to the `class` argument of `DT::datatable()` when `style = 'bootstrap'` (thanks, @pedropark99, #1089). In other words, you can now select the Bootstrap classes you want to use at `DT::datatable()` by using a list of Booleans that select the classes you want to use. In the example below, we are producing an HTML table that uses the `stripe` and `hover` Bootstrap classes: + + ```r + DT::datatable(mtcars, class = list(stripe = TRUE, compact = FALSE, hover = TRUE), style = "bootstrap") + ``` + +- Handle `NULL` return from `bslib::theme_version()` (thanks, @slodge-work, #1090). + +# CHANGES IN DT VERSION 0.29 + +- Support Bootstrap 5 with `datatable(style = "auto")` (thanks, @gadenbuie, #1074). + +- Fixed a bug that searching would fail when the keyword contains special characters such as `&` (thanks, @dfriend21 @olivier7121, #1075). + +- Deleted `()` after `eval` in a JS *comment* to prevent Google from treating it as malware, which is just a false alarm (thanks, @gorkang, #1080). + +# CHANGES IN DT VERSION 0.28 + +- Upgraded DataTables to v1.13.4 (thanks, @stla, #1063). + +- It is now possible to edit date columns with the help of calendar inputs (thanks, @stla, #1064). + # CHANGES IN DT VERSION 0.27 - `dataTableOutput()` gains a new `fill` parameter. When `TRUE` (the default), the widget's container element is allowed to grow/shrink to fit it's parent container so long as that parent is opinionated about its height and has been marked with `htmltools::bindFillRole(x, container = TRUE)`. (#2198) diff --git a/R/searchbuilder.R b/R/searchbuilder.R new file mode 100644 index 00000000..6d93887d --- /dev/null +++ b/R/searchbuilder.R @@ -0,0 +1,98 @@ +# server-side processing for the SearchBuilder extension +# https://datatables.net/extensions/searchbuilder/ + +sbEvaluateSearch = function(search, data) { + # https://datatables.net/reference/option/searchBuilder.preDefined + stopifnot(search$logic %in% c('AND', 'OR')) + Reduce( + switch(search$logic, AND = `&`, OR = `|`), + lapply(search$criteria, sbEvaluateCriteria, data) + ) +} + +sbEvaluateCriteria = function(criteria, data) { + # https://datatables.net/reference/option/searchBuilder.preDefined.criteria + if ('logic' %in% names(criteria)) { + # this is a sub-group + sbEvaluateSearch(criteria, data) + } else { + # this is a criteria + cond = criteria$condition + type = criteria$type + x = data[[criteria$origData %||% criteria$data]] + v = sbParseValue(sbExtractValue(criteria), type) + sbEvaluateCondition(cond, type, x, v) + } +} + +sbExtractValue = function(criteria) { + if (criteria$condition %in% c('between', '!between')) { + # array values are passed in a funny way to R + c(criteria$value1, criteria$value2) + } else { + criteria$value + } +} + +sbParseValue = function(value, type) { + # TODO: handle 'moment' and 'luxon' types mentioned in condition reference + if (type %in% c('string', 'html')) { + as.character(value) + } else if (type %in% c('num', 'num-fmt', 'html-num', 'html-num-fmt')) { + as.numeric(value) + } else if (type %in% c('date')) { + as.Date(value) + } else { + stop(sprintf('unsupported criteria type "%s"', type)) + } +} + +sbEvaluateCondition = function(condition, type, x, value) { + # https://datatables.net/reference/option/searchBuilder.preDefined.criteria.condition + if (type %in% c('string', 'html')) { + switch( + condition, + '!=' = x != value, + '!null' = !is.na(x) & x != '', + '=' = x == value, + 'contains' = grepl(value, x, fixed = TRUE), + '!contains' = !grepl(value, x, fixed = TRUE), + 'ends' = endsWith(as.character(x), value), + '!ends' = !endsWith(as.character(x), value), + 'null' = is.na(x) | x == '', + 'starts' = startsWith(as.character(x), value), + '!starts' = !startsWith(as.character(x), value), + stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type)) + ) + } else if (type %in% c('num', 'num-fmt', 'html-num', 'html-num-fmt')) { + switch( + condition, + '!=' = x != value, + '!null' = !is.na(x), + '<' = x < value, + '<=' = x <= value, + '=' = x == value, + '>' = x > value, + '>=' = x >= value, + 'between' = x >= value[1] & x <= value[2], + '!between' = x < value[1] | x > value[2], + 'null' = is.na(x), + stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type)) + ) + } else if (type %in% c('date', 'moment', 'luxon')) { + switch( + condition, + '!=' = x != value, + '!null' = !is.na(x), + '<' = x < value, + '=' = x == value, + '>' = x > value, + 'between' = x >= value[1] & x <= value[2], + '!between' = x < value[1] | x > value[2], + 'null' = is.na(x), + stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type)) + ) + } else { + stop(sprintf('unsupported criteria type "%s"', type)) + } +} diff --git a/R/shiny.R b/R/shiny.R index 835ad329..4e457f1c 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -631,6 +631,11 @@ dataTablesFilter = function(data, params) { # start searching with all rows i = seq_len(n) + # apply SearchBuilder query if present + if (!is.null(s <- q$searchBuilder)) { + i = which(sbEvaluateSearch(s, data)) + } + # search by columns if (length(i)) for (j in names(q$columns)) { col = q$columns[[j]] diff --git a/tests/testit/test-searchbuilder.R b/tests/testit/test-searchbuilder.R new file mode 100644 index 00000000..c8c71f51 --- /dev/null +++ b/tests/testit/test-searchbuilder.R @@ -0,0 +1,43 @@ +library(testit) + +assert('SearchBuilder condition evaluation works', { + (sbEvaluateCondition('>', 'num', 1:2, 1) == c(FALSE, TRUE)) + (sbEvaluateCondition('between', 'num', 7, c(2, 4)) == FALSE) + (sbEvaluateCondition('starts', 'string', 'foo', 'f') == TRUE) + (sbEvaluateCondition('starts', 'string', factor('foo'), 'f') == TRUE) + (sbEvaluateCondition('null', 'string', c('', NA)) == c(TRUE, TRUE)) +}) + +assert('SearchBuilder logic evaluation works', { + res = sbEvaluateSearch( + list( + logic = 'AND', + criteria = list( + list(condition = '<=', data = 'a', value = '4', type = 'num'), + list(condition = '>=', data = 'a', value = '2', type = 'num') + ) + ), + data.frame(a = 1:9) + ) + (setequal(which(res), 2:4)) +}) + +assert('SearchBuilder complex queries work', { + res = sbEvaluateSearch( + list( + logic = 'OR', + criteria = list( + list(condition = '=', data = 'a', value = '7', type = 'num'), + list( + logic = 'AND', + criteria = list( + list(condition = '<=', data = 'a', value = '4', type = 'num'), + list(condition = '>=', data = 'a', value = '2', type = 'num') + ) + ) + ) + ), + data.frame(a = 1:9) + ) + (setequal(which(res), c(2:4, 7))) +})