Compare commits

..

25 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
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
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 458 additions and 219 deletions

View File

@@ -3,11 +3,11 @@ Type: Package
Title: Web Application Framework for R
Version: 1.6.0.9000
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com", comment = c(ORCID = "0000-0002-1576-2126")),
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
person("JJ", "Allaire", role = "aut", email = "jj@rstudio.com"),
person("Carson", "Sievert", role = "aut", email = "carson@rstudio.com", comment = c(ORCID = "0000-0002-4958-2844")),
person("Barret", "Schloerke", role = "aut", email = "barret@rstudio.com", comment = c(ORCID = "0000-0001-9986-114X")),
person("Carson", "Sievert", role = "aut", email = "carson@rstudio.com"),
person("Barret", "Schloerke", role = "aut", email = "barret@rstudio.com"),
person("Yihui", "Xie", role = "aut", email = "yihui@rstudio.com"),
person("Jeff", "Allen", role = "aut", email = "jeff@rstudio.com"),
person("Jonathan", "McPherson", role = "aut", email = "jonathan@rstudio.com"),
@@ -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)
@@ -222,7 +223,6 @@ export(reactlogReset)
export(reactlogShow)
export(registerInputHandler)
export(registerThemeDependency)
export(register_devmode_option)
export(removeInputHandler)
export(removeModal)
export(removeNotification)

View File

@@ -17,7 +17,7 @@ shiny 1.6.0.9000
* 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)
@@ -28,10 +28,6 @@ shiny 1.6.0.9000
### Bug fixes
* Closed #3374: `quoToFunction()` now works correctly with nested quosures; and as a result, quasi-quotation with rendering function (e.g., `renderPrint()`, `renderPlot()`, etc) now works as expected with nested quosures. (#3373)
* Exported `register_devmode_option()`. This method was described in the documentation for `devmode()` but was never exported. See `?devmode()` for more details on how to register Shiny Developer options using `register_devmode_option()`. (#3364)
### Library updates
* Closed #3286: Updated to Font-Awesome 5.15.2. (#3288)

View File

@@ -43,7 +43,6 @@ bootstrapPage <- function(..., title = NULL, responsive = deprecated(), theme =
}
args <- list(
jqueryDependency(),
if (!is.null(title)) tags$head(tags$title(title)),
if (is.character(theme)) {
if (length(theme) > 1) stop("`theme` must point to a single CSS file, not multiple files.")
@@ -1014,12 +1013,9 @@ buildTabItem <- function(index, tabsetId, foundSelected, tabs = NULL,
buildNavItem <- function(divTag, tabsetId, index) {
id <- paste("tab", tabsetId, index, sep = "-")
# Get title attribute directory (not via tagGetAttribute()) so that contents
# don't get passed to as.character().
# https://github.com/rstudio/shiny/issues/3352
title <- divTag$attribs[["title"]]
value <- divTag$attribs[["data-value"]]
icon <- getIcon(iconClass = divTag$attribs[["data-icon-class"]])
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

View File

@@ -233,7 +233,6 @@ registered_devmode_options <- Map$new()
#' devmode_default = FALSE
#' )
#' ```
#'
#' @param name Name of option to look for in `options()`
#' @param default Default value to return if `in_devmode()` returns
#' `TRUE` and the specified option is not set in [`options()`].
@@ -244,7 +243,6 @@ registered_devmode_options <- Map$new()
#' `TRUE` and the specified option is not set in [`options()`]. For
#' `get_devmode_option()`, if `devmode_default` is missing, the
#' registered `devmode_default` value will be used.
#' @export
#' @examples
#' # Ex: Within shiny, we register the option "shiny.minified"
#' # to default to `FALSE` when in Dev Mode

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

@@ -2175,7 +2175,7 @@ ShinySession <- R6Class(
if (getOption("shiny.allowoutputreads", FALSE)) {
.subset2(x, 'impl')$getOutput(name)
} else {
rlang::abort(paste0("Can't read output '", name, "'"))
rlang::abort(paste0("Can't read output '", output, "'"))
}
}

View File

@@ -47,8 +47,27 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
)
}
jquery <- function() {
version <- getOption("shiny.jquery.version", 3)
if (version == 3) {
return(htmlDependency(
"jquery", version_jquery,
c(href = "shared"),
script = "jquery.min.js"
))
}
if (version == 1) {
return(htmlDependency(
"jquery", "1.12.4",
c(href = "shared/legacy"),
script = "jquery.min.js"
))
}
stop("Unsupported version of jQuery: ", version)
}
shiny_deps <- c(
list(jqueryDependency()),
list(jquery()),
shinyDependencies()
)
@@ -63,33 +82,6 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
enc2utf8(paste(collapse = "\n", html))
}
jqueryDependency <- function() {
version <- getOption("shiny.jquery.version", 3)
if (version == 3) {
return(htmlDependency(
"jquery", version_jquery,
src = c(
href = "shared",
file = "www/shared"
),
package = "shiny",
script = "jquery.min.js"
))
}
if (version == 1) {
return(htmlDependency(
"jquery", "1.12.4",
src = c(
href = "shared/legacy",
file = "www/shared/legacy"
),
package = "shiny",
script = "jquery.min.js"
))
}
stop("Unsupported version of jQuery: ", version)
}
shinyDependencies <- function() {
list(
bslib::bs_dependency_defer(shinyDependencyCSS),

View File

@@ -538,10 +538,9 @@ installExprFunction <- function(expr, name, eval.env = parent.frame(2),
#' @export
quoToFunction <- function(q, label, ..stacktraceon = FALSE) {
q <- as_quosure(q)
func <- as_function(q)
# as_function returns a function that takes `...`. We want one that takes no
# args.
formals(func) <- list()
# Use new_function() instead of as_function(), because as_function() adds an
# extra parent environment. (This may not actually be a problem, though.)
func <- new_function(NULL, get_expr(q), get_env(q))
wrapFunctionLabel(func, label, ..stacktraceon = ..stacktraceon)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@@ -1 +1 @@
Selectize.define("selectize-plugin-a11y",function(c){var t=this,l=13;typeof t.accessibility=="undefined"&&(t.accessibility={}),t.accessibility.helpers={randomId:function(e){for(var a="",s=e||10,r="abcdefghijklmnopqrstuvwxyz0123456789",n=r.length,o=0;o<s;o++)a+=r[Math.floor(n*Math.random())];return a}},t.accessibility.liveRegion={$region:"",speak:function(e){var a=$("<div></div>");a.text(e),this.$region.html(a)},domListener:function(){var e=new MutationObserver(function(a){a.forEach(function(s){var r=$(s.target);if(r.hasClass("items"))if(r.hasClass("dropdown-active")){t.$control_input.attr("aria-expanded","true");var n=t.$dropdown_content[0].children;for(i=0;i<n.length;i++){var o=n[i].attributes;o.role||n[i].setAttribute("role","option"),o.id||n[i].setAttribute("id",t.accessibility.helpers.randomId())}}else t.$control_input.attr("aria-expanded","false"),t.$control_input.removeAttr("aria-activedescendant");else r.hasClass("active")&&r.attr("data-value")&&(t.$control_input.attr("aria-activedescendant",r.attr("id")),t.accessibility.liveRegion.speak(r.text(),500))})});e.observe(t.$dropdown[0],{attributes:!0,attributeFilter:["class"],subtree:!0,attributeOldValue:!0}),e.observe(t.$control[0],{attributes:!0,attributeFilter:["class"]}),e.observe(t.$control_input[0],{attributes:!0,attributeFilter:["value"]})},setAttributes:function(){this.$region.attr({"aria-live":"assertive",role:"log","aria-relevant":"additions","aria-atomic":"true"})},setStyles:function(){this.$region.css({position:"absolute",width:"1px",height:"1px","margin-top":"-1px",clip:"rect(1px, 1px, 1px, 1px)",overflow:"hidden"})},init:function(){this.$region=$("<div>"),this.setAttributes(),this.setStyles(),$("body").append(this.$region),this.domListener()}},this.setup=function(){var e=t.setup;return function(){e.apply(this,arguments);var a=t.accessibility.helpers.randomId(),s=t.accessibility.helpers.randomId();t.$control.on("keydown",function(r){r.keyCode===l&&(t.settings.openOnFocus?(t.settings.openOnFocus=!1,t.focus(),setTimeout(function(){t.settings.openOnFocus=!0},0)):t.focus())}),t.$control_input.attr({role:"combobox","aria-expanded":"false",haspopup:"listbox","aria-owns":s,"aria-label":t.$wrapper.closest("[data-accessibility-selectize-label]").attr("data-accessibility-selectize-label")}),t.$dropdown_content.attr({role:"listbox",id:s}),t.accessibility.liveRegion.init()}}(),this.destroy=function(){var e=t.destroy;return function(){return t.accessibility.liveRegion.$region.remove(),e.apply(this,arguments)}}()});
Selectize.define("selectize-plugin-a11y",function(t){var e,s,a=this;void 0===a.accessibility&&(a.accessibility={}),a.accessibility.helpers={randomId:function(t){for(var e="",i=t||10,s="abcdefghijklmnopqrstuvwxyz0123456789",a=s.length,o=0;o<i;o++)e+=s[Math.floor(a*Math.random())];return e}},a.accessibility.liveRegion={$region:"",speak:function(t){var e=$("<div></div>");e.text(t),this.$region.html(e)},domListener:function(){var t=new MutationObserver(function(t){t.forEach(function(t){t=$(t.target);if(t.hasClass("items"))if(t.hasClass("dropdown-active")){a.$control_input.attr("aria-expanded","true");var e=a.$dropdown_content[0].children;for(i=0;i<e.length;i++){var s=e[i].attributes;s.role||e[i].setAttribute("role","option"),s.id||e[i].setAttribute("id",a.accessibility.helpers.randomId())}}else a.$control_input.attr("aria-expanded","false"),a.$control_input.removeAttr("aria-activedescendant");else t.hasClass("active")&&t.attr("data-value")&&(a.$control_input.attr("aria-activedescendant",t.attr("id")),a.accessibility.liveRegion.speak(t.text(),500))})});t.observe(a.$dropdown[0],{attributes:!0,attributeFilter:["class"],subtree:!0,attributeOldValue:!0}),t.observe(a.$control[0],{attributes:!0,attributeFilter:["class"]}),t.observe(a.$control_input[0],{attributes:!0,attributeFilter:["value"]})},setAttributes:function(){this.$region.attr({"aria-live":"assertive",role:"log","aria-relevant":"additions","aria-atomic":"true"})},setStyles:function(){this.$region.css({position:"absolute",width:"1px",height:"1px","margin-top":"-1px",clip:"rect(1px, 1px, 1px, 1px)",overflow:"hidden"})},init:function(){this.$region=$("<div>"),this.setAttributes(),this.setStyles(),$("body").append(this.$region),this.domListener()}},this.setup=(e=a.setup,function(){e.apply(this,arguments);a.accessibility.helpers.randomId();var t=a.accessibility.helpers.randomId();a.$control.on("keydown",function(t){13===t.keyCode&&(a.settings.openOnFocus?(a.settings.openOnFocus=!1,a.focus(),setTimeout(function(){a.settings.openOnFocus=!0},0)):a.focus())}),a.$control_input.attr({role:"combobox","aria-expanded":"false",haspopup:"listbox","aria-owns":t,"aria-label":a.$wrapper.closest("[data-accessibility-selectize-label]").attr("data-accessibility-selectize-label")}),a.$dropdown_content.attr({role:"listbox",id:t}),a.accessibility.liveRegion.init()}),this.destroy=(s=a.destroy,function(){return a.accessibility.liveRegion.$region.remove(),s.apply(this,arguments)})});

View File

@@ -3086,6 +3086,22 @@
$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");
}
@@ -4730,7 +4746,7 @@
if (!restyle) {
$head.append(links);
} else {
var refreshStyle = function refreshStyle2(href2, oldSheet, onload) {
var refreshStyle = function refreshStyle2(href2, oldSheet) {
var xhr = new XMLHttpRequest();
xhr.open("GET", href2);
xhr.onload = function() {
@@ -4744,7 +4760,6 @@
setTimeout(function() {
return removeSheet(oldSheet);
}, 500);
onload();
};
xhr.send();
};
@@ -4769,29 +4784,21 @@
var oldSheet = findSheet(link.attr("href"));
var href2 = link.attr("href") + "?restyle=" + new Date().getTime();
if (isIE()) {
refreshStyle(href2, oldSheet, scheduleCssReporter);
refreshStyle(href2, oldSheet);
} else {
link.attr("href", href2);
link.attr("onload", function() {
var dummy_id = "dummy-" + Math.floor(Math.random() * 99999999);
var css_string = "#" + dummy_id + " { color: #" + Math.floor(Math.random() * 16777215).toString(16) + "; transition: 0.2s all; visibility: hidden; position: fixed; top: 0; left: 0; }";
var base64_css_string = "data:text/css;base64," + btoa(css_string);
var dummy_link = document.createElement("link");
dummy_link.rel = "stylesheet";
dummy_link.type = "text/css";
dummy_link.href = base64_css_string;
var $dummy_el = import_jquery6.default("<div id=" + dummy_id + "></div>");
import_jquery6.default(document.body).append($dummy_el);
$dummy_el.one("transitionend", function() {
sendImageSize();
removeSheet(oldSheet);
$dummy_el.remove();
});
$head.append(dummy_link);
setTimeout(function() {
return removeSheet(oldSheet);
}, 500);
});
$head.append(link);
}
});
var bindDebouncer = new Debouncer(null, Shiny.bindAll, 100);
setTimeout(function() {
return bindDebouncer.normalCall();
}, 100);
}
}
if (dep.script && !restyle) {
@@ -6117,6 +6124,7 @@
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;
}
@@ -6138,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();
});
},
@@ -6147,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");
@@ -6449,7 +6465,6 @@
}
});
inputBindings.register(fileInputBinding, "shiny.fileInputBinding");
var sendImageSize;
function initShiny() {
var shinyapp = Shiny.shinyapp = new ShinyApp();
function bindOutputs() {
@@ -6774,9 +6789,9 @@
});
}
var sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0);
sendImageSize = function sendImageSize() {
function sendImageSize() {
sendImageSizeDebouncer.normalCall();
};
}
inputBatchSender.lastChanceCallback.push(function() {
if (sendImageSizeDebouncer.isPending())
sendImageSizeDebouncer.immediateCall();
@@ -7787,10 +7802,10 @@
return linkColor;
}
function isBS3() {
if (!import_jquery5.default.fn.tab) {
if (!import_jquery5.default.fn.tooltip) {
return false;
}
return import_jquery5.default.fn.tab.Constructor.VERSION.match(/^3\./);
return import_jquery5.default.fn.tooltip.Constructor.VERSION.match(/^3\./);
}
// src/shiny.ts

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

@@ -1,63 +0,0 @@
import {readdirSync, unlinkSync, writeFileSync} from "fs";
import esbuild from "esbuild";
import globalsPlugin from "esbuild-plugin-globals";
// import process from "process";
// let watch = process.argv.length >= 3 && process.argv[2] == "--watch";
let instdir = "../inst/";
let outdir = instdir + "/www/shared/";
let opts = {
bundle: false,
watch: false,
target: "es5",
sourcemap: false,
};
console.log("Building datepicker");
const locale_files = readdirSync(instdir + "www/shared/datepicker/js/locales/")
let require_files = locale_files.map(function(filename) {
return `require("./locales/${ filename }");`;
}).join("\n");
let tmpfile = instdir + "www/shared/datepicker/js/temp.js";
writeFileSync(tmpfile,
`require("./bootstrap-datepicker.js");
${require_files}`)
await esbuild.build({
...opts,
plugins:[
globalsPlugin({
jquery: "window.jQuery",
})
],
bundle: true,
entryPoints: [tmpfile],
outfile: instdir + "www/shared/datepicker/js/bootstrap-datepicker.min.js",
external: ['jquery'],
minify: true,
});
// Clean up
unlinkSync(tmpfile);
console.log("Building ionrangeslider");
await esbuild.build({
...opts,
entryPoints: [
instdir + "www/shared/ionrangeslider/js/ion.rangeSlider.js"
],
outfile: instdir + "www/shared/ionrangeslider/js/ion.rangeSlider.min.js",
minify: true,
});
console.log("Building selectize");
await esbuild.build({
...opts,
entryPoints: [
instdir + "www/shared/selectize/accessibility/js/selectize-plugin-a11y.js"
],
outfile: instdir + "www/shared/selectize/accessibility/js/selectize-plugin-a11y.min.js",
minify: true,
});

View File

@@ -42,11 +42,10 @@
},
"scripts": {
"watch": "yarn run build_shiny --watch",
"build": "yarn run build_shiny && yarn run bundle_external_libs",
"build": "yarn run build_shiny",
"setup_build_shiny": "yarn run lint && yarn run typescript-check",
"build_shiny": "yarn run setup_build_shiny && yarn run bundle_shiny",
"bundle_shiny": "node esbuild.config.mjs",
"bundle_external_libs": "node esbuild.external_libs.mjs",
"bundle_shiny_parcel2": "parcel build -d ../inst/www/shared --no-minify -o shiny.js src/index.ts",
"watch_parcel2": "yarn run setup_build_shiny && parcel run -d ../inst/www/shared -o shiny.js srcjs/index.ts",
"replace_shiny_version2": "replace --silent '\"[^\"]+\"; // @VERSION@' \"\\\"`node -e 'console.log(require(\"readcontrol\").readSync(\"../DESCRIPTION\").version)'`\\\"; // @VERSION@\" src/shiny.ts",

View File

@@ -1407,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");
}
@@ -4040,7 +4084,7 @@ function main(): void {
$head.append(links);
} else {
// This inline <style> based approach works for IE11
let refreshStyle = function (href, oldSheet, onload) {
let refreshStyle = function (href, oldSheet) {
const xhr = new XMLHttpRequest();
xhr.open("GET", href);
@@ -4053,7 +4097,6 @@ function main(): void {
$head.append(newStyle);
setTimeout(() => oldStyle.remove(), 500);
setTimeout(() => removeSheet(oldSheet), 500);
onload();
};
xhr.send();
};
@@ -4090,43 +4133,26 @@ function main(): void {
// <link> -based approach
if (isIE()) {
refreshStyle(href, oldSheet, scheduleCssReporter);
refreshStyle(href, oldSheet);
} else {
link.attr("href", href);
// Once the new <link> is loaded, schedule the old <link> to be removed
// on the next tick which is needed to avoid FOUC
link.attr("onload", () => {
const dummy_id = "dummy-" + Math.floor(Math.random() * 99999999);
const css_string =
"#" + dummy_id +
" { color: #" +
Math.floor(Math.random() * 16777215).toString(16) + "; " +
"transition: 0.2s all; " +
"visibility: hidden; " +
"position: fixed; top: 0; left: 0; }";
const base64_css_string =
"data:text/css;base64," + btoa(css_string);
let dummy_link = document.createElement("link");
dummy_link.rel = "stylesheet";
dummy_link.type = "text/css";
dummy_link.href = base64_css_string;
let $dummy_el = $("<div id=" + dummy_id + "></div>")
$(document.body).append($dummy_el);
$dummy_el
.one("transitionend", () => {
sendImageSize();
removeSheet(oldSheet);
$dummy_el.remove();
});
$head.append(dummy_link);
setTimeout(() => removeSheet(oldSheet), 500);
});
$head.append(link);
}
});
// Once the new styles are applied, CSS values that are accessible server-side
// (e.g., getCurrentOutputInfo(), output visibility, etc) may become outdated.
// At the time of writing, that means we need to do sendImageSize() &
// sendOutputHiddenState() again, which can be done by re-binding.
/* global Shiny */
const bindDebouncer = new Debouncer(null, Shiny.bindAll, 100);
setTimeout(() => bindDebouncer.normalCall(), 100);
}
}
@@ -5943,6 +5969,8 @@ function main(): void {
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()
}
@@ -5963,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();
}
);
@@ -5976,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");
@@ -6407,10 +6449,6 @@ function main(): void {
});
inputBindings.register(fileInputBinding, "shiny.fileInputBinding");
// This function gets defined in initShiny() and 'hoisted' so it can be reused
// (to send CSS info) inside of Shiny.renderDependencies()
let sendImageSize;
// "init_shiny.js"
function initShiny() {
const shinyapp = (Shiny.shinyapp = new ShinyApp());
@@ -6893,9 +6931,9 @@ function main(): void {
}
const sendImageSizeDebouncer = new Debouncer(null, doSendImageSize, 0);
sendImageSize = function () {
function sendImageSize() {
sendImageSizeDebouncer.normalCall();
};
}
// Make sure sendImageSize actually gets called before the inputBatchSender
// sends data to the server.
inputBatchSender.lastChanceCallback.push(function () {

View File

@@ -364,12 +364,12 @@ function getComputedLinkColor(el: HTMLElement): string {
function isBS3(): boolean {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!$.fn.tab) {
if (!$.fn.tooltip) {
return false;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return $.fn.tab.Constructor.VERSION.match(/^3\./);
return $.fn.tooltip.Constructor.VERSION.match(/^3\./);
}
export {

View File

@@ -96,19 +96,3 @@ test_that("tabPanelBody validates it's input", {
expect_error(tabPanelBody(""), "single, non-empty string")
expect_error(tabPanelBody(letters[1:2]), "single, non-empty string")
})
# https://github.com/rstudio/shiny/issues/3352
test_that("tabItem titles can contain tag objects", {
title <- tagList(tags$i("Hello"), "world")
x <- tabsetPanel(tabPanel(title, "tab content"))
x <- renderTags(x)
# Result should contain (with different whitespace):
# "<a ....> <i>Hello</i> world"
# As opposed to:
# "<a ....>&lt;i&gt;Hello&lt;/i&gt; world
expect_true(
grepl("<a [^>]+>\\s*<i>Hello</i>\\s+world", x$html)
)
})

View File

@@ -239,15 +239,3 @@ test_that("dateYMD works", {
c("2019/11/05", "")
)
})
test_that("quoToFunction handles nested quosures", {
quo_inner <- local({
x <- 1
rlang::quo(x)
})
quo_outer <- rlang::quo(!!quo_inner + 1)
func <- quoToFunction(quo_outer, "foo")
expect_identical(func(), 2)
})

View File

@@ -66,4 +66,4 @@ sass(
# Finally, run yarn build so the JS patches propogate to the minified files
withr::with_dir(rprojroot::find_package_root_file("tools"), system("yarn bundle_external_libs"))
withr::with_dir(rprojroot::find_package_root_file("tools"), system("yarn build"))

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
)
}
)