From 5c147bf83c30e18918e9c97406f091969bc1f4f2 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Fri, 18 Oct 2024 11:31:40 -0500 Subject: [PATCH 01/16] Allow selection in interactive tables. Add an option `ihtml_selection` (default NULL, can be "single" or "multiple") to enable selection in interactive gt tables. When this is on, make it available as an input in Shiny. Closes #354. Closes #1368. --- R/dt_options.R | 1 + R/opts.R | 30 +++++++++++++++++++++--------- R/render_as_i_html.R | 6 ++++-- R/tab_options.R | 24 ++++++++++++++++++++++++ man/opt_interactive.Rd | 13 ++++++++++++- man/tab_options.Rd | 10 ++++++++++ 6 files changed, 72 insertions(+), 12 deletions(-) diff --git a/R/dt_options.R b/R/dt_options.R index 2c3bb7b72..33d0fc189 100644 --- a/R/dt_options.R +++ b/R/dt_options.R @@ -256,6 +256,7 @@ dt_options_tbl <- "ihtml_page_size_default", FALSE, "interactive", "values", 10, "ihtml_page_size_values", FALSE, "interactive", "values", default_page_size_vec, "ihtml_pagination_type", FALSE, "interactive", "value", "numbers", + "ihtml_selection", FALSE, "interactive", "value", NA_character_, "page_orientation", FALSE, "page", "value", "portrait", "page_numbering", FALSE, "page", "logical", FALSE, "page_header_use_tbl_headings", FALSE, "page", "logical", FALSE, diff --git a/R/opts.R b/R/opts.R index 7efdd70a4..7a8b62cc3 100644 --- a/R/opts.R +++ b/R/opts.R @@ -337,6 +337,16 @@ get_colorized_params <- function( #' #' Height of the table in pixels. Defaults to `"auto"` for automatic sizing. #' +#' @param selection *Allow row selection* +#' +#' `scalar` // *default:* `NULL` +#' +#' The `selection` options allows users to select rows by clicking them. When +#' this option is `"single"`, clicking another value toggles selection of the +#' previously selected row off. When this option is `"multiple"`, multiple +#' rows can be selected at once. Selected values are available in Shiny apps +#' when `selection` is not `NULL` and the table is used in [render_gt()]. +#' #' @return An object of class `gt_tbl`. #' #' @section Examples: @@ -418,13 +428,14 @@ opt_interactive <- function( page_size_default = 10, page_size_values = c(10, 25, 50, 100), pagination_type = c("numbers", "jump", "simple"), - height = "auto" + height = "auto", + selection = NULL ) { # Perform input object validation stop_if_not_gt_tbl(data = data) - pagination_type <- + pagination_type <- rlang::arg_match0(pagination_type, values = c("numbers", "jump", "simple")) tab_options( @@ -443,7 +454,8 @@ opt_interactive <- function( ihtml.page_size_default = page_size_default, ihtml.page_size_values = page_size_values, ihtml.pagination_type = pagination_type, - ihtml.height = height + ihtml.height = height, + ihtml.selection = selection ) } @@ -1457,11 +1469,11 @@ opt_table_outline <- function( #' A name that is representative of a font stack (obtained via internally via #' the [system_fonts()] helper function). If provided, this new stack will #' replace any defined fonts and any `font` values will be prepended. -#' +#' #' @param size *Text size* -#' +#' #' `scalar` // *default:* `NULL` (`optional`) -#' +#' #' The text size for the entire table can be set by providing a `size` value. #' Can be specified as a single-length character vector with units of pixels #' (e.g., `12px`) or as a percentage (e.g., `80%`). If provided as a @@ -1484,11 +1496,11 @@ opt_table_outline <- function( #' `"normal"`, `"bold"`, `"lighter"`, `"bolder"`, or, a numeric value between #' `1` and `1000`, inclusive. Please note that typefaces have varying support #' for the numeric mapping of weight. -#' +#' #' @param color *Text color* -#' +#' #' `scalar` // *default:* `NULL` (`optional`) -#' +#' #' The `color` option defines the text color used throughout the table. A #' color name or a hexadecimal color code should be provided. #' diff --git a/R/render_as_i_html.R b/R/render_as_i_html.R index 56c7b8de7..ab862aa85 100644 --- a/R/render_as_i_html.R +++ b/R/render_as_i_html.R @@ -188,6 +188,8 @@ render_as_ihtml <- function(data, id) { page_size_default <- tbl_opts$ihtml_page_size_default page_size_values <- tbl_opts$ihtml_page_size_values pagination_type <- tbl_opts$ihtml_pagination_type + selection <- tbl_opts$ihtml_selection + onClick <- if (!is.null(selection)) "select" use_row_striping <- tbl_opts$row_striping_include_table_body row_striping_color <- tbl_opts$row_striping_background_color @@ -683,10 +685,10 @@ render_as_ihtml <- function(data, id) { paginateSubRows = FALSE, details = NULL, defaultExpanded = expand_groupname_col, - selection = NULL, + selection = selection, selectionId = NULL, defaultSelected = NULL, - onClick = NULL, + onClick = onClick, highlight = use_highlight, outlined = FALSE, bordered = FALSE, diff --git a/R/tab_options.R b/R/tab_options.R index d1306e3c5..5cdc3c6a8 100644 --- a/R/tab_options.R +++ b/R/tab_options.R @@ -455,6 +455,15 @@ #' #' Height of the table in pixels. Defaults to `"auto"` for automatic sizing. #' +#' @param ihtml.selection *Allow row selection* +#' +#' For interactive HTML output, this allows users to select rows by clicking +#' them. When this option is `"single"`, clicking another value toggles +#' selection of the previously selected row off. When this option is +#' `"multiple"`, multiple rows can be selected at once. Selected values are +#' available in Shiny apps when `ihtml.selection` is not `NULL` and the table +#' is used in [render_gt()]. +#' #' @param page.orientation *Set RTF page orientation* #' #' For RTF output, this provides an two options for page @@ -838,6 +847,7 @@ tab_options <- function( ihtml.page_size_values = NULL, ihtml.pagination_type = NULL, ihtml.height = NULL, + ihtml.selection = NULL, page.orientation = NULL, page.numbering = NULL, page.header.use_tbl_headings = NULL, @@ -1034,6 +1044,20 @@ set_super_options <- function(arg_vals) { ) } + if ("ihtml.selection" %in% names(arg_vals)) { + ihtml_selection_val <- arg_vals$ihtml.selection + if ( + !( + rlang::is_scalar_character(ihtml_selection_val) && + ihtml_selection_val %in% c("single", "multiple") + ) + ) { + cli::cli_abort(c( + "The chosen option for `ihtml.selection` (`{ihtml_selection_val}`) is invalid.", + "*" = "We can use either \"single\" or \"multiple\"." + )) + } + } arg_vals } diff --git a/man/opt_interactive.Rd b/man/opt_interactive.Rd index dfbe1c0cd..d261721d3 100644 --- a/man/opt_interactive.Rd +++ b/man/opt_interactive.Rd @@ -20,7 +20,8 @@ opt_interactive( page_size_default = 10, page_size_values = c(10, 25, 50, 100), pagination_type = c("numbers", "jump", "simple"), - height = "auto" + height = "auto", + selection = NULL ) } \arguments{ @@ -144,6 +145,16 @@ and 'next' buttons are displayed.} \item{height}{\emph{Height of interactive HTML table} Height of the table in pixels. Defaults to \code{"auto"} for automatic sizing.} + +\item{selection}{\emph{Allow row selection} + +\verb{scalar} // \emph{default:} \code{NULL} + +The \code{selection} options allows users to select rows by clicking them. When +this option is \code{"single"}, clicking another value toggles selection of the +previously selected row off. When this option is \code{"multiple"}, multiple +rows can be selected at once. Selected values are available in Shiny apps +when \code{selection} is not \code{NULL} and the table is used in \code{\link[=render_gt]{render_gt()}}.} } \value{ An object of class \code{gt_tbl}. diff --git a/man/tab_options.Rd b/man/tab_options.Rd index eedce1f8f..7f1d85965 100644 --- a/man/tab_options.Rd +++ b/man/tab_options.Rd @@ -178,6 +178,7 @@ tab_options( ihtml.page_size_values = NULL, ihtml.pagination_type = NULL, ihtml.height = NULL, + ihtml.selection = NULL, page.orientation = NULL, page.numbering = NULL, page.header.use_tbl_headings = NULL, @@ -595,6 +596,15 @@ displayed.} Height of the table in pixels. Defaults to \code{"auto"} for automatic sizing.} +\item{ihtml.selection}{\emph{Allow row selection} + +For interactive HTML output, this allows users to select rows by clicking +them. When this option is \code{"single"}, clicking another value toggles +selection of the previously selected row off. When this option is +\code{"multiple"}, multiple rows can be selected at once. Selected values are +available in Shiny apps when \code{ihtml.selection} is not \code{NULL} and the table +is used in \code{\link[=render_gt]{render_gt()}}.} + \item{page.orientation}{\emph{Set RTF page orientation} For RTF output, this provides an two options for page From e1bb50231fddb566b6f2a5ebb819d04abe6bc4a5 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Fri, 18 Oct 2024 13:54:56 -0500 Subject: [PATCH 02/16] Implement Shiny input. I have a bunch of console.log()s in here right now as I debug. If you run Shiny.bindAll(); in Chrome devtools, this works, but not before then so I'm missing a step somewhere. I'm not actually certain that the unsubscribe() method works, and I'm not yet certain how to test that. --- R/shiny.R | 17 +++++++-- inst/shiny/gtShiny.js | 84 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 inst/shiny/gtShiny.js diff --git a/R/shiny.R b/R/shiny.R index 83d7a9881..e05d3fb9c 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -175,7 +175,10 @@ render_gt <- function( table.align = align ) - html_tbl <- as.tags(result) + html_tbl <- htmltools::tagList( + as.tags(result), + shiny_deps() + ) dependencies <- lapply( @@ -194,6 +197,16 @@ render_gt <- function( ) } +shiny_deps <- function() { + htmltools::htmlDependency( + "gtShiny", + "1.0.0", + src = "shiny", + package = "gt", + script = "gtShiny.js" + ) +} + # gt_output() ------------------------------------------------------------------ #' Create a **gt** display table output element for Shiny #' @@ -264,7 +277,7 @@ gt_output <- function(outputId) { # Ensure that the shiny package is available rlang::check_installed("shiny", "to use `gt_output()`.") - shiny::htmlOutput(outputId) + shiny::htmlOutput(outputId, class = "gt_shiny") } #nocov end diff --git a/inst/shiny/gtShiny.js b/inst/shiny/gtShiny.js new file mode 100644 index 000000000..729c1df75 --- /dev/null +++ b/inst/shiny/gtShiny.js @@ -0,0 +1,84 @@ +/** + * Shiny InputBinding for a gtShiny Module. + */ +var gtShinyBinding = new Shiny.InputBinding(); + +$.extend(gtShinyBinding, { + /** + * Finds the gtShiny element within the given scope. + * + * @param {HTMLElement} scope - The scope in which to search for the gtShiny + * element. + * @returns {jQuery} The jQuery object containing the gtShiny element. + */ + find: function(scope) { + console.log('find() called'); + var found = $(scope).find('.gt_shiny'); + if (found.length) { + console.log('find() found', found.length, 'elements'); + } + return found; + }, + /** + * Gets the reactable element within the given gtShiny element. + * + * @param {HTMLElement} el - The element containing the gtShiny. + * @returns {HTMLElement|null} The reactable element, or null if not found. + */ + getReactable: function(el) { + console.log('getReactable() called'); + return el.querySelector('.reactable'); + }, + /** + * Gets the value of the selected rows from the gtShiny element. + * + * @param {HTMLElement} el - The element containing the gtShiny. + * @returns {Array|null} The selected row IDs, or null if no rows are selected. + */ + getValue: function(el) { + console.log('getValue() called'); + var rctbl = this.getReactable(el); + if (rctbl) { + console.log('Selected rows:', Reactable.getState(rctbl.id).selected); + } + return rctbl ? Reactable.getState(rctbl.id).selected : null; + }, + /** + * Update Shiny when a gtShiny element changes. + * + * @param {HTMLElement} el - The element containing the gtShiny. + * @param {function} callback - The callback to trigger when the gtShiny + * element changes. + */ + subscribe: function(el, callback) { + console.log('subscribe() called'); + var rctbl = this.getReactable(el); + if (rctbl) { + console.log('Subscribing to changes for:', rctbl.id); + rctbl.__reactableStateChangeListener = function() { + console.log('State change detected'); + callback(); + }; + Reactable.onStateChange(rctbl.id, rctbl.__reactableStateChangeListener); + } + }, + /** + * Unsubscribes from custom events for the gtShiny element. + * + * @param {HTMLElement} el - The element containing the gtShiny. + */ + unsubscribe: function(el) { + console.log('unsubscribe() called'); + var rctbl = this.getReactable(el); + if (rctbl && rctbl.__reactableStateChangeListener) { + console.log('Unsubscribing from changes for:', rctbl.id); + // Using a custom way to track and remove listeners + var listenerFn = rctbl.__reactableStateChangeListener; + Reactable.onStateChange(rctbl.id, listenerFn, { remove: true }); + delete rctbl.__reactableStateChangeListener; + } + } +}); + +// Register the input binding with Shiny +Shiny.inputBindings.register(gtShinyBinding, 'gt.gtShinyBinding'); From 79d435be9308a58936b24e9ada261a7ff0d23d7d Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Fri, 18 Oct 2024 15:35:28 -0500 Subject: [PATCH 03/16] Functional shiny binding via a MutationObserver. This feels somewhat hacky so I'm not sure this is the final thing yet. --- inst/shiny/gtShiny.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/inst/shiny/gtShiny.js b/inst/shiny/gtShiny.js index 729c1df75..3b9df0b0b 100644 --- a/inst/shiny/gtShiny.js +++ b/inst/shiny/gtShiny.js @@ -12,12 +12,7 @@ $.extend(gtShinyBinding, { * @returns {jQuery} The jQuery object containing the gtShiny element. */ find: function(scope) { - console.log('find() called'); - var found = $(scope).find('.gt_shiny'); - if (found.length) { - console.log('find() found', found.length, 'elements'); - } - return found; + return $(scope).find('.gt_shiny'); }, /** * Gets the reactable element within the given gtShiny element. @@ -26,7 +21,6 @@ $.extend(gtShinyBinding, { * @returns {HTMLElement|null} The reactable element, or null if not found. */ getReactable: function(el) { - console.log('getReactable() called'); return el.querySelector('.reactable'); }, /** @@ -36,11 +30,7 @@ $.extend(gtShinyBinding, { * @returns {Array|null} The selected row IDs, or null if no rows are selected. */ getValue: function(el) { - console.log('getValue() called'); var rctbl = this.getReactable(el); - if (rctbl) { - console.log('Selected rows:', Reactable.getState(rctbl.id).selected); - } return rctbl ? Reactable.getState(rctbl.id).selected : null; }, /** @@ -51,12 +41,10 @@ $.extend(gtShinyBinding, { * element changes. */ subscribe: function(el, callback) { - console.log('subscribe() called'); var rctbl = this.getReactable(el); if (rctbl) { console.log('Subscribing to changes for:', rctbl.id); rctbl.__reactableStateChangeListener = function() { - console.log('State change detected'); callback(); }; Reactable.onStateChange(rctbl.id, rctbl.__reactableStateChangeListener); @@ -71,8 +59,6 @@ $.extend(gtShinyBinding, { console.log('unsubscribe() called'); var rctbl = this.getReactable(el); if (rctbl && rctbl.__reactableStateChangeListener) { - console.log('Unsubscribing from changes for:', rctbl.id); - // Using a custom way to track and remove listeners var listenerFn = rctbl.__reactableStateChangeListener; Reactable.onStateChange(rctbl.id, listenerFn, { remove: true }); delete rctbl.__reactableStateChangeListener; @@ -82,3 +68,22 @@ $.extend(gtShinyBinding, { // Register the input binding with Shiny Shiny.inputBindings.register(gtShinyBinding, 'gt.gtShinyBinding'); + + +// Mutation Observer to detect when gtShiny tables are fully loaded +(function() { + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + var gtShinyObjs = document.querySelectorAll('.gt_shiny'); + gtShinyObjs.forEach(function(obj) { + var rows = obj.querySelectorAll('.rt-tbody .rt-tr'); + if (rows.length > 0) { + Shiny.bindAll(); + observer.disconnect(); // Stop observing once the element is found and loaded + } + }); + }); + }); + + observer.observe(document.body, { childList: true, subtree: true }); +})(); From dada9f7f81d0061d663523d40a95d2d5ef7a3dc1 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Mon, 21 Oct 2024 09:42:31 -0500 Subject: [PATCH 04/16] Add test for ihtml.selection option. --- tests/testthat/test-tab_options.R | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/testthat/test-tab_options.R b/tests/testthat/test-tab_options.R index e54e0e1b0..e55d64b45 100644 --- a/tests/testthat/test-tab_options.R +++ b/tests/testthat/test-tab_options.R @@ -1428,6 +1428,25 @@ test_that("The internal `opts_df` table can be correctly modified", { expect_equal(c(" ", " ")) }) +test_that("ihtml.selection option can be modified", { + # Check that specific suggested packages are available + check_suggests() + + tbl_html_single <- tab_options(data, ihtml.selection = "single") + tbl_html_multi <- tab_options(data, ihtml.selection = "multiple") + + c(dt_options_get_value(data = data, option = "ihtml_selection"), + dt_options_get_value(data = tbl_html_single, option = "ihtml_selection"), + dt_options_get_value(data = tbl_html_multi, option = "ihtml_selection") + ) %>% + expect_equal(c(NA_character_, "single", "multiple")) + + expect_error( + tab_options(data, ihtml.selection = "bad"), + "The chosen option for `ihtml\\.selection" + ) +}) + test_that("The `opts_df` getter/setter functions properly", { # Obtain a local copy of the internal `_options` table @@ -1811,3 +1830,4 @@ test_that("Vertical padding across several table parts can be applied", { snap_padded_tbl(padding_px = px(5)) snap_padded_tbl(padding_px = px(10)) }) + From 8f965dce3e7b7d7fb931f4a652ae377680048914 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Mon, 21 Oct 2024 12:37:09 -0500 Subject: [PATCH 05/16] Make gt_shiny work with reactive gt. --- inst/shiny/gtShiny.js | 80 +++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/inst/shiny/gtShiny.js b/inst/shiny/gtShiny.js index 3b9df0b0b..fb11c18f5 100644 --- a/inst/shiny/gtShiny.js +++ b/inst/shiny/gtShiny.js @@ -30,8 +30,12 @@ $.extend(gtShinyBinding, { * @returns {Array|null} The selected row IDs, or null if no rows are selected. */ getValue: function(el) { - var rctbl = this.getReactable(el); - return rctbl ? Reactable.getState(rctbl.id).selected : null; + var reactableElement = this.getReactable(el); + if (reactableElement) { + var selectedRows = Reactable.getState(reactableElement.id).selected; + return selectedRows ? selectedRows.map(function(row) { return row + 1; }) : null; + } + return null; }, /** * Update Shiny when a gtShiny element changes. @@ -41,14 +45,9 @@ $.extend(gtShinyBinding, { * element changes. */ subscribe: function(el, callback) { - var rctbl = this.getReactable(el); - if (rctbl) { - console.log('Subscribing to changes for:', rctbl.id); - rctbl.__reactableStateChangeListener = function() { - callback(); - }; - Reactable.onStateChange(rctbl.id, rctbl.__reactableStateChangeListener); - } + $(el).on('change.gtShiny', function(event) { + callback(); + }); }, /** * Unsubscribes from custom events for the gtShiny element. @@ -56,34 +55,57 @@ $.extend(gtShinyBinding, { * @param {HTMLElement} el - The element containing the gtShiny. */ unsubscribe: function(el) { - console.log('unsubscribe() called'); - var rctbl = this.getReactable(el); - if (rctbl && rctbl.__reactableStateChangeListener) { - var listenerFn = rctbl.__reactableStateChangeListener; - Reactable.onStateChange(rctbl.id, listenerFn, { remove: true }); - delete rctbl.__reactableStateChangeListener; - } + $(el).off('change.gtShiny'); } }); // Register the input binding with Shiny Shiny.inputBindings.register(gtShinyBinding, 'gt.gtShinyBinding'); +/** + * Processes all gtShiny elements on the page and executes a callback for + * each element that contains a populated Reactable table. + * + * @param {function} callback - The callback function to execute for each + * gtShiny element that has a Reactable table + * with rows. The callback receives the gtShiny + * element and the Reactable element as + * arguments. + */ +function processGtShinyElements(callback) { + var gtShinyObjs = document.querySelectorAll('.gt_shiny'); + gtShinyObjs.forEach(function(gtShinyElement) { + var reactableElement = gtShinyElement.querySelector('.reactable'); + if (reactableElement) { + var rows = reactableElement.querySelectorAll('.rt-tr'); + if (rows.length > 0) { + callback(gtShinyElement, reactableElement); + } + } + }); +} -// Mutation Observer to detect when gtShiny tables are fully loaded +// Mutation Observer to detect when gtShiny tables are fully loaded initially (function() { - var observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - var gtShinyObjs = document.querySelectorAll('.gt_shiny'); - gtShinyObjs.forEach(function(obj) { - var rows = obj.querySelectorAll('.rt-tbody .rt-tr'); - if (rows.length > 0) { - Shiny.bindAll(); - observer.disconnect(); // Stop observing once the element is found and loaded - } - }); + // Observer for setting up Shiny binding on first gt_shiny load + var bindObserver = new MutationObserver(function() { + processGtShinyElements(function(gtShinyElement, reactableElement) { + Shiny.bindAll(); + bindObserver.disconnect(); }); }); + bindObserver.observe(document.body, { childList: true, subtree: true }); - observer.observe(document.body, { childList: true, subtree: true }); + // Observer for setting up Reactable state change listeners + var stateChangeObserver = new MutationObserver(function() { + processGtShinyElements(function(gtShinyElement, reactableElement) { + if (!reactableElement.__reactableStateChangeListener) { + reactableElement.__reactableStateChangeListener = function() { + $(gtShinyElement).trigger('change.gtShiny'); + }; + Reactable.onStateChange(reactableElement.id, reactableElement.__reactableStateChangeListener); + } + }); + }); + stateChangeObserver.observe(document.body, { childList: true, subtree: true }); })(); From 96699449abfa30c84575f835e9e52a677fd60c37 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Mon, 21 Oct 2024 13:58:44 -0500 Subject: [PATCH 06/16] Docs and news. --- NEWS.md | 4 ++++ R/opts.R | 1 + man/opt_interactive.Rd | 1 + 3 files changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index bed0e6940..b801ec8b3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # gt (development version) +* Interactive tables can support selection through the `ihtml.selection` option. (@jonthegeek, #1909) + +* Tables embedded in Shiny apps with `gt_output()` and `render_gt()` with `ihtml.selection` enabled also act as inputs, reporting the row numbers that are selected (#354, #1368). (@jonthegeek, #1909) + # gt 0.11.1 ## Breaking changes diff --git a/R/opts.R b/R/opts.R index 7a8b62cc3..a1daa28b6 100644 --- a/R/opts.R +++ b/R/opts.R @@ -220,6 +220,7 @@ get_colorized_params <- function( #' - `ihtml.page_size_values` #' - `ihtml.pagination_type` #' - `ihtml.height` +#' - `ihtml.selection` #' #' @inheritParams fmt_number #' diff --git a/man/opt_interactive.Rd b/man/opt_interactive.Rd index d261721d3..f3ba337bf 100644 --- a/man/opt_interactive.Rd +++ b/man/opt_interactive.Rd @@ -183,6 +183,7 @@ This function serves as a shortcut for setting the following options in \item \code{ihtml.page_size_values} \item \code{ihtml.pagination_type} \item \code{ihtml.height} +\item \code{ihtml.selection} } } \section{Examples}{ From 2d7f3e6c9dd69c1083bb3ba9e883a651a186166e Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Mon, 21 Oct 2024 16:34:58 -0500 Subject: [PATCH 07/16] Defaults for selection need to be a little tricksy to make everyone happy. --- R/render_as_i_html.R | 1 + tests/testthat/helper-gt_object.R | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/R/render_as_i_html.R b/R/render_as_i_html.R index ab862aa85..4f0967fb2 100644 --- a/R/render_as_i_html.R +++ b/R/render_as_i_html.R @@ -189,6 +189,7 @@ render_as_ihtml <- function(data, id) { page_size_values <- tbl_opts$ihtml_page_size_values pagination_type <- tbl_opts$ihtml_pagination_type selection <- tbl_opts$ihtml_selection + if (is.na(selection)) selection <- NULL onClick <- if (!is.null(selection)) "select" use_row_striping <- tbl_opts$row_striping_include_table_body diff --git a/tests/testthat/helper-gt_object.R b/tests/testthat/helper-gt_object.R index 56c6a50e8..7034dca80 100644 --- a/tests/testthat/helper-gt_object.R +++ b/tests/testthat/helper-gt_object.R @@ -77,7 +77,7 @@ expect_tab <- function(tab, df) { expect_length(dt_substitutions_get(data = tab), 0) expect_equal(dim(dt_styles_get(data = tab)), c(0, 7)) # If adding a new option to tab_options(), update here - expect_equal(dim(dt_options_get(data = tab)), c(193, 5)) + expect_equal(dim(dt_options_get(data = tab)), c(194, 5)) expect_length(dt_transforms_get(data = tab), 0) # Expect that extracted df has the same column From 9f94e0b6b2f89ca9a1d3856368606d7c23d8903d Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 22 Oct 2024 11:15:52 -0500 Subject: [PATCH 08/16] Cleaner binding. --- R/shiny.R | 3 +- inst/shiny/gtShiny.js | 80 +++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 49 deletions(-) diff --git a/R/shiny.R b/R/shiny.R index e05d3fb9c..e86dab5a0 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -177,7 +177,8 @@ render_gt <- function( html_tbl <- htmltools::tagList( as.tags(result), - shiny_deps() + shiny_deps(), + htmltools::HTML("") ) dependencies <- diff --git a/inst/shiny/gtShiny.js b/inst/shiny/gtShiny.js index fb11c18f5..d9a0cb3ac 100644 --- a/inst/shiny/gtShiny.js +++ b/inst/shiny/gtShiny.js @@ -14,6 +14,33 @@ $.extend(gtShinyBinding, { find: function(scope) { return $(scope).find('.gt_shiny'); }, + /** + * Initializes the gtShiny element by setting up the Reactable state change + * listener once the Reactable is loaded. + * + * @param {HTMLElement} el - The element containing the gtShiny. + */ + initialize: function(el) { + var self = this; + var observer = new MutationObserver(function(mutations, obs) { + var reactableElement = self.getReactable(el); + if (reactableElement) { + var rows = reactableElement.querySelectorAll('.rt-tr'); + if (rows.length > 0) { + if (!reactableElement.__reactableStateChangeListener) { + reactableElement.__reactableStateChangeListener = function() { + $(el).trigger('change.gtShiny'); + }; + Reactable.onStateChange(reactableElement.id, reactableElement.__reactableStateChangeListener); + } + $(el).trigger('change.gtShiny'); + el.__initialized = true; + obs.disconnect(); + } + } + }); + observer.observe(el, { childList: true, subtree: true }); + }, /** * Gets the reactable element within the given gtShiny element. * @@ -30,6 +57,11 @@ $.extend(gtShinyBinding, { * @returns {Array|null} The selected row IDs, or null if no rows are selected. */ getValue: function(el) { + console.log('In getValue for:', el.id); + if (!el.__initialized) { + this.initialize(el); + return null; + } var reactableElement = this.getReactable(el); if (reactableElement) { var selectedRows = Reactable.getState(reactableElement.id).selected; @@ -61,51 +93,3 @@ $.extend(gtShinyBinding, { // Register the input binding with Shiny Shiny.inputBindings.register(gtShinyBinding, 'gt.gtShinyBinding'); - -/** - * Processes all gtShiny elements on the page and executes a callback for - * each element that contains a populated Reactable table. - * - * @param {function} callback - The callback function to execute for each - * gtShiny element that has a Reactable table - * with rows. The callback receives the gtShiny - * element and the Reactable element as - * arguments. - */ -function processGtShinyElements(callback) { - var gtShinyObjs = document.querySelectorAll('.gt_shiny'); - gtShinyObjs.forEach(function(gtShinyElement) { - var reactableElement = gtShinyElement.querySelector('.reactable'); - if (reactableElement) { - var rows = reactableElement.querySelectorAll('.rt-tr'); - if (rows.length > 0) { - callback(gtShinyElement, reactableElement); - } - } - }); -} - -// Mutation Observer to detect when gtShiny tables are fully loaded initially -(function() { - // Observer for setting up Shiny binding on first gt_shiny load - var bindObserver = new MutationObserver(function() { - processGtShinyElements(function(gtShinyElement, reactableElement) { - Shiny.bindAll(); - bindObserver.disconnect(); - }); - }); - bindObserver.observe(document.body, { childList: true, subtree: true }); - - // Observer for setting up Reactable state change listeners - var stateChangeObserver = new MutationObserver(function() { - processGtShinyElements(function(gtShinyElement, reactableElement) { - if (!reactableElement.__reactableStateChangeListener) { - reactableElement.__reactableStateChangeListener = function() { - $(gtShinyElement).trigger('change.gtShiny'); - }; - Reactable.onStateChange(reactableElement.id, reactableElement.__reactableStateChangeListener); - } - }); - }); - stateChangeObserver.observe(document.body, { childList: true, subtree: true }); -})(); From 9cb98abbef0f846b0713358e8d36846bd59ac9bb Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 22 Oct 2024 11:41:24 -0500 Subject: [PATCH 09/16] Fully re-bind when underlying reactable changes. --- R/shiny.R | 4 +++- inst/shiny/gtShiny.js | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/R/shiny.R b/R/shiny.R index e86dab5a0..18e5bdaac 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -178,7 +178,9 @@ render_gt <- function( html_tbl <- htmltools::tagList( as.tags(result), shiny_deps(), - htmltools::HTML("") + htmltools::HTML( + glue::glue("") + ) ) dependencies <- diff --git a/inst/shiny/gtShiny.js b/inst/shiny/gtShiny.js index d9a0cb3ac..bdf9a99e9 100644 --- a/inst/shiny/gtShiny.js +++ b/inst/shiny/gtShiny.js @@ -20,7 +20,10 @@ $.extend(gtShinyBinding, { * * @param {HTMLElement} el - The element containing the gtShiny. */ - initialize: function(el) { + initializeListener: function(el) { + if (el.__initialized) { + return; + } var self = this; var observer = new MutationObserver(function(mutations, obs) { var reactableElement = self.getReactable(el); @@ -41,6 +44,19 @@ $.extend(gtShinyBinding, { }); observer.observe(el, { childList: true, subtree: true }); }, + /** + * Initializes the gtShiny element by binding Shiny and setting up the listener. + * + * @param {string} id - The ID of the element to initialize. + */ + initialize: function(id) { + var el = document.getElementById(id); + if (el) { + Shiny.bindAll(); + el.__initialized = false; + this.initializeListener(el); + } + }, /** * Gets the reactable element within the given gtShiny element. * @@ -57,9 +73,8 @@ $.extend(gtShinyBinding, { * @returns {Array|null} The selected row IDs, or null if no rows are selected. */ getValue: function(el) { - console.log('In getValue for:', el.id); if (!el.__initialized) { - this.initialize(el); + this.initializeListener(el); return null; } var reactableElement = this.getReactable(el); From 5eaac487783ad074c3f899c2b4934d74edcc05d2 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 22 Oct 2024 11:44:34 -0500 Subject: [PATCH 10/16] Abstract `initialize_shiny_gt()`. --- R/shiny.R | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/R/shiny.R b/R/shiny.R index 18e5bdaac..4c368226a 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -178,9 +178,7 @@ render_gt <- function( html_tbl <- htmltools::tagList( as.tags(result), shiny_deps(), - htmltools::HTML( - glue::glue("") - ) + initialize_shiny_gt(name) ) dependencies <- @@ -210,6 +208,12 @@ shiny_deps <- function() { ) } +initialize_shiny_gt <- function(id) { + htmltools::HTML( + glue::glue("") + ) +} + # gt_output() ------------------------------------------------------------------ #' Create a **gt** display table output element for Shiny #' From c5740e65864ccd8203af91c1d9995f7810d05318 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Thu, 31 Oct 2024 15:54:54 -0500 Subject: [PATCH 11/16] Improved 2-way communication. New function `gt_update_select()` can be used to update the selection in a gt table in Shiny. The gt input also now differentiates between "nothing is selected because this table has just initialized" and "nothing is selected because the user deselected everything," which is very useful for updating related inputs and avoiding loops. I think this is done. Let me know if you'd like more tests or examples (or anything else)! I'd love to use this, so let me know what I need to do to help push it over the goal line! --- NAMESPACE | 1 + R/shiny.R | 46 +++++++++++++++++++++++++++++- inst/shiny/gtShiny.js | 63 +++++++++++++++++++++++++++++++++++++++-- man/gt_output.Rd | 1 + man/gt_update_select.Rd | 47 ++++++++++++++++++++++++++++++ man/render_gt.Rd | 9 ++++-- 6 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 man/gt_update_select.Rd diff --git a/NAMESPACE b/NAMESPACE index a68fff50f..7b31970ae 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -157,6 +157,7 @@ export(gt_latex_dependencies) export(gt_output) export(gt_preview) export(gt_split) +export(gt_update_select) export(gtsave) export(html) export(info_currencies) diff --git a/R/shiny.R b/R/shiny.R index 4c368226a..d7e42ff26 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -36,7 +36,11 @@ #' holding the **gt** table. The `width` and `height` arguments allow for sizing #' the container, and the `align` argument allows us to align the table within #' the container (some other fine-grained options for positioning are available -#' in [tab_options()]). +#' in [tab_options()]). If the table is interactive, the selected row indices +#' (relative to the underlying data, regardless of sorting) are available as +#' `input$id`, where `id` is the `outputId` used for this table in [gt_output()]. +#' If the user has deselected all rows, the value is `0` (vs `NULL` when the +#' table initializes). #' #' @param expr *Expression* #' @@ -287,4 +291,44 @@ gt_output <- function(outputId) { shiny::htmlOutput(outputId, class = "gt_shiny") } +# gt_update_select() ----------------------------------------------------------- +#' Update a **gt** selection in Shiny +#' +#' @description +#' +#' Update the selection in an interactive **gt** table rendered using +#' [render_gt()]. The table must be interactive and have selection enabled (see +#' [opt_interactive()]). +#' +#' @param outputId *Shiny output ID* +#' +#' `scalar` // **required** +#' +#' The id of the [gt_output()] element to update. +#' @param rows *Row indices* +#' +#' `` // **required** +#' +#' The id of the [gt_output()] element to update. +#' @param session *Shiny session* +#' +#' `scalar` // **required** +#' +#' The session in which the [gt_output()] element can be found. You almost +#' certainly want to leave this as the default value. +#' +#' @return A call to the JavaScript binding of the table. +#' @family Shiny functions +#' @section Function ID: +#' 12-3 +#' +#' @export +#' +#' @examples +gt_update_select <- function(outputId, + rows, + session = shiny::getDefaultReactiveDomain()) { + session$sendInputMessage(outputId, rows - 1) +} + #nocov end diff --git a/inst/shiny/gtShiny.js b/inst/shiny/gtShiny.js index bdf9a99e9..9e9658cfb 100644 --- a/inst/shiny/gtShiny.js +++ b/inst/shiny/gtShiny.js @@ -25,19 +25,32 @@ $.extend(gtShinyBinding, { return; } var self = this; + el.__clickFlag = false; + var observer = new MutationObserver(function(mutations, obs) { var reactableElement = self.getReactable(el); if (reactableElement) { var rows = reactableElement.querySelectorAll('.rt-tr'); if (rows.length > 0) { + if (!reactableElement.__clickListenerAdded) { + reactableElement.addEventListener('click', function() { + el.__clickFlag = true; + }); + reactableElement.__clickListenerAdded = true; + } + if (!reactableElement.__reactableStateChangeListener) { reactableElement.__reactableStateChangeListener = function() { $(el).trigger('change.gtShiny'); }; Reactable.onStateChange(reactableElement.id, reactableElement.__reactableStateChangeListener); } - $(el).trigger('change.gtShiny'); el.__initialized = true; + if (el.__awaiting_set && el.__awaiting_set.length) { + value = el.__awaiting_set; + el.__awaiting_set = null; + self.setValue(el, value); + } obs.disconnect(); } } @@ -75,15 +88,50 @@ $.extend(gtShinyBinding, { getValue: function(el) { if (!el.__initialized) { this.initializeListener(el); - return null; + return; // Table is reloading or not fully initialized } var reactableElement = this.getReactable(el); if (reactableElement) { var selectedRows = Reactable.getState(reactableElement.id).selected; - return selectedRows ? selectedRows.map(function(row) { return row + 1; }) : null; + if (selectedRows === undefined) { + return null; + } else if (selectedRows.length === 0) { + if (el.__clickFlag) { + el.__clickFlag = false; + return [0]; // [0] if nothing is selected due to user click + } else { + return null; // null if table is initializing or reloading + } + } else { + el.__clickFlag = false; + return selectedRows.map(function(row) { return row + 1; }); + } } return null; }, + /** + * Sets the value of the selected rows in the gtShiny element. + * + * @param {HTMLElement} el - The element containing the gtShiny. + * @param {Array} value - The row IDs to set as selected. + */ + setValue: function(el, value) { + if (!Array.isArray(value)) { + value = [value]; + } + if (!el.__initialized) { + el.__awaiting_set = value; + this.initializeListener(el); + return; + } + var reactableElement = this.getReactable(el); + if (reactableElement) { + var instance = Reactable.getInstance(reactableElement.id); + if (instance) { + instance.setRowsSelected(value); + } + } + }, /** * Update Shiny when a gtShiny element changes. * @@ -103,6 +151,15 @@ $.extend(gtShinyBinding, { */ unsubscribe: function(el) { $(el).off('change.gtShiny'); + }, + /** + * Receives a message from Shiny and sets the selected rows. + * + * @param {HTMLElement} el - The element containing the gtShiny. + * @param {Array} value - The row IDs to set as selected. + */ + receiveMessage: function(el, value){ + this.setValue(el, value); } }); diff --git a/man/gt_output.Rd b/man/gt_output.Rd index 5cfa69174..88c55ef9d 100644 --- a/man/gt_output.Rd +++ b/man/gt_output.Rd @@ -74,6 +74,7 @@ shinyApp(ui = ui, server = server) \seealso{ Other Shiny functions: +\code{\link{gt_update_select}()}, \code{\link{render_gt}()} } \concept{Shiny functions} diff --git a/man/gt_update_select.Rd b/man/gt_update_select.Rd new file mode 100644 index 000000000..e808a5eb2 --- /dev/null +++ b/man/gt_update_select.Rd @@ -0,0 +1,47 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shiny.R +\name{gt_update_select} +\alias{gt_update_select} +\title{Update a \strong{gt} selection in Shiny} +\usage{ +gt_update_select(outputId, rows, session = shiny::getDefaultReactiveDomain()) +} +\arguments{ +\item{outputId}{\emph{Shiny output ID} + +\verb{scalar} // \strong{required} + +The id of the \code{\link[=gt_output]{gt_output()}} element to update.} + +\item{rows}{\emph{Row indices} + +\verb{} // \strong{required} + +The id of the \code{\link[=gt_output]{gt_output()}} element to update.} + +\item{session}{\emph{Shiny session} + +\verb{scalar} // \strong{required} + +The session in which the \code{\link[=gt_output]{gt_output()}} element can be found. You almost +certainly want to leave this as the default value.} +} +\value{ +A call to the JavaScript binding of the table. +} +\description{ +Update the selection in an interactive \strong{gt} table rendered using +\code{\link[=render_gt]{render_gt()}}. The table must be interactive and have selection enabled (see +\code{\link[=opt_interactive]{opt_interactive()}}). +} +\section{Function ID}{ + +12-3 +} + +\seealso{ +Other Shiny functions: +\code{\link{gt_output}()}, +\code{\link{render_gt}()} +} +\concept{Shiny functions} diff --git a/man/render_gt.Rd b/man/render_gt.Rd index 645374db7..a24749e75 100644 --- a/man/render_gt.Rd +++ b/man/render_gt.Rd @@ -74,7 +74,11 @@ component. We have some options for controlling the size of the container holding the \strong{gt} table. The \code{width} and \code{height} arguments allow for sizing the container, and the \code{align} argument allows us to align the table within the container (some other fine-grained options for positioning are available -in \code{\link[=tab_options]{tab_options()}}). +in \code{\link[=tab_options]{tab_options()}}). If the table is interactive, the selected row indices +(relative to the underlying data, regardless of sorting) are available as +\code{input$id}, where \code{id} is the \code{outputId} used for this table in \code{\link[=gt_output]{gt_output()}}. +If the user has deselected all rows, the value is \code{0} (vs \code{NULL} when the +table initializes). } \section{Examples}{ @@ -124,6 +128,7 @@ shinyApp(ui = ui, server = server) \seealso{ Other Shiny functions: -\code{\link{gt_output}()} +\code{\link{gt_output}()}, +\code{\link{gt_update_select}()} } \concept{Shiny functions} From d243b5b3c10a6cd7a02844c3fb70805a3e90995e Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Thu, 31 Oct 2024 16:29:28 -0500 Subject: [PATCH 12/16] Fix documentation. --- R/shiny.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/R/shiny.R b/R/shiny.R index d7e42ff26..1a8ddbb4b 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -323,8 +323,6 @@ gt_output <- function(outputId) { #' 12-3 #' #' @export -#' -#' @examples gt_update_select <- function(outputId, rows, session = shiny::getDefaultReactiveDomain()) { From 40f144e8640db840ae777e6f324d7bbb2c78bc2e Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Fri, 1 Nov 2024 05:18:02 -0500 Subject: [PATCH 13/16] Add gt_update_select to _pkgdown.yml. --- pkgdown/_pkgdown.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index b20dd7e4d..39eaa8c9f 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -295,6 +295,7 @@ reference: contents: - render_gt - gt_output + - gt_update_select - title: Export and extraction functions desc: > From 1637ae7ba865dbeb1b18b18b76e7942304c11640 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Fri, 1 Nov 2024 10:14:09 -0500 Subject: [PATCH 14/16] Don't report value change on sort, filter, etc. Also refactored getValue for readability. --- inst/shiny/gtShiny.js | 46 ++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/inst/shiny/gtShiny.js b/inst/shiny/gtShiny.js index 9e9658cfb..5adbf4cda 100644 --- a/inst/shiny/gtShiny.js +++ b/inst/shiny/gtShiny.js @@ -32,13 +32,20 @@ $.extend(gtShinyBinding, { if (reactableElement) { var rows = reactableElement.querySelectorAll('.rt-tr'); if (rows.length > 0) { - if (!reactableElement.__clickListenerAdded) { - reactableElement.addEventListener('click', function() { - el.__clickFlag = true; + // Listener so we know it's an actual click, not another state change. + body_rows = reactableElement.querySelectorAll('.rt-tbody .rt-tr'); + if (body_rows.length > 0) { + body_rows.forEach(function(row) { + if (!row.__clickListenerAdded) { + row.addEventListener('click', function() { + el.__clickFlag = true; + }); + row.__clickListenerAdded = true; + } }); - reactableElement.__clickListenerAdded = true; } + // State change listener, so we fire getValue *after* the state updates. if (!reactableElement.__reactableStateChangeListener) { reactableElement.__reactableStateChangeListener = function() { $(el).trigger('change.gtShiny'); @@ -90,24 +97,27 @@ $.extend(gtShinyBinding, { this.initializeListener(el); return; // Table is reloading or not fully initialized } + var reactableElement = this.getReactable(el); - if (reactableElement) { - var selectedRows = Reactable.getState(reactableElement.id).selected; - if (selectedRows === undefined) { - return null; - } else if (selectedRows.length === 0) { - if (el.__clickFlag) { - el.__clickFlag = false; - return [0]; // [0] if nothing is selected due to user click - } else { - return null; // null if table is initializing or reloading - } - } else { + if (!reactableElement) { + return; // Initialization is finishing, state will report when finished. + } + + var selectedRows = Reactable.getState(reactableElement.id).selected; + if (selectedRows === undefined) { + return; // Initialization is finishing, state will report when finished. + } + + if (selectedRows.length === 0) { + if (el.__clickFlag) { el.__clickFlag = false; - return selectedRows.map(function(row) { return row + 1; }); + return [0]; // [0] if nothing is selected due to user click } + return null; // null if table is initializing or reloading } - return null; + + el.__clickFlag = false; + return selectedRows.map(function(row) { return row + 1; }); }, /** * Sets the value of the selected rows in the gtShiny element. From 689677b275c1232989fe713597d3d2ff70a8cc8f Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Fri, 1 Nov 2024 14:35:02 -0500 Subject: [PATCH 15/16] Rename selection option to selection_mode to avoid (some) confusion. I want to add the ability to pass in selection row numbers, so I needed to make room in the option name for another selection-related option. --- NEWS.md | 4 ++-- R/dt_options.R | 2 +- R/opts.R | 19 ++++++++++--------- R/render_as_i_html.R | 8 ++++---- R/tab_options.R | 18 +++++++++--------- man/opt_interactive.Rd | 17 +++++++++-------- man/tab_options.Rd | 8 ++++---- 7 files changed, 39 insertions(+), 37 deletions(-) diff --git a/NEWS.md b/NEWS.md index 033db2b2c..5c87bc870 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,8 @@ # gt (development version) -* Interactive tables can support selection through the `ihtml.selection` option. (@jonthegeek, #1909) +* Interactive tables can support selection through the `ihtml.selection_mode` option. (@jonthegeek, #1909) -* Tables embedded in Shiny apps with `gt_output()` and `render_gt()` with `ihtml.selection` enabled also act as inputs, reporting the row numbers that are selected (#354, #1368). (@jonthegeek, #1909) +* Tables embedded in Shiny apps with `gt_output()` and `render_gt()` with `ihtml.selection_mode` enabled also act as inputs, reporting the row numbers that are selected (#354, #1368). (@jonthegeek, #1909) # gt 0.11.1 diff --git a/R/dt_options.R b/R/dt_options.R index 33d0fc189..671e68972 100644 --- a/R/dt_options.R +++ b/R/dt_options.R @@ -256,7 +256,7 @@ dt_options_tbl <- "ihtml_page_size_default", FALSE, "interactive", "values", 10, "ihtml_page_size_values", FALSE, "interactive", "values", default_page_size_vec, "ihtml_pagination_type", FALSE, "interactive", "value", "numbers", - "ihtml_selection", FALSE, "interactive", "value", NA_character_, + "ihtml_selection_mode", FALSE, "interactive", "value", NA_character_, "page_orientation", FALSE, "page", "value", "portrait", "page_numbering", FALSE, "page", "logical", FALSE, "page_header_use_tbl_headings", FALSE, "page", "logical", FALSE, diff --git a/R/opts.R b/R/opts.R index a1daa28b6..037461153 100644 --- a/R/opts.R +++ b/R/opts.R @@ -220,7 +220,7 @@ get_colorized_params <- function( #' - `ihtml.page_size_values` #' - `ihtml.pagination_type` #' - `ihtml.height` -#' - `ihtml.selection` +#' - `ihtml.selection_mode` #' #' @inheritParams fmt_number #' @@ -338,15 +338,16 @@ get_colorized_params <- function( #' #' Height of the table in pixels. Defaults to `"auto"` for automatic sizing. #' -#' @param selection *Allow row selection* +#' @param selection_mode *Allow row selection* #' #' `scalar` // *default:* `NULL` #' -#' The `selection` options allows users to select rows by clicking them. When -#' this option is `"single"`, clicking another value toggles selection of the -#' previously selected row off. When this option is `"multiple"`, multiple -#' rows can be selected at once. Selected values are available in Shiny apps -#' when `selection` is not `NULL` and the table is used in [render_gt()]. +#' The `selection_mode` options allows users to select rows by clicking them. +#' When this option is `"single"`, clicking another value toggles selection +#' of the previously selected row off. When this option is `"multiple"`, +#' multiple rows can be selected at once. Selected values are available in +#' Shiny apps when `selection_mode` is not `NULL` and the table is used in +#' [render_gt()]. #' #' @return An object of class `gt_tbl`. #' @@ -430,7 +431,7 @@ opt_interactive <- function( page_size_values = c(10, 25, 50, 100), pagination_type = c("numbers", "jump", "simple"), height = "auto", - selection = NULL + selection_mode = NULL ) { # Perform input object validation @@ -456,7 +457,7 @@ opt_interactive <- function( ihtml.page_size_values = page_size_values, ihtml.pagination_type = pagination_type, ihtml.height = height, - ihtml.selection = selection + ihtml.selection_mode = selection_mode ) } diff --git a/R/render_as_i_html.R b/R/render_as_i_html.R index dc19c361b..ba9a1337a 100644 --- a/R/render_as_i_html.R +++ b/R/render_as_i_html.R @@ -188,9 +188,9 @@ render_as_ihtml <- function(data, id) { page_size_default <- tbl_opts$ihtml_page_size_default page_size_values <- tbl_opts$ihtml_page_size_values pagination_type <- tbl_opts$ihtml_pagination_type - selection <- tbl_opts$ihtml_selection - if (is.na(selection)) selection <- NULL - onClick <- if (!is.null(selection)) "select" + selection_mode <- tbl_opts$ihtml_selection_mode + if (is.na(selection_mode)) selection_mode <- NULL + onClick <- if (!is.null(selection_mode)) "select" use_row_striping <- tbl_opts$row_striping_include_table_body row_striping_color <- tbl_opts$row_striping_background_color @@ -686,7 +686,7 @@ render_as_ihtml <- function(data, id) { paginateSubRows = FALSE, details = NULL, defaultExpanded = expand_groupname_col, - selection = selection, + selection = selection_mode, selectionId = NULL, defaultSelected = NULL, onClick = onClick, diff --git a/R/tab_options.R b/R/tab_options.R index 246c9fb16..8b72a512e 100644 --- a/R/tab_options.R +++ b/R/tab_options.R @@ -455,14 +455,14 @@ #' #' Height of the table in pixels. Defaults to `"auto"` for automatic sizing. #' -#' @param ihtml.selection *Allow row selection* +#' @param ihtml.selection_mode *Allow row selection* #' #' For interactive HTML output, this allows users to select rows by clicking #' them. When this option is `"single"`, clicking another value toggles #' selection of the previously selected row off. When this option is #' `"multiple"`, multiple rows can be selected at once. Selected values are -#' available in Shiny apps when `ihtml.selection` is not `NULL` and the table -#' is used in [render_gt()]. +#' available in Shiny apps when `ihtml.selection_mode` is not `NULL` and the +#' table is used in [render_gt()]. #' #' @param page.orientation *Set RTF page orientation* #' @@ -847,7 +847,7 @@ tab_options <- function( ihtml.page_size_values = NULL, ihtml.pagination_type = NULL, ihtml.height = NULL, - ihtml.selection = NULL, + ihtml.selection_mode = NULL, page.orientation = NULL, page.numbering = NULL, page.header.use_tbl_headings = NULL, @@ -1044,16 +1044,16 @@ set_super_options <- function(arg_vals) { ) } - if ("ihtml.selection" %in% names(arg_vals)) { - ihtml_selection_val <- arg_vals$ihtml.selection + if ("ihtml.selection_mode" %in% names(arg_vals)) { + ihtml_selection_mode_val <- arg_vals$ihtml.selection_mode if ( !( - rlang::is_scalar_character(ihtml_selection_val) && - ihtml_selection_val %in% c("single", "multiple") + rlang::is_scalar_character(ihtml_selection_mode_val) && + ihtml_selection_mode_val %in% c("single", "multiple") ) ) { cli::cli_abort(c( - "The chosen option for `ihtml.selection` (`{ihtml_selection_val}`) is invalid.", + "The chosen option for `ihtml.selection_mode` (`{ihtml_selection_mode_val}`) is invalid.", "*" = "We can use either \"single\" or \"multiple\"." )) } diff --git a/man/opt_interactive.Rd b/man/opt_interactive.Rd index f3ba337bf..bb76240af 100644 --- a/man/opt_interactive.Rd +++ b/man/opt_interactive.Rd @@ -21,7 +21,7 @@ opt_interactive( page_size_values = c(10, 25, 50, 100), pagination_type = c("numbers", "jump", "simple"), height = "auto", - selection = NULL + selection_mode = NULL ) } \arguments{ @@ -146,15 +146,16 @@ and 'next' buttons are displayed.} Height of the table in pixels. Defaults to \code{"auto"} for automatic sizing.} -\item{selection}{\emph{Allow row selection} +\item{selection_mode}{\emph{Allow row selection} \verb{scalar} // \emph{default:} \code{NULL} -The \code{selection} options allows users to select rows by clicking them. When -this option is \code{"single"}, clicking another value toggles selection of the -previously selected row off. When this option is \code{"multiple"}, multiple -rows can be selected at once. Selected values are available in Shiny apps -when \code{selection} is not \code{NULL} and the table is used in \code{\link[=render_gt]{render_gt()}}.} +The \code{selection_mode} options allows users to select rows by clicking them. +When this option is \code{"single"}, clicking another value toggles selection +of the previously selected row off. When this option is \code{"multiple"}, +multiple rows can be selected at once. Selected values are available in +Shiny apps when \code{selection_mode} is not \code{NULL} and the table is used in +\code{\link[=render_gt]{render_gt()}}.} } \value{ An object of class \code{gt_tbl}. @@ -183,7 +184,7 @@ This function serves as a shortcut for setting the following options in \item \code{ihtml.page_size_values} \item \code{ihtml.pagination_type} \item \code{ihtml.height} -\item \code{ihtml.selection} +\item \code{ihtml.selection_mode} } } \section{Examples}{ diff --git a/man/tab_options.Rd b/man/tab_options.Rd index 232bcf6ef..7bdc03f5a 100644 --- a/man/tab_options.Rd +++ b/man/tab_options.Rd @@ -178,7 +178,7 @@ tab_options( ihtml.page_size_values = NULL, ihtml.pagination_type = NULL, ihtml.height = NULL, - ihtml.selection = NULL, + ihtml.selection_mode = NULL, page.orientation = NULL, page.numbering = NULL, page.header.use_tbl_headings = NULL, @@ -596,14 +596,14 @@ displayed.} Height of the table in pixels. Defaults to \code{"auto"} for automatic sizing.} -\item{ihtml.selection}{\emph{Allow row selection} +\item{ihtml.selection_mode}{\emph{Allow row selection} For interactive HTML output, this allows users to select rows by clicking them. When this option is \code{"single"}, clicking another value toggles selection of the previously selected row off. When this option is \code{"multiple"}, multiple rows can be selected at once. Selected values are -available in Shiny apps when \code{ihtml.selection} is not \code{NULL} and the table -is used in \code{\link[=render_gt]{render_gt()}}.} +available in Shiny apps when \code{ihtml.selection_mode} is not \code{NULL} and the +table is used in \code{\link[=render_gt]{render_gt()}}.} \item{page.orientation}{\emph{Set RTF page orientation} From 32705e23075617c64928550a00241c75d5109c20 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 5 Nov 2024 12:15:36 -0600 Subject: [PATCH 16/16] Fix tests for rename. --- tests/testthat/test-tab_options.R | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/testthat/test-tab_options.R b/tests/testthat/test-tab_options.R index e55d64b45..65ec80889 100644 --- a/tests/testthat/test-tab_options.R +++ b/tests/testthat/test-tab_options.R @@ -1432,18 +1432,18 @@ test_that("ihtml.selection option can be modified", { # Check that specific suggested packages are available check_suggests() - tbl_html_single <- tab_options(data, ihtml.selection = "single") - tbl_html_multi <- tab_options(data, ihtml.selection = "multiple") + tbl_html_single <- tab_options(data, ihtml.selection_mode = "single") + tbl_html_multi <- tab_options(data, ihtml.selection_mode = "multiple") - c(dt_options_get_value(data = data, option = "ihtml_selection"), - dt_options_get_value(data = tbl_html_single, option = "ihtml_selection"), - dt_options_get_value(data = tbl_html_multi, option = "ihtml_selection") + c(dt_options_get_value(data = data, option = "ihtml_selection_mode"), + dt_options_get_value(data = tbl_html_single, option = "ihtml_selection_mode"), + dt_options_get_value(data = tbl_html_multi, option = "ihtml_selection_mode") ) %>% expect_equal(c(NA_character_, "single", "multiple")) expect_error( - tab_options(data, ihtml.selection = "bad"), - "The chosen option for `ihtml\\.selection" + tab_options(data, ihtml.selection_mode = "bad"), + "The chosen option for `ihtml\\.selection_mode" ) })