diff --git a/NEWS.md b/NEWS.md index 0c31b5ca0..23eb91a25 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ * `value_box()` no longer defaults to `theme_color = "primary"`. To restore the previous behavior, please use `theme = "primary"`. In addition to the default style change, the `theme_color` is now deprecated in favor of `theme`. (#758) * `page_navbar()` now defaults to `underline = TRUE`, meaning that navigation links in the navbar now have underline styling by default (set `underline = FALSE` to revert to previous behavior). (#784) * `page()` now returns a `
` tag instead of `tagList()`. This change allows `page()` to treat named arguments as HTML attributes. (#809) +* The JS/CSS assets behind `{bslib}` components (e.g., `card()`, `value_box()`, etc) are all now bundled into one `htmlDependency()` and included with the return value of `bs_theme_dependencies()` (previously they were attached at the component-level). (#810) ## New features diff --git a/R/accordion.R b/R/accordion.R index be62b6e6f..bd771822e 100644 --- a/R/accordion.R +++ b/R/accordion.R @@ -113,8 +113,7 @@ accordion <- function(..., id = NULL, open = NULL, multiple = TRUE, class = NULL height = validateCssUnit(height) ), !!!attrs, - !!!children, - accordion_dependency() + !!!children ) tag <- tag_require(tag, version = 5, caller = "accordion()") @@ -276,14 +275,3 @@ check_character <- function(x, max_length = Inf, min_length = 1, call = rlang::c } x } - -accordion_dependency <- function() { - list( - component_dependency_js("accordion"), - bs_dependency_defer(accordion_dependency_sass) - ) -} - -accordion_dependency_sass <- function(theme) { - component_dependency_sass(theme, "accordion") -} diff --git a/R/bs-dependencies.R b/R/bs-dependencies.R index f43ba590f..5b97ed205 100644 --- a/R/bs-dependencies.R +++ b/R/bs-dependencies.R @@ -147,6 +147,7 @@ bs_theme_dependencies <- function( meta = list(viewport = "width=device-width, initial-scale=1, shrink-to-fit=no") ) ), + if (version >= 5) component_dependencies(), htmlDependencies(out_file) )) } diff --git a/R/card.R b/R/card.R index a8f443b85..725c43835 100644 --- a/R/card.R +++ b/R/card.R @@ -72,7 +72,6 @@ card <- function(..., full_screen = FALSE, height = NULL, max_height = NULL, min !!!attribs, !!!children, if (full_screen) full_screen_toggle(), - card_dependency(), card_init_js() ) @@ -281,17 +280,6 @@ full_screen_toggle <- function() { ) } -card_dependency <- function() { - list( - component_dependency_js("card"), - bs_dependency_defer(card_dependency_sass) - ) -} - -card_dependency_sass <- function(theme) { - component_dependency_sass(theme, "card") -} - card_init_js <- function() { tags$script( `data-bslib-card-init` = NA, diff --git a/R/input-switch.R b/R/input-switch.R index 2567fea8e..b3b16b67e 100644 --- a/R/input-switch.R +++ b/R/input-switch.R @@ -99,7 +99,6 @@ input_checkbox <- function(id, label, class = "bslib-input-checkbox", value = FA class = "form-check-label", `for` = id ) - ), - component_dependency_js("bslibShiny") + ) ) } diff --git a/R/layout.R b/R/layout.R index db3de1f39..0e7dd4a92 100644 --- a/R/layout.R +++ b/R/layout.R @@ -95,8 +95,7 @@ layout_column_wrap <- function( gap = validateCssUnit(gap) ), !!!attribs, - children, - component_dependency_css("grid") + children ) tag <- bindFillRole(tag, item = fill) @@ -221,8 +220,7 @@ layout_columns <- function( ), !!!row_heights_css_vars(row_heights), !!!attribs, - !!!children, - component_dependency_css("grid") + !!!children ) tag <- bindFillRole(tag, item = fill) diff --git a/R/nav-items.R b/R/nav-items.R index 4bc1d359b..b98d076ea 100644 --- a/R/nav-items.R +++ b/R/nav-items.R @@ -65,7 +65,7 @@ is_nav_item <- function(x) { #' @describeIn nav-items Adding spacing between nav items. #' @export nav_spacer <- function() { - div(class = "bslib-nav-spacer", component_dependency_css("nav_spacer")) + div(class = "bslib-nav-spacer") } is_nav_spacer <- function(x) { diff --git a/R/navs-legacy.R b/R/navs-legacy.R index 4c36cd6dd..d59fdf244 100644 --- a/R/navs-legacy.R +++ b/R/navs-legacy.R @@ -308,8 +308,7 @@ navbarPage_ <- function(title, # *Don't* wrap in bootstrapPage() (shiny::navbarPage()) does that part tagList( tags$nav(class = navbarClass, role = "navigation", containerDiv), - contentDiv, - component_dependency_css("page_navbar") + contentDiv ) } diff --git a/R/page.R b/R/page.R index 68f60b156..40dfa0ff7 100644 --- a/R/page.R +++ b/R/page.R @@ -71,8 +71,7 @@ page_fillable <- function(..., padding = NULL, gap = NULL, fillable_mobile = FAL "--bslib-page-fill-mobile-height" = if (fillable_mobile) "100%" else "auto" ), ..., - as_fillable_container(), - component_dependency_css("page_fillable") + as_fillable_container() ) } @@ -147,15 +146,10 @@ page_sidebar <- function(..., sidebar = NULL, title = NULL, fillable = TRUE, fil border = FALSE, border_radius = FALSE, ... - ), - bs_dependency_defer(page_sidebar_dependency_sass) + ) ) } -page_sidebar_dependency_sass <- function(theme) { - component_dependency_sass(theme, "page_sidebar") -} - #' @rdname page #' @inheritParams navset_bar diff --git a/R/sidebar.R b/R/sidebar.R index d8b264139..4b18fd8a7 100644 --- a/R/sidebar.R +++ b/R/sidebar.R @@ -250,7 +250,6 @@ layout_sidebar <- function( "--bslib-sidebar-max-height-mobile" = max_height_mobile ), !!!contents, - sidebar_dependency(), sidebar_init_js() ) @@ -312,15 +311,3 @@ sidebar_init_js <- function() { HTML("bslib.Sidebar.initCollapsibleAll()") ) } - - -sidebar_dependency <- function() { - list( - component_dependency_js("sidebar"), - bs_dependency_defer(sidebar_dependency_sass) - ) -} - -sidebar_dependency_sass <- function(theme) { - component_dependency_sass(theme, "sidebar") -} diff --git a/R/utils-deps.R b/R/utils-deps.R index d3955e0de..6bf3d6f64 100644 --- a/R/utils-deps.R +++ b/R/utils-deps.R @@ -1,62 +1,71 @@ -web_component <- function(tagName, ...) { - js_dep <- component_dependency_js("webComponents", type = "module") - args <- c(list(js_dep), rlang::list2(...)) - tag(tagName, args) +component_dependencies <- function() { + list( + component_dependency_js(), + bs_dependency_defer(component_dependency_sass) + ) } -component_dependency_js <- function(name, ...) { +component_dependency_js <- function() { minified <- get_shiny_devmode_option("shiny.minified", default = TRUE) htmlDependency( - name = paste0("bslib-", name, "-js"), + name = "bslib-component-js", version = get_package_version("bslib"), package = "bslib", - src = file.path("components", "dist", name), + src = "components/dist", script = list( - src = paste0(name, if (minified) ".min", ".js"), - ... - ), - all_files = TRUE + list(src = paste0("components", if (minified) ".min", ".js")), + list( + src = paste0("web-components", if (minified) ".min", ".js"), + type = "module" + ) + ) ) } # Pre-compiled component styles -component_dependency_css <- function(name) { +component_dependency_css <- function() { htmlDependency( - name = paste0("bslib-", name, "-styles"), + name = "bslib-component-css", version = get_package_version("bslib"), package = "bslib", - src = file.path("components", "dist", name), - stylesheet = paste0(name, ".css") + src = "components/dist", + stylesheet = "components.css" ) } # Run-time (Sass) component styles -component_dependency_sass <- function(theme, name) { +component_dependency_sass <- function(theme) { precompiled <- isTRUE(get_precompiled_option()) default_theme <- !is_bs_theme(theme) || identical(theme, bs_theme()) if (precompiled && default_theme) { - component_dependency_css(name) + component_dependency_css() } else { - component_dependency_sass_(theme, name) + component_dependency_sass_(theme) } } -component_dependency_sass_ <- function(theme, name) { - scss_files <- list( - path_components("scss", "mixins", "_mixins.scss"), - path_components("scss", paste0(name, ".scss")) +component_dependency_sass_ <- function(theme) { + scss_dir <- path_inst("components", "scss") + scss_files <- c( + file.path(scss_dir, "mixins", "_mixins.scss"), + dir(scss_dir, pattern = "\\.scss$", full.names = TRUE) ) bs_dependency( input = lapply(scss_files, sass_file), - # At least currently, when statically rendering a component, - # bs_dependency_defer() passes along a NULL theme. We should + # At least currently, when statically rendering a component, + # bs_dependency_defer() passes along a NULL theme. We should # eventually fix that, but for now, fall back to the default theme theme = theme %||% bs_theme(), - name = paste0("bslib-", name, "-styles"), + name = "bslib-component-css", version = get_package_version("bslib"), cache_key_extra = get_package_version("bslib"), .sass_args = list(options = sass_options(output_style = "compressed")) ) } + + +web_component <- function(tagName, ...) { + htmltools::tag(tagName, rlang::list2(...)) +} diff --git a/R/utils.R b/R/utils.R index 3c480e9c8..9b415c839 100644 --- a/R/utils.R +++ b/R/utils.R @@ -36,7 +36,6 @@ path_inst <- function(...) { } path_lib <- function(...) path_inst("lib", ...) -path_components <- function(...) path_inst("components", ...) is_shiny_app <- function() { # Make sure to not load shiny as a side-effect of calling this function. diff --git a/R/value-box.R b/R/value-box.R index a31aa1097..86be43680 100644 --- a/R/value-box.R +++ b/R/value-box.R @@ -157,8 +157,7 @@ value_box <- function( "--bslib-color-bg" = theme$bg ), !!!attribs, - contents, - as.card_item(value_box_dependency()) + contents ) as_fragment(tag_require(res, version = 5, caller = "value_box()")) @@ -196,45 +195,6 @@ value_box_auto_border_class <- function(theme, class = NULL) { return(NULL) } -value_box_dependency <- function() { - bs_dependency_defer(value_box_dependency_sass) -} - -value_box_dependency_sass <- function(theme) { - deps <- component_dependency_sass(theme, "value_box") - # Inject the icon gradient SVG into the dependency $head slot - deps$head <- value_box_dependency_icon_gradient() - deps -} - -value_box_dependency_icon_gradient <- local({ - cached <- NULL - - function() { - if (!is.null(cached)) return(cached) - - x <- tags$script(HTML( - sprintf( - "(function() { - function insert_svg() { - var temp = document.createElement('div'); - temp.innerHTML = `%s`; - document.body.appendChild(temp.firstChild); - } - if (document.readyState === 'complete') { - insert_svg(); - } else { - document.addEventListener('DOMContentLoaded', insert_svg); - } - })()", - read_utf8(path_inst("components", "assets", "icon-gradient.svg")) - ) - )) - - (cached <<- format(x)) - } -}) - #' @param name The name of the theme, e.g. `"primary"`, `"danger"`, `"purple"`. #' @param bg,fg The background and foreground colors for the theme. If only `bg` #' is provided, then the foreground color is automatically chosen from diff --git a/inst/components/assets/icon-gradient.svg b/inst/components/assets/icon-gradient.svg deleted file mode 100644 index c4596e683..000000000 --- a/inst/components/assets/icon-gradient.svg +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/inst/components/dist/accordion/accordion.css b/inst/components/dist/accordion/accordion.css deleted file mode 100644 index 45cd2f8c3..000000000 --- a/inst/components/dist/accordion/accordion.css +++ /dev/null @@ -1 +0,0 @@ -.accordion .accordion-header{font-size:calc(1.325rem + .9vw);margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2;color:var(--bs-heading-color);margin-bottom:0}@media (min-width: 1200px){.accordion .accordion-header{font-size:2rem}}.accordion .accordion-icon:not(:empty){margin-right:0.75rem;display:flex}.accordion .accordion-button:not(.collapsed){box-shadow:none}.accordion .accordion-button:not(.collapsed):focus{box-shadow:var(--bs-accordion-btn-focus-box-shadow)} diff --git a/inst/components/dist/accordion/accordion.js b/inst/components/dist/accordion/accordion.js deleted file mode 100644 index e761a9ab2..000000000 --- a/inst/components/dist/accordion/accordion.js +++ /dev/null @@ -1,176 +0,0 @@ -/*! bslib 0.5.1.9000 | (c) 2012-2023 RStudio, PBC. | License: MIT + file LICENSE */ -"use strict"; -(() => { - // srcts/src/components/_utils.ts - var InputBinding = window.Shiny ? Shiny.InputBinding : class { - }; - function registerBinding(inputBindingClass, name) { - if (window.Shiny) { - Shiny.inputBindings.register(new inputBindingClass(), "bslib." + name); - } - } - function hasDefinedProperty(obj, prop) { - return Object.prototype.hasOwnProperty.call(obj, prop) && obj[prop] !== void 0; - } - - // srcts/src/components/accordion.ts - var AccordionInputBinding = class extends InputBinding { - find(scope) { - return $(scope).find(".accordion.bslib-accordion-input"); - } - getValue(el) { - const items = this._getItemInfo(el); - const selected = items.filter((x) => x.isOpen()).map((x) => x.value); - return selected.length === 0 ? null : selected; - } - subscribe(el, callback) { - $(el).on( - "shown.bs.collapse.accordionInputBinding hidden.bs.collapse.accordionInputBinding", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function(event) { - callback(true); - } - ); - } - unsubscribe(el) { - $(el).off(".accordionInputBinding"); - } - receiveMessage(el, data) { - const method = data.method; - if (method === "set") { - this._setItems(el, data); - } else if (method === "open") { - this._openItems(el, data); - } else if (method === "close") { - this._closeItems(el, data); - } else if (method === "remove") { - this._removeItem(el, data); - } else if (method === "insert") { - this._insertItem(el, data); - } else if (method === "update") { - this._updateItem(el, data); - } else { - throw new Error(`Method not yet implemented: ${method}`); - } - } - _setItems(el, data) { - const items = this._getItemInfo(el); - const vals = this._getValues(el, items, data.values); - items.forEach((x) => { - vals.indexOf(x.value) > -1 ? x.show() : x.hide(); - }); - } - _openItems(el, data) { - const items = this._getItemInfo(el); - const vals = this._getValues(el, items, data.values); - items.forEach((x) => { - if (vals.indexOf(x.value) > -1) - x.show(); - }); - } - _closeItems(el, data) { - const items = this._getItemInfo(el); - const vals = this._getValues(el, items, data.values); - items.forEach((x) => { - if (vals.indexOf(x.value) > -1) - x.hide(); - }); - } - _insertItem(el, data) { - let targetItem = this._findItem(el, data.target); - if (!targetItem) { - targetItem = data.position === "before" ? el.firstElementChild : el.lastElementChild; - } - const panel = data.panel; - if (targetItem) { - Shiny.renderContent( - targetItem, - panel, - data.position === "before" ? "beforeBegin" : "afterEnd" - ); - } else { - Shiny.renderContent(el, panel); - } - if (this._isAutoClosing(el)) { - const val = $(panel.html).attr("data-value"); - $(el).find(`[data-value="${val}"] .accordion-collapse`).attr("data-bs-parent", "#" + el.id); - } - } - _removeItem(el, data) { - const targetItems = this._getItemInfo(el).filter( - (x) => data.target.indexOf(x.value) > -1 - ); - const unbindAll = Shiny == null ? void 0 : Shiny.unbindAll; - targetItems.forEach((x) => { - if (unbindAll) - unbindAll(x.item); - x.item.remove(); - }); - } - _updateItem(el, data) { - const target = this._findItem(el, data.target); - if (!target) { - throw new Error( - `Unable to find an accordion_panel() with a value of ${data.target}` - ); - } - if (hasDefinedProperty(data, "value")) { - target.dataset.value = data.value; - } - if (hasDefinedProperty(data, "body")) { - const body = target.querySelector(".accordion-body"); - Shiny.renderContent(body, data.body); - } - const header = target.querySelector(".accordion-header"); - if (hasDefinedProperty(data, "title")) { - const title = header.querySelector(".accordion-title"); - Shiny.renderContent(title, data.title); - } - if (hasDefinedProperty(data, "icon")) { - const icon = header.querySelector( - ".accordion-button > .accordion-icon" - ); - Shiny.renderContent(icon, data.icon); - } - } - _getItemInfo(el) { - const items = Array.from( - el.querySelectorAll(":scope > .accordion-item") - ); - return items.map((x) => this._getSingleItemInfo(x)); - } - _getSingleItemInfo(x) { - const collapse = x.querySelector(".accordion-collapse"); - const isOpen = () => $(collapse).hasClass("show"); - return { - item: x, - value: x.dataset.value, - isOpen, - show: () => { - if (!isOpen()) - $(collapse).collapse("show"); - }, - hide: () => { - if (isOpen()) - $(collapse).collapse("hide"); - } - }; - } - _getValues(el, items, values) { - let vals = values !== true ? values : items.map((x) => x.value); - const autoclose = this._isAutoClosing(el); - if (autoclose) { - vals = vals.slice(vals.length - 1, vals.length); - } - return vals; - } - _findItem(el, value) { - return el.querySelector(`[data-value="${value}"]`); - } - _isAutoClosing(el) { - return el.classList.contains("autoclose"); - } - }; - registerBinding(AccordionInputBinding, "accordion"); -})(); -//# sourceMappingURL=accordion.js.map diff --git a/inst/components/dist/accordion/accordion.js.map b/inst/components/dist/accordion/accordion.js.map deleted file mode 100644 index 44f533e42..000000000 --- a/inst/components/dist/accordion/accordion.js.map +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 3, - "sources": ["../../../../srcts/src/components/_utils.ts", "../../../../srcts/src/components/accordion.ts"], - "sourcesContent": ["import type { HtmlDep } from \"rstudio-shiny/srcts/types/src/shiny/render\";\n\nimport type { InputBinding as InputBindingType } from \"rstudio-shiny/srcts/types/src/bindings/input\";\n\n// Exclude undefined from T\ntype NotUndefined(\n prop: P,\n _obj: unknown\n): P => prop;\n\n/**\n * Converts property values to and from attribute values.\n */\nexport interface ComplexAttributeConverter