mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 07:58:11 -05:00
Compare commits
30 Commits
slider-che
...
navtreePan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
006cd8ed5e | ||
|
|
88cdd607b0 | ||
|
|
f9403c5e66 | ||
|
|
268c9afec3 | ||
|
|
5c919ae565 | ||
|
|
e29d92c5ff | ||
|
|
d04c7ae6ed | ||
|
|
7811f06e70 | ||
|
|
0a331e3366 | ||
|
|
32d0e146ad | ||
|
|
94a9b8a5f1 | ||
|
|
8ce0068f1d | ||
|
|
c8d76cf48f | ||
|
|
6159259699 | ||
|
|
9790b8f716 | ||
|
|
f9d82f17af | ||
|
|
3950f0e7fe | ||
|
|
886d369e7c | ||
|
|
ec97fe731d | ||
|
|
f8476973e5 | ||
|
|
2bde956fc9 | ||
|
|
2e33634726 | ||
|
|
f9ee5fcca2 | ||
|
|
124a89d7b4 | ||
|
|
1d608d2e25 | ||
|
|
f63d50d1b5 | ||
|
|
f8e09c15d1 | ||
|
|
959e4e41da | ||
|
|
36e679a9af | ||
|
|
c6693ead30 |
@@ -170,6 +170,7 @@ Collate:
|
||||
'mock-session.R'
|
||||
'modal.R'
|
||||
'modules.R'
|
||||
'navtreePanel.R'
|
||||
'notifications.R'
|
||||
'priorityqueue.R'
|
||||
'progress.R'
|
||||
|
||||
@@ -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
13
NEWS.md
@@ -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)
|
||||
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
351
R/bootstrap.R
351
R/bootstrap.R
@@ -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
|
||||
@@ -436,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")
|
||||
|
||||
@@ -447,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),
|
||||
@@ -475,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)
|
||||
),
|
||||
@@ -484,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)
|
||||
@@ -511,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
|
||||
@@ -656,6 +668,13 @@ 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
|
||||
#' @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.
|
||||
@@ -693,6 +712,7 @@ tabPanelBody <- function(value, ..., icon = NULL) {
|
||||
#' }
|
||||
#' @param position This argument is deprecated; it has been discontinued in
|
||||
#' Bootstrap 3.
|
||||
#' @inheritParams navbarPage
|
||||
#' @return A tabset that can be passed to [mainPanel()]
|
||||
#'
|
||||
#' @seealso [tabPanel()], [updateTabsetPanel()],
|
||||
@@ -742,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(
|
||||
@@ -753,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
|
||||
@@ -784,8 +806,10 @@ tabsetPanel <- function(...,
|
||||
#' navigation list.
|
||||
#' @param fluid `TRUE` to use fluid layout; `FALSE` to use fixed
|
||||
#' layout.
|
||||
#' @param widths Column withs of the navigation list and tabset content areas
|
||||
#' @param widths Column widths of the navigation list and tabset content areas
|
||||
#' respectively.
|
||||
#' @inheritParams tabsetPanel
|
||||
#' @inheritParams navbarPage
|
||||
#'
|
||||
#' @details You can include headers within the `navlistPanel` by including
|
||||
#' plain text elements in the list. Versions of Shiny before 0.11 supported
|
||||
@@ -812,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
|
||||
@@ -860,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 {
|
||||
@@ -876,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))
|
||||
}
|
||||
@@ -911,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
|
||||
@@ -934,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)
|
||||
}
|
||||
@@ -949,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
|
||||
#'
|
||||
|
||||
@@ -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
159
R/navtreePanel.R
Normal 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"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_)
|
||||
|
||||
50
inst/www/shared/navtree/navtree.css
Normal file
50
inst/www/shared/navtree/navtree.css
Normal 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);
|
||||
}
|
||||
11
inst/www/shared/navtree/navtree.js
Normal file
11
inst/www/shared/navtree/navtree.js
Normal 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
|
||||
});
|
||||
66
inst/www/shared/navtree/navtree.scss
Normal file
66
inst/www/shared/navtree/navtree.scss
Normal 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
|
||||
@@ -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
2
inst/www/shared/shiny.min.js
vendored
2
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,6 +8,8 @@ navlistPanel(
|
||||
...,
|
||||
id = NULL,
|
||||
selected = NULL,
|
||||
header = NULL,
|
||||
footer = NULL,
|
||||
well = TRUE,
|
||||
fluid = TRUE,
|
||||
widths = c(4, 8)
|
||||
@@ -25,13 +27,19 @@ value will correspond to the \code{value} argument that is passed to
|
||||
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.}
|
||||
|
||||
\item{fluid}{\code{TRUE} to use fluid layout; \code{FALSE} to use fixed
|
||||
layout.}
|
||||
|
||||
\item{widths}{Column withs of the navigation list and tabset content areas
|
||||
\item{widths}{Column widths of the navigation list and tabset content areas
|
||||
respectively.}
|
||||
}
|
||||
\description{
|
||||
|
||||
@@ -9,6 +9,8 @@ tabsetPanel(
|
||||
id = NULL,
|
||||
selected = NULL,
|
||||
type = c("tabs", "pills", "hidden"),
|
||||
header = NULL,
|
||||
footer = NULL,
|
||||
position = deprecated()
|
||||
)
|
||||
}
|
||||
@@ -32,6 +34,12 @@ 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.}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
339
tests/testthat/_snaps/tabPanel.md
Normal file
339
tests/testthat/_snaps/tabPanel.md
Normal 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>
|
||||
|
||||
@@ -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
15
tools/updateNavTreeList.R
Normal 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
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user