Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:

name: R-CMD-check.yaml

permissions: read-all

jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}

name: ${{ matrix.config.os }} (${{ matrix.config.r }})

strategy:
fail-fast: false
matrix:
config:
- {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-latest, r: 'oldrel-1'}

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes

steps:
- uses: actions/checkout@v4

- uses: r-lib/actions/setup-pandoc@v2

- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
http-user-agent: ${{ matrix.config.http-user-agent }}
use-public-rspm: true

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck
needs: check

- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ Suggests:
testthat (>= 3.1.0)
VignetteBuilder: knitr
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
Config/testthat/edition: 3
Config/roxygen2/version: 8.0.0
11 changes: 11 additions & 0 deletions R/dt2.R
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ print.dt2_theme <- function(x, ...) {
cat(" font_scale =", x$font_scale, "\n")
cat(" style =", x$style, "\n")
cat(" button_class =", x$button_class, "\n")
cat(" class =", x$class %||% "<none>", "\n")
invisible(x)
}

Expand Down Expand Up @@ -283,6 +284,16 @@ dt2 <- function(data,
options$responsive <- NULL # ensure extension is not loaded
}

# ---- Column names -----------------------------------------------------------
# Expose the data's column names as options$columns when the user didn't set
# them. This is the canonical list that name-based helpers resolve against and
# is equivalent to the column list dt2.js derives client-side, so it does not
# change rendering -- it just makes the names available downstream.
if (is.null(options$columns)) {
cn <- colnames(data)
if (!is.null(cn)) options$columns <- cn
}

# ---- Extensions auto-detect ------------------------------------------------
if (is.null(extensions)) {
extensions <- .dt2_detect_extensions(options)
Expand Down
6 changes: 6 additions & 0 deletions R/dt2_buttons.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
#' buttons container. If provided, DT2 will move the rendered buttons to that container after init.
#' @return The modified `options` list.
#' @details Requires the **Buttons** extension. For CSV/Excel/PDF you also need **JSZip** and **pdfMake** (incl. `vfs_fonts`).
#'
#' Prefer [dt2_use_buttons()] for the common case: it takes simple button ids,
#' styles them with a CSS class, and places them in the layout. Use
#' `dt2_buttons()` when you need full button objects or want to move the
#' rendered buttons into a custom container via `target`.
#' @seealso [dt2_use_buttons()]
#' @export
dt2_buttons <- function(options = list(),
buttons = c("copyHtml5", "csvHtml5", "excelHtml5", "pdfHtml5", "print"),
Expand Down
40 changes: 16 additions & 24 deletions R/dt2_formats.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,14 @@
dt2_format_number <- function(options = list(), col_specs,
thousands = NULL, decimal = NULL,
digits = 0, prefix = "", prefix_right = "") {
#`%||%` <- function(a, b) if (is.null(a)) b else a
if (is.character(col_specs)) col_specs <- match(col_specs, options$columns)
col_specs <- .dt2_name_to_idx(col_specs, options)
# DataTables v2 sugere DataTable.render.number(...)
# https://datatables.net/manual/data/renderers
js <- htmlwidgets::JS(
sprintf("DataTable.render.number(%s,%s,%d,%s,%s)",
if (is.null(thousands)) "null" else sprintf("'%s'", thousands),
if (is.null(decimal)) "null" else sprintf("'%s'", decimal),
.dt2_js_str(thousands), .dt2_js_str(decimal),
as.integer(digits),
sprintf("'%s'", prefix),
sprintf("'%s'", prefix_right))
.dt2_js_str(prefix), .dt2_js_str(prefix_right))
)
cds <- lapply(col_specs, function(i) list(targets = i - 1L, render = js))
options$columnDefs <- c(options$columnDefs %||% list(), cds)
Expand Down Expand Up @@ -57,14 +54,13 @@ dt2_format_number <- function(options = list(), col_specs,
dt2_format_datetime <- function(options = list(), col_specs,
from = NULL, to = "DD/MM/YYYY",
locale = NULL, def = NULL) {
#`%||%` <- function(a, b) if (is.null(a)) b else a
if (is.character(col_specs)) col_specs <- match(col_specs, options$columns)
col_specs <- .dt2_name_to_idx(col_specs, options)

args <- c(
if (is.null(from)) "undefined" else sprintf("'%s'", from),
if (is.null(to)) "undefined" else sprintf("'%s'", to),
if (is.null(locale)) "undefined" else sprintf("'%s'", locale),
if (is.null(def)) "undefined" else sprintf("'%s'", def)
.dt2_js_str(from, "undefined"),
.dt2_js_str(to, "undefined"),
.dt2_js_str(locale, "undefined"),
.dt2_js_str(def, "undefined")
)
# ver docs: https://datatables.net/plug-ins/dataRender/datetime + manual de renderers
js <- htmlwidgets::JS(sprintf("DataTable.render.datetime(%s)", paste(args, collapse = ", ")))
Expand All @@ -88,8 +84,7 @@ dt2_format_datetime <- function(options = list(), col_specs,
#' @seealso \url{https://datatables.net/reference/option/columns.render}
#' @export
dt2_cols_render_js <- function(options = list(), col_specs, js_render) {
#`%||%` <- function(a, b) if (is.null(a)) b else a
if (is.character(col_specs)) col_specs <- match(col_specs, options$columns)
col_specs <- .dt2_name_to_idx(col_specs, options)
stopifnot(inherits(js_render, "JS_EVAL"))
cds <- lapply(col_specs, function(i) list(targets = i - 1L, render = js_render))
options$columnDefs <- c(options$columnDefs %||% list(), cds)
Expand Down Expand Up @@ -121,8 +116,7 @@ dt2_cols_render_js <- function(options = list(), col_specs, js_render) {
dt2_cols_render_orthogonal <- function(options = list(), col_specs,
display = NULL, sort = NULL,
filter = NULL, type = NULL) {
#`%||%` <- function(a, b) if (is.null(a)) b else a
if (is.character(col_specs)) col_specs <- match(col_specs, options$columns)
col_specs <- .dt2_name_to_idx(col_specs, options)


# Build an {display, sort, filter, type} object with the supplied parts
Expand Down Expand Up @@ -159,8 +153,7 @@ dt2_cols_render_orthogonal <- function(options = list(), col_specs,
#' opts <- list(columns = names(mtcars))
#' opts <- dt2_format_number_abbrev(opts, c("hp","qsec"), digits = 1, locale = "pt-BR")
dt2_format_number_abbrev <- function(options = list(), col_specs, digits = 1, locale = NULL) {
`%||%` <- function(a, b) if (is.null(a)) b else a
if (is.character(col_specs)) col_specs <- match(col_specs, options$columns)
col_specs <- .dt2_name_to_idx(col_specs, options)

# JS renderer: abrevia com k/M/B e aplica toLocaleString(locale) na parte inteira se locale fornecido
js <- if (is.null(locale) || !nzchar(locale)) {
Expand Down Expand Up @@ -206,17 +199,17 @@ dt2_format_number_abbrev <- function(options = list(), col_specs, digits = 1, lo
#' @export
dt2_format_time_format <- function(options = list(), col_specs,
from = NULL, to = "L", locale = "pt-br") {
`%||%` <- function(a, b) if (is.null(a)) b else a
if (is.character(col_specs)) col_specs <- match(col_specs, options$columns)
col_specs <- .dt2_name_to_idx(col_specs, options)

# ativa locale no cliente (fallback para renders que usem moment direto)
options$`_momentLocale` <- locale

# DataTables v2: DataTable.render.datetime(from, to, locale)
renderer_call <- if (is.null(from)) {
sprintf("DataTable.render.datetime('%s','%s')", to, locale)
sprintf("DataTable.render.datetime(%s,%s)", .dt2_js_str(to), .dt2_js_str(locale))
} else {
sprintf("DataTable.render.datetime('%s','%s','%s')", from, to, locale)
sprintf("DataTable.render.datetime(%s,%s,%s)",
.dt2_js_str(from), .dt2_js_str(to), .dt2_js_str(locale))
}
render <- htmlwidgets::JS(renderer_call)

Expand All @@ -232,8 +225,7 @@ dt2_format_time_format <- function(options = list(), col_specs,
#' @return The modified `options` list with an updated `columnDefs` entry.
#' @export
dt2_format_time_relative <- function(options = list(), col_specs, locale = "pt-br") {
`%||%` <- function(a, b) if (is.null(a)) b else a
if (is.character(col_specs)) col_specs <- match(col_specs, options$columns)
col_specs <- .dt2_name_to_idx(col_specs, options)

# ativa locale no cliente (usado por dt2.js)
options$`_momentLocale` <- locale
Expand Down
4 changes: 2 additions & 2 deletions R/dt2_html.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#' @return Updated `options`.
#' @export
dt2_cols_html <- function(options = list(), cols, js_render) {
if (is.character(cols)) cols <- match(cols, options$columns)
cols <- .dt2_name_to_idx(cols, options)
cds <- lapply(cols, function(i) list(
targets = i - 1L,
render = js_render
Expand All @@ -25,7 +25,7 @@ dt2_cols_html <- function(options = list(), cols, js_render) {
#' @return Updated `options`.
#' @export
dt2_col_template <- function(options = list(), col, template) {
if (is.character(col)) col <- match(col, options$columns)
col <- .dt2_name_to_idx(col, options)
js <- htmlwidgets::JS(
sprintf(
"function(d,t,row,meta){ if(t!=='display') return d; var html=%s; return html.replace(/\\{\\{VAL\\}\\}/g, d); }",
Expand Down
4 changes: 2 additions & 2 deletions R/dt2_inputs.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#' @return Updated `options`.
#' @export
dt2_col_checkbox <- function(options = list(), col, input_id_prefix = "row_chk_", value_col = NULL) {
if (is.character(col)) col <- match(col, options$columns)
col <- .dt2_name_to_idx(col, options)

# HÍBRIDO: lê por índice (array) OU por nome (objeto)
if (is.null(value_col)) {
Expand Down Expand Up @@ -45,7 +45,7 @@ dt2_col_checkbox <- function(options = list(), col, input_id_prefix = "row_chk_"
#' @return Updated `options`.
#' @export
dt2_col_button <- function(options = list(), col, label = "Action", input_id_prefix = "row_btn_") {
if (is.character(col)) col <- match(col, options$columns)
col <- .dt2_name_to_idx(col, options)
js <- htmlwidgets::JS(sprintf(
"function(d,t,row,meta){ if(t!=='display') return d;
var rid = '%s' + (meta.row+1);
Expand Down
48 changes: 41 additions & 7 deletions R/dt2_options.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
#' @param ... Vectors like `c(col, "asc"/"desc")`. `col` may be name or 1-based index.
#' @return Updated `options`.
#' @export
#' @examples
#' opts <- list(columns = names(mtcars))
#' opts <- dt2_order(opts, c("mpg", "desc"))
#' dt2(mtcars, options = opts)
dt2_order <- function(options = list(), ...) {
ord <- lapply(list(...), function(x) {
idx <- if (is.character(x[[1]])) match(x[[1]], options$columns) else as.integer(x[[1]])
idx <- .dt2_name_to_idx(x[[1]], options)
list(idx - 1L, x[[2]])
})
options$order <- ord
Expand All @@ -18,6 +22,9 @@ dt2_order <- function(options = list(), ...) {
#' @param regex,smart,caseInsensitive Search flags.
#' @return Updated `options`.
#' @export
#' @examples
#' opts <- dt2_search_global(list(), value = "Toyota")
#' dt2(mtcars, options = opts)
dt2_search_global <- function(options = list(), value, regex = FALSE, smart = TRUE, caseInsensitive = TRUE) {
options$search <- list(value = value, regex = regex, smart = smart, caseInsensitive = caseInsensitive)
options
Expand All @@ -36,7 +43,12 @@ dt2_search_global <- function(options = list(), value, regex = FALSE, smart = TR
#' If `NULL`, uses the theme default (`"btn btn-sm btn-outline-secondary"`).
#' Applied per-button via `className`.
#' @return Updated `options`.
#' @seealso [dt2_buttons()] for a lower-level variant that takes full button
#' objects and can relocate the buttons container to a custom CSS selector.
#' @export
#' @examples
#' opts <- dt2_use_buttons(list(), buttons = c("copy", "csv", "excel"))
#' dt2(mtcars, options = opts)
dt2_use_buttons <- function(options = list(),
buttons = c("copy","csv","excel","print"),
position = "topEnd",
Expand All @@ -60,6 +72,14 @@ dt2_use_buttons <- function(options = list(),
#' @param lang_url URL to a JSON translation file.
#' @return Updated `options`.
#' @export
#' @examples
#' # Inline translation
#' opts <- dt2_language(list(), lang_list = list(search = "Buscar:"))
#' dt2(iris, options = opts)
#'
#' # Or load a ready-made translation file from the DataTables CDN
#' opts <- dt2_language(list(),
#' lang_url = "https://cdn.datatables.net/plug-ins/2.3.3/i18n/pt-BR.json")
dt2_language <- function(options = list(), lang_list = NULL, lang_url = NULL) {
if (!is.null(lang_url)) {
options$language <- list(url = lang_url)
Expand All @@ -77,7 +97,7 @@ dt2_language <- function(options = list(), lang_list = NULL, lang_url = NULL) {
dt2_cols_width <- function(options = list(), map_named) {
options$columnDefs <- c(options$columnDefs %||% list(),
lapply(names(map_named), function(nm) {
i <- match(nm, options$columns)
i <- .dt2_name_to_idx(nm, options)
list(targets = i-1L, width = unname(map_named[[nm]]))
})
)
Expand All @@ -92,7 +112,7 @@ dt2_cols_width <- function(options = list(), map_named) {
#' @export
dt2_cols_align <- function(options = list(), cols, align = c("left","center","right")) {
align <- match.arg(align)
idx <- if (is.character(cols)) match(cols, options$columns) else as.integer(cols)
idx <- .dt2_name_to_idx(cols, options)
cls <- switch(align, left="text-start", center="text-center", right="text-end")
options$columnDefs <- c(options$columnDefs %||% list(),
lapply(idx, function(i) list(targets = i-1L, className = cls))
Expand All @@ -106,23 +126,37 @@ dt2_cols_align <- function(options = list(), cols, align = c("left","center","ri
#' @return Updated `options`.
#' @export
dt2_cols_hide <- function(options = list(), cols) {
idx <- if (is.character(cols)) match(cols, options$columns) else as.integer(cols)
idx <- .dt2_name_to_idx(cols, options)
options$columnDefs <- c(options$columnDefs %||% list(),
lapply(idx, function(i) list(targets = i-1L, visible = FALSE))
)
options
}

#' Escape/unescape columns content
#'
#' Controls whether cell content is HTML-escaped before display.
#' @param options Options list.
#' @param cols Names or indices.
#' @param escape If FALSE, tells DT to trust HTML (use with care).
#' @param escape If `TRUE` (default), HTML special characters are escaped so the
#' raw text is shown. If `FALSE`, the content is rendered as raw HTML
#' (use with care; only for trusted content).
#' @return Updated `options`.
#' @export
dt2_cols_escape <- function(options = list(), cols, escape = TRUE) {
idx <- if (is.character(cols)) match(cols, options$columns) else as.integer(cols)
idx <- .dt2_name_to_idx(cols, options)
render <- if (escape) {
htmlwidgets::JS(
"function(d,t){ if(t!=='display'||d==null) return d;",
" return String(d)",
" .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')",
" .replace(/\"/g,'&quot;').replace(/'/g,'&#39;'); }"
)
} else {
htmlwidgets::JS("function(d,t){return d;}")
}
options$columnDefs <- c(options$columnDefs %||% list(),
lapply(idx, function(i) list(targets = i-1L, render = if (escape) htmlwidgets::JS("function(d,t){return d;}") else htmlwidgets::JS("function(d,t){return d;}")))
lapply(idx, function(i) list(targets = i - 1L, render = render))
)
options
}
Expand Down
7 changes: 6 additions & 1 deletion R/dt2_server_processing.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
parts <- strsplit(kv, "=", fixed = TRUE)
parts <- lapply(parts, function(x) utils::URLdecode(if (length(x) == 2) x[2] else ""))

keys <- vapply(strsplit(kv, "=", fixed = TRUE), `[[`, character(1), 1)
# Keys must be URL-decoded too: dt2.js encodes them with encodeURIComponent,
# so e.g. "search[value]" arrives as "search%5Bvalue%5D" and bracketed
# order keys as "order%5B0%5D%5Bcolumn%5D". Decoding here lets the lookups
# below (q[["search[value]"]], "order[i][column]") match.
keys <- vapply(strsplit(kv, "=", fixed = TRUE),
function(x) utils::URLdecode(x[[1]]), character(1))
q <- stats::setNames(parts, keys)

num <- function(x, default = NA_integer_) {
Expand Down
Loading
Loading