From 3ab548a5457cc322536cb722512d07e0ea13cb03 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 00:08:53 +0800 Subject: [PATCH 01/20] doc of shiny::renderDataTable updated due to upstream changes --- man/dataTableOutput.Rd | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/man/dataTableOutput.Rd b/man/dataTableOutput.Rd index 0725ce1e..f366a277 100644 --- a/man/dataTableOutput.Rd +++ b/man/dataTableOutput.Rd @@ -48,15 +48,10 @@ browsers to slow down or crash. Note that if you want to use \code{renderDataTable} with \code{shiny::bindCache()}, this must be \code{FALSE}.} -\item{env}{The parent environment for the reactive expression. By default, -this is the calling environment, the same as when defining an ordinary -non-reactive expression. If \code{expr} is a quosure and \code{quoted} is \code{TRUE}, -then \code{env} is ignored.} +\item{env}{The environment in which to evaluate \code{expr}.} -\item{quoted}{If it is \code{TRUE}, then the \code{\link[=quote]{quote()}}ed value of \code{expr} -will be used when \code{expr} is evaluated. If \code{expr} is a quosure and you -would like to use its expression as a value for \code{expr}, then you must set -\code{quoted} to \code{TRUE}.} +\item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This +is useful if you want to save an expression in a variable.} \item{funcFilter}{(for expert use only) passed to the \code{filter} argument of \code{\link{dataTableAjax}()}} From 10d0099df54ee909e382de831955f7d6af4019fc Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 01:07:40 +0800 Subject: [PATCH 02/20] implement colDefsTgtHandle() --- R/datatables.R | 45 ++++++++++++++++++++++++++++++++++ man/datatable.Rd | 17 +++++++++++++ tests/testit/test-datatables.R | 39 +++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/R/datatables.R b/R/datatables.R index e68133cc..0aca86dc 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -140,6 +140,23 @@ #' server-side processing mode well. Please set this argument to #' \code{'none'} if you really want to use the Select extension. #' } +#' \code{options$columnDefs}: +#' \enumerate{ +#' \item \code{columnDefs} is an option that provided by the DataTables library +#' itself, where the user can set various attributes for columns. It must be +#' provided as a list of list, where each sub-list must contain a vector named 'targets', +#' specifying the applied columns, i.e., +#' \code{list(list(..., targets = '_all'), list(..., targets = c(1, 2)))} +#' \item \code{columnDefs$targets} is a vector and should be one of: +#' \itemize{ +#' \item 0 or a positive integer: column index counting from the left +#' \item A negative integer: column index counting from the right +#' \item A string: the column name of the original data and not the ones that +#' could be changed via param \code{colnames}. +#' \item The string "_all": all columns (i.e. assign a default) +#' } +#' \item See \url{https://datatables.net/reference/option/columnDefs} for more. +#' } #' @note You are recommended to escape the table content for security reasons #' (e.g. XSS attacks) when using this function in Shiny or any other dynamic #' web applications. @@ -193,6 +210,9 @@ datatable = function( stop("'data' must be 2-dimensional (e.g. data frame or matrix)") } + # convert the targets + options[["columnDefs"]] = colDefsTgtHandle(options[["columnDefs"]], base::colnames(data), !is.null(rn)) + if (is.data.frame(data)) { data = as.data.frame(data) numc = unname(which(vapply(data, is.numeric, logical(1)))) @@ -478,6 +498,31 @@ classNameDefinedColumns = function(options, ncol) { unique(cols) } +colDefsTgtHandle = function(columnDefs, names, showRowName) { + convert = function(targets, names, showRowName) { + if (is.list(targets)) { + lapply(targets, convert, names = names, showRowName = showRowName) + } else if (is.character(targets)) { + is_all = targets == "_all" + if (all(is_all)) { + out = "_all" + } else if (any(is_all)) { + targets = list(targets[is_all], targets[!is_all]) + out = lapply(targets, convert, names = names, showRowName = showRowName) + } else { + out = unname(convertIdx(targets, names)) - !showRowName + } + out + } else { + targets + } + } + lapply(columnDefs, function(x) { + x[["targets"]] = convert(x[["targets"]], names, showRowName) + x + }) +} + # convert character indices to numeric convertIdx = function(i, names, n = length(names), invert = FALSE) { diff --git a/man/datatable.Rd b/man/datatable.Rd index 8e23f945..464d75eb 100644 --- a/man/datatable.Rd +++ b/man/datatable.Rd @@ -190,6 +190,23 @@ data frame) using the JavaScript library DataTables. server-side processing mode well. Please set this argument to \code{'none'} if you really want to use the Select extension. } + \code{options$columnDefs}: + \enumerate{ + \item \code{columnDefs} is an option that provided by the DataTables library + itself, where the user can set various attributes for columns. It must be + provided as a list of list, where each sub-list must contain a vector named 'targets', + specifying the applied columns, i.e., + \code{list(list(..., targets = '_all'), list(..., targets = c(1, 2)))} + \item \code{columnDefs$targets} is a vector and should be one of: + \itemize{ + \item 0 or a positive integer: column index counting from the left + \item A negative integer: column index counting from the right + \item A string: the column name of the original data and not the ones that + could be changed via param \code{colnames}. + \item The string "_all": all columns (i.e. assign a default) + } + \item See \url{https://datatables.net/reference/option/columnDefs} for more. + } } \note{ You are recommended to escape the table content for security reasons diff --git a/tests/testit/test-datatables.R b/tests/testit/test-datatables.R index 77f067ce..b54412d6 100644 --- a/tests/testit/test-datatables.R +++ b/tests/testit/test-datatables.R @@ -192,3 +192,42 @@ assert('warn autoHideNavigation if no pageLength', { datatable(head(iris, 5), autoHideNavigation = TRUE, options = list(pageLength = 20)) )) }) + +assert("colDefsTgtHandle() works", { + cols = c("A", "B", "C") + (colDefsTgtHandle(NULL, cols, TRUE) %==% list()) + (colDefsTgtHandle(NULL, cols, FALSE) %==% list()) + + defs = list( + list(1, targets = "_all"), + list(2, targets = 1L), + list(3, targets = "B"), + list(4, targets = c("A", "_all")), + list(5, targets = list(c("A", "C"), "_all")), + list(6, targets = list(1L, "_all")), + list(7, targets = list(1L, "C")), + list(8, targets = list(1L, "B", "_all")) + ) + res1 = list( + list(1, targets = "_all"), + list(2, targets = 1L), + list(3, targets = 2L), + list(4, targets = list("_all", 1L)), + list(5, targets = list(c(1L, 3L), "_all")), + list(6, targets = list(1L, "_all")), + list(7, targets = list(1L, 3L)), + list(8, targets = list(1L, 2L, "_all")) + ) + res2 = list( + list(1, targets = "_all"), + list(2, targets = 1L), + list(3, targets = 1L), + list(4, targets = list("_all", 0L)), + list(5, targets = list(c(0L, 2L), "_all")), + list(6, targets = list(1L, "_all")), + list(7, targets = list(1L, 2L)), + list(8, targets = list(1L, 1L, "_all")) + ) + (colDefsTgtHandle(defs, cols, TRUE) %==% res1) + (colDefsTgtHandle(defs, cols, FALSE) %==% res2) +}) From c741026b39bb9a987cb66209ebcad184695c4039 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 01:21:49 +0800 Subject: [PATCH 03/20] tweak doc --- R/datatables.R | 10 +++++----- man/datatable.Rd | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index 0aca86dc..53b19071 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -149,11 +149,11 @@ #' \code{list(list(..., targets = '_all'), list(..., targets = c(1, 2)))} #' \item \code{columnDefs$targets} is a vector and should be one of: #' \itemize{ -#' \item 0 or a positive integer: column index counting from the left -#' \item A negative integer: column index counting from the right -#' \item A string: the column name of the original data and not the ones that -#' could be changed via param \code{colnames}. -#' \item The string "_all": all columns (i.e. assign a default) +#' \item 0 or a positive integer: column index counting from the left. +#' \item A negative integer: column index counting from the right. +#' \item A string: the column name. Note, it must be the names of the +#' original data, not the ones that (could) be changed via param \code{colnames}. +#' \item The string "_all": all columns (i.e. assign a default). #' } #' \item See \url{https://datatables.net/reference/option/columnDefs} for more. #' } diff --git a/man/datatable.Rd b/man/datatable.Rd index 464d75eb..d587e5c9 100644 --- a/man/datatable.Rd +++ b/man/datatable.Rd @@ -199,11 +199,11 @@ data frame) using the JavaScript library DataTables. \code{list(list(..., targets = '_all'), list(..., targets = c(1, 2)))} \item \code{columnDefs$targets} is a vector and should be one of: \itemize{ - \item 0 or a positive integer: column index counting from the left - \item A negative integer: column index counting from the right - \item A string: the column name of the original data and not the ones that - could be changed via param \code{colnames}. - \item The string "_all": all columns (i.e. assign a default) + \item 0 or a positive integer: column index counting from the left. + \item A negative integer: column index counting from the right. + \item A string: the column name. Note, it must be the names of the + original data, not the ones that (could) be changed via param \code{colnames}. + \item The string "_all": all columns (i.e. assign a default). } \item See \url{https://datatables.net/reference/option/columnDefs} for more. } From 2abdb466e955fc93bc7c1f2811ef9b4f4eb6f4c7 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 01:21:56 +0800 Subject: [PATCH 04/20] bumper version and update NEWS --- DESCRIPTION | 2 +- NEWS.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index e99ecbf8..35f542c8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: DT Type: Package Title: A Wrapper of the JavaScript Library 'DataTables' -Version: 0.20.1 +Version: 0.20.2 Authors@R: c( person("Yihui", "Xie", email = "xie@yihui.name", role = c("aut", "cre")), person("Joe", "Cheng", role = "aut"), diff --git a/NEWS.md b/NEWS.md index 62701854..1ed2fc8e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # CHANGES IN DT VERSION 0.21 +## MAJOR CHANGES + +- Now users can provide column names of the data to `options$columnDefs$targets`. Previously, it only supports column indexes or "_all" (thanks, @shrektan #948). # CHANGES IN DT VERSION 0.20 From 916a87ca82c615ac43ce7ca0f29b232c7e2aed86 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 11:37:54 +0800 Subject: [PATCH 05/20] implement formatter --- R/datatables.R | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/R/datatables.R b/R/datatables.R index 53b19071..7f280d58 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -107,6 +107,10 @@ #' columns and the text areas for some other columns by setting #' \code{editable} to a list of the form \code{list(target = TARGET, numeric #' = INDICES1, area = INDICES2)}. +#' @param formatter should be a named list of formatting functions. The formatting +#' function will be applied on the column of data with the same name. The raw value +#' of the column will be renamed to "_ORDERDATA_{COLUMNNAME}_" internally and will +#' be used for data sorting. #' @details \code{selection}: #' \enumerate{ #' \item The argument could be a scalar string, which means the selection @@ -172,7 +176,7 @@ datatable = function( fillContainer = getOption('DT.fillContainer', NULL), autoHideNavigation = getOption('DT.autoHideNavigation', NULL), selection = c('multiple', 'single', 'none'), extensions = list(), plugins = NULL, - editable = FALSE + editable = FALSE, formatter = NULL ) { # yes, we all hate it From d43023f2f1d2f0c4fbd8f55b65b0c13729461e52 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 11:39:25 +0800 Subject: [PATCH 06/20] extract targetIdx() functions --- R/datatables.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index 53b19071..da1f055d 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -498,6 +498,10 @@ classNameDefinedColumns = function(options, ncol) { unique(cols) } +targetIdx = function(targets, names, showRowName) { + unname(convertIdx(targets, names)) - !showRowName +} + colDefsTgtHandle = function(columnDefs, names, showRowName) { convert = function(targets, names, showRowName) { if (is.list(targets)) { @@ -510,7 +514,7 @@ colDefsTgtHandle = function(columnDefs, names, showRowName) { targets = list(targets[is_all], targets[!is_all]) out = lapply(targets, convert, names = names, showRowName = showRowName) } else { - out = unname(convertIdx(targets, names)) - !showRowName + out = targetIdx(targets, names, showRowName) } out } else { @@ -523,7 +527,6 @@ colDefsTgtHandle = function(columnDefs, names, showRowName) { }) } - # convert character indices to numeric convertIdx = function(i, names, n = length(names), invert = FALSE) { if (!is.character(i)) return({ From 6906c34a49968b6f3e609a75c42f0ef32f80a686 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 12:00:09 +0800 Subject: [PATCH 07/20] tweak the doc --- R/datatables.R | 20 ++++++++++++++++---- man/datatable.Rd | 20 +++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index c1d0ba4d..a47073fd 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -107,10 +107,9 @@ #' columns and the text areas for some other columns by setting #' \code{editable} to a list of the form \code{list(target = TARGET, numeric #' = INDICES1, area = INDICES2)}. -#' @param formatter should be a named list of formatting functions. The formatting -#' function will be applied on the column of data with the same name. The raw value -#' of the column will be renamed to "_ORDERDATA_{COLUMNNAME}_" internally and will -#' be used for data sorting. +#' @param formatter should be a named list of formatting functions. Users can use +#' arbitrage R formatting function to style the DT columns. See details for more +#' information. #' @details \code{selection}: #' \enumerate{ #' \item The argument could be a scalar string, which means the selection @@ -161,6 +160,19 @@ #' } #' \item See \url{https://datatables.net/reference/option/columnDefs} for more. #' } +#' \code{formatter}: +#' \enumerate{ +#' \item The formatting function must take a vector as input and return +#' a character vector (or can be converted into charactor vector via \code{as.character()}) +#' and it will be applied on the column of data with the same name. +#' \item The value applied the function will be store into the column, without \bold{escaping}. +#' Thus, if it's intent to be escaped please escape the value via `htmltools::HTML()` in +#' the function body. +#' \item The raw value of the column will be renamed to "_RAW_{COLUMNNAME}_" internally. +#' This is used for data sorting and will be set to invisible automatically. Thus, we can +#' preserve the same order when sorting the columns, as if they're still in the raw value. +#' \item The default text-align of column will be decided by the raw value. +#' } #' @note You are recommended to escape the table content for security reasons #' (e.g. XSS attacks) when using this function in Shiny or any other dynamic #' web applications. diff --git a/man/datatable.Rd b/man/datatable.Rd index d587e5c9..c263defb 100644 --- a/man/datatable.Rd +++ b/man/datatable.Rd @@ -24,7 +24,8 @@ datatable( selection = c("multiple", "single", "none"), extensions = list(), plugins = NULL, - editable = FALSE + editable = FALSE, + formatter = NULL ) } \arguments{ @@ -151,6 +152,10 @@ all columns. Of course, you can request the numeric editing for some columns and the text areas for some other columns by setting \code{editable} to a list of the form \code{list(target = TARGET, numeric = INDICES1, area = INDICES2)}.} + +\item{formatter}{should be a named list of formatting functions. Users can use +arbitrage R formatting function to style the DT columns. See details for more +information.} } \description{ This function creates an HTML widget to display rectangular data (a matrix or @@ -207,6 +212,19 @@ data frame) using the JavaScript library DataTables. } \item See \url{https://datatables.net/reference/option/columnDefs} for more. } + \code{formatter}: + \enumerate{ + \item The formatting function must take a vector as input and return + a character vector (or can be converted into charactor vector via \code{as.character()}) + and it will be applied on the column of data with the same name. + \item The value applied the function will be store into the column, without \bold{escaping}. + Thus, if it's intent to be escaped please escape the value via `htmltools::HTML()` in + the function body. + \item The raw value of the column will be renamed to "_RAW_{COLUMNNAME}_" internally. + This is used for data sorting and will be set to invisible automatically. Thus, we can + preserve the same order when sorting the columns, as if they're still in the raw value. + \item The default text-align of column will be decided by the raw value. + } } \note{ You are recommended to escape the table content for security reasons From 6339c337e703a6ed23d8f98dcb1dbadd3d69e15c Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 12:16:11 +0800 Subject: [PATCH 08/20] implement applyFormatter() --- R/datatables.R | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/R/datatables.R b/R/datatables.R index a47073fd..45b9db46 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -595,6 +595,35 @@ sameSign = function(x, zero = 0L) { length(unique(as.vector(sign))) == 1L } +applyFormatter = function(data, formatter) { + if (!length(formatter)) return(data) + is_fun = vapply(formatter, is.function, TRUE) + if (any(!is_fun)) stop(sprintf( + "The formatter values at indexes %s are not functions", + toString(which(!is_fun)) + ), call. = FALSE) + format_cols = names(formatter) + raw_cols = sprintf("_RAW_%s_", format_cols) + col_exists = rep(TRUE, length(formatter)) + for (i in seq_along(formatter)) { + format_col = format_cols[i] + if (!format_col %in% colnames(data)) { + col_exists[i] = FALSE + next + } + raw_col = raw_cols[i] + format_fun = formatter[[i]] + raw_value = data[[format_col]] + data[[raw_col]] = raw_value + data[[format_col]] = as.character(format_fun(raw_value)) + } + raw_cols = raw_cols[col_exists] + format_cols = format_cols[col_exists] + attr(data, "DT.raw.cols") = raw_cols + attr(data, "DT.format.cols") = format_cols + data +} + #' Generate a table header or footer from column names #' #' Convenience functions to generate a table header (\samp{}) or From 9173171512ed2b4bef891e0abd00977b6536ac3f Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 12:47:13 +0800 Subject: [PATCH 09/20] tweak applyFormatter() --- R/datatables.R | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index 45b9db46..77d663dc 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -602,19 +602,18 @@ applyFormatter = function(data, formatter) { "The formatter values at indexes %s are not functions", toString(which(!is_fun)) ), call. = FALSE) - format_cols = names(formatter) - raw_cols = sprintf("_RAW_%s_", format_cols) + raw_cols = names(formatter) + format_cols = sprintf("_FORMAT_%s_", format_cols) col_exists = rep(TRUE, length(formatter)) for (i in seq_along(formatter)) { - format_col = format_cols[i] - if (!format_col %in% colnames(data)) { + raw_col = raw_cols[i] + if (!raw_col %in% colnames(data)) { col_exists[i] = FALSE next } - raw_col = raw_cols[i] + format_col = format_cols[i] format_fun = formatter[[i]] - raw_value = data[[format_col]] - data[[raw_col]] = raw_value + raw_value = data[[raw_col]] data[[format_col]] = as.character(format_fun(raw_value)) } raw_cols = raw_cols[col_exists] From a4b23dda54cf692abe0f7617fba27ba94a6f0332 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 12:51:27 +0800 Subject: [PATCH 10/20] tweak the doc --- R/datatables.R | 10 +++++----- man/datatable.Rd | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index 77d663dc..03e9aa1b 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -164,14 +164,14 @@ #' \enumerate{ #' \item The formatting function must take a vector as input and return #' a character vector (or can be converted into charactor vector via \code{as.character()}) -#' and it will be applied on the column of data with the same name. +#' and it will be applied on the column of data with the same name. Unnamed or non-exists +#' values will be omited. #' \item The value applied the function will be store into the column, without \bold{escaping}. #' Thus, if it's intent to be escaped please escape the value via `htmltools::HTML()` in #' the function body. -#' \item The raw value of the column will be renamed to "_RAW_{COLUMNNAME}_" internally. -#' This is used for data sorting and will be set to invisible automatically. Thus, we can -#' preserve the same order when sorting the columns, as if they're still in the raw value. -#' \item The default text-align of column will be decided by the raw value. +#' \item The formatted value of the column will be renamed to "_FORMAT_{COLUMNNAME}_" internally. +#' Thus, DataTables can read the formatted values when rendering. This will be set +#' to invisible automatically so that the users won't see them. #' } #' @note You are recommended to escape the table content for security reasons #' (e.g. XSS attacks) when using this function in Shiny or any other dynamic diff --git a/man/datatable.Rd b/man/datatable.Rd index c263defb..55c57cab 100644 --- a/man/datatable.Rd +++ b/man/datatable.Rd @@ -216,14 +216,14 @@ data frame) using the JavaScript library DataTables. \enumerate{ \item The formatting function must take a vector as input and return a character vector (or can be converted into charactor vector via \code{as.character()}) - and it will be applied on the column of data with the same name. + and it will be applied on the column of data with the same name. Unnamed or non-exists + values will be omited. \item The value applied the function will be store into the column, without \bold{escaping}. Thus, if it's intent to be escaped please escape the value via `htmltools::HTML()` in the function body. - \item The raw value of the column will be renamed to "_RAW_{COLUMNNAME}_" internally. - This is used for data sorting and will be set to invisible automatically. Thus, we can - preserve the same order when sorting the columns, as if they're still in the raw value. - \item The default text-align of column will be decided by the raw value. + \item The formatted value of the column will be renamed to "_FORMAT_{COLUMNNAME}_" internally. + Thus, DataTables can read the formatted values when rendering. This will be set + to invisible automatically so that the users won't see them. } } \note{ From 3b59cc5f5b33f6371b16f83b935f1b8d0b28c485 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 13:04:43 +0800 Subject: [PATCH 11/20] misc: unname vapply() in jsValues() --- R/format.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/format.R b/R/format.R index ef377318..ecd1b090 100644 --- a/R/format.R +++ b/R/format.R @@ -290,7 +290,7 @@ jsValues = function(x) { } else if (inherits(x, "Date")) { x = format(x, "%Y-%m-%d") } - vapply(x, jsonlite::toJSON, character(1), auto_unbox = TRUE) + vapply(x, jsonlite::toJSON, character(1), auto_unbox = TRUE, USE.NAMES = FALSE) } From 2258382e827aa8d2a16ef2ceed5901704924a63e Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 16:12:12 +0800 Subject: [PATCH 12/20] draft impl --- R/datatables.R | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/R/datatables.R b/R/datatables.R index 03e9aa1b..b75cf518 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -262,6 +262,20 @@ datatable = function( # disable CSS classes for ordered columns if (is.null(options[['orderClasses']])) options$orderClasses = FALSE + data = applyFormatter(data, formatter) + format_cols = attr(data, "DT.format.cols", exact = TRUE) + raw_cols = attr(data, "DT.raw.cols", exact = TRUE) + if (length(format_cols)) options = appendColumnDefs(options, list( + visible = FALSE, targets = targetIdx(format_cols, base::colnames(data), showRowName = !is.null(rn)) + )) + options$columnDefs = append( + options$columnDefs, colFormatter( + raw_cols, base::colnames(data), rownames = !is.null(rn), template = function(format_col_idx) { + sprintf("row[%d];", format_col_idx) + }, targetIdx(format_cols, base::colnames(data), showRowName = !is.null(rn)) + ), after = 0L + ) + cn = base::colnames(data) if (missing(colnames)) { colnames = cn @@ -603,7 +617,7 @@ applyFormatter = function(data, formatter) { toString(which(!is_fun)) ), call. = FALSE) raw_cols = names(formatter) - format_cols = sprintf("_FORMAT_%s_", format_cols) + format_cols = sprintf("_FORMAT_%s_", raw_cols) col_exists = rep(TRUE, length(formatter)) for (i in seq_along(formatter)) { raw_col = raw_cols[i] From 72455efc7a880667aa88485ab734fe6423513dbb Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 16:30:47 +0800 Subject: [PATCH 13/20] simplify implementation --- R/datatables.R | 26 ++++++++++++++------------ tests/testit/test-datatables.R | 19 ++++--------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index da1f055d..97b4ffa0 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -210,9 +210,6 @@ datatable = function( stop("'data' must be 2-dimensional (e.g. data frame or matrix)") } - # convert the targets - options[["columnDefs"]] = colDefsTgtHandle(options[["columnDefs"]], base::colnames(data), !is.null(rn)) - if (is.data.frame(data)) { data = as.data.frame(data) numc = unname(which(vapply(data, is.numeric, logical(1)))) @@ -228,6 +225,11 @@ datatable = function( data = cbind(' ' = rn, data) numc = numc + 1 # move indices of numeric columns to the right by 1 } + cn = base::colnames(data) + # ===== data, rn, cn has been prepared ======== # + + # convert the string targets + options[["columnDefs"]] = colDefsTgtHandle(options[["columnDefs"]], cn) # align numeric columns to the right if (length(numc)) { @@ -246,7 +248,6 @@ datatable = function( # disable CSS classes for ordered columns if (is.null(options[['orderClasses']])) options$orderClasses = FALSE - cn = base::colnames(data) if (missing(colnames)) { colnames = cn } else if (!is.null(names(colnames))) { @@ -498,23 +499,24 @@ classNameDefinedColumns = function(options, ncol) { unique(cols) } -targetIdx = function(targets, names, showRowName) { - unname(convertIdx(targets, names)) - !showRowName +targetIdx = function(targets, names) { + # return the js side idx which starts from zero + unname(convertIdx(targets, names)) - 1L } -colDefsTgtHandle = function(columnDefs, names, showRowName) { - convert = function(targets, names, showRowName) { +colDefsTgtHandle = function(columnDefs, names) { + convert = function(targets, names) { if (is.list(targets)) { - lapply(targets, convert, names = names, showRowName = showRowName) + lapply(targets, convert, names = names) } else if (is.character(targets)) { is_all = targets == "_all" if (all(is_all)) { out = "_all" } else if (any(is_all)) { targets = list(targets[is_all], targets[!is_all]) - out = lapply(targets, convert, names = names, showRowName = showRowName) + out = lapply(targets, convert, names = names) } else { - out = targetIdx(targets, names, showRowName) + out = targetIdx(targets, names) } out } else { @@ -522,7 +524,7 @@ colDefsTgtHandle = function(columnDefs, names, showRowName) { } } lapply(columnDefs, function(x) { - x[["targets"]] = convert(x[["targets"]], names, showRowName) + x[["targets"]] = convert(x[["targets"]], names) x }) } diff --git a/tests/testit/test-datatables.R b/tests/testit/test-datatables.R index b54412d6..529b2852 100644 --- a/tests/testit/test-datatables.R +++ b/tests/testit/test-datatables.R @@ -195,8 +195,8 @@ assert('warn autoHideNavigation if no pageLength', { assert("colDefsTgtHandle() works", { cols = c("A", "B", "C") - (colDefsTgtHandle(NULL, cols, TRUE) %==% list()) - (colDefsTgtHandle(NULL, cols, FALSE) %==% list()) + (colDefsTgtHandle(NULL, cols) %==% list()) + (colDefsTgtHandle(NULL, cols) %==% list()) defs = list( list(1, targets = "_all"), @@ -208,17 +208,7 @@ assert("colDefsTgtHandle() works", { list(7, targets = list(1L, "C")), list(8, targets = list(1L, "B", "_all")) ) - res1 = list( - list(1, targets = "_all"), - list(2, targets = 1L), - list(3, targets = 2L), - list(4, targets = list("_all", 1L)), - list(5, targets = list(c(1L, 3L), "_all")), - list(6, targets = list(1L, "_all")), - list(7, targets = list(1L, 3L)), - list(8, targets = list(1L, 2L, "_all")) - ) - res2 = list( + res = list( list(1, targets = "_all"), list(2, targets = 1L), list(3, targets = 1L), @@ -228,6 +218,5 @@ assert("colDefsTgtHandle() works", { list(7, targets = list(1L, 2L)), list(8, targets = list(1L, 1L, "_all")) ) - (colDefsTgtHandle(defs, cols, TRUE) %==% res1) - (colDefsTgtHandle(defs, cols, FALSE) %==% res2) + (colDefsTgtHandle(defs, cols) %==% res) }) From 914a8889936c7ab18f4eb9f728daeb2170e6165b Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 16:47:15 +0800 Subject: [PATCH 14/20] 2nd draft --- R/datatables.R | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index 0b6adb34..2bf4b575 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -241,11 +241,9 @@ datatable = function( data = cbind(' ' = rn, data) numc = numc + 1 # move indices of numeric columns to the right by 1 } - cn = base::colnames(data) - # ===== data, rn, cn has been prepared ======== # # convert the string targets - options[["columnDefs"]] = colDefsTgtHandle(options[["columnDefs"]], cn) + options[["columnDefs"]] = colDefsTgtHandle(options[["columnDefs"]], base::colnames(data)) # align numeric columns to the right if (length(numc)) { @@ -264,20 +262,24 @@ datatable = function( # disable CSS classes for ordered columns if (is.null(options[['orderClasses']])) options$orderClasses = FALSE - data = applyFormatter(data, formatter) - format_cols = attr(data, "DT.format.cols", exact = TRUE) - raw_cols = attr(data, "DT.raw.cols", exact = TRUE) - if (length(format_cols)) options = appendColumnDefs(options, list( - visible = FALSE, targets = targetIdx(format_cols, base::colnames(data), showRowName = !is.null(rn)) - )) - options$columnDefs = append( - options$columnDefs, colFormatter( - raw_cols, base::colnames(data), rownames = !is.null(rn), template = function(format_col_idx) { - sprintf("row[%d];", format_col_idx) - }, targetIdx(format_cols, base::colnames(data), showRowName = !is.null(rn)) - ), after = 0L - ) + formated = applyFormatter(data, formatter) + data = formated$data + if (length(formated$format_cols)) { + format_col_idx = targetIdx(formated$format_cols, base::colnames(data)) + raw_col_idx = targetIdx(formated$raw_cols, base::colnames(data)) + options = appendColumnDefs(options, list( + visible = FALSE, targets = format_col_idx + )) + for (i in seq_along(formated$format_cols)) options = appendColumnDefs(options, list( + targets = raw_col_idx[i], + render = JS(sprintf( + "function(data,type,row,meta) {return type!=='display'?data:row[%d];}", + format_col_idx[i] + )) + )) + } + cn = base::colnames(data) if (missing(colnames)) { colnames = cn } else if (!is.null(names(colnames))) { @@ -612,12 +614,14 @@ sameSign = function(x, zero = 0L) { } applyFormatter = function(data, formatter) { - if (!length(formatter)) return(data) + if (!length(formatter)) return(list(data = data)) + is_fun = vapply(formatter, is.function, TRUE) if (any(!is_fun)) stop(sprintf( "The formatter values at indexes %s are not functions", toString(which(!is_fun)) ), call. = FALSE) + raw_cols = names(formatter) format_cols = sprintf("_FORMAT_%s_", raw_cols) col_exists = rep(TRUE, length(formatter)) @@ -634,9 +638,7 @@ applyFormatter = function(data, formatter) { } raw_cols = raw_cols[col_exists] format_cols = format_cols[col_exists] - attr(data, "DT.raw.cols") = raw_cols - attr(data, "DT.format.cols") = format_cols - data + list(data = data, raw_cols = raw_cols, format_cols = format_cols) } #' Generate a table header or footer from column names From 707792471fefaae1d49d11ac9237134eb0216eab Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 16:57:11 +0800 Subject: [PATCH 15/20] refactor --- R/datatables.R | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index 2bf4b575..5c939fe2 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -262,19 +262,18 @@ datatable = function( # disable CSS classes for ordered columns if (is.null(options[['orderClasses']])) options$orderClasses = FALSE - formated = applyFormatter(data, formatter) - data = formated$data - if (length(formated$format_cols)) { - format_col_idx = targetIdx(formated$format_cols, base::colnames(data)) - raw_col_idx = targetIdx(formated$raw_cols, base::colnames(data)) + data = applyFormatter(data, formatter) + fmt_idx = attr(data, "DT.format.idx", exact = TRUE) + attr(data, "DT.format.idx") = NULL + if (length(fmt_idx$raw)) { options = appendColumnDefs(options, list( - visible = FALSE, targets = format_col_idx + visible = FALSE, targets = fmt_idx$format )) - for (i in seq_along(formated$format_cols)) options = appendColumnDefs(options, list( - targets = raw_col_idx[i], + for (i in seq_along(fmt_idx$format)) options = appendColumnDefs(options, list( + targets = fmt_idx$raw[i], render = JS(sprintf( "function(data,type,row,meta) {return type!=='display'?data:row[%d];}", - format_col_idx[i] + fmt_idx$format[i] )) )) } @@ -614,7 +613,7 @@ sameSign = function(x, zero = 0L) { } applyFormatter = function(data, formatter) { - if (!length(formatter)) return(list(data = data)) + if (!length(formatter)) return(data) is_fun = vapply(formatter, is.function, TRUE) if (any(!is_fun)) stop(sprintf( @@ -636,9 +635,11 @@ applyFormatter = function(data, formatter) { raw_value = data[[raw_col]] data[[format_col]] = as.character(format_fun(raw_value)) } - raw_cols = raw_cols[col_exists] - format_cols = format_cols[col_exists] - list(data = data, raw_cols = raw_cols, format_cols = format_cols) + attr(data, "DT.format.idx") = list( + raw = targetIdx(raw_cols[col_exists], base::colnames(data)), + format = targetIdx(format_cols[col_exists], base::colnames(data)) + ) + data } #' Generate a table header or footer from column names From 0c1f1ab0e203020b057c96917d1263e08971853d Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 17:19:31 +0800 Subject: [PATCH 16/20] handle escape --- R/datatables.R | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/R/datatables.R b/R/datatables.R index 5c939fe2..e8900c9b 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -241,6 +241,7 @@ datatable = function( data = cbind(' ' = rn, data) numc = numc + 1 # move indices of numeric columns to the right by 1 } + escape = makeLogicalEscape(escape, base::colnames(data)) # convert the string targets options[["columnDefs"]] = colDefsTgtHandle(options[["columnDefs"]], base::colnames(data)) @@ -266,6 +267,8 @@ datatable = function( fmt_idx = attr(data, "DT.format.idx", exact = TRUE) attr(data, "DT.format.idx") = NULL if (length(fmt_idx$raw)) { + # escape now a logical vector and we can append FALSE value after it + escape = c(escape, rep(FALSE, length(unique(fmt_idx$format)))) options = appendColumnDefs(options, list( visible = FALSE, targets = fmt_idx$format )) @@ -604,6 +607,17 @@ escapeToConfig = function(escape, colnames) { sprintf('"%s"', paste(escape, collapse = ',')) } +# `escape` can take many forms, making it difficult to process later, thus +# we standardize it into a logical vector +makeLogicalEscape = function(escape, colnames) { + out = rep(FALSE, length(colnames)) + if (isTRUE(escape)) out[] = TRUE + if (isFALSE(escape)) { } # do nothing + if (!is.numeric(escape)) out[convertIdx(escape, colnames)] = TRUE + if (is.logical(escape)) out[which(escape)] = TRUE + out +} + sameSign = function(x, zero = 0L) { if (length(x) == 0L) return(TRUE) if (is.list(x)) return(all(vapply(x, sameSign, TRUE, zero = zero))) From 5ffbb23c44159e6cb5b3c98aa35052707e4cca3e Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 17:58:06 +0800 Subject: [PATCH 17/20] correct doc --- R/datatables.R | 2 +- man/datatable.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index e8900c9b..9c6f138e 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -167,7 +167,7 @@ #' and it will be applied on the column of data with the same name. Unnamed or non-exists #' values will be omited. #' \item The value applied the function will be store into the column, without \bold{escaping}. -#' Thus, if it's intent to be escaped please escape the value via `htmltools::HTML()` in +#' Thus, if it's intent to be escaped please escape the value via `htmltools::htmlEscape()` in #' the function body. #' \item The formatted value of the column will be renamed to "_FORMAT_{COLUMNNAME}_" internally. #' Thus, DataTables can read the formatted values when rendering. This will be set diff --git a/man/datatable.Rd b/man/datatable.Rd index 55c57cab..9e880a2a 100644 --- a/man/datatable.Rd +++ b/man/datatable.Rd @@ -219,7 +219,7 @@ data frame) using the JavaScript library DataTables. and it will be applied on the column of data with the same name. Unnamed or non-exists values will be omited. \item The value applied the function will be store into the column, without \bold{escaping}. - Thus, if it's intent to be escaped please escape the value via `htmltools::HTML()` in + Thus, if it's intent to be escaped please escape the value via `htmltools::htmlEscape()` in the function body. \item The formatted value of the column will be renamed to "_FORMAT_{COLUMNNAME}_" internally. Thus, DataTables can read the formatted values when rendering. This will be set From 43854d06f4f9df0dd736c8672f84f57a37730cd9 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 18:08:08 +0800 Subject: [PATCH 18/20] refactor --- R/datatables.R | 59 +++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index 9c6f138e..3836a525 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -263,22 +263,12 @@ datatable = function( # disable CSS classes for ordered columns if (is.null(options[['orderClasses']])) options$orderClasses = FALSE - data = applyFormatter(data, formatter) - fmt_idx = attr(data, "DT.format.idx", exact = TRUE) - attr(data, "DT.format.idx") = NULL - if (length(fmt_idx$raw)) { + data = applyFormatter(data, formatter, options) + options = attr(data, "DT.format.options", exact = TRUE) + attr(data, "DT.format.options") = NULL + if (ncol(data) - length(escape)>0) { # escape now a logical vector and we can append FALSE value after it - escape = c(escape, rep(FALSE, length(unique(fmt_idx$format)))) - options = appendColumnDefs(options, list( - visible = FALSE, targets = fmt_idx$format - )) - for (i in seq_along(fmt_idx$format)) options = appendColumnDefs(options, list( - targets = fmt_idx$raw[i], - render = JS(sprintf( - "function(data,type,row,meta) {return type!=='display'?data:row[%d];}", - fmt_idx$format[i] - )) - )) + escape = c(escape, rep(FALSE, ncol(data) - length(escape))) } cn = base::colnames(data) @@ -626,33 +616,42 @@ sameSign = function(x, zero = 0L) { length(unique(as.vector(sign))) == 1L } -applyFormatter = function(data, formatter) { - if (!length(formatter)) return(data) - +applyFormatter = function(data, formatter, options) { is_fun = vapply(formatter, is.function, TRUE) if (any(!is_fun)) stop(sprintf( "The formatter values at indexes %s are not functions", toString(which(!is_fun)) ), call. = FALSE) + opt_attr = "DT.format.options" + attr(data, opt_attr) = options + # only keep formatter with valid names + formatter = formatter[names(formatter) %in% colnames(data)] + if (!length(formatter)) return(data) + raw_cols = names(formatter) - format_cols = sprintf("_FORMAT_%s_", raw_cols) - col_exists = rep(TRUE, length(formatter)) + format_cols = sprintf("_FORMAT_%s_", htmlEscape(raw_cols)) for (i in seq_along(formatter)) { raw_col = raw_cols[i] - if (!raw_col %in% colnames(data)) { - col_exists[i] = FALSE - next - } format_col = format_cols[i] format_fun = formatter[[i]] - raw_value = data[[raw_col]] - data[[format_col]] = as.character(format_fun(raw_value)) + # so that the function can be applied recursively + value = if (is.null(data[[format_col]])) data[[raw_col]] else data[[format_col]] + data[[format_col]] = as.character(format_fun(value)) } - attr(data, "DT.format.idx") = list( - raw = targetIdx(raw_cols[col_exists], base::colnames(data)), - format = targetIdx(format_cols[col_exists], base::colnames(data)) - ) + raw_idx = targetIdx(raw_cols, base::colnames(data)) + fmt_idx = targetIdx(format_cols, base::colnames(data)) + options = appendColumnDefs(options, list( + visible = FALSE, targets = fmt_idx + )) + for (i in seq_along(fmt_idx)) options = appendColumnDefs(options, list( + targets = raw_idx[i], + render = JS(sprintf( + "function(data,type,row,meta) {return type!=='display'?data:row[%d];}", + fmt_idx[i] + )) + )) + attr(data, opt_attr) = options data } From b9ffaeb8c59e48939bb8ef6559b5751de4591ee0 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 18:17:41 +0800 Subject: [PATCH 19/20] rm duplicates --- R/datatables.R | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/R/datatables.R b/R/datatables.R index 3836a525..ff320f1a 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -637,10 +637,13 @@ applyFormatter = function(data, formatter, options) { format_fun = formatter[[i]] # so that the function can be applied recursively value = if (is.null(data[[format_col]])) data[[raw_col]] else data[[format_col]] - data[[format_col]] = as.character(format_fun(value)) + data[[format_col]] = format_fun(value) } - raw_idx = targetIdx(raw_cols, base::colnames(data)) - fmt_idx = targetIdx(format_cols, base::colnames(data)) + + # There probably be duplicated cols, but we only apply them for columnDefs once + unique_cols = unique(cbind(raw_cols, format_cols)) + raw_idx = targetIdx(unique_cols[, 1], base::colnames(data)) + fmt_idx = targetIdx(unique_cols[, 2], base::colnames(data)) options = appendColumnDefs(options, list( visible = FALSE, targets = fmt_idx )) From 69aced4c5b1d5f85545b758e8ad981733ef5b3a8 Mon Sep 17 00:00:00 2001 From: shrektan Date: Sun, 5 Dec 2021 19:02:03 +0800 Subject: [PATCH 20/20] convert fmt cols into character explicitly --- R/datatables.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/datatables.R b/R/datatables.R index ff320f1a..9a312a66 100644 --- a/R/datatables.R +++ b/R/datatables.R @@ -644,6 +644,8 @@ applyFormatter = function(data, formatter, options) { unique_cols = unique(cbind(raw_cols, format_cols)) raw_idx = targetIdx(unique_cols[, 1], base::colnames(data)) fmt_idx = targetIdx(unique_cols[, 2], base::colnames(data)) + # must convert into character explicilty so that functions like formattable::percent can work + data[, fmt_idx] = lapply(data[, fmt_idx, drop = FALSE], as.character) options = appendColumnDefs(options, list( visible = FALSE, targets = fmt_idx ))