1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
|
#' A Bootstrap card component
#'
#' A general purpose container for grouping related UI elements together with a
#' border and optional padding. To learn more about [card()]s, see [this
#' article](https://rstudio.github.io/bslib/articles/cards.html).
#'
#' @param ... Unnamed arguments can be any valid child of an [htmltools
#' tag][htmltools::tags] (which includes card items such as [card_body()].
#' Named arguments become HTML attributes on returned UI element.
#' @param full_screen If `TRUE`, an icon will appear when hovering over the card
#' body. Clicking the icon expands the card to fit viewport size. Consider
#' pairing this feature with [card_body_fill()] to get output that responds to
#' changes in the size of the card.
#' @param height Any valid [CSS unit][htmltools::validateCssUnit] (e.g.,
#' `height="200px"`).
#' @param class Additional CSS classes for the returned UI element.
#' @param wrapper A function (which returns a UI element) to call on unnamed
#' arguments in `...` which are not already card item(s) (like
#' [card_header()], [card_body()], etc.). Note that non-card items are grouped
#' together into one `wrapper` call (e.g. given `card("a", "b",
#' card_body("c"), "d")`, `wrapper` would be called twice, once with `"a"` and
#' `"b"` and once with `"d"`). Consider setting `wrapper` to [card_body_fill]
#' if the entire card wants responsive sizing or `NULL` to avoid wrapping
#' altogether
#'
#' @return A [htmltools::div()] tag.
#'
#' @export
#' @seealso [card_body()] for putting stuff inside the card.
#' @seealso [navs_tab_card()] for cards with multiple tabs.
#' @seealso [layout_column_wrap()] for laying out multiple cards (or multiple
#' columns inside a card).
#' @examples
#'
#' library(htmltools)
#'
#' if (interactive()) {
#' card(
#' full_screen = TRUE,
#' card_header(
#' "This is the header"
#' ),
#' card_body(
#' p("This is the body."),
#' p("This is still the body.")
#' ),
#' card_footer(
#' "This is the footer"
#' )
#' )
#' }
#'
card <- function(..., full_screen = FALSE, height = NULL, class = NULL, wrapper = card_body) {
args <- rlang::list2(...)
argnames <- rlang::names2(args)
attribs <- args[nzchar(argnames)]
children <- as_card_items(args[!nzchar(argnames)], wrapper = wrapper)
tag <- div(
class = "card bslib-card",
style = css(height = validateCssUnit(height)),
!!!attribs,
!!!children,
if (full_screen) full_screen_toggle()
)
tag <- bindFillRole(tag, container = TRUE, item = TRUE)
tag <- tagAppendAttributes(tag, class = class)
as_fragment(
tag_require(tag, version = 5, caller = "card()")
)
}
as_card_items <- function(children, wrapper) {
# We don't want NULLs creating empty card bodys
children <- children[vapply(children, function(x) length(x) > 0, logical(1))]
if (!is.function(wrapper)) {
return(children)
}
# Any children that are `is.card_item` should be included verbatim. Any
# children that are not, should be wrapped in card_body(). Consecutive children
# that are not card_item, should be wrapped in a single card_body().
needs_wrap <- !vapply(children, is.card_item, logical(1))
needs_wrap_rle <- rle(needs_wrap)
start_indices <- c(1, head(cumsum(needs_wrap_rle$lengths) + 1, -1))
children <- mapply(
start_indices, needs_wrap_rle$lengths, needs_wrap_rle$values,
FUN = function(start, length, wrap) {
these_children <- children[start:(start + length - 1)]
if (wrap) {
list(wrapper(these_children))
} else {
these_children
}
},
SIMPLIFY = FALSE
)
unlist(children, recursive = FALSE)
}
#' Card items
#'
#' Components designed to be provided as direct children of a [card()]. For a
#' general overview of the [card()] API, see [this
#' article](https://rstudio.github.io/bslib/articles/cards.html).
#'
#' @param ... Unnamed arguments can be any valid child of an [htmltools
#' tag][htmltools::tags]. Named arguments become HTML attributes on returned
#' UI element.
#' @param fill whether to allow the `card_body()` to grow and shrink to fit its
#' `card()`.
#' @inheritParams card
#'
#' @return An [htmltools::div()] tag.
#'
#' @export
#' @seealso [card()] for creating a card component.
#' @seealso [navs_tab_card()] for cards with multiple tabs.
#' @seealso [layout_column_wrap()] for laying out multiple cards (or multiple
#' columns inside a card).
#'
#' @describeIn card_body A general container for the "main content" of a [card()].
card_body <- function(..., fill = FALSE, height = NULL, class = NULL) {
card_body_(
fill_item = fill,
height = height,
class = class,
...
)
}
#' @describeIn card_body Similar to `card_body(fill = TRUE)`, but also marks the
#' return element as a "fill container" (via [htmltools::bindFillRole()]) so
#' that its immediate children are allowed to grow and shrink to fit.
#' @param gap A [CSS length unit][htmltools::validateCssUnit()] defining the
#' `gap` (i.e., spacing) between elements provided to `...`.
#' @param max_height,max_height_full_screen,min_height Any valid [CSS length unit][htmltools::validateCssUnit()].
#' @export
card_body_fill <- function(..., gap = NULL, max_height = NULL, max_height_full_screen = max_height, min_height = NULL, class = NULL) {
register_runtime_package_check("`card_body_fill()`", "shiny", "1.7.3.9001")
register_runtime_package_check("`card_body_fill()`", "htmlwidgets", "1.5.4.9001")
card_body_(
fill_item = TRUE,
fill_container = TRUE,
class = class,
style = htmltools::css(
gap = validateCssUnit(gap),
min_height = validateCssUnit(min_height),
"--bslib-card-body-max-height" = validateCssUnit(max_height),
"--bslib-card-body-max-height-full-screen" = validateCssUnit(max_height_full_screen),
margin_top = "auto",
margin_bottom = "auto"
),
...
)
}
#' @describeIn card_body Similar to `card_header()` but without the border and background color.
#' @param container a function to generate an HTML element.
#' @export
card_title <- function(..., container = htmltools::h5) {
as.card_item(
container(style = css(margin_bottom = 0), class = "bslib-card-title", ...)
)
}
card_body_ <- function(..., fill_item = FALSE, fill_container = FALSE, height = NULL, class = NULL, container = htmltools::div) {
tag <- container(
class = "card-body",
style = css(
height = validateCssUnit(height),
# .card-body already adds `flex: 1 1 auto` so make sure to override it
flex = if (fill_item) "1 1 auto" else "0 0 auto"
),
...
)
tag <- bindFillRole(tag, item = fill_item, container = fill_container)
# Make sure user has the opportunity to override the classes added by bindFillRole()
tag <- tagAppendAttributes(tag, class = class)
as.card_item(tag)
}
#' @describeIn card_body A header (with border and background color) for the `card()`. Typically appears before a `card_body()`.
#' @param container a function that generates an [htmltools tag][htmltools::tags].
#' @export
card_header <- function(..., class = NULL, container = htmltools::div) {
as.card_item(
container(class = "card-header", class = class, ...)
)
}
#' @describeIn card_body A header (with border and background color) for the `card()`. Typically appears after a `card_body()`.
#' @export
card_footer <- function(..., class = NULL) {
as.card_item(
div(class = "card-footer", class = class, ...)
)
}
#' @describeIn card_body Include static (i.e., pre-generated) images.
#' @param file a file path pointing an image. The image will be base64 encoded
#' and provided to the `src` attribute of the `<img>`. Alternatively, you may
#' set this value to `NULL` and provide the `src` yourself.
#' @param href an optional URL to link to.
#' @param border_radius where to apply `border-radius` on the image.
#' @param mime_type the mime type of the `file`.
#' @param container a function to generate an HTML element to contain the image.
#' @param width Any valid [CSS unit][htmltools::validateCssUnit] (e.g., `width="100%"`).
#' @export
card_image <- function(
file, ..., href = NULL, border_radius = c("top", "bottom", "all", "none"),
mime_type = NULL, class = NULL, height = NULL, width = NULL, container = card_body_fill) {
src <- NULL
if (length(file) > 0) {
src <- base64enc::dataURI(
file = file, mime = mime_type %||% mime::guess_type(file)
)
}
image <- tags$img(
src = src,
class = "img-fluid",
class = switch(
match.arg(border_radius),
all = "card-img",
top = "card-img-top",
bottom = "card-img-bottom",
NULL
),
style = css(
height = validateCssUnit(height),
width = validateCssUnit(width)
),
...
)
image <- bindFillRole(image, item = TRUE)
image <- tagAppendAttributes(image, class = class)
if (!is.null(href)) {
image <- bindFillRole(tags$a(href = href, image), container = TRUE, item = TRUE)
}
if (is.function(container)) {
image <- container(image)
}
image
}
#' @describeIn card_body Mark an object as a card item. This will prevent the
#' [card()] from putting the object inside a `wrapper` (i.e., a
#' `card_body()`).
#' @param x an object to test (or coerce to) a card item.
#' @export
as.card_item <- function(x) {
class(x) <- c("card_item", class(x))
x
}
#' @rdname card_body
#' @export
is.card_item <- function(x) {
inherits(x, "card_item")
}
full_screen_toggle <- function() {
tags$span(
class = "bslib-full-screen-enter",
class = "badge rounded-pill bg-dark",
"data-bs-toggle" = "tooltip",
"data-bs-placement" = "bottom",
title = "Expand",
full_screen_toggle_icon(),
htmlDependency(
name = "bslib-card-full-screen",
version = get_package_version("bslib"),
package = "bslib",
src = "components",
script = "card-full-screen.js"
),
tags$script("data-bslib-card-needs-init" = NA, HTML(
"
var thisScript = document.querySelector('script[data-bslib-card-needs-init]');
if (!thisScript) throw new Error('Failed to register card() resize observer');
thisScript.removeAttribute('data-bslib-card-needs-init');
var card = $(thisScript).parents('.card').last();
if (!card) throw new Error('Failed to register card() resize observer');
// Let Shiny know to trigger resize when the card size changes
// TODO: shiny could/should do this itself (rstudio/shiny#3682)
var resizeEvent = window.document.createEvent('UIEvents');
resizeEvent.initUIEvent('resize', true, false, window, 0);
var ro = new ResizeObserver(() => { window.dispatchEvent(resizeEvent); });
ro.observe(card[0]);
// Enable tooltips (for the expand icon)
var tooltipList = card[0].querySelectorAll('[data-bs-toggle=\"tooltip\"]');
tooltipList.forEach(function(x) { new bootstrap.Tooltip(x); });
// In some complex fill-based layouts with multiple outputs (e.g., plotly),
// shiny initializes with the correct sizing, but in-between the 1st and last
// renderValue(), the size of the output containers can change, meaning every
// output but the 1st gets initialized with the wrong size during their
// renderValue(); and then after the render phase, shiny won't know trigger a
// resize since all the widgets will return to their original size
// (and thus, Shiny thinks there isn't any resizing to do).
// We workaround that situation by manually triggering a resize on the binding
// when the output container changes (this way, if the size is different during
// the render phase, Shiny will know about it)
$(document).on('shiny:value', function(x) {
var el = x.binding.el;
if (card[0].contains(el) && !$(el).data('bslib-output-observer')) {
var roo = new ResizeObserver(x.binding.onResize);
roo.observe(el);
$(el).data('bslib-output-observer', true);
}
});
"
))
)
}
# via bsicons::bs_icon("arrows-fullscreen")
full_screen_toggle_icon <- function() {
HTML('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-arrows-fullscreen " style="height:1em;width:1em;fill:currentColor;" aria-hidden="true" role="img" ><path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"></path></svg>')
}
# jcheng 2022-06-06: Removing for now; list items have more features than I'm
# ready to design an API for right now
#
# #' @rdname card_body
# #' @export
# card_list <- function(...) {
# res <- tags$ul(class = "list-group list-group-flush", ...)
# as.card_item(res)
# }
#
# #' @export
# card_list_item <- function(...) {
# tags$li(class = "list-group-item", ...)
# }
|