From 07de69becb775fee77ab26aa68c19d083709ef62 Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 10 Feb 2023 13:45:07 -0600 Subject: [PATCH 1/4] Add input_check_search() --- DESCRIPTION | 1 + NAMESPACE | 2 + R/input-check-search.R | 106 ++++++++++++++++++++++++++++++ inst/components/check-search.js | 106 ++++++++++++++++++++++++++++++ inst/components/check-search.scss | 25 +++++++ 5 files changed, 240 insertions(+) create mode 100644 R/input-check-search.R create mode 100644 inst/components/check-search.js create mode 100644 inst/components/check-search.scss diff --git a/DESCRIPTION b/DESCRIPTION index 99424402d..089323c43 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -61,6 +61,7 @@ Collate: 'deprecated.R' 'files.R' 'imports.R' + 'input-check-search.R' 'layout.R' 'nav-items.R' 'nav-update.R' diff --git a/NAMESPACE b/NAMESPACE index 4d6bc01d1..b9612c7b4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -61,6 +61,7 @@ export(font_collection) export(font_face) export(font_google) export(font_link) +export(input_check_search) export(is.card_item) export(is_bs_theme) export(layout_column_wrap) @@ -94,6 +95,7 @@ export(showcase_left_center) export(showcase_top_right) export(theme_bootswatch) export(theme_version) +export(update_check_search) export(value_box) export(version_default) export(versions) diff --git a/R/input-check-search.R b/R/input-check-search.R new file mode 100644 index 000000000..421e2403c --- /dev/null +++ b/R/input-check-search.R @@ -0,0 +1,106 @@ +#' A searchable list of checkboxes +#' +#' @param id an input id. +#' @param choices a vector/list of choices. If there are names on the on the vector, those names are used as the input value. +#' @param selected a vector/list of choices to select by default. +#' @param placeholder some text to appear when no search input is provided +#' @param height a valid CSS unit for the height of the input. +#' +#' @export +input_check_search <- function(id, choices, selected = NULL, placeholder = "🔍 Search", height = NULL, width = NULL) { + + tag <- div( + id = id, + class = "bslib-check-search", + style = css(height = height, width = width), + tags$a(class = "clear-options", role = "button", "Clear all"), + tags$input( + type = "text", + id = paste0(id, "-search"), + class = "form-control form-control-sm", + class = "shiny-no-bind", # TODO: require shiny PR + placeholder = placeholder, + autocomplete = "off" + ), + check_search_choices(id, choices, selected), + check_search_dependency() + ) + + tag <- tag_require(tag, version = 5, caller = "input_check_search") + + as_fragment(tag) +} + + +#' @export +update_check_search <- function(id, choices = NULL, selected = NULL, placeholder = NULL, height = NULL, session = shiny::getDefaultReactiveDomain()) { + if (!is.null(choices)) { + choices <- process_ui( + check_search_choices(id, choices, selected), + session + ) + } + + message <- dropNulls(list( + choices = choices, + selected = as.list(selected), # make sure this is always a JS array + placeholder = placeholder, + height = height + )) + session$sendInputMessage(id, message) +} + +check_search_choices <- function(id, choices, selected) { + if (is.null(names(choices)) && is.atomic(choices)) { + names(choices) <- choices + } + if (is.null(names(choices))) { + stop("names() must be provided on list() vectors provided to choices") + } + + vals <- rlang::names2(choices) + #if (!all(nzchar(vals))) { + # stop("Input values must be non-empty character strings") + #} + + is_selected <- vapply(vals, function(x) { + isTRUE(x %in% selected) || identical(selected, I("all")) + }, logical(1)) + + checks <- unname(Map( + vals, choices, is_selected, paste0(id, "-", seq_along(is_selected)), + f = form_check + )) + + # Always bring selections to the top + idx <- c(which(is_selected), which(!is_selected)) + + div( + class = "check-search-choices", + !!!checks[idx] + ) +} + +form_check <- function(val, lbl, checked, this_id) { + div( + class = "form-check", `data-value` = val, + tags$input( + type = "checkbox", + class = "form-check-input", + class = "shiny-no-bind", + id = this_id, + checked = if (checked) NA + ), + tags$label(class = "form-check-label", `for` = this_id, lbl) + ) +} + +check_search_dependency <- function() { + htmlDependency( + "bslib-check-search", + version = get_package_version("bslib"), + package = "bslib", + src = "components", + script = "check-search.js" + ) +} diff --git a/inst/components/check-search.js b/inst/components/check-search.js new file mode 100644 index 000000000..e69e2d74d --- /dev/null +++ b/inst/components/check-search.js @@ -0,0 +1,106 @@ +const checkSearchInputBinding = new Shiny.InputBinding(); +$.extend(checkSearchInputBinding, { + + find: function(scope) { + return $(scope).find(".bslib-check-search"); + }, + + getValue: function(el) { + const inputs = $(el).find(".form-check-input"); + let vals = []; + inputs.each(function(i) { + if (this.checked) { + vals.push($(this).parent(".form-check").attr("data-value")); + } + }); + return vals.length > 0 ? vals : null; + }, + + subscribe: function(el, callback) { + const self = this; + $(el).on('change.checkSearch', function(event) { + + const choices = $(event.target).parents(".check-search-choices"); + + // Move new selections to the top + const firstNotChecked = choices + .find("input:not(:checked)") + .parents(".form-check") + .last(); + const thisForm = $(event.target).parent(".form-check"); + firstNotChecked.before(thisForm); + + // TODO: if we're unchecking a box, should we move it back to it's "original" position??? + + self._resolveClearVisibility(el); + + callback(true); + }); + }, + + unsubscribe: function(el) { + $(el).off(".checkSearchInputBinding"); + }, + + initialize: function(el) { + el.oninput = onInput; + + function onInput(e) { + const needle = e.target.value.toLowerCase(); + + const haystack = $(e.target.parentNode).find(".form-check"); + haystack.each(function(i) { + const val = $(this).attr("data-value").toLowerCase(); + const display = val.includes(needle) ? "" : "none"; + $(this).css("display", display); + }); + } + + const clear = $(el).find(".clear-options"); + const self = this; + clear.click(function() { + self.receiveMessage(el, {selected: []}); + }); + + this._resolveClearVisibility(el); + }, + + receiveMessage: function(el, data) { + const $el = $(el); + if (data.hasOwnProperty("placeholder")) { + $el.find("input").attr("placeholder", data.placeholder); + return; + } + if (data.hasOwnProperty("height")) { + $el.css("height", data.height); + return; + } + // In this case, selected is already handled in the markup + if (data.hasOwnProperty("choices")) { + const choices = $el.find(".check-search-choices"); + Shiny.renderContent(choices, data.choices); + } else if (data.hasOwnProperty("selected")) { + const checks = $el.find(".form-check"); + checks.each(function(i) { + const val = $(this).attr("data-value"); + const checked = data.selected.indexOf(val) > -1; + this.querySelector("input").checked = checked; + }); + } + + // Since we're possibly changed the input value at this point, + // trigger a subscribe() event, so that the input value will actually update + $el.trigger("change.checkSearch"); + + this._resolveClearVisibility(el); + }, + + _resolveClearVisibility: function(el) { + const clear = $(el).find(".clear-options"); + const anySelected = $(el).find("input:checked").length > 0; + clear.css("visibility", anySelected ? "visible" : "hidden"); + } + +}); + +Shiny.inputBindings.register(checkSearchInputBinding); diff --git a/inst/components/check-search.scss b/inst/components/check-search.scss new file mode 100644 index 000000000..960318192 --- /dev/null +++ b/inst/components/check-search.scss @@ -0,0 +1,25 @@ +.bslib-check-search { + height: 200px; + width: fit-content; + width: -moz-fit-content; + + .form-control { + position: sticky; + margin-bottom: 5px; + } + + .clear-options { + visibility: hidden; + text-decoration: none; + float: right; + font-size: $font-size-sm; + font-weight: $font-weight-bold; + } + + .check-search-choices { + overflow: scroll; + height: 100%; + width: 100%; + padding-left: 0.2rem; + } +} From 68a584cec484e46e270e0c434be181029fb30dde Mon Sep 17 00:00:00 2001 From: cpsievert Date: Fri, 10 Feb 2023 19:48:49 +0000 Subject: [PATCH 2/4] `devtools::document()` (GitHub Actions) --- man/input_check_search.Rd | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 man/input_check_search.Rd diff --git a/man/input_check_search.Rd b/man/input_check_search.Rd new file mode 100644 index 000000000..8efc9d3f6 --- /dev/null +++ b/man/input_check_search.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/input-check-search.R +\name{input_check_search} +\alias{input_check_search} +\title{A searchable list of checkboxes} +\usage{ +input_check_search( + id, + choices, + selected = NULL, + placeholder = "🔍 Search", + height = NULL, + width = NULL +) +} +\arguments{ +\item{id}{an input id.} + +\item{choices}{a vector/list of choices. If there are names on the on the vector, those names are used as the input value.} + +\item{selected}{a vector/list of choices to select by default.} + +\item{placeholder}{some text to appear when no search input is provided} + +\item{height}{a valid CSS unit for the height of the input.} +} +\description{ +A searchable list of checkboxes +} From e847d5ab1952237b3a67f9b42341e3dfbbb19e8c Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 10 Feb 2023 14:00:47 -0600 Subject: [PATCH 3/4] Updates/fixes --- R/bs-theme.R | 1 + R/input-check-search.R | 11 +++++++---- .../{check-search.js => input_check_search.js} | 0 .../{check-search.scss => input_check_search.scss} | 0 4 files changed, 8 insertions(+), 4 deletions(-) rename inst/components/{check-search.js => input_check_search.js} (100%) rename inst/components/{check-search.scss => input_check_search.scss} (100%) diff --git a/R/bs-theme.R b/R/bs-theme.R index 08dbf86e4..f92c91da6 100644 --- a/R/bs-theme.R +++ b/R/bs-theme.R @@ -276,6 +276,7 @@ bootstrap_bundle <- function(version) { !!!rule_bundles(c( system_file("components", "accordion.scss", package = "bslib"), system_file("components", "card.scss", package = "bslib"), + system_file("components", "input_check_search.scss", package = "bslib"), system_file("components", "value_box.scss", package = "bslib"), system_file("components", "layout_column_wrap.scss", package = "bslib") )) diff --git a/R/input-check-search.R b/R/input-check-search.R index 421e2403c..4b2467e35 100644 --- a/R/input-check-search.R +++ b/R/input-check-search.R @@ -12,7 +12,10 @@ input_check_search <- function(id, choices, selected = NULL, placeholder = "🔍 tag <- div( id = id, class = "bslib-check-search", - style = css(height = height, width = width), + style = css( + height = validateCssUnit(height), + width = validateCssUnit(width) + ), tags$a(class = "clear-options", role = "button", "Clear all"), tags$input( type = "text", @@ -33,9 +36,9 @@ input_check_search <- function(id, choices, selected = NULL, placeholder = "🔍 #' @export -update_check_search <- function(id, choices = NULL, selected = NULL, placeholder = NULL, height = NULL, session = shiny::getDefaultReactiveDomain()) { +update_check_search <- function(id, choices = NULL, selected = NULL, placeholder = NULL, height = NULL, session = get_current_session()) { if (!is.null(choices)) { - choices <- process_ui( + choices <- processDeps( check_search_choices(id, choices, selected), session ) @@ -101,6 +104,6 @@ check_search_dependency <- function() { version = get_package_version("bslib"), package = "bslib", src = "components", - script = "check-search.js" + script = "input_check_search.js" ) } diff --git a/inst/components/check-search.js b/inst/components/input_check_search.js similarity index 100% rename from inst/components/check-search.js rename to inst/components/input_check_search.js diff --git a/inst/components/check-search.scss b/inst/components/input_check_search.scss similarity index 100% rename from inst/components/check-search.scss rename to inst/components/input_check_search.scss From c984c5ad356506a3e66c18bf84b5daadd97dd746 Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 10 Feb 2023 14:34:47 -0600 Subject: [PATCH 4/4] Add input_check_buttons() and input_radio_buttons() --- DESCRIPTION | 2 +- NAMESPACE | 6 +- R/bs-theme.R | 1 - R/input-button-group.R | 135 ++++++++++++++++++++++++ R/input-check-search.R | 109 ------------------- inst/components/input_check_search.js | 106 ------------------- inst/components/input_check_search.scss | 25 ----- inst/components/toggle-buttons.js | 47 +++++++++ man/input_check_buttons.Rd | 55 ++++++++++ 9 files changed, 242 insertions(+), 244 deletions(-) create mode 100644 R/input-button-group.R delete mode 100644 R/input-check-search.R delete mode 100644 inst/components/input_check_search.js delete mode 100644 inst/components/input_check_search.scss create mode 100644 inst/components/toggle-buttons.js create mode 100644 man/input_check_buttons.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 089323c43..c2375b6de 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -61,7 +61,7 @@ Collate: 'deprecated.R' 'files.R' 'imports.R' - 'input-check-search.R' + 'input-button-group.R' 'layout.R' 'nav-items.R' 'nav-update.R' diff --git a/NAMESPACE b/NAMESPACE index b9612c7b4..b83807616 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -61,7 +61,8 @@ export(font_collection) export(font_face) export(font_google) export(font_link) -export(input_check_search) +export(input_check_buttons) +export(input_radio_buttons) export(is.card_item) export(is_bs_theme) export(layout_column_wrap) @@ -95,7 +96,8 @@ export(showcase_left_center) export(showcase_top_right) export(theme_bootswatch) export(theme_version) -export(update_check_search) +export(update_check_buttons) +export(update_radio_buttons) export(value_box) export(version_default) export(versions) diff --git a/R/bs-theme.R b/R/bs-theme.R index f92c91da6..08dbf86e4 100644 --- a/R/bs-theme.R +++ b/R/bs-theme.R @@ -276,7 +276,6 @@ bootstrap_bundle <- function(version) { !!!rule_bundles(c( system_file("components", "accordion.scss", package = "bslib"), system_file("components", "card.scss", package = "bslib"), - system_file("components", "input_check_search.scss", package = "bslib"), system_file("components", "value_box.scss", package = "bslib"), system_file("components", "layout_column_wrap.scss", package = "bslib") )) diff --git a/R/input-button-group.R b/R/input-button-group.R new file mode 100644 index 000000000..019001fd7 --- /dev/null +++ b/R/input-button-group.R @@ -0,0 +1,135 @@ +#' Create a button group of radio/check boxes +#' +#' Use `input_check_buttons()` if multiple choices may be selected at once; otherwise, use `input_radio_buttons()` +#' +#' @inheritParams input_check_search +#' @param size size of the button group +#' @param bg a theme color to use for the btn modifier class +#' @export +input_check_buttons <- function(id, choices, ..., selected = NULL, size = c("md", "sm", "lg"), bg = "primary") { + size <- match.arg(size) + tag <- div( + id = id, + class = "btn-group bslib-toggle-buttons", + class = if (size != "md") paste0("btn-group-", size), + role = "group", + ..., + !!!input_buttons_container( + type = "checkbox", id = id, choices = choices, selected = selected, + size = size, bg = bg + ), + toggle_dependency() + ) + tag <- tag_require(tag, version = 5, caller = "input_check_buttons()") + as_fragment(tag) +} + +#' @export +#' @rdname input_check_buttons +update_check_buttons <- function(id, choices = NULL, selected = NULL, session = get_current_session()) { + if (!is.null(choices)) { + choices <- processDeps( + input_buttons_container(type = "checkbox", id, choices, selected), + session + ) + } + message <- dropNulls(list( + choices = choices, + selected = as.list(selected) + )) + session$sendInputMessage(id, message) +} + +#' @export +#' @rdname input_check_buttons +input_radio_buttons <- function(id, choices, ..., selected = NULL, size = c("md", "sm", "lg"), bg = "primary") { + size <- match.arg(size) + tag <- div( + id = id, + class = "btn-group bslib-toggle-buttons", + class = if (size != "md") paste0("btn-group-", size), + role = "group", + ..., + !!!input_buttons_container( + type = "checkbox", id = id, choices = choices, selected = selected, + size = size, bg = bg + ), + toggle_dependency() + ) + tag <- tag_require(tag, version = 5, caller = "input_radio_buttons()") + as_fragment(tag) +} + +#' @export +#' @rdname input_check_buttons +update_radio_buttons <- function(id, choices = NULL, selected = NULL, session = get_current_session()) { + + if (!is.null(choices)) { + choices <- processDeps( + input_buttons_container(type = "radio", id, choices, selected), + session + ) + } + message <- dropNulls(list( + choices = choices, + selected = as.list(selected) + )) + session$sendInputMessage(id, message) +} + + +input_buttons_container <- function(type = c("radio", "checkbox"), id, choices, selected, size = "md", bg = "primary") { + + if (is.null(names(choices)) && is.atomic(choices)) { + names(choices) <- choices + } + if (is.null(names(choices))) { + stop("names() must be provided on list() vectors provided to choices") + } + + vals <- rlang::names2(choices) + #if (!all(nzchar(vals))) { + # stop("Input values must be non-empty character strings") + #} + + is_checked <- vapply(vals, function(x) isTRUE(x %in% selected) || identical(I("all"), selected), logical(1)) + + if (!any(is_checked) && !identical(selected, I("none"))) { + is_checked[1] <- TRUE + } + + type <- match.arg(type) + if (type == "radio" && sum(is_checked) > 1) { + stop("input_radio_buttons() doesn't support more than one selected choice (do you want input_check_buttons() instead?)", call. = FALSE) + } + + inputs <- Map( + vals, choices, is_checked, paste0(id, "-", seq_along(is_checked)), + f = function(val, lbl, checked, this_id) { + list( + tags$input( + type = type, class = "btn-check", name = id, + id = this_id, autocomplete = "off", + `data-value` = val, + checked = if (checked) NA + ), + tags$label( + class = paste0("btn btn-outline-", bg), + `for` = this_id, lbl + ) + ) + } + ) + + inputs <- unlist(inputs, recursive = FALSE, use.names = FALSE) +} + +toggle_dependency <- function() { + htmltools::htmlDependency( + "bslib-toggle-buttons", + version = get_package_version("bslib"), + package = "bslib", + src = "components", + script = "toggle-buttons.js" + ) +} diff --git a/R/input-check-search.R b/R/input-check-search.R deleted file mode 100644 index 4b2467e35..000000000 --- a/R/input-check-search.R +++ /dev/null @@ -1,109 +0,0 @@ -#' A searchable list of checkboxes -#' -#' @param id an input id. -#' @param choices a vector/list of choices. If there are names on the on the vector, those names are used as the input value. -#' @param selected a vector/list of choices to select by default. -#' @param placeholder some text to appear when no search input is provided -#' @param height a valid CSS unit for the height of the input. -#' -#' @export -input_check_search <- function(id, choices, selected = NULL, placeholder = "🔍 Search", height = NULL, width = NULL) { - - tag <- div( - id = id, - class = "bslib-check-search", - style = css( - height = validateCssUnit(height), - width = validateCssUnit(width) - ), - tags$a(class = "clear-options", role = "button", "Clear all"), - tags$input( - type = "text", - id = paste0(id, "-search"), - class = "form-control form-control-sm", - class = "shiny-no-bind", # TODO: require shiny PR - placeholder = placeholder, - autocomplete = "off" - ), - check_search_choices(id, choices, selected), - check_search_dependency() - ) - - tag <- tag_require(tag, version = 5, caller = "input_check_search") - - as_fragment(tag) -} - - -#' @export -update_check_search <- function(id, choices = NULL, selected = NULL, placeholder = NULL, height = NULL, session = get_current_session()) { - if (!is.null(choices)) { - choices <- processDeps( - check_search_choices(id, choices, selected), - session - ) - } - - message <- dropNulls(list( - choices = choices, - selected = as.list(selected), # make sure this is always a JS array - placeholder = placeholder, - height = height - )) - session$sendInputMessage(id, message) -} - -check_search_choices <- function(id, choices, selected) { - if (is.null(names(choices)) && is.atomic(choices)) { - names(choices) <- choices - } - if (is.null(names(choices))) { - stop("names() must be provided on list() vectors provided to choices") - } - - vals <- rlang::names2(choices) - #if (!all(nzchar(vals))) { - # stop("Input values must be non-empty character strings") - #} - - is_selected <- vapply(vals, function(x) { - isTRUE(x %in% selected) || identical(selected, I("all")) - }, logical(1)) - - checks <- unname(Map( - vals, choices, is_selected, paste0(id, "-", seq_along(is_selected)), - f = form_check - )) - - # Always bring selections to the top - idx <- c(which(is_selected), which(!is_selected)) - - div( - class = "check-search-choices", - !!!checks[idx] - ) -} - -form_check <- function(val, lbl, checked, this_id) { - div( - class = "form-check", `data-value` = val, - tags$input( - type = "checkbox", - class = "form-check-input", - class = "shiny-no-bind", - id = this_id, - checked = if (checked) NA - ), - tags$label(class = "form-check-label", `for` = this_id, lbl) - ) -} - -check_search_dependency <- function() { - htmlDependency( - "bslib-check-search", - version = get_package_version("bslib"), - package = "bslib", - src = "components", - script = "input_check_search.js" - ) -} diff --git a/inst/components/input_check_search.js b/inst/components/input_check_search.js deleted file mode 100644 index e69e2d74d..000000000 --- a/inst/components/input_check_search.js +++ /dev/null @@ -1,106 +0,0 @@ -const checkSearchInputBinding = new Shiny.InputBinding(); -$.extend(checkSearchInputBinding, { - - find: function(scope) { - return $(scope).find(".bslib-check-search"); - }, - - getValue: function(el) { - const inputs = $(el).find(".form-check-input"); - let vals = []; - inputs.each(function(i) { - if (this.checked) { - vals.push($(this).parent(".form-check").attr("data-value")); - } - }); - return vals.length > 0 ? vals : null; - }, - - subscribe: function(el, callback) { - const self = this; - $(el).on('change.checkSearch', function(event) { - - const choices = $(event.target).parents(".check-search-choices"); - - // Move new selections to the top - const firstNotChecked = choices - .find("input:not(:checked)") - .parents(".form-check") - .last(); - const thisForm = $(event.target).parent(".form-check"); - firstNotChecked.before(thisForm); - - // TODO: if we're unchecking a box, should we move it back to it's "original" position??? - - self._resolveClearVisibility(el); - - callback(true); - }); - }, - - unsubscribe: function(el) { - $(el).off(".checkSearchInputBinding"); - }, - - initialize: function(el) { - el.oninput = onInput; - - function onInput(e) { - const needle = e.target.value.toLowerCase(); - - const haystack = $(e.target.parentNode).find(".form-check"); - haystack.each(function(i) { - const val = $(this).attr("data-value").toLowerCase(); - const display = val.includes(needle) ? "" : "none"; - $(this).css("display", display); - }); - } - - const clear = $(el).find(".clear-options"); - const self = this; - clear.click(function() { - self.receiveMessage(el, {selected: []}); - }); - - this._resolveClearVisibility(el); - }, - - receiveMessage: function(el, data) { - const $el = $(el); - if (data.hasOwnProperty("placeholder")) { - $el.find("input").attr("placeholder", data.placeholder); - return; - } - if (data.hasOwnProperty("height")) { - $el.css("height", data.height); - return; - } - // In this case, selected is already handled in the markup - if (data.hasOwnProperty("choices")) { - const choices = $el.find(".check-search-choices"); - Shiny.renderContent(choices, data.choices); - } else if (data.hasOwnProperty("selected")) { - const checks = $el.find(".form-check"); - checks.each(function(i) { - const val = $(this).attr("data-value"); - const checked = data.selected.indexOf(val) > -1; - this.querySelector("input").checked = checked; - }); - } - - // Since we're possibly changed the input value at this point, - // trigger a subscribe() event, so that the input value will actually update - $el.trigger("change.checkSearch"); - - this._resolveClearVisibility(el); - }, - - _resolveClearVisibility: function(el) { - const clear = $(el).find(".clear-options"); - const anySelected = $(el).find("input:checked").length > 0; - clear.css("visibility", anySelected ? "visible" : "hidden"); - } - -}); - -Shiny.inputBindings.register(checkSearchInputBinding); diff --git a/inst/components/input_check_search.scss b/inst/components/input_check_search.scss deleted file mode 100644 index 960318192..000000000 --- a/inst/components/input_check_search.scss +++ /dev/null @@ -1,25 +0,0 @@ -.bslib-check-search { - height: 200px; - width: fit-content; - width: -moz-fit-content; - - .form-control { - position: sticky; - margin-bottom: 5px; - } - - .clear-options { - visibility: hidden; - text-decoration: none; - float: right; - font-size: $font-size-sm; - font-weight: $font-weight-bold; - } - - .check-search-choices { - overflow: scroll; - height: 100%; - width: 100%; - padding-left: 0.2rem; - } -} diff --git a/inst/components/toggle-buttons.js b/inst/components/toggle-buttons.js new file mode 100644 index 000000000..23455e052 --- /dev/null +++ b/inst/components/toggle-buttons.js @@ -0,0 +1,47 @@ +var toggleButtonsInputBinding = new Shiny.InputBinding(); +$.extend(toggleButtonsInputBinding, { + + find: function(scope) { + return $(scope).find(".btn-group.bslib-toggle-buttons"); + }, + + getValue: function(el) { + var inputs = $(el).find("input.btn-check"); + var vals = []; + inputs.each(function(i) { + if (this.checked) { + vals.push($(this).attr("data-value")); + } + }); + return vals.length > 0 ? vals : null; + }, + + subscribe: function(el, callback) { + $(el).on( + 'change.toggleButtonsInputBinding', + function(event) { callback(true); } + ); + }, + + unsubscribe: function(el) { + $(el).off(".toggleButtonsInputBinding"); + }, + + receiveMessage: function(el, data) { + if (data.hasOwnProperty("choices")) { + Shiny.renderContent(el, data.choices); + } else if (data.hasOwnProperty("selected")) { + const inputs = $(el).find("input"); + inputs.each(function(i) { + const val = $(this).attr("data-value"); + const checked = data.selected.indexOf(val) > -1; + this.checked = checked; + }); + } + + $(el).trigger("change.toggleButtonsInputBinding"); + } + +}); + +Shiny.inputBindings.register(toggleButtonsInputBinding); diff --git a/man/input_check_buttons.Rd b/man/input_check_buttons.Rd new file mode 100644 index 000000000..e8b62cd76 --- /dev/null +++ b/man/input_check_buttons.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/input-button-group.R +\name{input_check_buttons} +\alias{input_check_buttons} +\alias{update_check_buttons} +\alias{input_radio_buttons} +\alias{update_radio_buttons} +\title{Create a button group of radio/check boxes} +\usage{ +input_check_buttons( + id, + choices, + ..., + selected = NULL, + size = c("md", "sm", "lg"), + bg = "primary" +) + +update_check_buttons( + id, + choices = NULL, + selected = NULL, + session = get_current_session() +) + +input_radio_buttons( + id, + choices, + ..., + selected = NULL, + size = c("md", "sm", "lg"), + bg = "primary" +) + +update_radio_buttons( + id, + choices = NULL, + selected = NULL, + session = get_current_session() +) +} +\arguments{ +\item{id}{an input id.} + +\item{choices}{a vector/list of choices. If there are names on the on the vector, those names are used as the input value.} + +\item{selected}{a vector/list of choices to select by default.} + +\item{size}{size of the button group} + +\item{bg}{a theme color to use for the btn modifier class} +} +\description{ +Use \code{input_check_buttons()} if multiple choices may be selected at once; otherwise, use \code{input_radio_buttons()} +}