Compare commits

..

30 Commits

Author SHA1 Message Date
Carson
006cd8ed5e JS changes are now captured in main.ts 2021-03-31 12:33:57 -05:00
Carson
88cdd607b0 fix merge conflict 2021-03-31 12:30:51 -05:00
Carson
f9403c5e66 Merge branch 'master' into navtreePanel 2021-03-31 12:28:00 -05:00
Carson Sievert
268c9afec3 Close #3299: bootstrapLib() should always call setCurrentTheme() when shiny is running (#3300) 2021-03-26 15:12:54 -05:00
Carson Sievert
5c919ae565 Make ensureTabsetHasVisibleTab() is aware of BS4+ markup (#3349)
* Close https://github.com/rstudio/shinycoreci-apps/issues/126: Make ensureTabsetHasVisibleTab() aware of BS4+ markup

* yarn build (GitHub Actions)

* Update srcts/src/main.ts

* yarn lint (GitHub Actions)

* yarn build (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2021-03-26 15:11:48 -05:00
Hadley Wickham
e29d92c5ff Allow trailing commas in more places (#3328)
* Allow trailing commas in more places

I grepped for list(...) and replaced with rlang::list2(...). This also enables !!! which is generally not important for Shiny because it automatically splices lists/tagLists, but I doubt it will affect any existing code.

* update news; no need to rlang::

* missed one

* Update NEWS.md

Co-authored-by: Hadley Wickham <h.wickham@gmail.com>

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2021-03-23 14:24:21 -05:00
Carson
d04c7ae6ed Attach dependencies to the nav (so that we can extract out the nav/content) 2021-03-23 10:27:49 -05:00
Carson
7811f06e70 Merge branch 'master' into navtreePanel 2021-03-22 16:55:29 -05:00
Carson
0a331e3366 update news 2021-03-22 16:00:39 -05:00
Carson Sievert
32d0e146ad Various improvements to tab panels (#3315)
* 'Native' Bootstrap 4 tabset panel support

* downgrade error to warning; improve the messaging

* Make tab anchor selectors more a bit more sensible and consistent across versions

* More of the same

* fix silly bug

* Be more careful about unpacking a .nav-item into a .dropdown-item

* Keep refactoring R logic to make it cleaner and easier to reuse elsewhere

* Go back to the purely class based CSS selectors for BS4 tab input

* Keep supporting off-label behavior of shiny.tag getting transformed into 'empty' nav/tab

* Add header and footer args to tabsetPanel()/navlistPanel() since there is precedence in navbarPage() and mention them in the warning

* Drop NULLs instead of creating an empty nav from them, closes #1928

* Remove tabPanelMenu() alias

* Add a card argument for wrapping content in a card

* Throw an error if card=T is used outside of a BS4+ context

* No more tabPanelMenu() alias

* Document (GitHub Actions)

* Port JS changes to TypeScript

* Allow liTag to be assigned a new value

* abort() is no longer being used

* Add some unit tests

* Document the new card argument

* Get tests passing on older R versions

* Get tests passing on older R versions

* Get tests passing on older R versions

* Skip snapshots on R < 3.6

* require dev version of htmltools

* remove card argument (at least for now)

* Document (GitHub Actions)

* Update tests/testthat/test-tabPanel.R

Co-authored-by: Winston Chang <winston@stdout.org>

* Have processDeps() call renderTags() on tagFunction() objects

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2021-03-22 12:37:57 -05:00
Carson
94a9b8a5f1 Generate pre-compiled 2021-03-09 14:29:47 -06:00
Carson
8ce0068f1d Get insertTab() of a navbarMenu() targetted at a navtreePanel() working 2021-03-09 14:29:34 -06:00
cpsievert
c8d76cf48f Document (GitHub Actions) 2021-03-09 17:32:13 +00:00
Carson
6159259699 Get insertTab() of a navbarMenu() targetted at a navtreePanel() working 2021-03-09 11:29:22 -06:00
Carson
9790b8f716 Get everything but bindings and dynamic behavior working 2021-03-09 11:28:21 -06:00
Carson
f9d82f17af Towards some better styles 2021-03-09 11:28:21 -06:00
Carson
3950f0e7fe wip treelistPanel() 2021-03-09 11:28:21 -06:00
Carson
886d369e7c Add a card argument for wrapping content in a card 2021-03-09 11:25:40 -06:00
Carson
ec97fe731d Remove tabPanelMenu() alias 2021-03-09 09:40:04 -06:00
Carson
f8476973e5 Drop NULLs instead of creating an empty nav from them, closes #1928 2021-03-05 19:12:54 -06:00
Carson
2bde956fc9 Add header and footer args to tabsetPanel()/navlistPanel() since there is precedence in navbarPage() and mention them in the warning 2021-03-05 18:15:13 -06:00
Carson
2e33634726 Keep supporting off-label behavior of shiny.tag getting transformed into 'empty' nav/tab 2021-03-05 16:07:51 -06:00
Carson
f9ee5fcca2 Go back to the purely class based CSS selectors for BS4 tab input 2021-03-05 16:05:00 -06:00
Carson
124a89d7b4 Keep refactoring R logic to make it cleaner and easier to reuse elsewhere 2021-03-04 17:22:35 -06:00
Carson
1d608d2e25 Be more careful about unpacking a .nav-item into a .dropdown-item 2021-03-04 12:39:09 -06:00
Carson
f63d50d1b5 fix silly bug 2021-03-04 12:17:26 -06:00
Carson
f8e09c15d1 More of the same 2021-03-04 11:47:05 -06:00
Carson
959e4e41da Make tab anchor selectors more a bit more sensible and consistent across versions 2021-03-04 11:14:03 -06:00
Carson
36e679a9af downgrade error to warning; improve the messaging 2021-03-04 10:47:07 -06:00
Carson
c6693ead30 'Native' Bootstrap 4 tabset panel support 2021-03-03 17:58:35 -06:00
27 changed files with 1364 additions and 294 deletions

View File

@@ -170,6 +170,7 @@ Collate:
'mock-session.R'
'modal.R'
'modules.R'
'navtreePanel.R'
'notifications.R'
'priorityqueue.R'
'progress.R'

View File

@@ -175,6 +175,7 @@ export(moduleServer)
export(navbarMenu)
export(navbarPage)
export(navlistPanel)
export(navtreePanel)
export(nearPoints)
export(need)
export(ns.sep)
@@ -408,6 +409,7 @@ importFrom(rlang,is_false)
importFrom(rlang,is_missing)
importFrom(rlang,is_na)
importFrom(rlang,is_quosure)
importFrom(rlang,list2)
importFrom(rlang,maybe_missing)
importFrom(rlang,missing_arg)
importFrom(rlang,new_function)

13
NEWS.md
View File

@@ -7,7 +7,18 @@ shiny 1.6.0.9000
* The `format` and `locale` arguments to `sliderInput()` have been removed. They have been deprecated since 0.10.2.2 (released on 2014-12-08).
### Minor new features and improvements
### New features and improvements
* All uses of `list(...)` have been replaced with `rlang::list2(...)`. This means that you can use trailing `,` without error and use rlang's `!!!` operator to "splice" a list of argument values into `...`. We think this'll be particularly useful for passing a list of `tabPanel()` to their consumers (i.e., `tabsetPanel()`, `navbarPage()`, etc). For example, `tabs <- list(tabPanel("A", "a"), tabPanel("B", "b")); navbarPage(!!!tabs)`. (#3315 and #3328)
* Numerous improvements tabset panels (i.e., `tabPanel()`, `navbarMenu()`, `tabsetPanel()`, `navbarPage()`, etc) (#3315):
* Closed #3322: `tabsetPanel()` and `navlistPanel()` gain `header`/`footer` arguments (inspired by `navbarPage()`'s already existing `header`/`footer`), making it easier to include content that should appear on every tab.
* Closed #3313 and #1823: More informative error when non-`tabPanel()`/`shiny.tag` objects are supplied to `...`.
* Closed #3321: New informative warning when `shiny.tag` object(s) are supplied to `...`. In this case we will continue to create an "empty" nav item and include the content on every tab, but the warning will mention the (new) `header`/`footer` args, which is likely what the user wants.
* Closed #3320: The HTML markup that `tabPanel()` et. al generate (for Bootstrap nav) is now Bootstrap 4+ compliant when used with `theme = bslib::bs_theme()`.
* Closed #1928: `NULL` values are now dropped instead of producing an empty nav item.
### Other improvements
* Shiny's core JavaScript code was converted to TypeScript. For the latest development information, please see the [README.md in `./srcts`](https://github.com/rstudio/shiny/tree/master/srcts). (#3296)

View File

@@ -396,7 +396,7 @@ mainPanel <- function(..., width = 8) {
#' }
#' @export
verticalLayout <- function(..., fluid = TRUE) {
lapply(list(...), function(row) {
lapply(list2(...), function(row) {
col <- column(12, row)
if (fluid)
fluidRow(col)
@@ -433,7 +433,7 @@ verticalLayout <- function(..., fluid = TRUE) {
#' @export
flowLayout <- function(..., cellArgs = list()) {
children <- list(...)
children <- list2(...)
childIdx <- !nzchar(names(children) %||% character(length(children)))
attribs <- children[!childIdx]
children <- children[childIdx]
@@ -516,7 +516,7 @@ inputPanel <- function(...) {
#' @export
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
children <- list(...)
children <- list2(...)
childIdx <- !nzchar(names(children) %||% character(length(children)))
attribs <- children[!childIdx]
children <- children[childIdx]
@@ -614,7 +614,7 @@ fillCol <- function(..., flex = 1, width = "100%", height = "100%") {
}
flexfill <- function(..., direction, flex, width = width, height = height) {
children <- list(...)
children <- list2(...)
attrs <- list()
if (!is.null(names(children))) {

View File

@@ -49,7 +49,7 @@ bootstrapPage <- function(..., title = NULL, responsive = deprecated(), theme =
tags$head(tags$link(rel="stylesheet", type="text/css", href=theme))
},
# remainder of tags passed to the function
list(...)
list2(...)
)
# If theme is a bslib::bs_theme() object, bootstrapLib() needs to come first
@@ -91,6 +91,10 @@ getLang <- function(ui) {
#' @export
bootstrapLib <- function(theme = NULL) {
tagFunction(function() {
if (isRunning()) {
setCurrentTheme(theme)
}
# If we're not compiling Bootstrap Sass (from bslib), return the
# static Bootstrap build.
if (!is_bs_theme(theme)) {
@@ -112,7 +116,6 @@ bootstrapLib <- function(theme = NULL) {
# Note also that since this is shinyOptions() (and not options()), the
# option is automatically reset when the app (or session) exits
if (isRunning()) {
setCurrentTheme(theme)
registerThemeDependency(bs_theme_deps)
} else {
@@ -163,6 +166,15 @@ getCurrentTheme <- function() {
getShinyOption("bootstrapTheme", default = NULL)
}
getCurrentVersion <- function() {
theme <- getCurrentTheme()
if (bslib::is_bs_theme(theme)) {
bslib::theme_version(theme)
} else {
strsplit(bootstrapVersion, ".", fixed = TRUE)[[1]][[1]]
}
}
setCurrentTheme <- function(theme) {
shinyOptions(bootstrapTheme = theme)
}
@@ -211,7 +223,7 @@ registerThemeDependency <- function(func) {
bootstrapDependency <- function(theme) {
htmlDependency(
"bootstrap", "3.4.1",
"bootstrap", bootstrapVersion,
c(
href = "shared/bootstrap",
file = system.file("www/shared/bootstrap", package = "shiny")
@@ -230,6 +242,8 @@ bootstrapDependency <- function(theme) {
)
}
bootstrapVersion <- "3.4.1"
#' @rdname bootstrapPage
#' @export
@@ -342,13 +356,20 @@ collapseSizes <- function(padding) {
#' Create a page with a top level navigation bar
#'
#' Create a page that contains a top level navigation bar that can be used to
#' toggle a set of [tabPanel()] elements. `navbarMenu()` can be used to create
#' an embedded menu within the navbar that in turns includes additional
#' `tabPanels`.
#' toggle a set of [tabPanel()] elements.
#'
#' @inheritParams navlistPanel
#' @inheritParams bootstrapPage
#' @param title The title to display in the navbar
#' @param ... [tabPanel()] elements to include in the page. The
#' `navbarMenu` function also accepts strings, which will be used as menu
#' section headers. If the string is a set of dashes like `"----"` a
#' horizontal separator will be displayed in the menu.
#' @param id If provided, you can use `input$`*`id`* in your
#' server logic to determine which of the current tabs is active. The value
#' will correspond to the `value` argument that is passed to
#' [tabPanel()].
#' @param selected The `value` (or, if none was supplied, the `title`)
#' of the tab that should be selected by default. If `NULL`, the first
#' tab will be selected.
#' @param position Determines whether the navbar should be displayed at the top
#' of the page with normal scrolling behavior (`"static-top"`), pinned at
#' the top (`"fixed-top"`), or pinned at the bottom
@@ -366,13 +387,26 @@ collapseSizes <- function(padding) {
#' elements into a menu when the width of the browser is less than 940 pixels
#' (useful for viewing on smaller touchscreen device)
#' @param collapsable Deprecated; use `collapsible` instead.
#' @param fluid `TRUE` to use a fluid layout. `FALSE` to use a fixed
#' layout.
#' @param responsive This option is deprecated; it is no longer optional with
#' Bootstrap 3.
#' @param windowTitle The title that should be displayed by the browser window.
#' Useful if `title` is not a string.
#' @inheritParams bootstrapPage
#' @param icon Optional icon to appear on a `navbarMenu` tab.
#'
#' @seealso [updateNavbarPage()], [insertTab()], [showTab()]
#' @return A UI defintion that can be passed to the [shinyUI] function.
#'
#' @details The `navbarMenu` function can be used to create an embedded
#' menu within the navbar that in turns includes additional tabPanels (see
#' example below).
#'
#' @seealso [tabPanel()], [tabsetPanel()],
#' [updateNavbarPage()], [insertTab()],
#' [showTab()]
#'
#' @family layout functions
#' @family tab layouts
#'
#' @examples
#' navbarPage("App Title",
@@ -416,10 +450,11 @@ navbarPage <- function(title,
pageTitle <- title
# navbar class based on options
# TODO: tagFunction() the navbar logic?
navbarClass <- "navbar navbar-default"
position <- match.arg(position)
if (!is.null(position))
navbarClass <- paste(navbarClass, " navbar-", position, sep = "")
navbarClass <- paste0(navbarClass, " navbar-", position)
if (inverse)
navbarClass <- paste(navbarClass, "navbar-inverse")
@@ -427,21 +462,14 @@ navbarPage <- function(title,
selected <- restoreInput(id = id, default = selected)
# build the tabset
tabs <- list(...)
tabset <- buildTabset(tabs, "nav navbar-nav", NULL, id, selected)
tabset <- buildTabset(..., ulClass = "nav navbar-nav", id = id, selected = selected)
# function to return plain or fluid class name
className <- function(name) {
if (fluid)
paste(name, "-fluid", sep="")
else
name
}
containerClass <- paste0("container", if (fluid) "-fluid")
# built the container div dynamically to support optional collapsibility
if (collapsible) {
navId <- paste("navbar-collapse-", p_randomInt(1000, 10000), sep="")
containerDiv <- div(class=className("container"),
navId <- paste0("navbar-collapse-", p_randomInt(1000, 10000))
containerDiv <- div(class=containerClass,
div(class="navbar-header",
tags$button(type="button", class="navbar-toggle collapsed",
`data-toggle`="collapse", `data-target`=paste0("#", navId),
@@ -455,7 +483,7 @@ navbarPage <- function(title,
div(class="navbar-collapse collapse", id=navId, tabset$navList)
)
} else {
containerDiv <- div(class=className("container"),
containerDiv <- div(class=containerClass,
div(class="navbar-header",
span(class="navbar-brand", pageTitle)
),
@@ -464,7 +492,7 @@ navbarPage <- function(title,
}
# build the main tab content div
contentDiv <- div(class=className("container"))
contentDiv <- div(class=containerClass)
if (!is.null(header))
contentDiv <- tagAppendChild(contentDiv, div(class="row", header))
contentDiv <- tagAppendChild(contentDiv, tabset$content)
@@ -491,11 +519,15 @@ navbarPage <- function(title,
navbarMenu <- function(title, ..., menuName = title, icon = NULL) {
structure(list(title = title,
menuName = menuName,
tabs = list(...),
tabs = list2(...),
iconClass = iconClass(icon)),
class = "shiny.navbarmenu")
}
isNavbarMenu <- function(x) {
inherits(x, "shiny.navbarmenu")
}
#' Create a well panel
#'
#' Creates a panel with a slightly inset border and grey background. Equivalent
@@ -603,10 +635,6 @@ helpText <- function(...) {
#' Create a tab panel
#'
#' `tabPanel()` creates a tab panel that can be included within a
#' [tabsetPanel()], [navListPanel()], or [navbarPage()]. `tabPanelBody()`
#' drops the `title`, making it most suitable for use within
#' `tabsetPanel(type = "hidden")`.
#'
#' @param title Display title for tab
#' @param ... UI elements to include within the tab
@@ -615,8 +643,9 @@ helpText <- function(...) {
#' `id`, then the title will be used.
#' @param icon Optional icon to appear on the tab. This attribute is only
#' valid when using a `tabPanel` within a [navbarPage()].
#' @return A tab that can be passed to [tabsetPanel()]
#'
#' @family tab layouts
#' @seealso [tabsetPanel()]
#'
#' @examples
#' # Show a tabset that includes a plot, summary, and
@@ -629,6 +658,7 @@ helpText <- function(...) {
#' )
#' )
#' @export
#' @describeIn tabPanel Create a tab panel that can be included within a [tabsetPanel()] or a [navbarPage()].
tabPanel <- function(title, ..., value = title, icon = NULL) {
div(
class = "tab-pane",
@@ -638,8 +668,16 @@ tabPanel <- function(title, ..., value = title, icon = NULL) {
...
)
}
isTabPanel <- function(x) {
if (!inherits(x, "shiny.tag")) return(FALSE)
class <- tagGetAttribute(x, "class") %||% ""
"tab-pane" %in% strsplit(class, "\\s+")[[1]]
}
#' @export
#' @rdname tabPanel
#' @describeIn tabPanel Create a tab panel that drops the title argument.
#' This function should be used within `tabsetPanel(type = "hidden")`. See [tabsetPanel()] for example usage.
tabPanelBody <- function(value, ..., icon = NULL) {
if (
!is.character(value) ||
@@ -674,8 +712,11 @@ tabPanelBody <- function(value, ..., icon = NULL) {
#' }
#' @param position This argument is deprecated; it has been discontinued in
#' Bootstrap 3.
#' @seealso [updateTabsetPanel()], [insertTab()], [showTab()]
#' @family tab layouts
#' @inheritParams navbarPage
#' @return A tabset that can be passed to [mainPanel()]
#'
#' @seealso [tabPanel()], [updateTabsetPanel()],
#' [insertTab()], [showTab()]
#'
#' @examples
#' # Show a tabset that includes a plot, summary, and
@@ -721,6 +762,8 @@ tabsetPanel <- function(...,
id = NULL,
selected = NULL,
type = c("tabs", "pills", "hidden"),
header = NULL,
footer = NULL,
position = deprecated()) {
if (lifecycle::is_present(position)) {
shinyDeprecated(
@@ -732,18 +775,18 @@ tabsetPanel <- function(...,
if (!is.null(id))
selected <- restoreInput(id = id, default = selected)
# build the tabset
tabs <- list(...)
type <- match.arg(type)
tabset <- buildTabset(..., ulClass = paste0("nav nav-", type), id = id, selected = selected)
tabset <- buildTabset(tabs, paste0("nav nav-", type), NULL, id, selected)
# create the content
first <- tabset$navList
second <- tabset$content
# create the tab div
tags$div(class = "tabbable", first, second)
tags$div(
class = "tabbable",
!!!dropNulls(list(
tabset$navList,
header,
tabset$content,
footer
))
)
}
#' Create a navigation list panel
@@ -751,20 +794,31 @@ tabsetPanel <- function(...,
#' Create a navigation list panel that provides a list of links on the left
#' which navigate to a set of tabPanels displayed to the right.
#'
#' @inheritParams tabsetPanel
#' @param ... [tabPanel()] elements to include in the navbar.
#' Plain strings will be converted to headers.
#' @param ... [tabPanel()] elements to include in the navlist
#' @param id If provided, you can use `input$`*`id`* in your
#' server logic to determine which of the current navlist items is active. The
#' value will correspond to the `value` argument that is passed to
#' [tabPanel()].
#' @param selected The `value` (or, if none was supplied, the `title`)
#' of the navigation item that should be selected by default. If `NULL`,
#' the first navigation will be selected.
#' @param well `TRUE` to place a well (gray rounded rectangle) around the
#' navigation list.
#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
#' layout.
#' @param widths Column widths of the navigation list and tabset content areas
#' respectively.
#' @inheritParams tabsetPanel
#' @inheritParams navbarPage
#'
#' @details
#' @details You can include headers within the `navlistPanel` by including
#' plain text elements in the list. Versions of Shiny before 0.11 supported
#' separators with "------", but as of 0.11, separators were no longer
#' supported. This is because version 0.11 switched to Bootstrap 3, which
#' doesn't support separators.
#'
#' @seealso [updateTabsetPanel()], [insertTab()], [showTab()]
#' @family tab layouts
#' @seealso [tabPanel()], [updateNavlistPanel()],
#' [insertTab()], [showTab()]
#'
#' @examples
#' fluidPage(
@@ -782,37 +836,30 @@ tabsetPanel <- function(...,
navlistPanel <- function(...,
id = NULL,
selected = NULL,
header = NULL,
footer = NULL,
well = TRUE,
fluid = TRUE,
widths = c(4, 8)) {
# text filter for headers
textFilter <- function(text) {
tags$li(class="navbar-brand", text)
}
if (!is.null(id))
selected <- restoreInput(id = id, default = selected)
# build the tabset
tabs <- list(...)
tabset <- buildTabset(tabs,
"nav nav-pills nav-stacked",
textFilter,
id,
selected)
# create the columns
columns <- list(
column(widths[[1]], class=ifelse(well, "well", ""), tabset$navList),
column(widths[[2]], tabset$content)
tabset <- buildTabset(
..., ulClass = "nav nav-pills nav-stacked",
textFilter = function(text) tags$li(class = "navbar-brand", text),
id = id, selected = selected
)
# return the row
if (fluid)
fluidRow(columns)
else
fixedRow(columns)
row <- if (fluid) fluidRow else fixedRow
row(
column(widths[[1]], class = if (well) "well", tabset$navList),
column(
widths[[2]],
!!!dropNulls(list(header, tabset$content, footer))
)
)
}
# Helpers to build tabsetPanels (& Co.) and their elements
@@ -830,14 +877,14 @@ containsSelectedTab <- function(tabs) {
}
findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
tabs <- lapply(tabs, function(div) {
if (foundSelected || is.character(div)) {
tabs <- lapply(tabs, function(x) {
if (foundSelected || is.character(x)) {
# Strings are not selectable items
} else if (inherits(div, "shiny.navbarmenu")) {
} else if (isNavbarMenu(x)) {
# Recur for navbarMenus
res <- findAndMarkSelectedTab(div$tabs, selected, foundSelected)
div$tabs <- res$tabs
res <- findAndMarkSelectedTab(x$tabs, selected, foundSelected)
x$tabs <- res$tabs
foundSelected <<- res$foundSelected
} else {
@@ -846,16 +893,16 @@ findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
# mark first available item as selected
if (is.null(selected)) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
x <- markTabAsSelected(x)
} else {
tabValue <- div$attribs$`data-value` %||% div$attribs$title
tabValue <- x$attribs$`data-value` %||% x$attribs$title
if (identical(selected, tabValue)) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
x <- markTabAsSelected(x)
}
}
}
return(div)
return(x)
})
return(list(tabs = tabs, foundSelected = foundSelected))
}
@@ -881,9 +928,10 @@ navbarMenuTextFilter <- function(text) {
# This function is called internally by navbarPage, tabsetPanel
# and navlistPanel
buildTabset <- function(tabs, ulClass, textFilter = NULL, id = NULL,
buildTabset <- function(..., ulClass, textFilter = NULL, id = NULL,
selected = NULL, foundSelected = FALSE) {
tabs <- dropNulls(list2(...))
res <- findAndMarkSelectedTab(tabs, selected, foundSelected)
tabs <- res$tabs
foundSelected <- res$foundSelected
@@ -904,10 +952,10 @@ buildTabset <- function(tabs, ulClass, textFilter = NULL, id = NULL,
tabs = tabs, textFilter = textFilter)
tabNavList <- tags$ul(class = ulClass, id = id,
`data-tabsetid` = tabsetId, lapply(tabs, "[[", 1))
`data-tabsetid` = tabsetId, !!!lapply(tabs, "[[", "liTag"))
tabContent <- tags$div(class = "tab-content",
`data-tabsetid` = tabsetId, lapply(tabs, "[[", 2))
`data-tabsetid` = tabsetId, !!!lapply(tabs, "[[", "divTag"))
list(navList = tabNavList, content = tabContent)
}
@@ -919,56 +967,173 @@ buildTabset <- function(tabs, ulClass, textFilter = NULL, id = NULL,
buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
divTag = NULL, textFilter = NULL) {
divTag <- if (!is.null(divTag)) divTag else tabs[[index]]
divTag <- divTag %||% tabs[[index]]
# Handles navlistPanel() headers and dropdown dividers
if (is.character(divTag) && !is.null(textFilter)) {
# text item: pass it to the textFilter if it exists
liTag <- textFilter(divTag)
divTag <- NULL
} else if (inherits(divTag, "shiny.navbarmenu")) {
# navbarMenu item: build the child tabset
tabset <- buildTabset(divTag$tabs, "dropdown-menu",
navbarMenuTextFilter, foundSelected = foundSelected)
# if this navbarMenu contains a selected item, mark it active
containsSelected <- containsSelectedTab(divTag$tabs)
liTag <- tags$li(
class = paste0("dropdown", if (containsSelected) " active"),
tags$a(href = "#",
class = "dropdown-toggle", `data-toggle` = "dropdown",
`data-value` = divTag$menuName,
getIcon(iconClass = divTag$iconClass),
divTag$title, tags$b(class = "caret")
),
tabset$navList # inner tabPanels items
)
# list of tab content divs from the child tabset
divTag <- tabset$content$children
} else {
# tabPanel item: create the tab's liTag and divTag
tabId <- paste("tab", tabsetId, index, sep = "-")
liTag <- tags$li(
tags$a(
href = paste("#", tabId, sep = ""),
`data-toggle` = "tab",
`data-value` = divTag$attribs$`data-value`,
getIcon(iconClass = divTag$attribs$`data-icon-class`),
divTag$attribs$title
)
)
# if this tabPanel is selected item, mark it active
if (isTabSelected(divTag)) {
liTag$attribs$class <- "active"
divTag$attribs$class <- "tab-pane active"
}
divTag$attribs$id <- tabId
divTag$attribs$title <- NULL
return(list(liTag = textFilter(divTag), divTag = NULL))
}
return(list(liTag = liTag, divTag = divTag))
if (isNavbarMenu(divTag)) {
# tabPanelMenu item: build the child tabset
tabset <- buildTabset(
!!!divTag$tabs, ulClass = "dropdown-menu",
textFilter = navbarMenuTextFilter,
foundSelected = foundSelected
)
return(buildDropdown(divTag, tabset))
}
if (isTabPanel(divTag)) {
return(buildNavItem(divTag, tabsetId, index))
}
# The behavior is undefined at this point, so construct a condition message
msg <- paste0(
"Expected a collection `tabPanel()`s",
if (is.null(textFilter)) " and `navbarMenu()`.",
if (!is.null(textFilter)) ", `navbarMenu()`, and/or character strings.",
" Consider using `header` or `footer` if you wish to place content above (or below) every panel's contents"
)
# Luckily this case has never worked, so it's safe to throw here
# https://github.com/rstudio/shiny/issues/3313
if (!inherits(divTag, "shiny.tag")) {
stop(msg, call. = FALSE)
}
# Unfortunately, this 'off-label' use case creates an 'empty' nav and includes
# the divTag content on every tab. There shouldn't be any reason to be relying on
# this behavior since we now have pre/post arguments, so throw a warning, but still
# support the use case since we don't make breaking changes
warning(msg, call. = FALSE)
return(buildNavItem(divTag, tabsetId, index))
}
buildNavItem <- function(divTag, tabsetId, index) {
id <- paste("tab", tabsetId, index, sep = "-")
title <- tagGetAttribute(divTag, "title")
value <- tagGetAttribute(divTag, "data-value")
icon <- getIcon(iconClass = tagGetAttribute(divTag, "data-icon-class"))
active <- isTabSelected(divTag)
divTag <- tagAppendAttributes(divTag, class = if (active) "active")
divTag$attribs$id <- id
divTag$attribs$title <- NULL
list(
divTag = divTag,
liTag = tagFunction(function() {
navItem <- if ("3" %in% getCurrentVersion()) bs3NavItem else bs4NavItem
navItem(id, title, value, icon, active)
})
)
}
buildDropdown <- function(divTag, tabset) {
title <- divTag$title
value <- divTag$menuName
icon <- getIcon(iconClass = divTag$iconClass)
active <- containsSelectedTab(divTag$tabs)
list(
# list of tab content divs from the child tabset
divTag = tabset$content$children,
liTag = tagFunction(function() {
if ("3" %in% getCurrentVersion()) {
bs3NavItemDropdown(title, value, icon, active, tabset$navList)
} else {
# In BS4, dropdown nav anchors can't be wrapped in a <li> tag
# and also need .nav-link replaced with .dropdown-item to be
# styled sensibly
items <- tabset$navList
items$children <- lapply(items$children, function(x) {
# x should be a tagFunction() due to the else block below
x <- if (inherits(x, "shiny.tag.function")) x() else x
# Replace <li class="nav-item"><a class="nav-link"></a></li>
# with <a class="dropdown-item"></a>
if (tagHasClass(x, "nav-item")) {
x <- x$children[[1]]
x$attribs$class <- "dropdown-item"
}
x
})
bs4NavItemDropdown(title, value, icon, active, items)
}
})
)
}
bs3NavItemDropdown <- function(title, value, icon, active, items) {
tags$li(
class = "dropdown",
class = if (active) "active", # BS3
tags$a(
href = "#",
class = "dropdown-toggle",
`data-toggle` = "dropdown",
`data-value` = value,
icon,
title, tags$b(class = "caret")
),
items
)
}
bs3NavItem <- function(id, title, value, icon, active) {
tags$li(
class = if (active) "active",
tags$a(
href = paste0("#", id),
`data-toggle` = "tab",
`data-value` = value,
icon,
title
)
)
}
bs4NavItemDropdown <- function(title, value, icon, active, items) {
tags$li(
class = "dropdown",
class = "nav-item",
tags$a(
href = "#",
class = "dropdown-toggle",
class = "nav-link",
class = if (active) "active",
`data-toggle` = "dropdown",
`data-value` = value,
icon,
title,
tags$b(class = "caret") # TODO: can be removed?
),
items
)
}
bs4NavItem <- function(id, title, value, icon, active) {
tags$li(
class = "nav-item",
tags$a(
class = "nav-link",
class = if (active) "active",
href = paste0("#", id),
`data-toggle` = "tab",
`data-value` = value,
icon,
title
)
)
}
# TODO: something like this should exist in htmltools
tagHasClass <- function(x, class) {
if (!inherits(x, "shiny.tag")) return(FALSE)
classes <- unlist(x$attribs[names(x$attribs) %in% "class"], use.names = FALSE)
if (!length(classes)) return(FALSE)
classes <- unlist(strsplit(classes, split = "\\s+"), use.names = FALSE)
isTRUE(class %in% classes)
}
#' Create a text output element
#'

View File

@@ -49,6 +49,12 @@ processDeps <- function(tags, session) {
)
names(dependencies) <- NULL
# If ui is a tagFunction() (e.g., insertTab() et al),
# then doRenderTags() won't work...
if (inherits(ui, "shiny.tag.function")) {
ui <- renderTags(ui)$html
}
list(
html = doRenderTags(ui),
deps = dependencies

159
R/navtreePanel.R Normal file
View File

@@ -0,0 +1,159 @@
#' @export
navtreePanel <- function(..., id = NULL,
selected = NULL,
fluid = TRUE,
# Also allow for string to determine padding in a flex layout?
widths = c(3, 9)) {
# TODO: how to incorporate this into a sidebar layout?
if (!is.null(id))
selected <- restoreInput(id = id, default = selected)
tabset <- buildTreePanel(..., ulClass = "nav nav-navtree", id = id, selected = selected)
row <- if (fluid) fluidRow else fixedRow
navList <- attachDependencies(
tabset$navList,
bslib::bs_dependency_defer(navtreeCssDependency)
)
row(
column(widths[[1]], navList),
column(widths[[2]], tabset$content)
)
}
# Algorithm inspired by buildTabset() but we need different HTML/CSS,
# and menus are rendered as collapse toggles instead of Bootstrap dropdowns
buildTreePanel <- function(..., ulClass, id = NULL, selected = NULL, foundSelected = FALSE, depth = 0) {
tabs <- list2(...)
res <- findAndMarkSelectedTab(tabs, selected, foundSelected)
tabs <- res$tabs
foundSelected <- res$foundSelected
# add input class if we have an id
if (!is.null(id)) ulClass <- paste(ulClass, "shiny-tab-input")
if (anyNamed(tabs)) {
nms <- names(tabs)
nms <- nms[nzchar(nms)]
stop("Tabs should all be unnamed arguments, but some are named: ",
paste(nms, collapse = ", "))
}
tabsetId <- p_randomInt(1000, 10000)
tabs <- lapply(
seq_len(length(tabs)), buildTreeItem,
tabsetId = tabsetId,
foundSelected = foundSelected,
tabs = tabs, depth = depth
)
list(
navList = tags$ul(
class = ulClass, id = id,
`data-tabsetid` = tabsetId,
!!!lapply(tabs, "[[", "liTag")
),
content = div(
class = "tab-content",
`data-tabsetid` = tabsetId,
!!!lapply(tabs, "[[", "divTag")
)
)
}
buildTreeItem <- function(index, tabsetId, foundSelected, tabs = NULL, divTag = NULL, depth = 0) {
divTag <- divTag %||% tabs[[index]]
subMenuPadding <- if (depth > 0) css(padding_left = paste0(depth * 1.25, "rem"))
if (isNavbarMenu(divTag)) {
icon <- getIcon(iconClass = divTag$iconClass)
if (!is.null(icon)) {
warning("Configurable icons are not yet supported in navtreePanel().")
}
tabset <- buildTreePanel(
!!!divTag$tabs, ulClass = "nav nav-navtree",
foundSelected = foundSelected, depth = depth + 1
)
# Sort of like .dropdown in the tabsetPanel() case,
# but utilizes collapsing (which is recursive) instead of dropdown
active <- containsSelectedTab(divTag$tabs)
menuId <- paste0("collapse-", p_randomInt(1000, 10000))
liTag <- tags$li(
tags$a(
class = if (!active) "collapsed",
"data-toggle" = "collapse",
"data-value" = divTag$menuName,
"data-target" = paste0("#", menuId),
role = "button",
style = subMenuPadding,
getIcon(iconClass = divTag$iconClass),
divTag$title
),
div(
class = "collapse",
class = if (active) "show in",
id = menuId,
tabset$navList
)
)
return(list(liTag = liTag, divTag = tabset$content$children))
}
if (isTabPanel(divTag)) {
# Borrow from the usual nav logic so we get the right
# li.active vs li > a.active (BS4) markup
navItem <- buildNavItem(divTag, tabsetId, index)
liTag <- navItem$liTag
return(
list(
divTag = navItem$divTag,
liTag = tagFunction(function() {
# Incoming liTag should be a tagFunction()
liTag <- if (inherits(liTag, "shiny.tag.function")) liTag() else liTag
# Add padding for sub menu anchors
liTag$children[[1]] <- tagAppendAttributes(
liTag$children[[1]], style = subMenuPadding
)
liTag
})
)
)
}
abort("navtreePanel() items must be tabPanel()s and/or tabPanelMenu()s")
}
navtreeCssDependency <- function(theme) {
name <- "navtreePanel"
version <- packageVersion("shiny")
if (!is_bs_theme(theme)) {
# TODO: Should we allow navtreePanel() to be statically rendered?
# Can/should we move away from href="shared/*"?
htmlDependency(
name = name, version = version,
src = c(href = "shared/navtree", file = "www/shared/navtree"),
package = "shiny",
stylesheet = "navtree.css",
script = "navtree.js"
)
} else {
navtree <- system.file(package = "shiny", "www/shared/navtree")
bslib::bs_dependency(
sass::sass_file(file.path(navtree, "navtree.scss")),
theme = theme,
name = name,
version = version,
cache_key_extra = version,
.dep_args = list(script = file.path(navtree, "navtree.js"))
)
}
}

View File

@@ -568,7 +568,7 @@ ReactiveValues <- R6Class(
#' @seealso [isolate()] and [is.reactivevalues()].
#' @export
reactiveValues <- function(...) {
args <- list(...)
args <- list2(...)
if ((length(args) > 0) && (is.null(names(args)) || any(names(args) == "")))
rlang::abort("All arguments passed to reactiveValues() must be named.")
@@ -1915,7 +1915,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
#' @export
reactiveFileReader <- function(intervalMillis, session, filePath, readFunc, ...) {
filePath <- coerceToFunc(filePath)
extraArgs <- list(...)
extraArgs <- list2(...)
reactivePoll(
intervalMillis, session,

View File

@@ -178,7 +178,7 @@ getShinyOption <- function(name, default = NULL) {
#' @aliases shiny-options
#' @export
shinyOptions <- function(...) {
newOpts <- list(...)
newOpts <- list2(...)
if (length(newOpts) > 0) {
# If we're within a session, modify at the session level.

View File

@@ -14,7 +14,7 @@
#' quo enquo as_function get_expr get_env new_function enquos
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject
#' enquos0 zap_srcref %||% is_na
#' is_false
#' is_false list2
#' missing_arg is_missing maybe_missing
#' @importFrom ellipsis
#' check_dots_empty check_dots_unnamed

View File

@@ -1176,7 +1176,7 @@ reactiveStop <- function(message = "", class = NULL) {
#'
#' }
validate <- function(..., errorClass = character(0)) {
results <- sapply(list(...), function(x) {
results <- sapply(list2(...), function(x) {
# Detect NULL or NA
if (is.null(x))
return(NA_character_)

View File

@@ -0,0 +1,50 @@
.nav-navtree {
display: block !important;
}
.nav-navtree li.active, .nav-navtree li > a.active, .nav-navtree li a:focus {
font-weight: 600;
background-color: rgba(0, 123, 255, 0.3) !important;
color: rgba(33, 37, 41, 0.85);
}
.nav-navtree li:not(.active) > a:not(.active):hover {
background-color: rgba(33, 37, 41, 0.1);
}
.nav-navtree li a {
display: inline-flex !important;
align-items: center !important;
width: 100% !important;
padding: .1875rem .5rem;
border: 1px solid #dee2e6 !important;
color: rgba(33, 37, 41, 0.65) !important;
text-decoration: none !important;
}
.nav-navtree li a i {
padding-right: 1.25rem !important;
}
.nav-navtree li a[data-toggle="collapse"]::before {
width: 1.25em;
line-height: 0;
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%2833, 37, 41, 0.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
transition: transform 0.35s ease;
transform-origin: .5em 50%;
padding-right: 1rem;
}
@media (prefers-reduced-motion: reduce) {
.nav-navtree li a[data-toggle="collapse"]::before {
transition: none;
}
}
.nav-navtree li a[data-toggle="collapse"]:not(.collapsed) {
color: rgba(33, 37, 41, 0.85);
}
.nav-navtree li a[data-toggle="collapse"]:not(.collapsed)::before {
transform: rotate(90deg);
}

View File

@@ -0,0 +1,11 @@
// nav-navtree is built on a combination of Bootstrap's tab &
// collapse components, but the tab component isn't smart enough to
// know about the deactive when are activated. Note that this logic also
// exists in input_binding_tabinput.js and is repeated here in case this
// component wants to be statically rendered
$(document).on("shown.bs.tab", ".nav-navtree", function(e) {
var tgt = $(e.target);
var nav = tgt.parents(".nav-navtree");
nav.find("li").not(tgt).removeClass("active"); // BS3
nav.find("li > a").not(tgt).removeClass("active"); // BS4
});

View File

@@ -0,0 +1,66 @@
// Inspired by BS5's sidebar nav
// https://github.com/twbs/bootstrap/blob/548be2e/site/assets/scss/_sidebar.scss#L7
// As well as
// https://chniter.github.io/bstreeview/
$body-color: $text-color !default; // BS3compat
$link-decoration: none !default;
$link-hover-decoration: underline !default;
$sidebar-collapse-icon-color: rgba($body-color, 0.5) !default;
$sidebar-collapse-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'><path fill='none' stroke='#{$sidebar-collapse-icon-color}' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/></svg>") !default;
// TODO: make this more configurable from the R side?
$navtree-anchor-color: rgba($body-color, .65) !default;
$navtree-active-bg: rgba($component-active-bg, .3) !default;
$navtree-active-color: rgba($body-color, .85) !default;
.nav-navtree {
// Override collapse behaviors
display: block !important;
li {
// Handle both li.active (BS3) and li > a.active (BS4)
&.active, > a.active, a:focus {
font-weight: 600;
background-color: $navtree-active-bg !important;
color: $navtree-active-color;
}
&:not(.active) > a:not(.active):hover {
background-color: rgba($body-color, .1);
}
a {
display: inline-flex !important; // Needed for centering/transforming the collapse icon
align-items: center !important;
width: 100% !important;
padding: .1875rem .5rem;
border: $border-width solid $border-color !important;
color: $navtree-anchor-color !important;
text-decoration: none !important;
i {
padding-right: 1.25rem !important;
}
// Add chevron if there's a submenu
&[data-toggle="collapse"] {
&::before {
width: 1.25em;
line-height: 0; // Align in the middle
content: escape-svg($sidebar-collapse-icon);
@include transition(transform .35s ease);
transform-origin: .5em 50%;
padding-right: 1rem;
}
&:not(.collapsed) {
color: $navtree-active-color;
&::before {
transform: rotate(90deg);
}
}
}
} // a
} // li
} // nav

View File

@@ -1198,6 +1198,26 @@
};
});
// node_modules/core-js/internals/regexp-exec-abstract.js
var require_regexp_exec_abstract = __commonJS(function(exports2, module2) {
var classof2 = require_classof_raw();
var regexpExec2 = require_regexp_exec();
module2.exports = function(R, S) {
var exec = R.exec;
if (typeof exec === "function") {
var result = exec.call(R, S);
if (typeof result !== "object") {
throw TypeError("RegExp exec method returned something other than an Object or null");
}
return result;
}
if (classof2(R) !== "RegExp") {
throw TypeError("RegExp#exec called on incompatible receiver");
}
return regexpExec2.call(R, S);
};
});
// node_modules/core-js/internals/get-substitution.js
var require_get_substitution = __commonJS(function(exports2, module2) {
var toObject4 = require_to_object();
@@ -1246,26 +1266,6 @@
};
});
// node_modules/core-js/internals/regexp-exec-abstract.js
var require_regexp_exec_abstract = __commonJS(function(exports2, module2) {
var classof2 = require_classof_raw();
var regexpExec2 = require_regexp_exec();
module2.exports = function(R, S) {
var exec = R.exec;
if (typeof exec === "function") {
var result = exec.call(R, S);
if (typeof result !== "object") {
throw TypeError("RegExp exec method returned something other than an Object or null");
}
return result;
}
if (classof2(R) !== "RegExp") {
throw TypeError("RegExp#exec called on incompatible receiver");
}
return regexpExec2.call(R, S);
};
});
// node_modules/core-js/internals/is-regexp.js
var require_is_regexp = __commonJS(function(exports2, module2) {
var isObject3 = require_is_object();
@@ -2996,7 +2996,9 @@
function getTargetTabs($tabset, $tabContent, target) {
var dataValue = "[data-value='" + $escape(target) + "']";
var $aTag = $tabset.find("a" + dataValue);
var $liTag = $aTag.parent();
var $liTag = $aTag.parent("li");
if ($liTag.length === 0)
$liTag = $aTag;
if ($liTag.length === 0) {
throw "There is no tabPanel (or navbarMenu) with value (or menuName) equal to '" + target + "'";
}
@@ -3005,7 +3007,10 @@
if ($aTag.attr("data-toggle") === "dropdown") {
var $dropdownTabset = $aTag.find("+ ul.dropdown-menu");
var dropdownId = $dropdownTabset.attr("data-tabsetid");
var $dropdownLiTags = $dropdownTabset.find("a[data-toggle='tab']").parent("li");
var $dropdownLiTags = $dropdownTabset.find("a[data-toggle='tab']");
if ($dropdownLiTags.parent("li").length > 0) {
$dropdownLiTags = $dropdownLiTags.parent("li");
}
$dropdownLiTags.each(function(i, el) {
$liTags.push(import_jquery6.default(el));
});
@@ -3036,6 +3041,9 @@
if (message.target !== null) {
target = getTargetTabs($tabset, $tabContent, message.target);
$targetLiTag = target.$liTag;
if ($targetLiTag.hasClass("dropdown-item")) {
$liTag = $aTag.removeClass("nav-link").addClass("dropdown-item");
}
}
var dropdown = getDropdown();
if (dropdown !== null) {
@@ -3047,7 +3055,10 @@
if ($aTag.attr("data-toggle") === "tab") {
var index = getTabIndex($tabset, tabsetId);
var tabId = "tab-" + tabsetId + "-" + index;
$liTag.find("> a").attr("href", "#" + tabId);
var anchor = $liTag.find("> a");
if (anchor.length === 0)
anchor = $liTag;
anchor.attr("href", "#" + tabId);
$divTag.attr("id", tabId);
}
if (message.position === "before") {
@@ -3075,13 +3086,29 @@
$tabContent[0].appendChild(el);
Shiny.renderContent(el, el.innerHTML || el.textContent);
});
if ($tabset.hasClass("nav-navtree") && $liTag.hasClass("dropdown")) {
var collapseId = "collapse-" + tabsetId + "-" + getTabIndex($tabset, tabsetId);
$tabset.find(".dropdown").each(function(i, el) {
var $el = import_jquery6.default(el).removeClass("dropdown nav-item");
$el.find(".dropdown-toggle").removeClass("dropdown-toggle nav-link").addClass(message.select ? "" : "collapsed").attr("data-toggle", "collapse").attr("data-target", "#" + collapseId);
var collapse = import_jquery6.default("<div>").addClass("collapse" + (message.select ? " show" : "")).attr("id", collapseId);
var menu = $el.find(".dropdown-menu").removeClass("dropdown-menu").addClass("nav nav-navtree").wrap(collapse);
var depth = $el.parents(".nav-navtree").length - 1;
if (depth > 0) {
$el.find("a").css("padding-left", depth + "rem");
}
if (menu.find("li").length === 0) {
menu.find("a").removeClass("dropdown-item").addClass("nav-link").wrap("<li class='nav-item'></li>");
}
});
}
if (message.select) {
$liTag.find("a").tab("show");
}
function getTabIndex($tabset2, tabsetId2) {
var existingTabIds = [0];
$tabset2.find("> li").each(function() {
var $tab = import_jquery6.default(this).find("> a[data-toggle='tab']");
$tabset2.find("a[data-toggle='tab']").each(function() {
var $tab = import_jquery6.default(this);
if ($tab.length > 0) {
var href = $tab.attr("href").replace(/.*(?=#[^\s]+$)/, "");
var _index = href.replace("#tab-" + tabsetId2 + "-", "");
@@ -3116,9 +3143,9 @@
}
});
function ensureTabsetHasVisibleTab($tabset) {
if ($tabset.find("li.active").not(".dropdown").length === 0) {
var inputBinding = $tabset.data("shiny-input-binding");
if (!inputBinding.getValue($tabset)) {
var destTabValue = getFirstTab($tabset);
var inputBinding = $tabset.data("shiny-input-binding");
var evt = jQuery.Event("shiny:updateinput");
evt.binding = inputBinding;
$tabset.trigger(evt);
@@ -6084,7 +6111,7 @@
return import_jquery6.default(scope).find("ul.nav.shiny-tab-input");
},
getValue: function getValue(el) {
var anchor = import_jquery6.default(el).find("li:not(.dropdown).active").children("a");
var anchor = isBS3() ? import_jquery6.default(el).find("li:not(.dropdown).active > a") : import_jquery6.default(el).find(".nav-link:not(.dropdown-toggle).active, .dropdown-menu > .dropdown-item.active");
if (anchor.length === 1)
return this._getTabName(anchor);
return null;
@@ -6093,10 +6120,11 @@
var self2 = this;
var success = false;
if (value) {
var anchors = import_jquery6.default(el).find("li:not(.dropdown)").children("a");
var anchors = isBS3() ? import_jquery6.default(el).find("li:not(.dropdown) > a") : import_jquery6.default(el).find(".nav-link:not(.dropdown-toggle), .dropdown-menu > .dropdown-item");
anchors.each(function() {
if (self2._getTabName(import_jquery6.default(this)) === value) {
import_jquery6.default(this).tab("show");
import_jquery6.default(this).parents(".collapse").collapse("show");
success = true;
return false;
}
@@ -6118,7 +6146,9 @@
import_jquery6.default(el).trigger("change");
},
subscribe: function subscribe(el, callback) {
var deactivateOtherTabs = this._deactivateOtherTabs;
import_jquery6.default(el).on("change shown.bootstrapTabInputBinding shown.bs.tab.bootstrapTabInputBinding", function(event) {
deactivateOtherTabs(event);
callback();
});
},
@@ -6127,6 +6157,12 @@
},
_getTabName: function _getTabName(anchor) {
return anchor.attr("data-value") || anchor.text();
},
_deactivateOtherTabs: function _deactivateOtherTabs(event) {
var tgt = import_jquery6.default(event.target);
var nav = tgt.parents(".nav-navtree");
nav.find("li").not(tgt).removeClass("active");
nav.find("li > a").not(tgt).removeClass("active");
}
});
inputBindings.register(bootstrapTabInputBinding, "shiny.bootstrapTabInput");
@@ -7259,28 +7295,68 @@
}, {unsafe: true});
}
// node_modules/core-js/modules/es.string.replace.js
// node_modules/core-js/modules/es.string.match.js
"use strict";
var fixRegExpWellKnownSymbolLogic = require_fix_regexp_well_known_symbol_logic();
var anObject2 = require_an_object();
var toLength4 = require_to_length();
var toInteger2 = require_to_integer();
var requireObjectCoercible = require_require_object_coercible();
var advanceStringIndex = require_advance_string_index();
var getSubstitution = require_get_substitution();
var regExpExec = require_regexp_exec_abstract();
fixRegExpWellKnownSymbolLogic("match", 1, function(MATCH, nativeMatch, maybeCallNative) {
return [
function match(regexp) {
var O = requireObjectCoercible(this);
var matcher = regexp == void 0 ? void 0 : regexp[MATCH];
return matcher !== void 0 ? matcher.call(regexp, O) : new RegExp(regexp)[MATCH](String(O));
},
function(regexp) {
var res = maybeCallNative(nativeMatch, regexp, this);
if (res.done)
return res.value;
var rx = anObject2(regexp);
var S = String(this);
if (!rx.global)
return regExpExec(rx, S);
var fullUnicode = rx.unicode;
rx.lastIndex = 0;
var A = [];
var n = 0;
var result;
while ((result = regExpExec(rx, S)) !== null) {
var matchStr = String(result[0]);
A[n] = matchStr;
if (matchStr === "")
rx.lastIndex = advanceStringIndex(S, toLength4(rx.lastIndex), fullUnicode);
n++;
}
return n === 0 ? null : A;
}
];
});
// node_modules/core-js/modules/es.string.replace.js
"use strict";
var fixRegExpWellKnownSymbolLogic2 = require_fix_regexp_well_known_symbol_logic();
var anObject3 = require_an_object();
var toLength5 = require_to_length();
var toInteger2 = require_to_integer();
var requireObjectCoercible2 = require_require_object_coercible();
var advanceStringIndex2 = require_advance_string_index();
var getSubstitution = require_get_substitution();
var regExpExec2 = require_regexp_exec_abstract();
var max3 = Math.max;
var min2 = Math.min;
var maybeToString = function(it) {
return it === void 0 ? it : String(it);
};
fixRegExpWellKnownSymbolLogic("replace", 2, function(REPLACE, nativeReplace, maybeCallNative, reason) {
fixRegExpWellKnownSymbolLogic2("replace", 2, function(REPLACE, nativeReplace, maybeCallNative, reason) {
var REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE = reason.REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE;
var REPLACE_KEEPS_$0 = reason.REPLACE_KEEPS_$0;
var UNSAFE_SUBSTITUTE = REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE ? "$" : "$0";
return [
function replace(searchValue, replaceValue) {
var O = requireObjectCoercible(this);
var O = requireObjectCoercible2(this);
var replacer = searchValue == void 0 ? void 0 : searchValue[REPLACE];
return replacer !== void 0 ? replacer.call(searchValue, O, replaceValue) : nativeReplace.call(String(O), searchValue, replaceValue);
},
@@ -7290,7 +7366,7 @@
if (res.done)
return res.value;
}
var rx = anObject2(regexp);
var rx = anObject3(regexp);
var S = String(this);
var functionalReplace = typeof replaceValue === "function";
if (!functionalReplace)
@@ -7302,7 +7378,7 @@
}
var results = [];
while (true) {
var result = regExpExec(rx, S);
var result = regExpExec2(rx, S);
if (result === null)
break;
results.push(result);
@@ -7310,7 +7386,7 @@
break;
var matchStr = String(result[0]);
if (matchStr === "")
rx.lastIndex = advanceStringIndex(S, toLength4(rx.lastIndex), fullUnicode);
rx.lastIndex = advanceStringIndex2(S, toLength5(rx.lastIndex), fullUnicode);
}
var accumulatedResult = "";
var nextSourcePosition = 0;
@@ -7342,13 +7418,13 @@
// node_modules/core-js/modules/es.string.split.js
"use strict";
var fixRegExpWellKnownSymbolLogic2 = require_fix_regexp_well_known_symbol_logic();
var fixRegExpWellKnownSymbolLogic3 = require_fix_regexp_well_known_symbol_logic();
var isRegExp = require_is_regexp();
var anObject3 = require_an_object();
var requireObjectCoercible2 = require_require_object_coercible();
var anObject4 = require_an_object();
var requireObjectCoercible3 = require_require_object_coercible();
var speciesConstructor = require_species_constructor();
var advanceStringIndex2 = require_advance_string_index();
var toLength5 = require_to_length();
var advanceStringIndex3 = require_advance_string_index();
var toLength6 = require_to_length();
var callRegExpExec = require_regexp_exec_abstract();
var regexpExec = require_regexp_exec();
var fails5 = require_fails();
@@ -7358,11 +7434,11 @@
var SUPPORTS_Y = !fails5(function() {
return !RegExp(MAX_UINT32, "y");
});
fixRegExpWellKnownSymbolLogic2("split", 2, function(SPLIT, nativeSplit, maybeCallNative) {
fixRegExpWellKnownSymbolLogic3("split", 2, function(SPLIT, nativeSplit, maybeCallNative) {
var internalSplit;
if ("abbc".split(/(b)*/)[1] == "c" || "test".split(/(?:)/, -1).length != 4 || "ab".split(/(?:ab)*/).length != 2 || ".".split(/(.?)(.?)/).length != 4 || ".".split(/()()/).length > 1 || "".split(/.?/).length) {
internalSplit = function(separator, limit) {
var string = String(requireObjectCoercible2(this));
var string = String(requireObjectCoercible3(this));
var lim = limit === void 0 ? MAX_UINT32 : limit >>> 0;
if (lim === 0)
return [];
@@ -7405,7 +7481,7 @@
internalSplit = nativeSplit;
return [
function split(separator, limit) {
var O = requireObjectCoercible2(this);
var O = requireObjectCoercible3(this);
var splitter = separator == void 0 ? void 0 : separator[SPLIT];
return splitter !== void 0 ? splitter.call(separator, O, limit) : internalSplit.call(String(O), separator, limit);
},
@@ -7413,7 +7489,7 @@
var res = maybeCallNative(internalSplit, regexp, this, limit, internalSplit !== nativeSplit);
if (res.done)
return res.value;
var rx = anObject3(regexp);
var rx = anObject4(regexp);
var S = String(this);
var C = speciesConstructor(rx, RegExp);
var unicodeMatching = rx.unicode;
@@ -7431,8 +7507,8 @@
splitter.lastIndex = SUPPORTS_Y ? q : 0;
var z = callRegExpExec(splitter, SUPPORTS_Y ? S : S.slice(q));
var e;
if (z === null || (e = min3(toLength5(splitter.lastIndex + (SUPPORTS_Y ? 0 : q)), S.length)) === p) {
q = advanceStringIndex2(S, q, unicodeMatching);
if (z === null || (e = min3(toLength6(splitter.lastIndex + (SUPPORTS_Y ? 0 : q)), S.length)) === p) {
q = advanceStringIndex3(S, q, unicodeMatching);
} else {
A.push(S.slice(p, q));
if (A.length === lim)
@@ -7725,6 +7801,12 @@
el.removeChild(div);
return linkColor;
}
function isBS3() {
if (!import_jquery5.default.fn.tooltip) {
return false;
}
return import_jquery5.default.fn.tooltip.Constructor.VERSION.match(/^3\./);
}
// src/shiny.ts
var Shiny;
@@ -7855,9 +7937,9 @@
var $22 = require_export();
var fails6 = require_fails();
var ArrayBufferModule = require_array_buffer();
var anObject4 = require_an_object();
var anObject5 = require_an_object();
var toAbsoluteIndex3 = require_to_absolute_index();
var toLength6 = require_to_length();
var toLength7 = require_to_length();
var speciesConstructor2 = require_species_constructor();
var ArrayBuffer3 = ArrayBufferModule.ArrayBuffer;
var DataView2 = ArrayBufferModule.DataView;
@@ -7868,12 +7950,12 @@
$22({target: "ArrayBuffer", proto: true, unsafe: true, forced: INCORRECT_SLICE}, {
slice: function slice2(start, end) {
if (nativeArrayBufferSlice !== void 0 && end === void 0) {
return nativeArrayBufferSlice.call(anObject4(this), start);
return nativeArrayBufferSlice.call(anObject5(this), start);
}
var length = anObject4(this).byteLength;
var length = anObject5(this).byteLength;
var first = toAbsoluteIndex3(start, length);
var fin = toAbsoluteIndex3(end === void 0 ? length : end, length);
var result = new (speciesConstructor2(this, ArrayBuffer3))(toLength6(fin - first));
var result = new (speciesConstructor2(this, ArrayBuffer3))(toLength7(fin - first));
var viewSource = new DataView2(this);
var viewTarget = new DataView2(result);
var index = 0;
@@ -7991,46 +8073,6 @@
var j;
var key;
// node_modules/core-js/modules/es.string.match.js
"use strict";
var fixRegExpWellKnownSymbolLogic3 = require_fix_regexp_well_known_symbol_logic();
var anObject5 = require_an_object();
var toLength7 = require_to_length();
var requireObjectCoercible3 = require_require_object_coercible();
var advanceStringIndex3 = require_advance_string_index();
var regExpExec2 = require_regexp_exec_abstract();
fixRegExpWellKnownSymbolLogic3("match", 1, function(MATCH, nativeMatch, maybeCallNative) {
return [
function match(regexp) {
var O = requireObjectCoercible3(this);
var matcher = regexp == void 0 ? void 0 : regexp[MATCH];
return matcher !== void 0 ? matcher.call(regexp, O) : new RegExp(regexp)[MATCH](String(O));
},
function(regexp) {
var res = maybeCallNative(nativeMatch, regexp, this);
if (res.done)
return res.value;
var rx = anObject5(regexp);
var S = String(this);
if (!rx.global)
return regExpExec2(rx, S);
var fullUnicode = rx.unicode;
rx.lastIndex = 0;
var A = [];
var n = 0;
var result;
while ((result = regExpExec2(rx, S)) !== null) {
var matchStr = String(result[0]);
A[n] = matchStr;
if (matchStr === "")
rx.lastIndex = advanceStringIndex3(S, toLength7(rx.lastIndex), fullUnicode);
n++;
}
return n === 0 ? null : A;
}
];
});
// node_modules/core-js/modules/es.string.search.js
"use strict";
var fixRegExpWellKnownSymbolLogic4 = require_fix_regexp_well_known_symbol_logic();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -28,8 +28,10 @@ navbarMenu(title, ..., menuName = title, icon = NULL)
\arguments{
\item{title}{The title to display in the navbar}
\item{...}{\code{\link[=tabPanel]{tabPanel()}} elements to include in the navbar.
Plain strings will be converted to headers.}
\item{...}{\code{\link[=tabPanel]{tabPanel()}} elements to include in the page. The
\code{navbarMenu} function also accepts strings, which will be used as menu
section headers. If the string is a set of dashes like \code{"----"} a
horizontal separator will be displayed in the menu.}
\item{id}{If provided, you can use \verb{input$}\emph{\code{id}} in your
server logic to determine which of the current tabs is active. The value
@@ -63,7 +65,7 @@ elements into a menu when the width of the browser is less than 940 pixels
\item{collapsable}{Deprecated; use \code{collapsible} instead.}
\item{fluid}{\code{TRUE} to use fluid layout; \code{FALSE} to use fixed
\item{fluid}{\code{TRUE} to use a fluid layout. \code{FALSE} to use a fixed
layout.}
\item{responsive}{This option is deprecated; it is no longer optional with
@@ -91,11 +93,17 @@ is needed if you want to insert/remove or show/hide an entire
\item{icon}{Optional icon to appear on a \code{navbarMenu} tab.}
}
\value{
A UI defintion that can be passed to the \link{shinyUI} function.
}
\description{
Create a page that contains a top level navigation bar that can be used to
toggle a set of \code{\link[=tabPanel]{tabPanel()}} elements. \code{navbarMenu()} can be used to create
an embedded menu within the navbar that in turns includes additional
\code{tabPanels}.
toggle a set of \code{\link[=tabPanel]{tabPanel()}} elements.
}
\details{
The \code{navbarMenu} function can be used to create an embedded
menu within the navbar that in turns includes additional tabPanels (see
example below).
}
\examples{
navbarPage("App Title",
@@ -115,7 +123,9 @@ navbarPage("App Title",
)
}
\seealso{
\code{\link[=updateNavbarPage]{updateNavbarPage()}}, \code{\link[=insertTab]{insertTab()}}, \code{\link[=showTab]{showTab()}}
\code{\link[=tabPanel]{tabPanel()}}, \code{\link[=tabsetPanel]{tabsetPanel()}},
\code{\link[=updateNavbarPage]{updateNavbarPage()}}, \code{\link[=insertTab]{insertTab()}},
\code{\link[=showTab]{showTab()}}
Other layout functions:
\code{\link{fillPage}()},
@@ -125,11 +135,5 @@ Other layout functions:
\code{\link{sidebarLayout}()},
\code{\link{splitLayout}()},
\code{\link{verticalLayout}()}
Other tab layouts:
\code{\link{navlistPanel}()},
\code{\link{tabPanel}()},
\code{\link{tabsetPanel}()}
}
\concept{layout functions}
\concept{tab layouts}

View File

@@ -8,23 +8,30 @@ navlistPanel(
...,
id = NULL,
selected = NULL,
header = NULL,
footer = NULL,
well = TRUE,
fluid = TRUE,
widths = c(4, 8)
)
}
\arguments{
\item{...}{\code{\link[=tabPanel]{tabPanel()}} elements to include in the navbar.
Plain strings will be converted to headers.}
\item{...}{\code{\link[=tabPanel]{tabPanel()}} elements to include in the navlist}
\item{id}{If provided, you can use \verb{input$}\emph{\code{id}} in your
server logic to determine which of the current tabs is active. The value
will correspond to the \code{value} argument that is passed to
server logic to determine which of the current navlist items is active. The
value will correspond to the \code{value} argument that is passed to
\code{\link[=tabPanel]{tabPanel()}}.}
\item{selected}{The \code{value} (or, if none was supplied, the \code{title})
of the tab that should be selected by default. If \code{NULL}, the first
tab will be selected.}
of the navigation item that should be selected by default. If \code{NULL},
the first navigation will be selected.}
\item{header}{Tag or list of tags to display as a common header above all
tabPanels.}
\item{footer}{Tag or list of tags to display as a common footer below all
tabPanels}
\item{well}{\code{TRUE} to place a well (gray rounded rectangle) around the
navigation list.}
@@ -40,7 +47,11 @@ Create a navigation list panel that provides a list of links on the left
which navigate to a set of tabPanels displayed to the right.
}
\details{
You can include headers within the \code{navlistPanel} by including
plain text elements in the list. Versions of Shiny before 0.11 supported
separators with "------", but as of 0.11, separators were no longer
supported. This is because version 0.11 switched to Bootstrap 3, which
doesn't support separators.
}
\examples{
fluidPage(
@@ -56,11 +67,6 @@ fluidPage(
)
}
\seealso{
\code{\link[=updateTabsetPanel]{updateTabsetPanel()}}, \code{\link[=insertTab]{insertTab()}}, \code{\link[=showTab]{showTab()}}
Other tab layouts:
\code{\link{navbarPage}()},
\code{\link{tabPanel}()},
\code{\link{tabsetPanel}()}
\code{\link[=tabPanel]{tabPanel()}}, \code{\link[=updateNavlistPanel]{updateNavlistPanel()}},
\code{\link[=insertTab]{insertTab()}}, \code{\link[=showTab]{showTab()}}
}
\concept{tab layouts}

View File

@@ -21,12 +21,20 @@ that this tab is selected. If omitted and \code{tabsetPanel} has an
\item{icon}{Optional icon to appear on the tab. This attribute is only
valid when using a \code{tabPanel} within a \code{\link[=navbarPage]{navbarPage()}}.}
}
\description{
\code{tabPanel()} creates a tab panel that can be included within a
\code{\link[=tabsetPanel]{tabsetPanel()}}, \code{\link[=navListPanel]{navListPanel()}}, or \code{\link[=navbarPage]{navbarPage()}}. \code{tabPanelBody()}
drops the \code{title}, making it most suitable for use within
\code{tabsetPanel(type = "hidden")}.
\value{
A tab that can be passed to \code{\link[=tabsetPanel]{tabsetPanel()}}
}
\description{
Create a tab panel
}
\section{Functions}{
\itemize{
\item \code{tabPanel}: Create a tab panel that can be included within a \code{\link[=tabsetPanel]{tabsetPanel()}} or a \code{\link[=navbarPage]{navbarPage()}}.
\item \code{tabPanelBody}: Create a tab panel that drops the title argument.
This function should be used within \code{tabsetPanel(type = "hidden")}. See \code{\link[=tabsetPanel]{tabsetPanel()}} for example usage.
}}
\examples{
# Show a tabset that includes a plot, summary, and
# table view of the generated distribution
@@ -39,9 +47,5 @@ mainPanel(
)
}
\seealso{
Other tab layouts:
\code{\link{navbarPage}()},
\code{\link{navlistPanel}()},
\code{\link{tabsetPanel}()}
\code{\link[=tabsetPanel]{tabsetPanel()}}
}
\concept{tab layouts}

View File

@@ -9,6 +9,8 @@ tabsetPanel(
id = NULL,
selected = NULL,
type = c("tabs", "pills", "hidden"),
header = NULL,
footer = NULL,
position = deprecated()
)
}
@@ -32,9 +34,18 @@ conjunction with \code{\link[=tabPanelBody]{tabPanelBody()}} and \code{\link[=up
active tab via other input controls. (See example below)}
}}
\item{header}{Tag or list of tags to display as a common header above all
tabPanels.}
\item{footer}{Tag or list of tags to display as a common footer below all
tabPanels}
\item{position}{This argument is deprecated; it has been discontinued in
Bootstrap 3.}
}
\value{
A tabset that can be passed to \code{\link[=mainPanel]{mainPanel()}}
}
\description{
Create a tabset that contains \code{\link[=tabPanel]{tabPanel()}} elements. Tabsets are
useful for dividing output into multiple independently viewable sections.
@@ -80,11 +91,6 @@ if (interactive()) {
}
}
\seealso{
\code{\link[=updateTabsetPanel]{updateTabsetPanel()}}, \code{\link[=insertTab]{insertTab()}}, \code{\link[=showTab]{showTab()}}
Other tab layouts:
\code{\link{navbarPage}()},
\code{\link{navlistPanel}()},
\code{\link{tabPanel}()}
\code{\link[=tabPanel]{tabPanel()}}, \code{\link[=updateTabsetPanel]{updateTabsetPanel()}},
\code{\link[=insertTab]{insertTab()}}, \code{\link[=showTab]{showTab()}}
}
\concept{tab layouts}

View File

@@ -34,6 +34,7 @@ import {
updateLabel,
getComputedLinkColor,
makeBlob,
isBS3,
} from "./utils";
import { isQt, isIE, IEVersion } from "./utils/browser";
@@ -1241,8 +1242,10 @@ function main(): void {
function getTargetTabs($tabset, $tabContent, target) {
const dataValue = "[data-value='" + $escape(target) + "']";
const $aTag = $tabset.find("a" + dataValue);
const $liTag = $aTag.parent();
let $liTag = $aTag.parent("li");
// BS3 dropdown anchors are wrapped in <li>, but they can't be in BS4
if ($liTag.length === 0) $liTag = $aTag;
if ($liTag.length === 0) {
throw (
"There is no tabPanel (or navbarMenu) with value" +
@@ -1259,10 +1262,12 @@ function main(): void {
const $dropdownTabset = $aTag.find("+ ul.dropdown-menu");
const dropdownId = $dropdownTabset.attr("data-tabsetid");
const $dropdownLiTags = $dropdownTabset
.find("a[data-toggle='tab']")
.parent("li");
let $dropdownLiTags = $dropdownTabset.find("a[data-toggle='tab']");
// BS3 dropdown anchors are wrapped in <li>, but they can't be in BS4
if ($dropdownLiTags.parent("li").length > 0) {
$dropdownLiTags = $dropdownLiTags.parent("li");
}
$dropdownLiTags.each(function (i, el) {
$liTags.push($(el));
});
@@ -1286,7 +1291,7 @@ function main(): void {
let tabsetId = $parentTabset.attr("data-tabsetid");
const $divTag = $(message.divTag.html);
const $liTag = $(message.liTag.html);
let $liTag = $(message.liTag.html);
const $aTag = $liTag.find("> a");
// Unless the item is being prepended/appended, the target tab
@@ -1297,6 +1302,12 @@ function main(): void {
if (message.target !== null) {
target = getTargetTabs($tabset, $tabContent, message.target);
$targetLiTag = target.$liTag;
// If the target is a (BS4) .dropdown-item, then we can't insert
// <li class='nav-item'><a class='nav-link'>...</a></li>,
// instead, we need <a class='dropdown-item'>...</a>
if ($targetLiTag.hasClass("dropdown-item")) {
$liTag = $aTag.removeClass("nav-link").addClass("dropdown-item");
}
}
// If the item is to be placed inside a navbarMenu (dropdown),
@@ -1321,7 +1332,11 @@ function main(): void {
const index = getTabIndex($tabset, tabsetId);
const tabId = "tab-" + tabsetId + "-" + index;
$liTag.find("> a").attr("href", "#" + tabId);
let anchor = $liTag.find("> a");
// BS3 dropdown anchors are wrapped in <li>, but they can't be in BS4
if (anchor.length === 0) anchor = $liTag;
anchor.attr("href", "#" + tabId);
$divTag.attr("id", tabId);
}
@@ -1392,6 +1407,50 @@ function main(): void {
Shiny.renderContent(el, el.innerHTML || el.textContent);
});
// If we're inserting a navbarMenu() into a navtreePanel() target, we need
// to transform buildTabset() output (i.e., a .dropdown component) to
// buildTreePanel() output (i.e., a .collapse component), because
// insertTab() et al. doesn't know about the relevant tabset container
if ($tabset.hasClass("nav-navtree") && $liTag.hasClass("dropdown")) {
let collapseId =
"collapse-" + tabsetId + "-" + getTabIndex($tabset, tabsetId);
$tabset.find(".dropdown").each(function (i, el) {
let $el = $(el).removeClass("dropdown nav-item");
$el
.find(".dropdown-toggle")
.removeClass("dropdown-toggle nav-link")
.addClass(message.select ? "" : "collapsed")
.attr("data-toggle", "collapse")
.attr("data-target", "#" + collapseId);
let collapse = $("<div>")
.addClass("collapse" + (message.select ? " show" : ""))
.attr("id", collapseId);
let menu = $el
.find(".dropdown-menu")
.removeClass("dropdown-menu")
.addClass("nav nav-navtree")
.wrap(collapse);
let depth = $el.parents(".nav-navtree").length - 1;
if (depth > 0) {
$el.find("a").css("padding-left", depth + "rem");
}
if (menu.find("li").length === 0) {
menu
.find("a")
.removeClass("dropdown-item")
.addClass("nav-link")
.wrap("<li class='nav-item'></li>");
}
});
}
if (message.select) {
$liTag.find("a").tab("show");
}
@@ -1411,8 +1470,8 @@ function main(): void {
// loop through all existing tabs, find the one with highest id
// (since this is based on a numeric counter), and increment
$tabset.find("> li").each(function () {
const $tab = $(this).find("> a[data-toggle='tab']");
$tabset.find("a[data-toggle='tab']").each(function () {
const $tab = $(this);
if ($tab.length > 0) {
// remove leading url if it exists. (copy of bootstrap url stripper)
@@ -1465,13 +1524,16 @@ function main(): void {
// If the given tabset has no active tabs, select the first one
function ensureTabsetHasVisibleTab($tabset) {
if ($tabset.find("li.active").not(".dropdown").length === 0) {
const inputBinding = $tabset.data("shiny-input-binding");
// Use the getValue() method to avoid duplicating the CSS selector
// for querying the DOM for the currently active tab
if (!inputBinding.getValue($tabset)) {
// Note: destTabValue may be null. We still want to proceed
// through the below logic and setValue so that the input
// value for the tabset gets updated (i.e. input$tabsetId
// should be null if there are no tabs).
const destTabValue = getFirstTab($tabset);
const inputBinding = $tabset.data("shiny-input-binding");
const evt = jQuery.Event("shiny:updateinput");
evt.binding = inputBinding;
@@ -5881,7 +5943,12 @@ function main(): void {
return $(scope).find("ul.nav.shiny-tab-input");
},
getValue: function (el) {
const anchor = $(el).find("li:not(.dropdown).active").children("a");
// prettier-ignore
let anchor = isBS3()
? $(el).find("li:not(.dropdown).active > a")
: $(el).find(
".nav-link:not(.dropdown-toggle).active, .dropdown-menu > .dropdown-item.active"
);
if (anchor.length === 1) return this._getTabName(anchor);
@@ -5892,11 +5959,18 @@ function main(): void {
let success = false;
if (value) {
const anchors = $(el).find("li:not(.dropdown)").children("a");
// prettier-ignore
let anchors = isBS3()
? $(el).find("li:not(.dropdown) > a")
: $(el).find(
".nav-link:not(.dropdown-toggle), .dropdown-menu > .dropdown-item"
);
anchors.each(function () {
if (self._getTabName($(this)) === value) {
$(this).tab("show");
// navtreePanel()'s navbarMenu() uses collapsing -- expand the menu when activated!
$(this).parents(".collapse").collapse("show");
success = true;
return false; // Break out of each()
}
@@ -5917,9 +5991,12 @@ function main(): void {
$(el).trigger("change");
},
subscribe: function (el, callback) {
let deactivateOtherTabs = this._deactivateOtherTabs;
$(el).on(
"change shown.bootstrapTabInputBinding shown.bs.tab.bootstrapTabInputBinding",
function (event) {
deactivateOtherTabs(event);
callback();
}
);
@@ -5930,6 +6007,17 @@ function main(): void {
_getTabName: function (anchor) {
return anchor.attr("data-value") || anchor.text();
},
// nav-navtree is built on a combination of Bootstrap's tab &
// collapse components, but the tab component isn't smart enough to
// know about the deactive when are activated. Note that this logic is
// very similar to shinydashboard's deactivateOtherTabs() (in tab.js)
_deactivateOtherTabs: function (event) {
let tgt = $(event.target);
let nav = tgt.parents(".nav-navtree");
nav.find("li").not(tgt).removeClass("active"); // BS3
nav.find("li > a").not(tgt).removeClass("active"); // BS4
},
});
inputBindings.register(bootstrapTabInputBinding, "shiny.bootstrapTabInput");

View File

@@ -361,6 +361,17 @@ function getComputedLinkColor(el: HTMLElement): string {
return linkColor;
}
function isBS3(): boolean {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!$.fn.tooltip) {
return false;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return $.fn.tooltip.Constructor.VERSION.match(/^3\./);
}
export {
escapeHTML,
randomId,
@@ -384,4 +395,5 @@ export {
updateLabel,
getComputedLinkColor,
makeBlob,
isBS3,
};

View File

@@ -0,0 +1,339 @@
# tabsetPanel() markup is correct
Code
default
Output
<div class="tabbable">
<ul class="nav nav-tabs" data-tabsetid="4785">
<li class="active">
<a href="#tab-4785-1" data-toggle="tab" data-value="A">A</a>
</li>
<li>
<a href="#tab-4785-2" data-toggle="tab" data-value="B">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</li>
</ul>
</li>
</ul>
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
---
Code
pills
Output
<div class="tabbable">
<ul class="nav nav-pills" data-tabsetid="4785">
<li>
<a href="#tab-4785-1" data-toggle="tab" data-value="A">A</a>
</li>
<li class="active">
<a href="#tab-4785-2" data-toggle="tab" data-value="B">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</li>
</ul>
</li>
</ul>
<div class="content-header"></div>
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane active" data-icon-class="fab fa-github" data-value="B" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
<div class="content-footer"></div>
</div>
---
Code
bslib_tags(x)
Output
<div class="tabbable">
<ul class="nav nav-tabs" data-tabsetid="4785">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" data-value="A" href="#tab-4785-1">A</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#tab-4785-2" data-toggle="tab" data-value="B">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown nav-item">
<a class="dropdown-toggle nav-link" data-toggle="dropdown" data-value="Menu" href="#">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<a class="dropdown-item" href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</ul>
</li>
</ul>
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
---
Code
bslib_tags(x)
Output
<div class="tabbable">
<ul class="nav nav-pills" data-tabsetid="4785">
<li class="nav-item">
<a class="nav-link" href="#tab-4785-1" data-toggle="tab" data-value="A">A</a>
</li>
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" data-value="B" href="#tab-4785-2">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown nav-item">
<a class="dropdown-toggle nav-link" data-toggle="dropdown" data-value="Menu" href="#">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<a class="dropdown-item" href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</ul>
</li>
</ul>
<div class="content-header"></div>
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane active" data-icon-class="fab fa-github" data-value="B" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
<div class="content-footer"></div>
</div>
# navbarPage() markup is correct
Code
nav_page
Output
<nav class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand">Title</span>
</div>
<ul class="nav navbar-nav" data-tabsetid="4785">
<li class="active">
<a href="#tab-4785-1" data-toggle="tab" data-value="A">A</a>
</li>
<li>
<a href="#tab-4785-2" data-toggle="tab" data-value="B">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
---
Code
bslib_tags(x)
Output
<nav class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand">Title</span>
</div>
<ul class="nav navbar-nav" data-tabsetid="4785">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" data-value="A" href="#tab-4785-1">A</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#tab-4785-2" data-toggle="tab" data-value="B">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown nav-item">
<a class="dropdown-toggle nav-link" data-toggle="dropdown" data-value="Menu" href="#">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<a class="dropdown-item" href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</ul>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
# String input is handled properly
Code
nav_list
Output
<div class="row">
<div class="col-sm-4 well">
<ul class="nav nav-pills nav-stacked" data-tabsetid="4785">
<li class="navbar-brand">A header</li>
<li class="active">
<a href="#tab-4785-2" data-toggle="tab" data-value="A">A</a>
</li>
<li>
<a href="#tab-4785-3" data-toggle="tab" data-value="B">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</li>
</ul>
</li>
</ul>
</div>
<div class="col-sm-8">
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-2">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github" id="tab-4785-3">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
</div>
---
Code
bslib_tags(x)
Output
<div class="row">
<div class="col-sm-4 well">
<ul class="nav nav-pills nav-stacked" data-tabsetid="4785">
<li class="navbar-brand">A header</li>
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" data-value="A" href="#tab-4785-2">A</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#tab-4785-3" data-toggle="tab" data-value="B">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown nav-item">
<a class="dropdown-toggle nav-link" data-toggle="dropdown" data-value="Menu" href="#">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<a class="dropdown-item" href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</ul>
</li>
</ul>
</div>
<div class="col-sm-8">
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-2">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github" id="tab-4785-3">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
</div>
# Shiny.tag input produces a warning
Code
tab_tags
Output
<div class="tabbable">
<ul class="nav nav-tabs" data-tabsetid="4785">
<li class="active">
<a href="#tab-4785-1" data-toggle="tab"></a>
</li>
<li>
<a href="#tab-4785-2" data-toggle="tab" data-value="A">A</a>
</li>
<li>
<a href="#tab-4785-3" data-toggle="tab" data-value="B">
<i class=" fab fa-github fa-fw" role="presentation" aria-label=" icon"></i>
B
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-value="C">C</a>
</li>
</ul>
</li>
</ul>
<div class="tab-content" data-tabsetid="4785">
<div class="active" id="tab-4785-1">A div</div>
<div class="tab-pane" data-value="A" id="tab-4785-2">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github" id="tab-4785-3">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>

View File

@@ -1,4 +1,87 @@
# tabsetPanel() et al. use p_randomInt() to generate ids (which uses withPrivateSeed()),
# so we need to fix Shiny's private seed in order to make their HTML output deterministic
navlist_panel <- function(...) {
withPrivateSeed(set.seed(100))
navlistPanel(...)
}
navbar_page <- function(...) {
withPrivateSeed(set.seed(100))
navbarPage(...)
}
tabset_panel <- function(...) {
withPrivateSeed(set.seed(100))
tabsetPanel(...)
}
expect_snapshot2 <- function(...) {
if (getRversion() < "3.6.0") {
skip("Skipping snapshots on R < 3.6 because of different RNG method")
}
expect_snapshot(...)
}
expect_snapshot_bslib <- function(x, ...) {
expect_snapshot2(bslib_tags(x), ...)
}
# Simulates the UI tags that would be produced by
# shinyApp(bootstrapPage(theme), function(...) {})
bslib_tags <- function(ui, theme = bslib::bs_theme()) {
old_theme <- getCurrentTheme()
on.exit(setCurrentTheme(old_theme), add = TRUE)
setCurrentTheme(theme)
htmltools::renderTags(ui)$html
}
panels <- list(
tabPanel("A", "a"),
tabPanel("B", "b", icon = icon("github")),
navbarMenu("Menu", tabPanel("C", "c"))
)
test_that("tabsetPanel() markup is correct", {
default <- tabset_panel(!!!panels)
pills <- tabset_panel(
!!!panels, type = "pills", selected = "B",
header = div(class = "content-header"),
footer = div(class = "content-footer")
)
# BS3
expect_snapshot2(default)
expect_snapshot2(pills)
# BS4
expect_snapshot_bslib(default)
expect_snapshot_bslib(pills)
})
test_that("navbarPage() markup is correct", {
nav_page <- navbar_page("Title", !!!panels)
expect_snapshot2(nav_page)
expect_snapshot_bslib(nav_page)
})
# navlistPanel() can handle strings, but the others can't
test_that("String input is handled properly", {
nav_list <- navlist_panel(!!!c(list("A header"), panels))
expect_snapshot2(nav_list)
expect_snapshot_bslib(nav_list)
expect_error(
tabsetPanel(!!!c(list("A header"), panels)),
"tabPanel"
)
})
test_that("Shiny.tag input produces a warning", {
panels3 <- c(list(div("A div")), panels)
tab_tags <- expect_warning(tabset_panel(!!!panels3))
# Carson March 12th, 2021: Yes, he 'empty nav' output here isn't
# sensible (which is why we now throw a warning), but it's probably
# too late to change the behavior (it could break user code to do
# anything different)
expect_snapshot2(tab_tags)
})
test_that("tabPanelBody validates it's input", {
expect_silent(tabPanelBody("a", "content1", "content2", icon = icon("table")))

15
tools/updateNavTreeList.R Normal file
View File

@@ -0,0 +1,15 @@
library(sass)
home <- rprojroot::find_package_root_file("inst/www/shared/navtree")
# TODO: write a unit test
withr::with_dir(
home, {
sass_partial(
sass_file("navtree.scss"),
bslib::bs_theme(),
output = "navtree.css",
cache = FALSE,
write_attachments = FALSE
)
}
)