Compare commits

..

63 Commits

Author SHA1 Message Date
Carson
f694e708c1 Add comment; removing logging 2023-03-31 10:28:39 -05:00
Carson
66a972604e Use actionQueue to guarantee bind happens after async rendering has completed 2023-03-31 10:23:18 -05:00
Carson
91df0b15b7 Update news 2023-03-29 14:57:23 -05:00
Carson
a1e5514f81 Merge branch 'main' into bindAfterRegister 2023-03-29 14:49:40 -05:00
Carson
d93136ca1b lint; yarn build 2023-03-29 14:47:20 -05:00
Carson Sievert
4702ff480c Update srcts/src/shiny/init.ts 2023-03-29 14:41:59 -05:00
Carson Sievert
490803fc10 Update srcts/src/shiny/init.ts 2023-03-29 14:36:30 -05:00
Winston Chang
4d05a568c1 Remove unneeded packages from package.json 2023-03-06 17:01:43 -06:00
Winston Chang
1330325519 Rebuild docs 2023-03-01 21:38:10 -06:00
Winston Chang
92d850efa6 Rebuild shiny.js 2023-03-01 21:26:59 -06:00
Winston Chang
7bf56125eb Update @types/node 2023-03-01 21:26:59 -06:00
Winston Chang
69f861cc8a Rebuild yarn.lock 2023-03-01 21:20:56 -06:00
Winston Chang
a94be7b128 Fix brush resetting behavior. Closes #3785 2023-03-01 20:58:26 -06:00
Winston Chang
703766fb2e Merge pull request #3782 from rstudio/plot-interact-init 2023-02-24 16:59:42 -06:00
Winston Chang
8e73749e21 Bump fastmap dependency to 1.1.1 2023-02-24 10:30:07 -06:00
wch
dc8ffa115b Sync package version (GitHub Actions) 2023-02-23 20:32:23 +00:00
Winston Chang
a0385da0d7 Rebuild shiny.js 2023-02-23 14:18:21 -06:00
Winston Chang
a6b7dee4cd Send initial values for plot interaction 2023-02-23 14:14:01 -06:00
Winston Chang
f9ff5c2637 Bump version to 1.7.4.9002 2023-01-25 11:19:40 -06:00
Winston Chang
6a1fbc57f4 Clarify comments 2023-01-25 11:18:20 -06:00
Winston Chang
38337a926f Ensure that reactiveValues keys and values are sorted (#3774) 2023-01-25 11:10:05 -06:00
Winston Chang
bf6b87886c Merge pull request #3775 from rstudio/map-loadtime 2023-01-24 13:55:37 -06:00
Winston Chang
33e6b0a305 Add on_load function for registering expressions to run on load 2023-01-23 17:25:26 -06:00
Winston Chang
cb5eac052f Initialize Map objects at load time instead of build time 2023-01-23 16:26:44 -06:00
Winston Chang
39fee3782f Merge pull request #3772 from rstudio/fix-slider-stoppropagation 2023-01-23 10:59:47 -06:00
Winston Chang
654f30a312 Udpate NEWS 2023-01-20 17:04:21 -06:00
Winston Chang
a763da2b94 Fix stopPropagation error in ion.rangeSlider 2023-01-20 17:00:12 -06:00
Carson Sievert
1f752b6420 Merge branch 'main' into bindAfterRegister 2023-01-18 15:15:54 -06:00
Jon Calder
0c177d30dc Fix two typos in insertUI() docs (#3712)
* Fix two typos in insertUI() docs

* document()

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2023-01-13 11:41:44 -06:00
gsmolinski
20f8a181d4 Change size 'xl' of modalDialog to 'l' if Bootstrap 3 (#3593)
* closes issue #3631 - documenting that 'xl' modal dialog will be changed to 'm' in Bootstrap 3

* Update R/modal.R

adds note about how to switch to Bootstrap 4+ with bslib

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>

* add note about how to use Bootstrap 4+ with bslib to get 'xl' modal dialog

* Update NEWS.md

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2023-01-12 11:41:49 -06:00
Carson Sievert
eebcf70bb9 Add snapshot test for #3519 (#3520)
* Add snapshot test for https://github.com/rstudio/shiny/issues/3519 which was fixed via https://github.com/rstudio/bslib/pull/372

* sync package version (GitHub Actions)

* yarn build (GitHub Actions)

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-01-12 10:38:06 -06:00
wch
0f00ecfc20 Sync package version (GitHub Actions) 2023-01-06 22:08:33 +00:00
Winston Chang
0fe804012a Merge branch 'main' into bindAfterRegister 2023-01-06 16:00:57 -06:00
Winston Chang
e7d62f55ca Merge pull request #3666 from rstudio/async-load-script-2 2023-01-06 13:47:52 -08:00
Winston Chang
3a4e5f3982 Rebuild JS and CSS 2023-01-06 15:40:58 -06:00
Winston Chang
3381c3a6b9 Bump version to 1.7.4.9001 2023-01-06 15:39:42 -06:00
Winston Chang
e42c920587 Merge branch 'main' into async-load-script-2 2023-01-06 15:39:19 -06:00
Winston Chang
4635665394 Build shiny.js 2022-12-22 11:53:29 -06:00
Winston Chang
08ff066fa3 Append script elements one at a time 2022-12-22 11:53:13 -06:00
Winston Chang
816072fc29 Use Promise.allSettled 2022-12-21 16:40:45 -06:00
Carson Sievert
5eb442aa03 Make ?shiny-package topic internal in pkgdown (#3758)
* Make ?shiny-package help page internal

Otherwise, pkgdown wants it to appear in the reference, which we probably don't want

* Revert "Make ?shiny-package help page internal"

This reverts commit 4ab4cb0e46.

* Use pkgdown's  to drop the shiny-package contents (only in the pkgdown reference)

* Avoid 'incomplete final line' warning when reading pkgdown.yml
2022-12-16 10:05:12 -06:00
Carson Sievert
c32db50585 Run yarn build (#3757)
* Run yarn build

* `devtools::document()` (GitHub Actions)

* Sync package version (GitHub Actions)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2022-12-15 11:37:11 -06:00
Carson Sievert
1d9dde52df Start new version (#3756) 2022-12-15 11:16:28 -06:00
Carson Sievert
6176f03ad0 v1.7.4 release candidate (#3749)
* Start release candidate

* Get rid of warnings about qplot() usage in tests

* Clean up news

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* Remote remotes (htmltools is now in CRAN)

* Change header syntax in NEWS.md (to match what usethis does)

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2022-12-15 11:12:19 -06:00
Winston Chang
0fc1be52eb Render deps before modal or notification element is created 2022-12-13 17:21:59 -06:00
Winston Chang
db2ad780c0 Don't ignore errors when loading or executing a script 2022-12-01 17:16:59 -06:00
Winston Chang
5cd848bd28 Await running each action in actionQueue 2022-12-01 17:01:00 -06:00
Winston Chang
a063540407 Build shiny.js 2022-11-01 21:30:29 -05:00
Winston Chang
aa932532f3 Add sync and async versions of renderContent, renderHtml, renderDependencies 2022-11-01 21:30:15 -05:00
Winston Chang
8160f8c726 Add .d.ts files 2022-10-31 16:51:13 -05:00
Winston Chang
af900d1037 Use actionQueue 2022-10-31 16:51:13 -05:00
Winston Chang
49320e6edd Make HtmlOutputBinding.renderValue an async function 2022-10-31 16:51:13 -05:00
Winston Chang
4308887296 Fix types for message.multiple 2022-10-31 16:51:13 -05:00
Winston Chang
dffd8bc7fd Commit .d.ts files 2022-10-31 16:51:13 -05:00
Winston Chang
554f835293 Make sure not to send input values during dispatchMessage 2022-10-31 16:51:13 -05:00
Winston Chang
50e7b6768d Use async queue to handle incoming messages 2022-10-31 16:51:13 -05:00
Winston Chang
db222af7e0 Make sure dynamic scripts run in order 2022-10-31 16:51:13 -05:00
Winston Chang
5b688707b7 Add await for renderContent() calls 2022-10-31 16:51:13 -05:00
Winston Chang
8dfd8f5b33 Convert renderDependency() to async 2022-10-31 16:51:13 -05:00
Winston Chang
ed12e92d60 Merge branch 'main' into bindAfterRegister 2022-10-05 12:46:13 -05:00
Carson
d0bf86e5e2 Add a clear() method to Callbacks 2022-07-07 13:37:37 -05:00
Carson
b023350b90 Introduce an onRegister() method on BindingRegistry to help solve the problem with sharing state 2022-07-07 13:36:21 -05:00
Carson
7bccfeb774 Close #3635: attempt another bind when registering a binding outside a renderHtml() context 2022-07-07 13:35:07 -05:00
91 changed files with 13311 additions and 5970 deletions

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.7.4
Version: 1.7.4.9002
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com", comment = c(ORCID = "0000-0002-1576-2126")),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
@@ -87,7 +87,7 @@ Imports:
tools,
crayon,
rlang (>= 0.4.10),
fastmap (>= 1.1.0),
fastmap (>= 1.1.1),
withr,
commonmark (>= 1.7),
glue (>= 1.3.2),
@@ -202,7 +202,7 @@ Collate:
'version_selectize.R'
'version_strftime.R'
'viewer.R'
RoxygenNote: 7.2.2
RoxygenNote: 7.2.3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RdMacros: lifecycle

22
NEWS.md
View File

@@ -1,3 +1,23 @@
# shiny 1.7.4.9002
## Full changelog
### Breaking changes
### New features and improvements
* Closed #789: `<script>` loaded from dynamic UI are no longer loaded using synchronous `XMLHttpRequest` (via jQuery). (#3666)
* For `reactiveValues()` objects, whenever the `$names()` or `$values()` methods are called, the keys are now returned in the order that they were inserted. (#3774)
* `Map` objects are now initialized at load time instead of build time. This avoids potential problems that could arise from storing `fastmap` objects into the built Shiny package. (#3775)
* Closed #3635: `window.Shiny.outputBindings` and `window.Shiny.inputBindings` gain a `onRegister()` method, to register callbacks that execute whenever a new binding is registered. Internally, Shiny uses this to check whether it should re-bind to the DOM when a binding has been registered. (#3638)
### Bug fixes
* Fixed #3771: Sometimes the error `ion.rangeSlider.min.js: i.stopPropagation is not a function` would appear in the JavaScript console. (#3772)
# shiny 1.7.4
## Full changelog
@@ -10,7 +30,7 @@
### New features and improvements
* `plotOutput()`, `imageOutput()`, and `uiOutput()` gain a `fill` argument. If `TRUE` (the default for `plotOutput()`), the output container is allowed to grow/shrink to fit a fill container (created via `htmltools::bindFillRole()`) with an opinionated height. This means `plotOutput()` will grow/shrink by default [inside of `bslib::card_body_fill()`](https://rstudio.github.io/bslib/articles/cards.html#responsive-sizing), but `imageOutput()` and `uiOutput()` will have to opt-in to similar behavior with `fill = TRUE`. (#3715)
* `plotOutput()`, `imageOutput()`, and `uiOutput()` gain a `fill` argument. If `TRUE` (the default for `plotOutput()`), the output container is allowed to grow/shrink to fit a fill container (created via `htmltools::bindFillRole()`) with an opinionated height. This means `plotOutput()` will grow/shrink by default [inside of `bslib::card_body_fill()`](https://rstudio.github.io/bslib/articles/cards.html#responsive-sizing), but `imageOutput()` and `uiOutput()` will have to opt-in to similar behavior with `fill = TRUE`. (#3715)
* Closed #3687: Updated jQuery-UI to v1.13.2. (#3697)

View File

@@ -452,8 +452,10 @@ RestoreInputSet <- R6Class("RestoreInputSet",
)
)
# This is a fastmap::faststack(); value is assigned in .onLoad().
restoreCtxStack <- NULL
on_load({
restoreCtxStack <- fastmap::faststack()
})
withRestoreContext <- function(ctx, expr) {
restoreCtxStack$push(ctx)

View File

@@ -190,8 +190,10 @@ devmode_inform <- function(
#' @include map.R
registered_devmode_options <- Map$new()
registered_devmode_options <- NULL
on_load({
registered_devmode_options <- Map$new()
})
#' @describeIn devmode Registers a Shiny Developer Mode option with an updated
#' value and Developer message. This registration method allows package
@@ -340,21 +342,22 @@ get_devmode_option <- function(
}
on_load({
register_devmode_option(
"shiny.autoreload",
"Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
TRUE
)
register_devmode_option(
"shiny.autoreload",
"Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
TRUE
)
register_devmode_option(
"shiny.minified",
"Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
FALSE
)
register_devmode_option(
"shiny.minified",
"Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
FALSE
)
register_devmode_option(
"shiny.fullstacktrace",
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
TRUE
)
register_devmode_option(
"shiny.fullstacktrace",
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
TRUE
)
})

View File

@@ -7,9 +7,9 @@
# the private seed during load.
withPrivateSeed(set.seed(NULL))
# Create this at the top level, but since the object is from a different
# package, we don't want to bake it into the built binary package.
restoreCtxStack <<- fastmap::faststack()
for (expr in on_load_exprs) {
eval(expr, envir = environment(.onLoad))
}
# Make sure these methods are available to knitr if shiny is loaded but not
# attached.
@@ -23,3 +23,11 @@
# https://github.com/rstudio/shiny/issues/2630
register_upgrade_message("htmlwidgets", 1.5)
}
on_load_exprs <- list()
# Register an expression to be evaluated when the package is loaded (in the
# .onLoad function).
on_load <- function(expr) {
on_load_exprs[[length(on_load_exprs) + 1]] <<- substitute(expr)
}

View File

@@ -559,4 +559,6 @@ MessageLogger = R6Class(
)
)
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
on_load({
rLog <- RLog$new("shiny.reactlog", "shiny.reactlog.console")
})

View File

@@ -182,8 +182,8 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
# $ xmax : num 3.78
# $ ymin : num 17.1
# $ ymax : num 20.4
# $ panelvar1: int 6
# $ panelvar2: int 0
# $ panelvar1: chr "6"
# $ panelvar2: chr "0
# $ coords_css:List of 4
# ..$ xmin: int 260
# ..$ xmax: int 298
@@ -367,8 +367,8 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
# $ img_css_ratio:List of 2
# ..$ x: num 1.25
# ..$ y: num 1.25
# $ panelvar1 : int 6
# $ panelvar2 : int 0
# $ panelvar1 : chr "6"
# $ panelvar2 : chr "0"
# $ mapping :List of 4
# ..$ x : chr "wt"
# ..$ y : chr "mpg"

View File

@@ -1,6 +1,6 @@
#' Insert and remove UI objects
#'
#' These functions allow you to dynamically add and remove arbirary UI
#' These functions allow you to dynamically add and remove arbitrary UI
#' into your app, whenever you want, as many times as you want.
#' Unlike [renderUI()], the UI generated with `insertUI()` is persistent:
#' once it's created, it stays there until removed by `removeUI()`. Each
@@ -11,7 +11,7 @@
#' function.
#'
#' It's particularly useful to pair `removeUI` with `insertUI()`, but there is
#' no restriction on what you can use on. Any element that can be selected
#' no restriction on what you can use it on. Any element that can be selected
#' through a jQuery selector can be removed through this function.
#'
#' @param selector A string that is accepted by jQuery's selector

View File

@@ -43,7 +43,10 @@ removeModal <- function(session = getDefaultReactiveDomain()) {
#' @param title An optional title for the dialog.
#' @param footer UI for footer. Use `NULL` for no footer.
#' @param size One of `"s"` for small, `"m"` (the default) for medium,
#' or `"l"` for large.
#' `"l"` for large, or `"xl"` for extra large. Note that `"xl"` only
#' works with Bootstrap 4 and above (to opt-in to Bootstrap 4+,
#' pass [bslib::bs_theme()] to the `theme` argument of a page container
#' like [fluidPage()]).
#' @param easyClose If `TRUE`, the modal dialog can be dismissed by
#' clicking outside the dialog box, or be pressing the Escape key. If
#' `FALSE` (the default), the modal dialog can't be dismissed in those

View File

@@ -326,6 +326,9 @@ ReactiveValues <- R6Class(
.dedupe = logical(0),
# Key, asList(), or names() have been retrieved
.hasRetrieved = list(),
# All names, in insertion order. The names are also stored in the .values
# object, but it does not preserve order.
.nameOrder = character(0),
initialize = function(
@@ -403,6 +406,11 @@ ReactiveValues <- R6Class(
return(invisible())
}
# If it's new, append key to the name order
if (!key_exists) {
.nameOrder[length(.nameOrder) + 1] <<- key
}
# set the value for better logging
.values$set(key, value)
@@ -444,14 +452,13 @@ ReactiveValues <- R6Class(
},
names = function() {
nameValues <- .values$keys()
if (!isTRUE(.hasRetrieved$names)) {
domain <- getDefaultReactiveDomain()
rLog$defineNames(.reactId, nameValues, .label, domain)
rLog$defineNames(.reactId, .nameOrder, .label, domain)
.hasRetrieved$names <<- TRUE
}
.namesDeps$register()
return(nameValues)
return(.nameOrder)
},
# Get a metadata value. Does not trigger reactivity.
@@ -499,7 +506,7 @@ ReactiveValues <- R6Class(
},
toList = function(all.names=FALSE) {
listValue <- .values$values()
listValue <- .values$mget(.nameOrder)
if (!all.names) {
listValue <- listValue[!grepl("^\\.", base::names(listValue))]
}

View File

@@ -1,5 +1,9 @@
# Create a map for input handlers and register the defaults.
inputHandlers <- Map$new()
# Create a Map object for input handlers and register the defaults.
# This is assigned in .onLoad time.
inputHandlers <- NULL
on_load({
inputHandlers <- Map$new()
})
#' Register an Input Handler
#'
@@ -125,115 +129,117 @@ applyInputHandlers <- function(inputs, shinysession = getDefaultReactiveDomain()
inputs
}
on_load({
# Takes a list-of-lists and returns a matrix. The lists
# must all be the same length. NULL is replaced by NA.
registerInputHandler("shiny.matrix", function(data, ...) {
if (length(data) == 0)
return(matrix(nrow=0, ncol=0))
# Takes a list-of-lists and returns a matrix. The lists
# must all be the same length. NULL is replaced by NA.
registerInputHandler("shiny.matrix", function(data, ...) {
if (length(data) == 0)
return(matrix(nrow=0, ncol=0))
m <- matrix(unlist(lapply(data, function(x) {
sapply(x, function(y) {
ifelse(is.null(y), NA, y)
})
})), nrow = length(data[[1]]), ncol = length(data))
return(m)
})
registerInputHandler("shiny.number", function(val, ...){
ifelse(is.null(val), NA, val)
})
registerInputHandler("shiny.password", function(val, shinysession, name) {
# Mark passwords as not serializable
setSerializer(name, serializerUnserializable)
val
})
registerInputHandler("shiny.date", function(val, ...){
# First replace NULLs with NA, then convert to Date vector
datelist <- ifelse(lapply(val, is.null), NA, val)
res <- NULL
tryCatch({
res <- as.Date(unlist(datelist))
},
error = function(e) {
# It's possible for client to send a string like "99999-01-01", which
# as.Date can't handle.
warning(e$message)
res <<- as.Date(rep(NA, length(datelist)))
}
)
res
})
registerInputHandler("shiny.datetime", function(val, ...){
# First replace NULLs with NA, then convert to POSIXct vector
times <- lapply(val, function(x) {
if (is.null(x)) NA
else x
m <- matrix(unlist(lapply(data, function(x) {
sapply(x, function(y) {
ifelse(is.null(y), NA, y)
})
})), nrow = length(data[[1]]), ncol = length(data))
return(m)
})
as.POSIXct(unlist(times), origin = "1970-01-01", tz = "UTC")
})
registerInputHandler("shiny.action", function(val, shinysession, name) {
# mark up the action button value with a special class so we can recognize it later
class(val) <- c("shinyActionButtonValue", class(val))
val
})
registerInputHandler("shiny.file", function(val, shinysession, name) {
# This function is only used when restoring a Shiny fileInput. When a file is
# uploaded the usual way, it takes a different code path and won't hit this
# function.
if (is.null(val))
return(NULL)
# The data will be a named list of lists; convert to a data frame.
val <- as.data.frame(lapply(val, unlist), stringsAsFactors = FALSE)
# `val$datapath` should be a filename without a path, for security reasons.
if (basename(val$datapath) != val$datapath) {
stop("Invalid '/' found in file input path.")
}
# Prepend the persistent dir
oldfile <- file.path(getCurrentRestoreContext()$dir, val$datapath)
# Copy the original file to a new temp dir, so that a restored session can't
# modify the original.
newdir <- file.path(tempdir(), createUniqueId(12))
dir.create(newdir)
val$datapath <- file.path(newdir, val$datapath)
file.copy(oldfile, val$datapath)
# Need to mark this input value with the correct serializer. When a file is
# uploaded the usual way (instead of being restored), this occurs in
# session$`@uploadEnd`.
setSerializer(name, serializerFileInput)
snapshotPreprocessInput(name, snapshotPreprocessorFileInput)
val
})
# to be used with !!!answer
registerInputHandler("shiny.symbolList", function(val, ...) {
if (is.null(val)) {
list()
} else {
lapply(val, as.symbol)
}
})
# to be used with !!answer
registerInputHandler("shiny.symbol", function(val, ...) {
if (is.null(val) || identical(val, "")) {
NULL
} else {
as.symbol(val)
}
registerInputHandler("shiny.number", function(val, ...){
ifelse(is.null(val), NA, val)
})
registerInputHandler("shiny.password", function(val, shinysession, name) {
# Mark passwords as not serializable
setSerializer(name, serializerUnserializable)
val
})
registerInputHandler("shiny.date", function(val, ...){
# First replace NULLs with NA, then convert to Date vector
datelist <- ifelse(lapply(val, is.null), NA, val)
res <- NULL
tryCatch({
res <- as.Date(unlist(datelist))
},
error = function(e) {
# It's possible for client to send a string like "99999-01-01", which
# as.Date can't handle.
warning(e$message)
res <<- as.Date(rep(NA, length(datelist)))
}
)
res
})
registerInputHandler("shiny.datetime", function(val, ...){
# First replace NULLs with NA, then convert to POSIXct vector
times <- lapply(val, function(x) {
if (is.null(x)) NA
else x
})
as.POSIXct(unlist(times), origin = "1970-01-01", tz = "UTC")
})
registerInputHandler("shiny.action", function(val, shinysession, name) {
# mark up the action button value with a special class so we can recognize it later
class(val) <- c("shinyActionButtonValue", class(val))
val
})
registerInputHandler("shiny.file", function(val, shinysession, name) {
# This function is only used when restoring a Shiny fileInput. When a file is
# uploaded the usual way, it takes a different code path and won't hit this
# function.
if (is.null(val))
return(NULL)
# The data will be a named list of lists; convert to a data frame.
val <- as.data.frame(lapply(val, unlist), stringsAsFactors = FALSE)
# `val$datapath` should be a filename without a path, for security reasons.
if (basename(val$datapath) != val$datapath) {
stop("Invalid '/' found in file input path.")
}
# Prepend the persistent dir
oldfile <- file.path(getCurrentRestoreContext()$dir, val$datapath)
# Copy the original file to a new temp dir, so that a restored session can't
# modify the original.
newdir <- file.path(tempdir(), createUniqueId(12))
dir.create(newdir)
val$datapath <- file.path(newdir, val$datapath)
file.copy(oldfile, val$datapath)
# Need to mark this input value with the correct serializer. When a file is
# uploaded the usual way (instead of being restored), this occurs in
# session$`@uploadEnd`.
setSerializer(name, serializerFileInput)
snapshotPreprocessInput(name, snapshotPreprocessorFileInput)
val
})
# to be used with !!!answer
registerInputHandler("shiny.symbolList", function(val, ...) {
if (is.null(val)) {
list()
} else {
lapply(val, as.symbol)
}
})
# to be used with !!answer
registerInputHandler("shiny.symbol", function(val, ...) {
if (is.null(val) || identical(val, "")) {
NULL
} else {
as.symbol(val)
}
})
})

View File

@@ -1,7 +1,12 @@
#' @include server-input-handlers.R
appsByToken <- Map$new()
appsNeedingFlush <- Map$new()
appsByToken <- NULL
appsNeedingFlush <- NULL
on_load({
appsByToken <- Map$new()
appsNeedingFlush <- Map$new()
})
# Provide a character representation of the WS that can be used
# as a key in a Map.
@@ -122,7 +127,10 @@ decodeMessage <- function(data) {
return(mainMessage)
}
autoReloadCallbacks <- Callbacks$new()
autoReloadCallbacks <- NULL
on_load({
autoReloadCallbacks <- Callbacks$new()
})
createAppHandlers <- function(httpHandlers, serverFuncSource) {
appvars <- new.env()

View File

@@ -753,7 +753,7 @@
x = $handle.offset().left;
x += ($handle.width() / 2) - 1;
this.pointerClick("single", {preventDefault: function () {}, pageX: x});
this.pointerClick("single", {preventDefault: function () {}, stopPropagation: function () {}, pageX: x});
}
},

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,2 +1,2 @@
/*! shiny 1.7.4 | (c) 2012-2022 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.7.4.9002 | (c) 2012-2023 RStudio, PBC. | License: GPL-3 | file LICENSE */
#showcase-well{border-radius:0}.shiny-code{background-color:#fff;margin-bottom:0}.shiny-code code{font-family:Menlo,Consolas,Courier New,monospace}.shiny-code-container{margin-top:20px;clear:both}.shiny-code-container h3{display:inline;margin-right:15px}.showcase-header{font-size:16px;font-weight:400}.showcase-code-link{text-align:right;padding:15px}#showcase-app-container{vertical-align:top}#showcase-code-tabs{margin-right:15px}#showcase-code-tabs pre{border:none;line-height:1em}#showcase-code-tabs .nav,#showcase-code-tabs ul{margin-bottom:0}#showcase-code-tabs .tab-content{border-style:solid;border-color:#e5e5e5;border-width:0px 1px 1px 1px;overflow:auto;border-bottom-right-radius:4px;border-bottom-left-radius:4px}#showcase-app-code{width:100%}#showcase-code-position-toggle{float:right}#showcase-sxs-code{padding-top:20px;vertical-align:top}.showcase-code-license{display:block;text-align:right}#showcase-code-content pre{background-color:#fff}

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,3 +1,3 @@
/*! shiny 1.7.4 | (c) 2012-2022 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.7.4.9002 | (c) 2012-2023 RStudio, PBC. | License: GPL-3 | file LICENSE */
"use strict";(function(){var a=eval;window.addEventListener("message",function(i){var e=i.data;e.code&&a(e.code)});})();
//# sourceMappingURL=shiny-testmode.js.map

View File

@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../../../srcts/src/utils/eval.ts", "../../../srcts/extras/shiny-testmode.ts"],
"sourcesContent": ["//esbuild.github.io/content-types/#direct-eval\n//tl/dr;\n// * Direct usage of `eval(\"x\")` is bad with bundled code.\n// * Instead, use indirect calls to `eval` such as `indirectEval(\"x\")`\n// * Even just renaming the function works well enough.\n// > This is known as \"indirect eval\" because eval is not being called directly, and so does not trigger the grammatical special case for direct eval in the JavaScript VM. You can call indirect eval using any syntax at all except for an expression of the exact form eval('x'). For example, var eval2 = eval; eval2('x') and [eval][0]('x') and window.eval('x') are all indirect eval calls.\n// > When you use indirect eval, the code is evaluated in the global scope instead of in the inline scope of the caller.\nvar indirectEval = eval;\nexport { indirectEval };", "/* eslint-disable unicorn/filename-case */\nimport { indirectEval } from \"../src/utils/eval\"; // Listen for messages from parent frame. This file is only added when the\n// shiny.testmode option is TRUE.\n\nwindow.addEventListener(\"message\", function (e) {\n var message = e.data;\n if (message.code) indirectEval(message.code);\n});"],
"mappings": ";yBAOA,IAAIA,EAAe,KCHnB,OAAO,iBAAiB,UAAW,SAAUC,EAAG,CAC9C,IAAIC,EAAUD,EAAE,KACZC,EAAQ,MAAMC,EAAaD,EAAQ,IAAI,CAC7C,CAAC",
"sourcesContent": ["//esbuild.github.io/content-types/#direct-eval\n//tl/dr;\n// * Direct usage of `eval(\"x\")` is bad with bundled code.\n// * Instead, use indirect calls to `eval` such as `indirectEval(\"x\")`\n// * Even just renaming the function works well enough.\n// > This is known as \"indirect eval\" because eval is not being called directly, and so does not trigger the grammatical special case for direct eval in the JavaScript VM. You can call indirect eval using any syntax at all except for an expression of the exact form eval('x'). For example, var eval2 = eval; eval2('x') and [eval][0]('x') and window.eval('x') are all indirect eval calls.\n// > When you use indirect eval, the code is evaluated in the global scope instead of in the inline scope of the caller.\n\nvar indirectEval = eval;\nexport { indirectEval };", "/* eslint-disable unicorn/filename-case */\nimport { indirectEval } from \"../src/utils/eval\";\n\n// Listen for messages from parent frame. This file is only added when the\n// shiny.testmode option is TRUE.\nwindow.addEventListener(\"message\", function (e) {\n var message = e.data;\n if (message.code) indirectEval(message.code);\n});"],
"mappings": ";yBAQA,IAAIA,EAAe,KCHnB,OAAO,iBAAiB,UAAW,SAAUC,EAAG,CAC9C,IAAIC,EAAUD,EAAE,KACZC,EAAQ,MAAMC,EAAaD,EAAQ,IAAI,CAC7C,CAAC",
"names": ["indirectEval", "e", "message", "indirectEval"]
}

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

View File

@@ -64,7 +64,7 @@ updated and all observers have been run (default).}
\item{session}{The shiny session. Advanced use only.}
}
\description{
These functions allow you to dynamically add and remove arbirary UI
These functions allow you to dynamically add and remove arbitrary UI
into your app, whenever you want, as many times as you want.
Unlike \code{\link[=renderUI]{renderUI()}}, the UI generated with \code{insertUI()} is persistent:
once it's created, it stays there until removed by \code{removeUI()}. Each
@@ -76,7 +76,7 @@ function.
}
\details{
It's particularly useful to pair \code{removeUI} with \code{insertUI()}, but there is
no restriction on what you can use on. Any element that can be selected
no restriction on what you can use it on. Any element that can be selected
through a jQuery selector can be removed through this function.
}
\examples{

View File

@@ -17,7 +17,7 @@ memoryCache(
\arguments{
\item{max_size}{Maximum size of the cache, in bytes. If the cache exceeds
this size, cached objects will be removed according to the value of the
\code{evict}. Use \code{Inf} for no size limit. The default is 1 gigabyte.}
\code{evict}. Use \code{Inf} for no size limit. The default is 512 megabytes.}
\item{max_age}{Maximum age of files in cache before they are evicted, in
seconds. Use \code{Inf} for no age limit.}

View File

@@ -24,7 +24,10 @@ modalButton(label, icon = NULL)
\item{footer}{UI for footer. Use \code{NULL} for no footer.}
\item{size}{One of \code{"s"} for small, \code{"m"} (the default) for medium,
or \code{"l"} for large.}
\code{"l"} for large, or \code{"xl"} for extra large. Note that \code{"xl"} only
works with Bootstrap 4 and above (to opt-in to Bootstrap 4+,
pass \code{\link[bslib:bs_theme]{bslib::bs_theme()}} to the \code{theme} argument of a page container
like \code{\link[=fluidPage]{fluidPage()}}).}
\item{easyClose}{If \code{TRUE}, the modal dialog can be dismissed by
clicking outside the dialog box, or be pressing the Escape key. If

View File

@@ -3,7 +3,7 @@
"homepage": "https://shiny.rstudio.com",
"repository": "github:rstudio/shiny",
"name": "@types/rstudio-shiny",
"version": "1.7.4",
"version": "1.7.4-alpha.9002",
"license": "GPL-3.0-only",
"main": "",
"browser": "",
@@ -40,12 +40,11 @@
"@types/jest": "^26.0.23",
"@types/jqueryui": "1.12.16",
"@types/lodash": "^4.14.170",
"@types/node": "^15.6.1",
"@types/node": "^18.14.2",
"@types/showdown": "^1.9.3",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1",
"autoprefixer": "^10.2.6",
"bootstrap-datepicker": "1.9.0",
"browserslist": "^4.19.1",
"caniuse-lite": "^1.0.30001312",
"core-js": "^3.13.0",
@@ -60,9 +59,8 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unicorn": "^43.0.2",
"fs-readdir-recursive": "^1.1.0",
"ion-rangeslider": "2.3.1",
"jest": "^26.6.3",
"jquery": "3.6.0",
"jquery": "^3.6.0",
"lodash": "^4.17.21",
"madge": "^4.0.2",
"node-gyp": "^8.1.0",
@@ -71,8 +69,6 @@
"prettier": "^2.7.1",
"readcontrol": "^1.0.0",
"replace": "^1.2.1",
"selectize": "0.12.4",
"strftime": "0.9.2",
"ts-jest": "^26",
"ts-node": "^10.9.1",
"type-coverage": "^2.22.0",

View File

@@ -2,7 +2,7 @@ import $ from "jquery";
import { OutputBinding } from "./outputBinding";
import { shinyUnbindAll } from "../../shiny/initedMethods";
import { renderContent } from "../../shiny/render";
import { renderContentAsync } from "../../shiny/render";
import type { ErrorsMessageValue } from "../../shiny/shinyapp";
class HtmlOutputBinding extends OutputBinding {
@@ -13,11 +13,11 @@ class HtmlOutputBinding extends OutputBinding {
shinyUnbindAll(el);
this.renderError(el, err);
}
renderValue(
override async renderValue(
el: HTMLElement,
data: Parameters<typeof renderContent>[1]
): void {
renderContent(el, data);
data: Parameters<typeof renderContentAsync>[1]
): Promise<void> {
await renderContentAsync(el, data);
}
}

View File

@@ -11,7 +11,7 @@ class OutputBinding {
throw "Not implemented";
scope;
}
renderValue(el: HTMLElement, data: unknown): void {
renderValue(el: HTMLElement, data: unknown): Promise<void> | void {
throw "Not implemented";
el;
data;
@@ -21,9 +21,9 @@ class OutputBinding {
return el.getAttribute("data-input-id") || el.id;
}
onValueChange(el: HTMLElement, data: unknown): void {
async onValueChange(el: HTMLElement, data: unknown): Promise<void> {
this.clearError(el);
this.renderValue(el, data);
await this.renderValue(el, data);
}
onValueError(el: HTMLElement, err: ErrorsMessageValue): void {
this.renderError(el, err);

View File

@@ -31,8 +31,8 @@ class OutputBindingAdapter {
getId(): string {
return this.binding.getId(this.el);
}
onValueChange(data: unknown): void {
this.binding.onValueChange(this.el, data);
async onValueChange(data: unknown): Promise<void> {
await this.binding.onValueChange(this.el, data);
}
onValueError(err: ErrorsMessageValue): void {
this.binding.onValueError(this.el, err);

View File

@@ -1,4 +1,5 @@
import { mergeSort } from "../utils";
import { Callbacks } from "../utils/callbacks";
interface BindingBase {
name: string;
@@ -14,6 +15,7 @@ class BindingRegistry<Binding extends BindingBase> {
name!: string;
bindings: Array<BindingObj<Binding>> = [];
bindingNames: { [key: string]: BindingObj<Binding> } = {};
registerCallbacks: Callbacks = new Callbacks();
register(binding: Binding, bindingName: string, priority = 0): void {
const bindingObj = { binding, priority };
@@ -23,6 +25,12 @@ class BindingRegistry<Binding extends BindingBase> {
this.bindingNames[bindingName] = bindingObj;
binding.name = bindingName;
}
this.registerCallbacks.invoke();
}
onRegister(fn: () => void, once = true): void {
this.registerCallbacks.register(fn, once);
}
setPriority(bindingName: string, priority: number): void {

View File

@@ -58,6 +58,7 @@ type BrushOpts = {
type Brush = {
reset: () => void;
hasOldBrush: () => boolean;
importOldBrush: () => void;
isInsideBrush: (offsetCss: Offset) => boolean;
isInResizeArea: (offsetCss: Offset) => boolean;
@@ -173,10 +174,15 @@ function createBrush(
if ($div) $div.remove();
}
function hasOldBrush(): boolean {
const oldDiv = $el.find("#" + el.id + "_brush");
return oldDiv.length > 0;
}
// If there's an existing brush div, use that div to set the new brush's
// settings, provided that the x, y, and panel variables have the same names,
// and there's a panel with matching panel variable values.
function importOldBrush() {
function importOldBrush(): void {
const oldDiv = $el.find("#" + el.id + "_brush");
if (oldDiv.length === 0) return;
@@ -617,6 +623,7 @@ function createBrush(
return {
reset: reset,
hasOldBrush,
importOldBrush: importOldBrush,
isInsideBrush: isInsideBrush,
isInResizeArea: isInResizeArea,

View File

@@ -57,6 +57,9 @@ function createClickHandler(
): CreateHandler {
const clickInfoSender = coordmap.mouseCoordinateSender(inputId, clip);
// Send initial (null) value on creation.
clickInfoSender(null);
return {
mousedown: function (e) {
// Listen for left mouse button only
@@ -90,6 +93,9 @@ function createHoverHandler(
hoverInfoSender = new Throttler(null, sendHoverInfo, delay);
else hoverInfoSender = new Debouncer(null, sendHoverInfo, delay);
// Send initial (null) value on creation.
hoverInfoSender.immediateCall(null);
// What to do when mouse exits the image
let mouseout: () => void;
@@ -233,6 +239,11 @@ function createBrushHandler(
brushInfoSender = new Debouncer(null, sendBrushInfo, opts.brushDelay);
}
// Send initial (null) value on creation.
if (!brush.hasOldBrush()) {
brushInfoSender.immediateCall();
}
function mousedown(e: JQuery.MouseDownEvent) {
// This can happen when mousedown inside the graphic, then mouseup
// outside, then mousedown inside. Just ignore the second

View File

@@ -6,9 +6,9 @@ import type { ShinyApp } from "../shiny/shinyapp";
class InputBatchSender implements InputPolicy {
target!: InputPolicy; // We need this field to satisfy the InputPolicy interface
shinyapp: ShinyApp;
timerId: ReturnType<typeof setTimeout> | null = null;
pendingData: { [key: string]: unknown } = {};
reentrant = false;
sendIsEnqueued = false;
lastChanceCallback: Array<() => void> = [];
constructor(shinyapp: ShinyApp) {
@@ -21,8 +21,11 @@ class InputBatchSender implements InputPolicy {
if (!this.reentrant) {
if (opts.priority === "event") {
this._sendNow();
} else if (!this.timerId) {
this.timerId = setTimeout(this._sendNow.bind(this), 0);
} else if (!this.sendIsEnqueued) {
this.shinyapp.actionQueue.enqueue(() => {
this.sendIsEnqueued = false;
this._sendNow();
});
}
}
}
@@ -34,7 +37,6 @@ class InputBatchSender implements InputPolicy {
this.reentrant = true;
try {
this.timerId = null;
this.lastChanceCallback.forEach((callback) => callback());
const currentData = this.pendingData;

View File

@@ -6,7 +6,14 @@ import { $escape, compareVersion } from "../utils";
import { showNotification, removeNotification } from "./notifications";
import { showModal, removeModal } from "./modal";
import { showReconnectDialog, hideReconnectDialog } from "./reconnectDialog";
import { renderContent, renderDependencies, renderHtml } from "./render";
import {
renderContentAsync,
renderContent,
renderDependenciesAsync,
renderDependencies,
renderHtmlAsync,
renderHtml,
} from "./render";
import { initShiny } from "./init";
import type {
shinyBindAll,
@@ -40,8 +47,11 @@ interface Shiny {
createSocket?: () => WebSocket;
showReconnectDialog: typeof showReconnectDialog;
hideReconnectDialog: typeof hideReconnectDialog;
renderDependenciesAsync: typeof renderDependenciesAsync;
renderDependencies: typeof renderDependencies;
renderContentAsync: typeof renderContentAsync;
renderContent: typeof renderContent;
renderHtmlAsync: typeof renderHtmlAsync;
renderHtml: typeof renderHtml;
user: string;
progressHandlers?: ShinyApp["progressHandlers"];
@@ -90,8 +100,11 @@ function setShiny(windowShiny_: Shiny): void {
windowShiny.addCustomMessageHandler = addCustomMessageHandler;
windowShiny.showReconnectDialog = showReconnectDialog;
windowShiny.hideReconnectDialog = hideReconnectDialog;
windowShiny.renderDependenciesAsync = renderDependenciesAsync;
windowShiny.renderDependencies = renderDependencies;
windowShiny.renderContentAsync = renderContentAsync;
windowShiny.renderContent = renderContent;
windowShiny.renderHtmlAsync = renderHtmlAsync;
windowShiny.renderHtml = renderHtml;
$(function () {

View File

@@ -150,6 +150,24 @@ function initShiny(windowShiny: Shiny): void {
(x) => x.value
);
// When future bindings are registered, rebind to the DOM once the current
// event loop is done. This is necessary since the binding might be registered
// after Shiny has already bound to the DOM (#3635)
let enqueuedCount = 0;
const enqueueRebind = () => {
enqueuedCount++;
windowShiny.shinyapp?.actionQueue.enqueue(() => {
enqueuedCount--;
// If this function is scheduled more than once in the queue, don't do anything.
// Only do the bindAll when we get to the last instance of this function in the queue.
if (enqueuedCount === 0) {
windowShiny.bindAll?.(document.documentElement);
}
});
};
inputBindings.onRegister(enqueueRebind, false);
outputBindings.onRegister(enqueueRebind, false);
// The server needs to know the size of each image and plot output element,
// in case it is auto-sizing
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(

View File

@@ -1,12 +1,28 @@
import $ from "jquery";
import { shinyUnbindAll } from "./initedMethods";
import { renderContent } from "./render";
import type { HtmlDep } from "./render";
import { renderContentAsync, renderDependenciesAsync } from "./render";
// Show a modal dialog. This is meant to handle two types of cases: one is
// that the content is a Bootstrap modal dialog, and the other is that the
// content is non-Bootstrap. Bootstrap modals require some special handling,
// which is coded in here.
function show({ html = "", deps = [] } = {}): void {
async function show({
html = "",
deps = [],
}: {
html?: string;
deps?: HtmlDep[];
} = {}): Promise<void> {
// Normally we'd first create the modal's DOM elements, then call
// `renderContentAsync(x, {html: html, deps: deps})`, but that has a potential
// problem with async rendering. If we did that, then an empty modal (from
// this function) could show up and then sit there empty while the
// dependencies load (asynchronously), and only after all that get filled with
// content for the modal. So instead we'll render the deps here, then render
// the modal, then render the content in the modal.
await renderDependenciesAsync(deps);
// If there was an existing Bootstrap modal, then there will be a modal-
// backdrop div that was added outside of the modal wrapper, and it must be
// removed; otherwise there can be multiple of these divs.
@@ -44,7 +60,7 @@ function show({ html = "", deps = [] } = {}): void {
});
// Set/replace contents of wrapper with html.
renderContent($modal, { html: html, deps: deps });
await renderContentAsync($modal, { html: html });
}
function remove(): void {

View File

@@ -3,12 +3,13 @@ import $ from "jquery";
import { $escape, randomId } from "../utils";
import { shinyUnbindAll } from "./initedMethods";
import type { HtmlDep } from "./render";
import { renderContent } from "./render";
import { renderDependenciesAsync } from "./render";
import { renderContentAsync } from "./render";
// Milliseconds to fade in or out
const fadeDuration = 250;
function show({
async function show({
html = "",
action = "",
deps = [],
@@ -24,9 +25,18 @@ function show({
id?: string | null;
closeButton?: boolean;
type?: string | null;
} = {}): ReturnType<typeof randomId> {
} = {}): Promise<ReturnType<typeof randomId>> {
if (!id) id = randomId();
// Normally we'd first create the notification's DOM elements, then call
// `renderContentAsync(x, {html: html, deps: deps})`, but that has a potential
// problem with async rendering. If we did that, then an empty notification
// (from this function) could show up and then sit there empty while the
// dependencies load (asynchronously), and only after all that get filled with
// content for the notification. So instead we'll render the deps here, then
// render the notification, then render the content in the notification.
await renderDependenciesAsync(deps);
// Create panel if necessary
createPanel();
@@ -42,7 +52,8 @@ function show({
`<div class="shiny-notification-content-action">${action}</div>`;
const $content = $notification.find(".shiny-notification-content");
renderContent($content, { html: newHtml, deps: deps });
// Set/replace contents of wrapper with html.
await renderContentAsync($content, { html: newHtml });
// Remove any existing classes of the form 'shiny-notification-xxxx'.
// The xxxx would be strings like 'warning'.

View File

@@ -12,15 +12,73 @@ import { sendImageSizeFns } from "./sendImageSize";
import { renderHtml as singletonsRenderHtml } from "./singletons";
import type { WherePosition } from "./singletons";
function renderDependencies(dependencies: HtmlDep[] | null): void {
if (dependencies) {
dependencies.forEach(renderDependency);
}
}
// There are synchronous and asynchronous versions of the exported functions
// renderContent(), renderHtml(), and renderDependencies(). This is because they
// the original versions of these functions were synchronous, but we added
// support for asynchronous rendering, to avoid the deprecated XMLHttpRequest
// function (https://github.com/rstudio/shiny/pull/3666).
//
// At the bottom, there is the appendScriptTags(), which calls $.append(), which
// in turn calls (synchronous) XMLHttpRequest(); and its counterpart
// appendScriptTagsAsync(), which uses a different (asynchronous) method. The
// sync and async versions of this function necessitate the sync and async
// versions of the other functions.
//
// The async versions of these functions are used internally and should be used
// for new external code when possible, but for backward compatibility for
// external code that calls these functions, we'll keep the synchronous versions
// around as well.
// =============================================================================
// renderContent
// =============================================================================
// Render HTML in a DOM element, add dependencies, and bind Shiny
// inputs/outputs. `content` can be null, a string, or an object with
// properties 'html' and 'deps'.
async function renderContentAsync(
el: BindScope,
content: string | { html: string; deps?: HtmlDep[] } | null,
where: WherePosition = "replace"
): Promise<void> {
if (where === "replace") {
shinyUnbindAll(el);
}
let html = "";
let dependencies: HtmlDep[] = [];
if (content === null) {
html = "";
} else if (typeof content === "string") {
html = content;
} else if (typeof content === "object") {
html = content.html;
dependencies = content.deps || [];
}
await renderHtmlAsync(html, el, dependencies, where);
let scope: BindScope = el;
if (where === "replace") {
shinyInitializeInputs(el);
shinyBindAll(el);
} else {
const $parent = $(el).parent();
if ($parent.length > 0) {
scope = $parent;
if (where === "beforeBegin" || where === "afterEnd") {
const $grandparent = $parent.parent();
if ($grandparent.length > 0) scope = $grandparent;
}
}
shinyInitializeInputs(scope);
shinyBindAll(scope);
}
}
function renderContent(
el: BindScope,
content: string | { html: string; deps?: HtmlDep[] } | null,
@@ -65,6 +123,20 @@ function renderContent(
}
}
// =============================================================================
// renderHtml
// =============================================================================
// Render HTML in a DOM element, inserting singletons into head as needed
async function renderHtmlAsync(
html: string,
el: BindScope,
dependencies: HtmlDep[],
where: WherePosition = "replace"
): Promise<ReturnType<typeof singletonsRenderHtml>> {
await renderDependenciesAsync(dependencies);
return singletonsRenderHtml(html, el, where);
}
// Render HTML in a DOM element, inserting singletons into head as needed
function renderHtml(
html: string,
@@ -76,6 +148,30 @@ function renderHtml(
return singletonsRenderHtml(html, el, where);
}
// =============================================================================
// renderDependencies
// =============================================================================
async function renderDependenciesAsync(
dependencies: HtmlDep[] | null
): Promise<void> {
if (dependencies) {
for (const dep of dependencies) {
await renderDependencyAsync(dep);
}
}
}
function renderDependencies(dependencies: HtmlDep[] | null): void {
if (dependencies) {
for (const dep of dependencies) {
renderDependency(dep);
}
}
}
// =============================================================================
// HTML dependency types
// =============================================================================
type HtmlDepVersion = string;
type MetaItem = {
@@ -126,6 +222,10 @@ type HtmlDepNormalized = {
attachment: AttachmentItem[];
head?: string;
};
// =============================================================================
// renderDependency helper functions
// =============================================================================
const htmlDependencies: { [key: string]: HtmlDepVersion } = {};
function registerDependency(name: string, version: HtmlDepVersion): void {
@@ -147,93 +247,6 @@ function needsRestyle(dep: HtmlDepNormalized) {
return htmlDependencies[names[idx]] === dep.version;
}
// Client-side dependency resolution and rendering
function renderDependency(dep_: HtmlDep) {
const dep = normalizeHtmlDependency(dep_);
// Convert stylesheet objs to links early, because if `restyle` is true, we'll
// pass them through to `addStylesheetsAndRestyle` below.
const stylesheetLinks = dep.stylesheet.map((x) => {
// Add "rel" and "type" fields if not already present.
if (!hasDefinedProperty(x, "rel")) x.rel = "stylesheet";
if (!hasDefinedProperty(x, "type")) x.type = "text/css";
const link = document.createElement("link");
Object.entries(x).forEach(function ([attr, val]: [
string,
string | undefined
]) {
if (attr === "href") {
val = encodeURI(val as string);
}
// If val isn't truthy (e.g., null), consider it a boolean attribute
link.setAttribute(attr, val ? val : "");
});
return link;
});
// If a restyle is needed, do that stuff and return. Note that other items
// (like scripts) aren't added, because they would have been added in a
// previous run.
if (needsRestyle(dep)) {
addStylesheetsAndRestyle(stylesheetLinks);
return true;
}
if (hasDefinedProperty(htmlDependencies, dep.name)) return false;
registerDependency(dep.name, dep.version);
const $head = $("head").first();
// Add each type of element to the DOM.
dep.meta.forEach((x) => {
const meta = document.createElement("meta");
for (const [attr, val] of Object.entries(x)) {
meta.setAttribute(attr, val);
}
$head.append(meta);
});
if (stylesheetLinks.length !== 0) {
$head.append(stylesheetLinks);
}
dep.script.forEach((x) => {
const script = document.createElement("script");
Object.entries(x).forEach(function ([attr, val]) {
if (attr === "src") {
val = encodeURI(val);
}
// If val isn't truthy (e.g., null), consider it a boolean attribute
script.setAttribute(attr, val ? val : "");
});
$head.append(script);
});
dep.attachment.forEach((x) => {
const link = $("<link rel='attachment'>")
.attr("id", dep.name + "-" + x.key + "-attachment")
.attr("href", encodeURI(x.href));
$head.append(link);
});
if (dep.head) {
const $newHead = $("<head></head>");
$newHead.html(dep.head);
$head.append($newHead.children());
}
return true;
}
function addStylesheetsAndRestyle(links: HTMLLinkElement[]): void {
const $head = $("head").first();
@@ -354,6 +367,206 @@ function addStylesheetsAndRestyle(links: HTMLLinkElement[]): void {
});
}
function getStylesheetLinkTags(dep: HtmlDepNormalized): HTMLLinkElement[] {
// Convert stylesheet objs to links early, because if `restyle` is true, we'll
// pass them through to `addStylesheetsAndRestyle` below.
return dep.stylesheet.map((x) => {
// Add "rel" and "type" fields if not already present.
if (!hasDefinedProperty(x, "rel")) x.rel = "stylesheet";
if (!hasDefinedProperty(x, "type")) x.type = "text/css";
const link = document.createElement("link");
Object.entries(x).forEach(function ([attr, val]: [
string,
string | undefined
]) {
if (attr === "href") {
val = encodeURI(val as string);
}
// If val isn't truthy (e.g., null), consider it a boolean attribute
link.setAttribute(attr, val ? val : "");
});
return link;
});
}
function appendStylesheetLinkTags(
dep: HtmlDepNormalized,
$head: JQuery<HTMLElement>
): void {
const stylesheetLinks = getStylesheetLinkTags(dep);
if (stylesheetLinks.length !== 0) {
$head.append(stylesheetLinks);
}
}
function appendScriptTags(dep: HtmlDepNormalized, $head: JQuery<HTMLElement>) {
dep.script.forEach((x) => {
const script = document.createElement("script");
Object.entries(x).forEach(function ([attr, val]) {
if (attr === "src") {
val = encodeURI(val);
}
// If val isn't truthy (e.g., null), consider it a boolean attribute
script.setAttribute(attr, val ? val : "");
});
$head.append(script);
});
}
async function appendScriptTagsAsync(dep: HtmlDepNormalized): Promise<void> {
const scriptPromises: Array<Promise<any>> = [];
dep.script.forEach((x) => {
const script = document.createElement("script");
if (!hasDefinedProperty(x, "async")) {
// Set async to false by default, so that if there are multiple script
// tags, they are guaranteed to run in order. For dynamically added
// <script> tags, browsers set async to true by default, which differs
// from static <script> tags in the html, which default to false.
//
// Refs:
// https://stackoverflow.com/a/8996894/412655
// https://jason-ge.medium.com/dynamically-load-javascript-files-in-order-5318ac6bcc61
//
// Note that one odd thing about these dynamically-created <script> tags
// is that even though the JS object's `x.script` property is true, it
// does NOT show up as a property on the <script> element.
script.async = false;
}
Object.entries(x).forEach(function ([attr, val]) {
if (attr === "src") {
val = encodeURI(val);
}
// If val isn't truthy (e.g., null), consider it a boolean attribute
script.setAttribute(attr, val ? val : "");
});
const p = new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
script.onload = (e: Event) => {
resolve(null);
};
script.onerror = (e: Event | string) => {
reject(e);
};
});
scriptPromises.push(p);
document.head.append(script);
});
await Promise.allSettled(scriptPromises);
}
function appendMetaTags(
dep: HtmlDepNormalized,
$head: JQuery<HTMLElement>
): void {
dep.meta.forEach((x) => {
const meta = document.createElement("meta");
for (const [attr, val] of Object.entries(x)) {
meta.setAttribute(attr, val);
}
$head.append(meta);
});
}
function appendAttachmentLinkTags(
dep: HtmlDepNormalized,
$head: JQuery<HTMLElement>
): void {
dep.attachment.forEach((x) => {
const link = $("<link rel='attachment'>")
.attr("id", dep.name + "-" + x.key + "-attachment")
.attr("href", encodeURI(x.href));
$head.append(link);
});
}
function appendExtraHeadContent(
dep: HtmlDepNormalized,
$head: JQuery<HTMLElement>
): void {
if (dep.head) {
const $newHead = $("<head></head>");
$newHead.html(dep.head);
$head.append($newHead.children());
}
}
// =============================================================================
// renderDependency
// =============================================================================
// Client-side dependency resolution and rendering
async function renderDependencyAsync(dep_: HtmlDep): Promise<boolean> {
const dep = normalizeHtmlDependency(dep_);
// If a restyle is needed, do that stuff and return. Note that other items
// (like scripts) aren't added, because they would have been added in a
// previous run.
if (needsRestyle(dep)) {
addStylesheetsAndRestyle(getStylesheetLinkTags(dep));
return true;
}
if (hasDefinedProperty(htmlDependencies, dep.name)) return false;
registerDependency(dep.name, dep.version);
const $head = $("head").first();
// Add each type of element to the DOM.
appendMetaTags(dep, $head);
appendStylesheetLinkTags(dep, $head);
await appendScriptTagsAsync(dep);
appendAttachmentLinkTags(dep, $head);
appendExtraHeadContent(dep, $head);
return true;
}
// Old-school synchronous version of renderDependencyAsync. This function is
// here to preserve compatibility with outside packages that use it. The
// implementation is the same except that it calls appendScriptTags() instead of
// appendScriptTagsAsync().
function renderDependency(dep_: HtmlDep): boolean {
const dep = normalizeHtmlDependency(dep_);
// If a restyle is needed, do that stuff and return. Note that other items
// (like scripts) aren't added, because they would have been added in a
// previous run.
if (needsRestyle(dep)) {
addStylesheetsAndRestyle(getStylesheetLinkTags(dep));
return true;
}
if (hasDefinedProperty(htmlDependencies, dep.name)) return false;
registerDependency(dep.name, dep.version);
const $head = $("head").first();
// Add each type of element to the DOM.
appendMetaTags(dep, $head);
appendStylesheetLinkTags(dep, $head);
appendScriptTags(dep, $head);
appendAttachmentLinkTags(dep, $head);
appendExtraHeadContent(dep, $head);
return true;
}
// Convert legacy HtmlDependency to new HTMLDependency format. This is
// idempotent; new HTMLDependency objects are returned unchanged.
function normalizeHtmlDependency(dep: HtmlDep): HtmlDepNormalized {
@@ -470,5 +683,13 @@ function normalizeHtmlDependency(dep: HtmlDep): HtmlDepNormalized {
return result;
}
export { renderDependencies, renderContent, renderHtml, registerDependency };
export {
renderContentAsync,
renderContent,
renderHtmlAsync,
renderHtml,
renderDependenciesAsync,
renderDependencies,
registerDependency,
};
export type { HtmlDep };

View File

@@ -10,7 +10,7 @@ import {
import { isQt } from "../utils/browser";
import { showNotification, removeNotification } from "./notifications";
import { showModal, removeModal } from "./modal";
import { renderContent, renderHtml } from "./render";
import { renderContentAsync, renderHtmlAsync } from "./render";
import type { HtmlDep } from "./render";
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
import { resetBrush } from "../imageutils/resetBrush";
@@ -25,9 +25,10 @@ import type { InputBinding } from "../bindings";
import { indirectEval } from "../utils/eval";
import type { WherePosition } from "./singletons";
import type { UploadInitValue, UploadEndValue } from "../file/fileProcessor";
import { AsyncQueue } from "../utils/asyncQueue";
type ResponseValue = UploadEndValue | UploadInitValue;
type Handler = (message: any) => void;
type Handler = (message: any) => Promise<void> | void;
type ShinyWebSocket = WebSocket & {
allowReconnect?: boolean;
@@ -108,6 +109,13 @@ function addCustomMessageHandler(type: string, handler: Handler): void {
class ShinyApp {
$socket: ShinyWebSocket | null = null;
// An asynchronous queue of functions. This is sort of like an event loop for
// Shiny, to allow scheduling async callbacks so that they can run in order
// without overlapping. This is used for handling incoming messages from the
// server and scheduling outgoing messages to the server, and can be used for
// other things tasks as well.
actionQueue = new AsyncQueue<() => Promise<void> | void>();
config: {
workerId: string;
sessionId: string;
@@ -228,9 +236,11 @@ class ShinyApp {
socket.send(msg as string);
}
this.startActionQueueLoop();
};
socket.onmessage = (e) => {
this.dispatchMessage(e.data);
this.actionQueue.enqueue(async () => await this.dispatchMessage(e.data));
};
// Called when a successfully-opened websocket is closed, or when an
// attempt to open a connection fails.
@@ -253,6 +263,19 @@ class ShinyApp {
return socket;
}
async startActionQueueLoop(): Promise<void> {
// eslint-disable-next-line no-constant-condition
while (true) {
const action = await this.actionQueue.dequeue();
try {
await action();
} catch (e) {
console.error(e);
}
}
}
sendInput(values: InputValues): void {
const msg = JSON.stringify({
method: "update",
@@ -459,7 +482,7 @@ class ShinyApp {
}
}
receiveOutput<T>(name: string, value: T): T | undefined {
async receiveOutput<T>(name: string, value: T): Promise<T | undefined> {
const binding = this.$bindings[name];
const evt: ShinyEventValue = $.Event("shiny:value");
@@ -478,7 +501,7 @@ class ShinyApp {
$(binding ? binding.el : document).trigger(evt);
if (!evt.isDefaultPrevented() && binding) {
binding.onValueChange(evt.value);
await binding.onValueChange(evt.value);
}
return value;
@@ -598,7 +621,7 @@ class ShinyApp {
// // Added in shiny init method
// Shiny.addCustomMessageHandler = addCustomMessageHandler;
dispatchMessage(data: ArrayBufferLike | string): void {
async dispatchMessage(data: ArrayBufferLike | string): Promise<void> {
let msgObj: ShinyEventMessage["message"] = {};
if (typeof data === "string") {
@@ -627,7 +650,7 @@ class ShinyApp {
if (evt.isDefaultPrevented()) return;
// Send msgObj.foo and msgObj.bar to appropriate handlers
this._sendMessagesToHandlers(
await this._sendMessagesToHandlers(
evt.message,
messageHandlers,
messageHandlerOrder
@@ -640,11 +663,11 @@ class ShinyApp {
// A function for sending messages to the appropriate handlers.
// - msgObj: the object containing messages, with format {msgObj.foo, msObj.bar
private _sendMessagesToHandlers(
private async _sendMessagesToHandlers(
msgObj: { [key: string]: unknown },
handlers: { [key: string]: Handler },
handlerOrder: string[]
): void {
): Promise<void> {
// Dispatch messages to handlers, if handler is present
for (let i = 0; i < handlerOrder.length; i++) {
const msgType = handlerOrder[i];
@@ -652,7 +675,7 @@ class ShinyApp {
if (hasOwnProperty(msgObj, msgType)) {
// Execute each handler with 'this' referring to the present value of
// 'this'
handlers[msgType].call(this, msgObj[msgType]);
await handlers[msgType].call(this, msgObj[msgType]);
}
}
}
@@ -662,7 +685,7 @@ class ShinyApp {
// * Use arrow functions to allow the Types to propagate.
// * However, `_sendMessagesToHandlers()` will adjust the `this` context to the same _`this`_.
addMessageHandler("values", (message: { [key: string]: any }) => {
addMessageHandler("values", async (message: { [key: string]: any }) => {
for (const name in this.$bindings) {
if (hasOwnProperty(this.$bindings, name))
this.$bindings[name].showProgress(false);
@@ -670,17 +693,14 @@ class ShinyApp {
for (const key in message) {
if (hasOwnProperty(message, key)) {
this.receiveOutput(key, message[key]);
await this.receiveOutput(key, message[key]);
}
}
});
addMessageHandler(
"errors",
function (
this: ShinyApp,
message: { [key: string]: ErrorsMessageValue }
) {
(message: { [key: string]: ErrorsMessageValue }) => {
for (const key in message) {
if (hasOwnProperty(message, key))
this.receiveError(key, message[key]);
@@ -725,13 +745,10 @@ class ShinyApp {
addMessageHandler(
"progress",
function (
this: ShinyApp,
message: { type: string; message: { id: string } }
) {
async (message: { type: string; message: { id: string } }) => {
if (message.type && message.message) {
// @ts-expect-error; Unknown values handled with followup if statement
const handler = this.progressHandlers[message.type];
const handler = await this.progressHandlers[message.type];
if (handler) handler.call(this, message.message);
}
@@ -740,13 +757,13 @@ class ShinyApp {
addMessageHandler(
"notification",
(
async (
message:
| { type: "remove"; message: string }
| { type: "show"; message: Parameters<typeof showNotification>[0] }
| { type: void }
) => {
if (message.type === "show") showNotification(message.message);
if (message.type === "show") await showNotification(message.message);
else if (message.type === "remove") removeNotification(message.message);
else throw "Unkown notification type: " + message.type;
}
@@ -754,13 +771,13 @@ class ShinyApp {
addMessageHandler(
"modal",
(
async (
message:
| { type: "remove"; message: string }
| { type: "show"; message: Parameters<typeof showModal>[0] }
| { type: void }
) => {
if (message.type === "show") showModal(message.message);
if (message.type === "show") await showModal(message.message);
// For 'remove', message content isn't used
else if (message.type === "remove") removeModal();
else throw "Unkown modal type: " + message.type;
@@ -860,12 +877,12 @@ class ShinyApp {
addMessageHandler(
"shiny-insert-ui",
(message: {
async (message: {
selector: string;
content: { html: string; deps: HtmlDep[] };
multiple: false | void;
multiple: boolean;
where: WherePosition;
}) => {
}): Promise<void> => {
const targets = $(message.selector);
if (targets.length === 0) {
@@ -877,19 +894,24 @@ class ShinyApp {
message.selector +
'") could not be found in the DOM.'
);
renderHtml(message.content.html, $([]), message.content.deps);
await renderHtmlAsync(
message.content.html,
$([]),
message.content.deps
);
} else {
targets.each(function (i, target) {
renderContent(target, message.content, message.where);
return message.multiple;
});
for (const target of targets) {
await renderContentAsync(target, message.content, message.where);
// If multiple is false, only render to the first target.
if (message.multiple === false) break;
}
}
}
);
addMessageHandler(
"shiny-remove-ui",
(message: { selector: string; multiple: false | void }) => {
(message: { selector: string; multiple: boolean }) => {
const els = $(message.selector);
els.each(function (i, el) {
@@ -897,8 +919,8 @@ class ShinyApp {
$(el).remove();
// If `multiple` is false, returning false terminates the function
// and no other elements are removed; if `multiple` is true,
// returning true continues removing all remaining elements.
return message.multiple;
// returning nothing continues removing all remaining elements.
return message.multiple === false ? false : undefined;
});
}
);
@@ -978,7 +1000,7 @@ class ShinyApp {
addMessageHandler(
"shiny-insert-tab",
(message: {
async (message: {
inputId: string;
divTag: { html: string; deps: HtmlDep[] };
liTag: { html: string; deps: HtmlDep[] };
@@ -986,7 +1008,7 @@ class ShinyApp {
position: "after" | "before" | void;
select: boolean;
menuName: string;
}) => {
}): Promise<void> => {
const $parentTabset = getTabset(message.inputId);
let $tabset = $parentTabset;
const $tabContent = getTabContent($tabset);
@@ -1060,7 +1082,7 @@ class ShinyApp {
}
}
renderContent($liTag[0], {
await renderContentAsync($liTag[0], {
html: $liTag.html(),
deps: message.liTag.deps,
});
@@ -1095,13 +1117,13 @@ class ShinyApp {
// lower-level functions that renderContent uses. Like if we pre-process
// the value of message.divTag.html for singletons, we could do that, then
// render dependencies, then do $tabContent.append($divTag).
renderContent(
await renderContentAsync(
$tabContent[0],
{ html: "", deps: message.divTag.deps },
// @ts-expect-error; TODO-barret; There is no usage of beforeend
"beforeend"
);
$divTag.get().forEach((el) => {
for (const el of $divTag.get()) {
// Must not use jQuery for appending el to the doc, we don't want any
// scripts to run (since they will run when renderContent takes a crack).
$tabContent[0].appendChild(el);
@@ -1110,8 +1132,8 @@ class ShinyApp {
// and not the whole tag. That's fine in this case because we control the
// R code that generates this HTML, and we know that the element is not
// a script tag.
renderContent(el, el.innerHTML || el.textContent);
});
await renderContentAsync(el, el.innerHTML || el.textContent);
}
if (message.select) {
$liTag.find("a").tab("show");
@@ -1379,17 +1401,17 @@ class ShinyApp {
},
// Open a page-level progress bar
open: function (message: {
open: async function (message: {
style: "notification" | "old";
id: string;
}): void {
}): Promise<void> {
if (message.style === "notification") {
// For new-style (starting in Shiny 0.14) progress indicators that use
// the notification API.
// Progress bar starts hidden; will be made visible if a value is provided
// during updates.
showNotification({
await showNotification({
html:
`<div id="shiny-progress-${message.id}" class="shiny-progress-notification">` +
'<div class="progress active" style="display: none;"><div class="progress-bar"></div></div>' +

View File

@@ -0,0 +1,42 @@
// Adapted from https://stackoverflow.com/a/47157945/412655
export class AsyncQueue<T> {
private $promises: Array<Promise<T>> = [];
private $resolvers: Array<(x: T) => void> = [];
private _add() {
const p: Promise<T> = new Promise((resolve) => {
this.$resolvers.push(resolve);
});
this.$promises.push(p);
}
enqueue(x: T): void {
if (!this.$resolvers.length) this._add();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const resolve = this.$resolvers.shift()!;
resolve(x);
}
async dequeue(): Promise<T> {
if (!this.$promises.length) this._add();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const promise = this.$promises.shift()!;
return promise;
}
isEmpty(): boolean {
return !this.$promises.length;
}
isBlocked(): boolean {
return !!this.$resolvers.length;
}
get length(): number {
return this.$promises.length - this.$resolvers.length;
}
}

View File

@@ -0,0 +1,45 @@
type Cb = {
once: boolean;
fn: () => void;
};
type Cbs = {
[key: string]: Cb;
};
class Callbacks {
callbacks: Cbs = {};
id = 0;
register(fn: () => void, once = true): () => void {
this.id += 1;
const id = this.id;
this.callbacks[id] = { fn, once };
return () => {
delete this.callbacks[id];
};
}
invoke(): void {
for (const id in this.callbacks) {
const cb = this.callbacks[id];
try {
cb.fn();
} finally {
if (cb.once) delete this.callbacks[id];
}
}
}
clear(): void {
this.callbacks = {};
}
count(): number {
return Object.keys(this.callbacks).length;
}
}
export { Callbacks };

View File

@@ -1,5 +1,5 @@
import { InputBinding } from "./inputBinding";
declare type ActionButtonReceiveMessageData = {
type ActionButtonReceiveMessageData = {
label?: string;
icon?: string | [];
};

View File

@@ -1,7 +1,7 @@
import { InputBinding } from "./inputBinding";
declare type CheckedHTMLElement = HTMLInputElement;
declare type CheckboxChecked = CheckedHTMLElement["checked"];
declare type CheckboxReceiveMessageData = {
type CheckedHTMLElement = HTMLInputElement;
type CheckboxChecked = CheckedHTMLElement["checked"];
type CheckboxReceiveMessageData = {
value?: CheckboxChecked;
label?: string;
};

View File

@@ -1,16 +1,16 @@
import { InputBinding } from "./inputBinding";
import type { CheckedHTMLElement } from "./checkbox";
declare type CheckboxGroupHTMLElement = CheckedHTMLElement;
declare type ValueLabelObject = {
type CheckboxGroupHTMLElement = CheckedHTMLElement;
type ValueLabelObject = {
value: HTMLInputElement["value"];
label: string;
};
declare type CheckboxGroupReceiveMessageData = {
type CheckboxGroupReceiveMessageData = {
options?: string;
value?: Parameters<CheckboxGroupInputBinding["setValue"]>[1];
label: string;
};
declare type CheckboxGroupValue = CheckboxGroupHTMLElement["value"];
type CheckboxGroupValue = CheckboxGroupHTMLElement["value"];
declare class CheckboxGroupInputBinding extends InputBinding {
find(scope: HTMLElement): JQuery<HTMLElement>;
getValue(el: CheckboxGroupHTMLElement): CheckboxGroupValue[];

View File

@@ -9,7 +9,7 @@ declare global {
bsDatepicker(methodName: string, params: Date | null): void;
}
}
declare type DateReceiveMessageData = {
type DateReceiveMessageData = {
label: string;
min?: Date | null;
max?: Date | null;

View File

@@ -1,6 +1,6 @@
import { formatDateUTC } from "../../utils";
import { DateInputBindingBase } from "./date";
declare type DateRangeReceiveMessageData = {
type DateRangeReceiveMessageData = {
label: string;
min?: Date;
max?: Date;

View File

@@ -1,7 +1,7 @@
import { BindingRegistry } from "../registry";
import { InputBinding } from "./inputBinding";
import { FileInputBinding } from "./fileinput";
declare type InitInputBindings = {
type InitInputBindings = {
inputBindings: BindingRegistry<InputBinding>;
fileInputBinding: FileInputBinding;
};

View File

@@ -1,6 +1,6 @@
import { TextInputBindingBase } from "./text";
declare type NumberHTMLElement = HTMLInputElement;
declare type NumberReceiveMessageData = {
type NumberHTMLElement = HTMLInputElement;
type NumberReceiveMessageData = {
label: string;
value?: string | null;
min?: string | null;

View File

@@ -1,10 +1,10 @@
import { InputBinding } from "./inputBinding";
declare type RadioHTMLElement = HTMLInputElement;
declare type ValueLabelObject = {
type RadioHTMLElement = HTMLInputElement;
type ValueLabelObject = {
value: HTMLInputElement["value"];
label: string;
};
declare type RadioReceiveMessageData = {
type RadioReceiveMessageData = {
value?: string | [];
options?: ValueLabelObject[];
label: string;

View File

@@ -1,18 +1,18 @@
/// <reference types="selectize" />
import { InputBinding } from "./inputBinding";
import type { NotUndefined } from "../../utils/extraTypes";
declare type SelectHTMLElement = HTMLSelectElement & {
type SelectHTMLElement = HTMLSelectElement & {
nonempty: boolean;
};
declare type SelectInputReceiveMessageData = {
type SelectInputReceiveMessageData = {
label: string;
options?: string;
config?: string;
url?: string;
value?: string;
};
declare type SelectizeOptions = Selectize.IOptions<string, unknown>;
declare type SelectizeInfo = Selectize.IApi<string, unknown> & {
type SelectizeOptions = Selectize.IOptions<string, unknown>;
type SelectizeInfo = Selectize.IApi<string, unknown> & {
settings: SelectizeOptions;
};
declare class SelectInputBinding extends InputBinding {

View File

@@ -1,7 +1,7 @@
import type { TextHTMLElement } from "./text";
import { TextInputBindingBase } from "./text";
declare type TimeFormatter = (fmt: string, dt: Date) => string;
declare type SliderReceiveMessageData = {
type TimeFormatter = (fmt: string, dt: Date) => string;
type SliderReceiveMessageData = {
label: string;
value?: Array<number | string> | number | string;
min?: number;

View File

@@ -1,5 +1,5 @@
import { InputBinding } from "./inputBinding";
declare type TabInputReceiveMessageData = {
type TabInputReceiveMessageData = {
value?: string;
};
declare class BootstrapTabInputBinding extends InputBinding {

View File

@@ -1,6 +1,6 @@
import { InputBinding } from "./inputBinding";
declare type TextHTMLElement = HTMLInputElement;
declare type TextReceiveMessageData = {
type TextHTMLElement = HTMLInputElement;
type TextReceiveMessageData = {
label: string;
value?: TextHTMLElement["value"];
placeholder?: TextHTMLElement["placeholder"];

View File

@@ -1,9 +1,9 @@
import { OutputBinding } from "./outputBinding";
import { renderContent } from "../../shiny/render";
import { renderContentAsync } from "../../shiny/render";
import type { ErrorsMessageValue } from "../../shiny/shinyapp";
declare class HtmlOutputBinding extends OutputBinding {
find(scope: HTMLElement): JQuery<HTMLElement>;
onValueError(el: HTMLElement, err: ErrorsMessageValue): void;
renderValue(el: HTMLElement, data: Parameters<typeof renderContent>[1]): void;
renderValue(el: HTMLElement, data: Parameters<typeof renderContentAsync>[1]): Promise<void>;
}
export { HtmlOutputBinding };

View File

@@ -1,6 +1,6 @@
import { BindingRegistry } from "../registry";
import { OutputBinding } from "./outputBinding";
declare type InitOutputBindings = {
type InitOutputBindings = {
outputBindings: BindingRegistry<OutputBinding>;
};
declare function initOutputBindings(): InitOutputBindings;

View File

@@ -2,9 +2,9 @@ import type { ErrorsMessageValue } from "../../shiny/shinyapp";
declare class OutputBinding {
name: string;
find(scope: HTMLElement | JQuery<HTMLElement>): JQuery<HTMLElement>;
renderValue(el: HTMLElement, data: unknown): void;
renderValue(el: HTMLElement, data: unknown): Promise<void> | void;
getId(el: HTMLElement): string;
onValueChange(el: HTMLElement, data: unknown): void;
onValueChange(el: HTMLElement, data: unknown): Promise<void>;
onValueError(el: HTMLElement, err: ErrorsMessageValue): void;
renderError(el: HTMLElement, err: ErrorsMessageValue): void;
clearError(el: HTMLElement): void;

View File

@@ -8,7 +8,7 @@ declare class OutputBindingAdapter {
binding: OutputBinding;
constructor(el: HTMLElement, binding: OutpuBindingWithResize);
getId(): string;
onValueChange(data: unknown): void;
onValueChange(data: unknown): Promise<void>;
onValueError(err: ErrorsMessageValue): void;
showProgress(show: boolean): void;
onResize(): void;

View File

@@ -1,3 +1,4 @@
import { Callbacks } from "../utils/callbacks";
interface BindingBase {
name: string;
}
@@ -12,7 +13,9 @@ declare class BindingRegistry<Binding extends BindingBase> {
bindingNames: {
[key: string]: BindingObj<Binding>;
};
registerCallbacks: Callbacks;
register(binding: Binding, bindingName: string, priority?: number): void;
onRegister(fn: () => void, once?: boolean): void;
setPriority(bindingName: string, priority: number): void;
getPriority(bindingName: string): number | false;
getBindings(): Array<BindingObj<Binding>>;

View File

@@ -1,7 +1,7 @@
import type { JQueryEventHandlerBase } from "bootstrap";
import "jquery";
declare type EvtPrefix<T extends string> = `${T}.${string}`;
declare type EvtFn<T extends JQuery.Event> = ((evt: T) => void) | null | undefined;
type EvtPrefix<T extends string> = `${T}.${string}`;
type EvtFn<T extends JQuery.Event> = ((evt: T) => void) | null | undefined;
declare global {
interface JQuery {
on(events: EvtPrefix<"change">, handler: EvtFn<JQuery.DragEvent>): this;

View File

@@ -1,11 +1,11 @@
import type { ShinyApp } from "../shiny/shinyapp";
declare type JobId = string;
declare type UploadUrl = string;
declare type UploadInitValue = {
type JobId = string;
type UploadUrl = string;
type UploadInitValue = {
jobId: JobId;
uploadUrl: UploadUrl;
};
declare type UploadEndValue = never;
type UploadEndValue = never;
declare class FileProcessor {
files: File[];
fileIndex: number;

View File

@@ -1,15 +1,15 @@
import type { Coordmap } from "./initCoordmap";
import type { Panel } from "./initPanelScales";
import type { Offset } from "./findbox";
declare type Bounds = {
type Bounds = {
xmin: number;
xmax: number;
ymin: number;
ymax: number;
};
declare type BoundsCss = Bounds;
declare type BoundsData = Bounds;
declare type ImageState = {
type BoundsCss = Bounds;
type BoundsData = Bounds;
type ImageState = {
brushing: boolean;
dragging: boolean;
resizing: boolean;
@@ -26,7 +26,7 @@ declare type ImageState = {
panel: Panel | null;
changeStartBounds: Bounds;
};
declare type BrushOpts = {
type BrushOpts = {
brushDirection: "x" | "xy" | "y";
brushClip: boolean;
brushFill: string;
@@ -36,8 +36,9 @@ declare type BrushOpts = {
brushDelay?: number;
brushResetOnNew?: boolean;
};
declare type Brush = {
type Brush = {
reset: () => void;
hasOldBrush: () => boolean;
importOldBrush: () => void;
isInsideBrush: (offsetCss: Offset) => boolean;
isInResizeArea: (offsetCss: Offset) => boolean;

View File

@@ -3,14 +3,14 @@ import type { BoundsCss, Bounds, BrushOpts } from "./createBrush";
import type { Offset } from "./findbox";
import type { Coordmap } from "./initCoordmap";
import type { Panel } from "./initPanelScales";
declare type CreateHandler = {
type CreateHandler = {
mousemove?: (e: JQuery.MouseMoveEvent) => void;
mouseout?: (e: JQuery.MouseOutEvent) => void;
mousedown?: (e: JQuery.MouseDownEvent) => void;
onResetImg: () => void;
onResize: ((e: JQuery.ResizeEvent) => void) | null;
};
declare type BrushInfo = {
type BrushInfo = {
xmin: number;
xmax: number;
ymin: number;
@@ -28,9 +28,9 @@ declare type BrushInfo = {
brushId?: string;
outputId?: string;
};
declare type InputId = Parameters<Coordmap["mouseCoordinateSender"]>[0];
declare type Clip = Parameters<Coordmap["mouseCoordinateSender"]>[1];
declare type NullOutside = Parameters<Coordmap["mouseCoordinateSender"]>[2];
type InputId = Parameters<Coordmap["mouseCoordinateSender"]>[0];
type Clip = Parameters<Coordmap["mouseCoordinateSender"]>[1];
type NullOutside = Parameters<Coordmap["mouseCoordinateSender"]>[2];
declare function createClickHandler(inputId: InputId, clip: Clip, coordmap: Coordmap): CreateHandler;
declare function createHoverHandler(inputId: InputId, delay: number, delayType: string | "throttle", clip: Clip, nullOutside: NullOutside, coordmap: Coordmap): CreateHandler;
declare function createBrushHandler(inputId: InputId, $el: JQuery<HTMLElement>, opts: BrushOpts, coordmap: Coordmap, outputId: BrushInfo["outputId"]): CreateHandler;

View File

@@ -1,5 +1,5 @@
import type { Bounds } from "./createBrush";
declare type Offset = {
type Offset = {
x: number;
y: number;
};

View File

@@ -3,13 +3,13 @@ import type { Offset } from "./findbox";
import type { Bounds } from "./createBrush";
import type { Panel, PanelInit } from "./initPanelScales";
declare function findOrigin($el: JQuery<HTMLElement>): Offset;
declare type OffsetCss = {
type OffsetCss = {
[key: string]: number;
};
declare type OffsetImg = {
type OffsetImg = {
[key: string]: number;
};
declare type CoordmapInit = {
type CoordmapInit = {
panels: PanelInit[];
dims: {
height: number;
@@ -19,7 +19,7 @@ declare type CoordmapInit = {
width: null;
};
};
declare type Coordmap = {
type Coordmap = {
panels: Panel[];
dims: {
height: number;

View File

@@ -1,6 +1,6 @@
import type { Offset } from "./findbox";
import type { Bounds } from "./createBrush";
declare type PanelInit = {
type PanelInit = {
domain: {
top: number;
bottom: number;
@@ -24,7 +24,7 @@ declare type PanelInit = {
[key: string]: number | string;
};
};
declare type Panel = PanelInit & {
type Panel = PanelInit & {
scaleDataToImg: {
(val: Bounds, clip?: boolean): Bounds;
};

View File

@@ -3,11 +3,11 @@ import type { ShinyApp } from "../shiny/shinyapp";
declare class InputBatchSender implements InputPolicy {
target: InputPolicy;
shinyapp: ShinyApp;
timerId: ReturnType<typeof setTimeout> | null;
pendingData: {
[key: string]: unknown;
};
reentrant: boolean;
sendIsEnqueued: boolean;
lastChanceCallback: Array<() => void>;
constructor(shinyapp: ShinyApp);
setInput(nameType: string, value: unknown, opts: InputPolicyOpts): void;

View File

@@ -1,5 +1,5 @@
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
declare type LastSentValues = {
type LastSentValues = {
[key: string]: {
[key: string]: string;
};

View File

@@ -1,6 +1,6 @@
import type { InputBinding } from "../bindings";
declare type EventPriority = "deferred" | "event" | "immediate";
declare type InputPolicyOpts = {
type EventPriority = "deferred" | "event" | "immediate";
type InputPolicyOpts = {
priority: EventPriority;
el?: HTMLElement;
binding?: InputBinding;

View File

@@ -1,6 +1,6 @@
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
import type { InputRatePolicy } from "./inputRatePolicy";
declare type RatePolicyModes = "debounce" | "direct" | "throttle";
type RatePolicyModes = "debounce" | "direct" | "throttle";
declare class InputRateDecorator implements InputPolicy {
target: InputPolicy;
inputRatePolicies: {

View File

@@ -1,8 +1,8 @@
import type { InputBinding, OutputBinding } from "../bindings";
import type { BindingRegistry } from "../bindings/registry";
import type { InputRateDecorator, InputValidateDecorator } from "../inputPolicies";
declare type BindScope = HTMLElement | JQuery<HTMLElement>;
declare type BindInputsCtx = {
type BindScope = HTMLElement | JQuery<HTMLElement>;
type BindInputsCtx = {
inputs: InputValidateDecorator;
inputsRate: InputRateDecorator;
inputBindings: BindingRegistry<InputBinding>;

View File

@@ -4,7 +4,7 @@ import { $escape, compareVersion } from "../utils";
import { showNotification, removeNotification } from "./notifications";
import { showModal, removeModal } from "./modal";
import { showReconnectDialog, hideReconnectDialog } from "./reconnectDialog";
import { renderContent, renderDependencies, renderHtml } from "./render";
import { renderContentAsync, renderContent, renderDependenciesAsync, renderDependencies, renderHtmlAsync, renderHtml } from "./render";
import type { shinyBindAll, shinyForgetLastInputValue, shinySetInputValue, shinyInitializeInputs, shinyUnbindAll } from "./initedMethods";
import type { Handler, ShinyApp } from "./shinyapp";
import { addCustomMessageHandler } from "./shinyapp";
@@ -30,8 +30,11 @@ interface Shiny {
createSocket?: () => WebSocket;
showReconnectDialog: typeof showReconnectDialog;
hideReconnectDialog: typeof hideReconnectDialog;
renderDependenciesAsync: typeof renderDependenciesAsync;
renderDependencies: typeof renderDependencies;
renderContentAsync: typeof renderContentAsync;
renderContent: typeof renderContent;
renderHtmlAsync: typeof renderHtmlAsync;
renderHtml: typeof renderHtml;
user: string;
progressHandlers?: ShinyApp["progressHandlers"];

View File

@@ -1,6 +1,7 @@
declare function show({ html, deps }?: {
html?: string | undefined;
deps?: never[] | undefined;
}): void;
import type { HtmlDep } from "./render";
declare function show({ html, deps, }?: {
html?: string;
deps?: HtmlDep[];
}): Promise<void>;
declare function remove(): void;
export { show as showModal, remove as removeModal };

View File

@@ -8,6 +8,6 @@ declare function show({ html, action, deps, duration, id, closeButton, type, }?:
id?: string | null;
closeButton?: boolean;
type?: string | null;
}): ReturnType<typeof randomId>;
}): Promise<ReturnType<typeof randomId>>;
declare function remove(id: string): void;
export { show as showNotification, remove as removeNotification };

View File

@@ -1,33 +1,39 @@
import type { BindScope } from "./bind";
import { renderHtml as singletonsRenderHtml } from "./singletons";
import type { WherePosition } from "./singletons";
declare function renderDependencies(dependencies: HtmlDep[] | null): void;
declare function renderContentAsync(el: BindScope, content: string | {
html: string;
deps?: HtmlDep[];
} | null, where?: WherePosition): Promise<void>;
declare function renderContent(el: BindScope, content: string | {
html: string;
deps?: HtmlDep[];
} | null, where?: WherePosition): void;
declare function renderHtmlAsync(html: string, el: BindScope, dependencies: HtmlDep[], where?: WherePosition): Promise<ReturnType<typeof singletonsRenderHtml>>;
declare function renderHtml(html: string, el: BindScope, dependencies: HtmlDep[], where?: WherePosition): ReturnType<typeof singletonsRenderHtml>;
declare type HtmlDepVersion = string;
declare type MetaItem = {
declare function renderDependenciesAsync(dependencies: HtmlDep[] | null): Promise<void>;
declare function renderDependencies(dependencies: HtmlDep[] | null): void;
type HtmlDepVersion = string;
type MetaItem = {
name: string;
content: string;
[x: string]: string;
};
declare type StylesheetItem = {
type StylesheetItem = {
href: string;
rel?: string;
type?: string;
};
declare type ScriptItem = {
type ScriptItem = {
src: string;
[x: string]: string;
};
declare type AttachmentItem = {
type AttachmentItem = {
key: string;
href: string;
[x: string]: string;
};
declare type HtmlDep = {
type HtmlDep = {
name: string;
version: HtmlDepVersion;
restyle?: boolean;
@@ -45,5 +51,5 @@ declare type HtmlDep = {
head?: string;
};
declare function registerDependency(name: string, version: HtmlDepVersion): void;
export { renderDependencies, renderContent, renderHtml, registerDependency };
export { renderContentAsync, renderContent, renderHtmlAsync, renderHtml, renderDependenciesAsync, renderDependencies, registerDependency, };
export type { HtmlDep };

View File

@@ -1,24 +1,26 @@
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { UploadInitValue, UploadEndValue } from "../file/fileProcessor";
declare type ResponseValue = UploadEndValue | UploadInitValue;
declare type Handler = (message: any) => void;
declare type ShinyWebSocket = WebSocket & {
import { AsyncQueue } from "../utils/asyncQueue";
type ResponseValue = UploadEndValue | UploadInitValue;
type Handler = (message: any) => Promise<void> | void;
type ShinyWebSocket = WebSocket & {
allowReconnect?: boolean;
};
declare type ErrorsMessageValue = {
type ErrorsMessageValue = {
message: string;
call: string[];
type?: string[];
};
declare type OnSuccessRequest = (value: ResponseValue) => void;
declare type OnErrorRequest = (err: string) => void;
declare type InputValues = {
type OnSuccessRequest = (value: ResponseValue) => void;
type OnErrorRequest = (err: string) => void;
type InputValues = {
[key: string]: unknown;
};
declare type MessageValue = Parameters<WebSocket["send"]>[0];
type MessageValue = Parameters<WebSocket["send"]>[0];
declare function addCustomMessageHandler(type: string, handler: Handler): void;
declare class ShinyApp {
$socket: ShinyWebSocket | null;
actionQueue: AsyncQueue<() => Promise<void> | void>;
config: {
workerId: string;
sessionId: string;
@@ -50,6 +52,7 @@ declare class ShinyApp {
private scheduledReconnect;
reconnect(): void;
createSocket(): ShinyWebSocket;
startActionQueueLoop(): Promise<void>;
sendInput(values: InputValues): void;
$notifyDisconnected(): void;
$removeSocket(): void;
@@ -63,13 +66,13 @@ declare class ShinyApp {
makeRequest(method: string, args: unknown[], onSuccess: OnSuccessRequest, onError: OnErrorRequest, blobs: Array<ArrayBuffer | Blob | string> | undefined): void;
$sendMsg(msg: MessageValue): void;
receiveError(name: string, error: ErrorsMessageValue): void;
receiveOutput<T>(name: string, value: T): T | undefined;
receiveOutput<T>(name: string, value: T): Promise<T | undefined>;
bindOutput(id: string, binding: OutputBindingAdapter): OutputBindingAdapter;
unbindOutput(id: string, binding: OutputBindingAdapter): boolean;
private _narrowScopeComponent;
private _narrowScope;
$updateConditionals(): void;
dispatchMessage(data: ArrayBufferLike | string): void;
dispatchMessage(data: ArrayBufferLike | string): Promise<void>;
private _sendMessagesToHandlers;
private _init;
progressHandlers: {
@@ -79,7 +82,7 @@ declare class ShinyApp {
open: (message: {
style: "notification" | "old";
id: string;
}) => void;
}) => Promise<void>;
update: (message: {
style: "notification" | "old";
id: string;

View File

@@ -2,7 +2,7 @@ import type { BindScope } from "./bind";
declare const knownSingletons: {
[key: string]: boolean;
};
declare type WherePosition = "afterBegin" | "afterEnd" | "beforeBegin" | "beforeEnd" | "replace";
type WherePosition = "afterBegin" | "afterEnd" | "beforeBegin" | "beforeEnd" | "replace";
declare function renderHtml(html: string, el: BindScope, where: WherePosition): ReturnType<typeof processHtml>;
declare function registerNames(s: string[] | string): void;
declare function processHtml(val: string): {

10
srcts/types/src/utils/asyncQueue.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
export declare class AsyncQueue<T> {
private $promises;
private $resolvers;
private _add;
enqueue(x: T): void;
dequeue(): Promise<T>;
isEmpty(): boolean;
isBlocked(): boolean;
get length(): number;
}

16
srcts/types/src/utils/callbacks.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
type Cb = {
once: boolean;
fn: () => void;
};
type Cbs = {
[key: string]: Cb;
};
declare class Callbacks {
callbacks: Cbs;
id: number;
register(fn: () => void, once?: boolean): () => void;
invoke(): void;
clear(): void;
count(): number;
}
export { Callbacks };

View File

@@ -1,11 +1,11 @@
declare type AnyFunction = (...args: any[]) => any;
declare type AnyVoidFunction = (...args: any[]) => void;
declare type MapValuesUnion<T> = T[keyof T];
declare type MapWithResult<X, R> = {
type AnyFunction = (...args: any[]) => any;
type AnyVoidFunction = (...args: any[]) => void;
type MapValuesUnion<T> = T[keyof T];
type MapWithResult<X, R> = {
[Property in keyof X]: R;
};
/**
* Exclude undefined from T
*/
declare type NotUndefined<T> = T extends undefined ? never : T;
type NotUndefined<T> = T extends undefined ? never : T;
export type { AnyFunction, AnyVoidFunction, MapValuesUnion, MapWithResult, NotUndefined, };

View File

@@ -1,4 +1,4 @@
declare type UserAgent = typeof window.navigator.userAgent;
type UserAgent = typeof window.navigator.userAgent;
declare let userAgent: UserAgent;
declare function setUserAgent(userAgent_: UserAgent): void;
export type { UserAgent };

View File

@@ -142,6 +142,41 @@
<div class="content-footer"></div>
</div>
---
Code
dropdown_active
Output
<div class="tabbable">
<ul class="nav nav-tabs" data-tabsetid="4785">
<li>
<a href="#tab-4785-1" data-toggle="tab" data-bs-toggle="tab" data-value="A">A</a>
</li>
<li>
<a href="#tab-4785-2" data-toggle="tab" data-bs-toggle="tab" data-value="B">
<i aria-label="github icon" class="fab fa-github fa-fw" role="presentation"></i>
B
</a>
</li>
<li class="dropdown active">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-bs-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li class="active">
<a href="#tab-1502-1" data-toggle="tab" data-bs-toggle="tab" data-value="C">C</a>
</li>
</ul>
</li>
</ul>
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github fa-fw" id="tab-4785-2">b</div>
<div class="tab-pane active" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
# navbarPage() markup is correct
Code

View File

@@ -126,6 +126,23 @@ test_that("ReactiveValues", {
expect_error(values$a <- 1)
})
test_that("reactiveValues keys are sorted", {
values <- reactiveValues(b=2, a=0)
values$C <- 13
values$A <- 0
values$c <- 3
values$B <- 12
# Setting an existing value shouldn't change order
values$a <- 1
values$A <- 11
expect_identical(isolate(names(values)), c("b", "a", "C", "A", "c", "B"))
expect_identical(
isolate(reactiveValuesToList(values)),
list(b=2, a=1, C=13, A=11, c=3, B=12)
)
})
test_that("reactiveValues() has useful print method", {
verify_output(test_path("print-reactiveValues.txt"), {
x <- reactiveValues(x = 1, y = 2, z = 3)

View File

@@ -41,7 +41,6 @@ panels <- list(
)
test_that("tabsetPanel() markup is correct", {
default <- tabset_panel(!!!panels)
pills <- tabset_panel(
!!!panels, type = "pills", selected = "B",
@@ -54,6 +53,11 @@ test_that("tabsetPanel() markup is correct", {
# BS4
expect_snapshot_bslib(default)
expect_snapshot_bslib(pills)
# Make sure .active class gets added to both the .dropdown as well as the
# .dropdown-menu's tab
dropdown_active <- tabset_panel(!!!panels, selected = "C")
expect_snapshot2(dropdown_active)
})
test_that("navbarPage() markup is correct", {

View File

@@ -218,3 +218,7 @@ reference:
- shinyUI
- shinyServer
- exprToFunction
# This section is silently dropped by pkgdown https://github.com/r-lib/pkgdown/pull/1783
- title: internal
contents:
- shiny-package

View File

@@ -2,6 +2,15 @@ diff --git a/inst/www/shared/ionrangeslider/js/ion.rangeSlider.js b/inst/www/sha
index 2fe2c8d..89d204e 100644
--- a/inst/www/shared/ionrangeslider/js/ion.rangeSlider.js
+++ b/inst/www/shared/ionrangeslider/js/ion.rangeSlider.js
@@ -753,7 +753,7 @@
x = $handle.offset().left;
x += ($handle.width() / 2) - 1;
- this.pointerClick("single", {preventDefault: function () {}, pageX: x});
+ this.pointerClick("single", {preventDefault: function () {}, stopPropagation: function () {}, pageX: x});
}
},
@@ -816,6 +816,7 @@
*/
pointerDown: function (target, e) {

View File

@@ -1,12 +1,13 @@
{
"declaration": true,
"compilerOptions": {
"target": "ES5",
"target": "es2020",
"isolatedModules": true,
"esModuleInterop": true,
"declaration": true,
"declarationDir": "./srcts/types",
"emitDeclarationOnly": true,
"moduleResolution": "node",
// Can not use `types: []` to disable injecting NodeJS types. More types are
// needed than just the DOM's `window.setTimeout`
// "types": [],

4528
yarn.lock

File diff suppressed because it is too large Load Diff