mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 07:58:11 -05:00
Compare commits
46 Commits
init-promi
...
fix/bindin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42b6b8bdde | ||
|
|
4a01dde46b | ||
|
|
6c281f377c | ||
|
|
33d6686223 | ||
|
|
20b2669e76 | ||
|
|
db123a1508 | ||
|
|
0952f3e0a7 | ||
|
|
13ca8dfc57 | ||
|
|
79f42f5846 | ||
|
|
9a35b01e23 | ||
|
|
5bf0701939 | ||
|
|
e5083f4938 | ||
|
|
ce6a562a3c | ||
|
|
b6bcfc8683 | ||
|
|
d37beeece7 | ||
|
|
79ee25620f | ||
|
|
82c678a1eb | ||
|
|
458924569a | ||
|
|
501b012b2b | ||
|
|
ee1aac847a | ||
|
|
7785a76a67 | ||
|
|
79af1d6c92 | ||
|
|
a145add5d4 | ||
|
|
abf71389be | ||
|
|
2e2114f99d | ||
|
|
09d415502f | ||
|
|
c489fef4ff | ||
|
|
9d12b0fca7 | ||
|
|
cc9b9d4e6a | ||
|
|
34f9e4484d | ||
|
|
03a3f8f886 | ||
|
|
b900db0c74 | ||
|
|
5fb3ebc2d9 | ||
|
|
fbc6b2df57 | ||
|
|
6208225354 | ||
|
|
e22b693418 | ||
|
|
c7ca49c634 | ||
|
|
d84aa94762 | ||
|
|
89e2c18531 | ||
|
|
43d36c08dc | ||
|
|
4bc330e5dd | ||
|
|
56ab530d87 | ||
|
|
599209a036 | ||
|
|
15b5fa6c01 | ||
|
|
3f4676d9a6 | ||
|
|
bb89cf9235 |
@@ -35,10 +35,6 @@ rules:
|
||||
|
||||
default-case:
|
||||
- error
|
||||
indent:
|
||||
- error
|
||||
- 2
|
||||
- SwitchCase: 1
|
||||
linebreak-style:
|
||||
- error
|
||||
- unix
|
||||
|
||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"arcanis.vscode-zipfs",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 1.8.1.9001
|
||||
Version: 1.10.0.9000
|
||||
Authors@R: c(
|
||||
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@posit.co", comment = c(ORCID = "0000-0002-1576-2126")),
|
||||
person("Joe", "Cheng", role = "aut", email = "joe@posit.co"),
|
||||
@@ -83,7 +83,7 @@ Imports:
|
||||
R6 (>= 2.0),
|
||||
sourcetools,
|
||||
later (>= 1.0.0),
|
||||
promises (>= 1.1.0),
|
||||
promises (>= 1.3.2),
|
||||
tools,
|
||||
crayon,
|
||||
rlang (>= 0.4.10),
|
||||
@@ -95,6 +95,7 @@ Imports:
|
||||
cachem (>= 1.1.0),
|
||||
lifecycle (>= 0.2.0)
|
||||
Suggests:
|
||||
coro (>= 1.1.0),
|
||||
datasets,
|
||||
DT,
|
||||
Cairo (>= 1.5-5),
|
||||
|
||||
68
NEWS.md
68
NEWS.md
@@ -1,8 +1,62 @@
|
||||
# shiny (development version)
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Fixed a bug with modals where calling `removeModal()` too quickly after `showModal()` would fail to remove the modal if the remove modal message was received while the modal was in the process of being revealed. (#4173)
|
||||
|
||||
# shiny 1.10.0
|
||||
|
||||
## New features and improvements
|
||||
|
||||
* Added new functions, `useBusyIndicators()` and `busyIndicatorOptions()`, for enabling and customizing busy indication. Busy indicators provide a visual cue to users when the server is busy calculating outputs or otherwise serving requests to the client. When enabled, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. (#4040)
|
||||
* When busy indicators are enabled (i.e., `useBusyIndicators()` is in the UI), Shiny now:
|
||||
* Shows the pulse indicator when dynamic UI elements are recalculating and no other spinners are visible in the app. (#4137)
|
||||
* Makes the pulse indicator slightly smaller by default and improves its appearance to better blend with any background. (#4122)
|
||||
|
||||
* Improve collection of deep stack traces (stack traces that are tracked across steps in an async promise chain) with `{coro}` async generators such as `{elmer}` chat streams. Previously, Shiny treated each iteration of an async generator as a distinct deep stack, leading to pathologically long stack traces; now, Shiny only keeps/prints unique deep stack trace, discarding duplicates. (#4156)
|
||||
|
||||
* Added an example to the `ExtendedTask` documentation. (@daattali #4087)
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Fixed a bug in `conditionalPanel()` that would cause the panel to repeatedly show/hide itself when the provided condition was not boolean. (@kamilzyla, #4127)
|
||||
|
||||
* Fixed a bug with `sliderInput()` when used as a range slider that made it impossible to change the slider value when both handles were at the maximum value. (#4131)
|
||||
|
||||
* `dateInput()` and `dateRangeInput()` no longer send immediate updates to the server when the user is typing a date input. Instead, it waits until the user presses Enter or clicks out of the field to send the update, avoiding spurious and incorrect date values. Note that an update is still sent immediately when the field is cleared. (#3664)
|
||||
|
||||
* Fixed a bug in `onBookmark()` hook that caused elements to not be excluded from URL bookmarking. (#3762)
|
||||
|
||||
* Fixed a bug with stack trace capturing that caused reactives with very long async promise chains (hundreds/thousands of steps) to become extremely slow. Chains this long are unlikely to be written by hand, but `{coro}` async generators and `{elmer}` async streaming were easily creating problematically long chains. (#4155)
|
||||
|
||||
* Duplicate input and output IDs -- e.g. using `"debug"` for two inputs or two outputs -- or shared IDs -- e.g. using `"debug"` as the `inputId` for an input and an output -- now result in a console warning message, but not an error. When `devmode()` is enabled, an informative message is shown in the Shiny Client Console. We recommend all Shiny devs enable `devmode()` when developing Shiny apps locally. (#4101)
|
||||
|
||||
* Updating the choices of a `selectizeInput()` via `updateSelectizeInput()` with `server = TRUE` no longer retains the selected choice as a deselected option if the current value is not part of the new choices. (@dvg-p4 #4142)
|
||||
|
||||
* Fixed a bug where stack traces from `observeEvent()` were being stripped of stack frames too aggressively. (#4163)
|
||||
|
||||
# shiny 1.9.1
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Fixed a bug introduced in v1.9.0 where the boundaries of hover/click/brush regions on plots were being incorrectly scaled when browser zoom was used. (#4111)
|
||||
|
||||
# shiny 1.9.0
|
||||
|
||||
## New busy indication feature
|
||||
|
||||
Add the new `useBusyIndicators()` function to any UI definition to:
|
||||
1. Add a spinner overlay on calculating/recalculating outputs.
|
||||
2. Show a page-level pulsing banner when Shiny is busy calculating something (e.g., a download, side-effect, etc), but no calculating/recalculating outputs are visible.
|
||||
|
||||
In a future version of Shiny, busy indication will be enabled by default, so we encourage you to try it out now, provide feedback, and report any issues.
|
||||
|
||||
In addition, various properties of the spinners and pulse can be customized with `busyIndicatorOptions()`. For more details, see `?busyIndicatorOptions`. (#4040, #4104)
|
||||
|
||||
## New features and improvements
|
||||
|
||||
* The client-side TypeScript code for Shiny has been refactored so that the `Shiny` object is now an instance of class `ShinyClass`. (#4063)
|
||||
|
||||
* In TypeScript, the `Shiny` object has a new property `initializedPromise`, which is a Promise-like object that can be `await`ed or chained with `.then()`. This Promise-like object corresponds to the `shiny:sessioninitialized` JavaScript event, but is easier to use because it can be used both before and after the events have occurred. (#4063)
|
||||
|
||||
* Output bindings now include the `.recalculating` CSS class when they are first bound, up until the first render. This makes it possible/easier to show progress indication when the output is calculating for the first time. (#4039)
|
||||
|
||||
@@ -18,6 +72,8 @@
|
||||
|
||||
* Fixed a recent issue with `uiOutput()` and `conditionalPanel()` not properly lower opacity when recalculation (in a Bootstrap 5 context). (#4027)
|
||||
|
||||
* Image outputs that were scaled by CSS had certain regions that were unresponsive to hover/click/brush handlers. (#3234)
|
||||
|
||||
# shiny 1.8.1.1
|
||||
|
||||
* In v1.8.1, shiny.js starting throwing an error when input/output bindings have duplicate IDs. This error is now only thrown when `shiny::devmode(TRUE)` is enabled, so the issue is still made discoverable through the JS error console, but avoids unnecessarily breaking apps that happen to work with duplicate IDs. (#4019)
|
||||
@@ -1000,7 +1056,7 @@ Shiny can now display notifications on the client browser by using the `showNoti
|
||||
<img src="http://shiny.rstudio.com/images/notification.png" alt="notification" width="50%"/>
|
||||
</p>
|
||||
|
||||
[Here](https://shiny.rstudio.com/articles/notifications.html)'s our article about it, and the [reference documentation](https://shiny.rstudio.com/reference/shiny/latest/showNotification.html).
|
||||
[Here](https://shiny.rstudio.com/articles/notifications.html)'s our article about it, and the [reference documentation](https://shiny.posit.co/r/reference/shiny/latest/shownotification.html).
|
||||
|
||||
## Progress indicators
|
||||
|
||||
@@ -1009,7 +1065,7 @@ If your Shiny app contains computations that take a long time to complete, a pro
|
||||
**_Important note_:**
|
||||
> If you were already using progress bars and had customized them with your own CSS, you can add the `style = "old"` argument to your `withProgress()` call (or `Progress$new()`). This will result in the same appearance as before. You can also call `shinyOptions(progress.style = "old")` in your app's server function to make all progress indicators use the old styling.
|
||||
|
||||
To see new progress bars in action, see [this app](https://gallery.shinyapps.io/085-progress/) in the gallery. You can also learn more about this in [our article](https://shiny.rstudio.com/articles/progress.html) and in the reference documentation (either for the easier [`withProgress` functional API](https://shiny.rstudio.com/reference/shiny/latest/withProgress.html) or the more complicated, but more powerful, [`Progress` object-oriented API](https://shiny.rstudio.com/reference/shiny/latest/Progress.html).
|
||||
To see new progress bars in action, see [this app](https://gallery.shinyapps.io/085-progress/) in the gallery. You can also learn more about this in [our article](https://shiny.rstudio.com/articles/progress.html) and in the reference documentation (either for the easier [`withProgress` functional API](https://shiny.posit.co/r/reference/shiny/latest/withprogress.html) or the more complicated, but more powerful, [`Progress` object-oriented API](https://shiny.posit.co/r/reference/shiny/latest/progress.html).
|
||||
|
||||
## Reconnection
|
||||
|
||||
@@ -1023,7 +1079,7 @@ Shiny has now built-in support for displaying modal dialogs like the one below (
|
||||
<img src="http://shiny.rstudio.com/images/modal-dialog.png" alt="modal-dialog" width="50%"/>
|
||||
</p>
|
||||
|
||||
To learn more about this, read [our article](https://shiny.rstudio.com/articles/modal-dialogs.html) and the [reference documentation](https://shiny.rstudio.com/reference/shiny/latest/modalDialog.html).
|
||||
To learn more about this, read [our article](https://shiny.rstudio.com/articles/modal-dialogs.html) and the [reference documentation](https://shiny.posit.co/r/reference/shiny/latest/modaldialog.html).
|
||||
|
||||
## `insertUI` and `removeUI`
|
||||
|
||||
@@ -1031,7 +1087,7 @@ Sometimes in a Shiny app, arbitrary HTML UI may need to be created on-the-fly in
|
||||
|
||||
See [this simple demo app](https://gallery.shinyapps.io/111-insert-ui/) of how one could use `insertUI` and `removeUI` to insert and remove text elements using a queue. Also see [this other app](https://gallery.shinyapps.io/insertUI/) that demonstrates how to insert and remove a few common Shiny input objects. Finally, [this app](https://gallery.shinyapps.io/insertUI-modules/) shows how to dynamically insert modules using `insertUI`.
|
||||
|
||||
For more, read [our article](https://shiny.rstudio.com/articles/dynamic-ui.html) about dynamic UI generation and the reference documentation about [`insertUI`](https://shiny.rstudio.com/reference/shiny/latest/insertUI.html) and [`removeUI`](https://shiny.rstudio.com/reference/shiny/latest/insertUI.html).
|
||||
For more, read [our article](https://shiny.rstudio.com/articles/dynamic-ui.html) about dynamic UI generation and the reference documentation about [`insertUI`](https://shiny.posit.co/r/reference/shiny/latest/insertui.html) and [`removeUI`](https://shiny.posit.co/r/reference/shiny/latest/insertui.html).
|
||||
|
||||
## Documentation for connecting to an external database
|
||||
|
||||
@@ -1065,7 +1121,7 @@ There are many more minor features, small improvements, and bug fixes than we ca
|
||||
<img src="http://shiny.rstudio.com/images/render-table.png" alt="render-table" width="75%"/>
|
||||
</p>
|
||||
|
||||
For more, read our [short article](https://shiny.rstudio.com/articles/render-table.html) about this update, experiment with all the new features in this [demo app](https://gallery.shinyapps.io/109-render-table/), or check out the [reference documentation](https://shiny.rstudio.com/reference/shiny/latest/renderTable.html).
|
||||
For more, read our [short article](https://shiny.rstudio.com/articles/render-table.html) about this update, experiment with all the new features in this [demo app](https://gallery.shinyapps.io/109-render-table/), or check out the [reference documentation](https://shiny.posit.co/r/reference/shiny/latest/rendertable.html).
|
||||
|
||||
## Full changelog
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
|
||||
#' cache by putting this at the top of your app.R, server.R, or global.R:
|
||||
#'
|
||||
#' ```
|
||||
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache")))
|
||||
#' ```
|
||||
#'
|
||||
#' This will create a subdirectory in your system temp directory named
|
||||
|
||||
@@ -99,13 +99,13 @@ saveShinySaveState <- function(state) {
|
||||
|
||||
# Encode the state to a URL. This does not save to disk.
|
||||
encodeShinySaveState <- function(state) {
|
||||
exclude <- c(state$exclude, "._bookmark_")
|
||||
inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
|
||||
|
||||
# Allow user-supplied onSave function to do things like add state$values.
|
||||
if (!is.null(state$onSave))
|
||||
isolate(state$onSave(state))
|
||||
|
||||
exclude <- c(state$exclude, "._bookmark_")
|
||||
inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
|
||||
|
||||
inputVals <- vapply(inputVals,
|
||||
function(x) toJSON(x, strict_atomic = FALSE),
|
||||
character(1),
|
||||
|
||||
@@ -172,9 +172,10 @@ setCurrentTheme <- function(theme) {
|
||||
|
||||
#' Register a theme dependency
|
||||
#'
|
||||
#' This function registers a function that returns an [htmlDependency()] or list
|
||||
#' of such objects. If `session$setCurrentTheme()` is called, the function will
|
||||
#' be re-executed, and the resulting html dependency will be sent to the client.
|
||||
#' This function registers a function that returns an
|
||||
#' [htmltools::htmlDependency()] or list of such objects. If
|
||||
#' `session$setCurrentTheme()` is called, the function will be re-executed, and
|
||||
#' the resulting html dependency will be sent to the client.
|
||||
#'
|
||||
#' Note that `func` should **not** be an anonymous function, or a function which
|
||||
#' is defined within the calling function. This is so that,
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#' output.
|
||||
#' @param pulse Whether to show a pulsing banner at the top of the page when the
|
||||
#' app is busy.
|
||||
#' @param fade Whether to fade recalculating outputs. A value of `FALSE` is
|
||||
#' equivalent to `busyIndicatorOptions(fade_opacity=1)`.
|
||||
#'
|
||||
#' @export
|
||||
#' @seealso [busyIndicatorOptions()] for customizing the appearance of the busy
|
||||
@@ -48,7 +50,7 @@
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
|
||||
useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE, fade = TRUE) {
|
||||
|
||||
rlang::check_dots_empty()
|
||||
|
||||
@@ -62,20 +64,33 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
|
||||
}
|
||||
})
|
||||
|
||||
js <- HTML(paste(js, collapse = "\n"))
|
||||
|
||||
# TODO: it'd be nice if htmltools had something like a page_attrs() that allowed us
|
||||
# to do this without needing to inject JS into the head.
|
||||
tags$script(js)
|
||||
res <- tags$script(HTML(paste(js, collapse = "\n")))
|
||||
|
||||
if (!fade) {
|
||||
res <- tagList(res, fadeOptions(opacity = 1))
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#' Customize busy indicator options
|
||||
#'
|
||||
#' When busy indicators are enabled (see [useBusyIndicators()]), a spinner is
|
||||
#' shown on each calculating/recalculating output, and a pulsing banner is shown
|
||||
#' at the top of the page when the app is otherwise busy. This function allows
|
||||
#' you to customize the appearance of those busy indicators. To apply the
|
||||
#' customization, include the result of this function inside the app's UI.
|
||||
#' @description
|
||||
#' Shiny automatically includes busy indicators, which more specifically means:
|
||||
#' 1. Calculating/recalculating outputs have a spinner overlay.
|
||||
#' 2. Outputs fade out/in when recalculating.
|
||||
#' 3. When no outputs are calculating/recalculating, but Shiny is busy
|
||||
#' doing something else (e.g., a download, side-effect, etc), a page-level
|
||||
#' pulsing banner is shown.
|
||||
#'
|
||||
#' This function allows you to customize the appearance of these busy indicators
|
||||
#' by including the result of this function inside the app's UI. Note that,
|
||||
#' unless `spinner_selector` (or `fade_selector`) is specified, the spinner/fade
|
||||
#' customization applies to the parent element. If the customization should
|
||||
#' instead apply to the entire page, set `spinner_selector = 'html'` and
|
||||
#' `fade_selector = 'html'`.
|
||||
#'
|
||||
#' @param ... Currently ignored.
|
||||
#' @param spinner_type The type of spinner. Pre-bundled types include:
|
||||
@@ -97,6 +112,11 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
|
||||
#' @param spinner_selector A character string containing a CSS selector for
|
||||
#' scoping the spinner customization. The default (`NULL`) will apply the
|
||||
#' spinner customization to the parent element of the spinner.
|
||||
#' @param fade_opacity The opacity (a number between 0 and 1) for recalculating
|
||||
#' output. Set to 1 to "disable" the fade.
|
||||
#' @param fade_selector A character string containing a CSS selector for
|
||||
#' scoping the spinner customization. The default (`NULL`) will apply the
|
||||
#' spinner customization to the parent element of the spinner.
|
||||
#' @param pulse_background A CSS background definition for the pulse. The
|
||||
#' default uses a
|
||||
#' [linear-gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient)
|
||||
@@ -107,7 +127,7 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
|
||||
#' time.
|
||||
#'
|
||||
#' @export
|
||||
#' @seealso [useBusyIndicators()] for enabling/disabling busy indicators.
|
||||
#' @seealso [useBusyIndicators()] to disable/enable busy indicators.
|
||||
#' @examplesIf rlang::is_interactive()
|
||||
#'
|
||||
#' library(bslib)
|
||||
@@ -162,6 +182,8 @@ busyIndicatorOptions <- function(
|
||||
spinner_size = NULL,
|
||||
spinner_delay = NULL,
|
||||
spinner_selector = NULL,
|
||||
fade_opacity = NULL,
|
||||
fade_selector = NULL,
|
||||
pulse_background = NULL,
|
||||
pulse_height = NULL,
|
||||
pulse_speed = NULL
|
||||
@@ -177,6 +199,7 @@ busyIndicatorOptions <- function(
|
||||
delay = spinner_delay,
|
||||
selector = spinner_selector
|
||||
),
|
||||
fadeOptions(opacity = fade_opacity, selector = fade_selector),
|
||||
pulseOptions(
|
||||
background = pulse_background,
|
||||
height = pulse_height,
|
||||
@@ -224,6 +247,26 @@ spinnerOptions <- function(type = NULL, color = NULL, size = NULL, delay = NULL,
|
||||
tags$style(css, id = id)
|
||||
}
|
||||
|
||||
fadeOptions <- function(opacity = NULL, selector = NULL) {
|
||||
if (is.null(opacity) && is.null(selector)) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
css_vars <- htmltools::css(
|
||||
"--shiny-fade-opacity" = opacity
|
||||
)
|
||||
|
||||
id <- NULL
|
||||
if (is.null(selector)) {
|
||||
id <- paste0("fade-options-", p_randomInt(100, 1000000))
|
||||
selector <- sprintf(":has(> #%s)", id)
|
||||
}
|
||||
|
||||
css <- HTML(paste0(selector, " {", css_vars, "}"))
|
||||
|
||||
tags$style(css, id = id)
|
||||
}
|
||||
|
||||
pulseOptions <- function(background = NULL, height = NULL, speed = NULL) {
|
||||
if (is.null(background) && is.null(height) && is.null(speed)) {
|
||||
return(NULL)
|
||||
@@ -244,6 +287,8 @@ busyIndicatorDependency <- function() {
|
||||
version = get_package_version("shiny"),
|
||||
src = "www/shared/busy-indicators",
|
||||
package = "shiny",
|
||||
stylesheet = "busy-indicators.css"
|
||||
stylesheet = "busy-indicators.css",
|
||||
# TODO-future: In next release make spinners and pulse opt-out
|
||||
# head = as.character(useBusyIndicators())
|
||||
)
|
||||
}
|
||||
|
||||
239
R/conditions.R
239
R/conditions.R
@@ -130,6 +130,44 @@ captureStackTraces <- function(expr) {
|
||||
#' @include globals.R
|
||||
.globals$deepStack <- NULL
|
||||
|
||||
getCallStackDigest <- function(callStack, warn = FALSE) {
|
||||
dg <- attr(callStack, "shiny.stack.digest", exact = TRUE)
|
||||
if (!is.null(dg)) {
|
||||
return(dg)
|
||||
}
|
||||
|
||||
if (isTRUE(warn)) {
|
||||
rlang::warn(
|
||||
"Call stack doesn't have a cached digest; expensively computing one now",
|
||||
.frequency = "once",
|
||||
.frequency_id = "deepstack-uncached-digest-warning"
|
||||
)
|
||||
}
|
||||
|
||||
rlang::hash(getCallNames(callStack))
|
||||
}
|
||||
|
||||
saveCallStackDigest <- function(callStack) {
|
||||
attr(callStack, "shiny.stack.digest") <- getCallStackDigest(callStack, warn = FALSE)
|
||||
callStack
|
||||
}
|
||||
|
||||
# Appends a call stack to a list of call stacks, but only if it's not already
|
||||
# in the list. The list is deduplicated by digest; ideally the digests on the
|
||||
# list are cached before calling this function (you will get a warning if not).
|
||||
appendCallStackWithDedupe <- function(lst, x) {
|
||||
digests <- vapply(lst, getCallStackDigest, character(1), warn = TRUE)
|
||||
xdigest <- getCallStackDigest(x, warn = TRUE)
|
||||
stopifnot(all(nzchar(digests)))
|
||||
stopifnot(length(xdigest) == 1)
|
||||
stopifnot(nzchar(xdigest))
|
||||
if (xdigest %in% digests) {
|
||||
return(lst)
|
||||
} else {
|
||||
return(c(lst, list(x)))
|
||||
}
|
||||
}
|
||||
|
||||
createStackTracePromiseDomain <- function() {
|
||||
# These are actually stateless, we wouldn't have to create a new one each time
|
||||
# if we didn't want to. They're pretty cheap though.
|
||||
@@ -142,13 +180,14 @@ createStackTracePromiseDomain <- function() {
|
||||
currentStack <- sys.calls()
|
||||
currentParents <- sys.parents()
|
||||
attr(currentStack, "parents") <- currentParents
|
||||
currentStack <- saveCallStackDigest(currentStack)
|
||||
currentDeepStack <- .globals$deepStack
|
||||
}
|
||||
function(...) {
|
||||
# Fulfill time
|
||||
if (deepStacksEnabled()) {
|
||||
origDeepStack <- .globals$deepStack
|
||||
.globals$deepStack <- c(currentDeepStack, list(currentStack))
|
||||
.globals$deepStack <- appendCallStackWithDedupe(currentDeepStack, currentStack)
|
||||
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
|
||||
}
|
||||
|
||||
@@ -165,13 +204,14 @@ createStackTracePromiseDomain <- function() {
|
||||
currentStack <- sys.calls()
|
||||
currentParents <- sys.parents()
|
||||
attr(currentStack, "parents") <- currentParents
|
||||
currentStack <- saveCallStackDigest(currentStack)
|
||||
currentDeepStack <- .globals$deepStack
|
||||
}
|
||||
function(...) {
|
||||
# Fulfill time
|
||||
if (deepStacksEnabled()) {
|
||||
origDeepStack <- .globals$deepStack
|
||||
.globals$deepStack <- c(currentDeepStack, list(currentStack))
|
||||
.globals$deepStack <- appendCallStackWithDedupe(currentDeepStack, currentStack)
|
||||
on.exit(.globals$deepStack <- origDeepStack, add = TRUE)
|
||||
}
|
||||
|
||||
@@ -199,6 +239,7 @@ doCaptureStack <- function(e) {
|
||||
calls <- sys.calls()
|
||||
parents <- sys.parents()
|
||||
attr(calls, "parents") <- parents
|
||||
calls <- saveCallStackDigest(calls)
|
||||
attr(e, "stack.trace") <- calls
|
||||
}
|
||||
if (deepStacksEnabled()) {
|
||||
@@ -281,88 +322,115 @@ printStackTrace <- function(cond,
|
||||
full = get_devmode_option("shiny.fullstacktrace", FALSE),
|
||||
offset = getOption("shiny.stacktraceoffset", TRUE)) {
|
||||
|
||||
should_drop <- !full
|
||||
should_strip <- !full
|
||||
should_prune <- !full
|
||||
|
||||
stackTraceCalls <- c(
|
||||
stackTraces <- c(
|
||||
attr(cond, "deep.stack.trace", exact = TRUE),
|
||||
list(attr(cond, "stack.trace", exact = TRUE))
|
||||
)
|
||||
|
||||
stackTraceParents <- lapply(stackTraceCalls, attr, which = "parents", exact = TRUE)
|
||||
stackTraceCallNames <- lapply(stackTraceCalls, getCallNames)
|
||||
stackTraceCalls <- lapply(stackTraceCalls, offsetSrcrefs, offset = offset)
|
||||
|
||||
# Use dropTrivialFrames logic to remove trailing bits (.handleSimpleError, h)
|
||||
if (should_drop) {
|
||||
# toKeep is a list of logical vectors, of which elements (stack frames) to keep
|
||||
toKeep <- lapply(stackTraceCallNames, dropTrivialFrames)
|
||||
# We apply the list of logical vector indices to each data structure
|
||||
stackTraceCalls <- mapply(stackTraceCalls, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
stackTraceCallNames <- mapply(stackTraceCallNames, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
stackTraceParents <- mapply(stackTraceParents, FUN = `[`, toKeep, SIMPLIFY = FALSE)
|
||||
# Stripping of stack traces is the one step where the different stack traces
|
||||
# interact. So we need to do this in one go, instead of individually within
|
||||
# printOneStackTrace.
|
||||
if (!full) {
|
||||
stripResults <- stripStackTraces(lapply(stackTraces, getCallNames))
|
||||
} else {
|
||||
# If full is TRUE, we don't want to strip anything
|
||||
stripResults <- rep_len(list(TRUE), length(stackTraces))
|
||||
}
|
||||
|
||||
delayedAssign("all_true", {
|
||||
# List of logical vectors that are all TRUE, the same shape as
|
||||
# stackTraceCallNames. Delay the evaluation so we don't create it unless
|
||||
# we need it, but if we need it twice then we don't pay to create it twice.
|
||||
lapply(stackTraceCallNames, function(st) {
|
||||
rep_len(TRUE, length(st))
|
||||
})
|
||||
})
|
||||
|
||||
# stripStackTraces and lapply(stackTraceParents, pruneStackTrace) return lists
|
||||
# of logical vectors. Use mapply(FUN = `&`) to boolean-and each pair of the
|
||||
# logical vectors.
|
||||
toShow <- mapply(
|
||||
if (should_strip) stripStackTraces(stackTraceCallNames) else all_true,
|
||||
if (should_prune) lapply(stackTraceParents, pruneStackTrace) else all_true,
|
||||
FUN = `&`,
|
||||
mapply(
|
||||
seq_along(stackTraces),
|
||||
rev(stackTraces),
|
||||
rev(stripResults),
|
||||
FUN = function(i, trace, stripResult) {
|
||||
if (is.integer(trace)) {
|
||||
noun <- if (trace > 1L) "traces" else "trace"
|
||||
message("[ reached getOption(\"shiny.deepstacktrace\") -- omitted ", trace, " more stack ", noun, " ]")
|
||||
} else {
|
||||
if (i != 1) {
|
||||
message("From earlier call:")
|
||||
}
|
||||
printOneStackTrace(
|
||||
stackTrace = trace,
|
||||
stripResult = stripResult,
|
||||
full = full,
|
||||
offset = offset
|
||||
)
|
||||
}
|
||||
# No mapply return value--we're just printing
|
||||
NULL
|
||||
},
|
||||
SIMPLIFY = FALSE
|
||||
)
|
||||
|
||||
dfs <- mapply(seq_along(stackTraceCalls), rev(stackTraceCalls), rev(stackTraceCallNames), rev(toShow), FUN = function(i, calls, nms, index) {
|
||||
st <- data.frame(
|
||||
num = rev(which(index)),
|
||||
call = rev(nms[index]),
|
||||
loc = rev(getLocs(calls[index])),
|
||||
category = rev(getCallCategories(calls[index])),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
|
||||
if (i != 1) {
|
||||
message("From earlier call:")
|
||||
}
|
||||
|
||||
if (nrow(st) == 0) {
|
||||
message(" [No stack trace available]")
|
||||
} else {
|
||||
width <- floor(log10(max(st$num))) + 1
|
||||
formatted <- paste0(
|
||||
" ",
|
||||
formatC(st$num, width = width),
|
||||
": ",
|
||||
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
|
||||
if (category == "pkg")
|
||||
crayon::silver(name)
|
||||
else if (category == "user")
|
||||
crayon::blue$bold(name)
|
||||
else
|
||||
crayon::white(name)
|
||||
}),
|
||||
"\n"
|
||||
)
|
||||
cat(file = stderr(), formatted, sep = "")
|
||||
}
|
||||
|
||||
st
|
||||
}, SIMPLIFY = FALSE)
|
||||
|
||||
invisible()
|
||||
}
|
||||
|
||||
printOneStackTrace <- function(stackTrace, stripResult, full, offset) {
|
||||
calls <- offsetSrcrefs(stackTrace, offset = offset)
|
||||
callNames <- getCallNames(stackTrace)
|
||||
parents <- attr(stackTrace, "parents", exact = TRUE)
|
||||
|
||||
should_drop <- !full
|
||||
should_strip <- !full
|
||||
should_prune <- !full
|
||||
|
||||
if (should_drop) {
|
||||
toKeep <- dropTrivialFrames(callNames)
|
||||
calls <- calls[toKeep]
|
||||
callNames <- callNames[toKeep]
|
||||
parents <- parents[toKeep]
|
||||
stripResult <- stripResult[toKeep]
|
||||
}
|
||||
|
||||
toShow <- rep(TRUE, length(callNames))
|
||||
if (should_prune) {
|
||||
toShow <- toShow & pruneStackTrace(parents)
|
||||
}
|
||||
if (should_strip) {
|
||||
toShow <- toShow & stripResult
|
||||
}
|
||||
|
||||
# If we're running in testthat, hide the parts of the stack trace that can
|
||||
# vary based on how testthat was launched. It's critical that this is not
|
||||
# happen at the same time as dropTrivialFrames, which happens before
|
||||
# pruneStackTrace; because dropTrivialTestFrames removes calls from the top
|
||||
# (or bottom? whichever is the oldest?) of the stack, it breaks `parents`
|
||||
# which is based on absolute indices of calls. dropTrivialFrames gets away
|
||||
# with this because it only removes calls from the opposite side of the stack.
|
||||
toShow <- toShow & dropTrivialTestFrames(callNames)
|
||||
|
||||
st <- data.frame(
|
||||
num = rev(which(toShow)),
|
||||
call = rev(callNames[toShow]),
|
||||
loc = rev(getLocs(calls[toShow])),
|
||||
category = rev(getCallCategories(calls[toShow])),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
|
||||
if (nrow(st) == 0) {
|
||||
message(" [No stack trace available]")
|
||||
} else {
|
||||
width <- floor(log10(max(st$num))) + 1
|
||||
formatted <- paste0(
|
||||
" ",
|
||||
formatC(st$num, width = width),
|
||||
": ",
|
||||
mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) {
|
||||
if (category == "pkg")
|
||||
crayon::silver(name)
|
||||
else if (category == "user")
|
||||
crayon::blue$bold(name)
|
||||
else
|
||||
crayon::white(name)
|
||||
}),
|
||||
"\n"
|
||||
)
|
||||
cat(file = stderr(), formatted, sep = "")
|
||||
}
|
||||
|
||||
invisible(st)
|
||||
}
|
||||
|
||||
stripStackTraces <- function(stackTraces, values = FALSE) {
|
||||
score <- 1L # >=1: show, <=0: hide
|
||||
lapply(seq_along(stackTraces), function(i) {
|
||||
@@ -458,6 +526,33 @@ dropTrivialFrames <- function(callnames) {
|
||||
)
|
||||
}
|
||||
|
||||
dropTrivialTestFrames <- function(callnames) {
|
||||
if (!identical(Sys.getenv("TESTTHAT_IS_SNAPSHOT"), "true")) {
|
||||
return(rep_len(TRUE, length(callnames)))
|
||||
}
|
||||
|
||||
hideable <- callnames %in% c(
|
||||
"test",
|
||||
"devtools::test",
|
||||
"test_check",
|
||||
"testthat::test_check",
|
||||
"test_dir",
|
||||
"testthat::test_dir",
|
||||
"test_file",
|
||||
"testthat::test_file",
|
||||
"test_local",
|
||||
"testthat::test_local"
|
||||
)
|
||||
|
||||
firstGoodCall <- min(which(!hideable))
|
||||
toRemove <- firstGoodCall - 1L
|
||||
|
||||
c(
|
||||
rep_len(FALSE, toRemove),
|
||||
rep_len(TRUE, length(callnames) - toRemove)
|
||||
)
|
||||
}
|
||||
|
||||
offsetSrcrefs <- function(calls, offset = TRUE) {
|
||||
if (offset) {
|
||||
srcrefs <- getSrcRefs(calls)
|
||||
|
||||
@@ -41,6 +41,54 @@
|
||||
#' is, a function that quickly returns a promise) and allows even that very
|
||||
#' session to immediately unblock and carry on with other user interactions.
|
||||
#'
|
||||
#' @examplesIf rlang::is_interactive() && rlang::is_installed("future")
|
||||
#'
|
||||
#' library(shiny)
|
||||
#' library(bslib)
|
||||
#' library(future)
|
||||
#' plan(multisession)
|
||||
#'
|
||||
#' ui <- page_fluid(
|
||||
#' titlePanel("Extended Task Demo"),
|
||||
#' p(
|
||||
#' 'Click the button below to perform a "calculation"',
|
||||
#' "that takes a while to perform."
|
||||
#' ),
|
||||
#' input_task_button("recalculate", "Recalculate"),
|
||||
#' p(textOutput("result"))
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' rand_task <- ExtendedTask$new(function() {
|
||||
#' future(
|
||||
#' {
|
||||
#' # Slow operation goes here
|
||||
#' Sys.sleep(2)
|
||||
#' sample(1:100, 1)
|
||||
#' },
|
||||
#' seed = TRUE
|
||||
#' )
|
||||
#' })
|
||||
#'
|
||||
#' # Make button state reflect task.
|
||||
#' # If using R >=4.1, you can do this instead:
|
||||
#' # rand_task <- ExtendedTask$new(...) |> bind_task_button("recalculate")
|
||||
#' bind_task_button(rand_task, "recalculate")
|
||||
#'
|
||||
#' observeEvent(input$recalculate, {
|
||||
#' # Invoke the extended in an observer
|
||||
#' rand_task$invoke()
|
||||
#' })
|
||||
#'
|
||||
#' output$result <- renderText({
|
||||
#' # React to updated results when the task completes
|
||||
#' number <- rand_task$result()
|
||||
#' paste0("Your number is ", number, ".")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#'
|
||||
#' @export
|
||||
ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
|
||||
public = list(
|
||||
|
||||
14
R/react.R
14
R/react.R
@@ -53,10 +53,12 @@ Context <- R6Class(
|
||||
|
||||
promises::with_promise_domain(reactivePromiseDomain(), {
|
||||
withReactiveDomain(.domain, {
|
||||
env <- .getReactiveEnvironment()
|
||||
rLog$enter(.reactId, id, .reactType, .domain)
|
||||
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
|
||||
env$runWith(self, func)
|
||||
captureStackTraces({
|
||||
env <- .getReactiveEnvironment()
|
||||
rLog$enter(.reactId, id, .reactType, .domain)
|
||||
on.exit(rLog$exit(.reactId, id, .reactType, .domain), add = TRUE)
|
||||
env$runWith(self, func)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
@@ -223,9 +225,7 @@ wrapForContext <- function(func, ctx) {
|
||||
|
||||
function(...) {
|
||||
.getReactiveEnvironment()$runWith(ctx, function() {
|
||||
captureStackTraces(
|
||||
func(...)
|
||||
)
|
||||
func(...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -951,7 +951,10 @@ Observable <- R6Class(
|
||||
#' See the [Shiny tutorial](https://shiny.rstudio.com/tutorial/) for
|
||||
#' more information about reactive expressions.
|
||||
#'
|
||||
#' @param x For `is.reactive()`, an object to test. For `reactive()`, an expression. When passing in a [`quo()`]sure with `reactive()`, remember to use [`rlang::inject()`] to distinguish that you are passing in the content of your quosure, not the expression of the quosure.
|
||||
#' @param x For `is.reactive()`, an object to test. For `reactive()`, an
|
||||
#' expression. When passing in a [`rlang::quo()`]sure with `reactive()`,
|
||||
#' remember to use [`rlang::inject()`] to distinguish that you are passing in
|
||||
#' the content of your quosure, not the expression of the quosure.
|
||||
#' @template param-env
|
||||
#' @templateVar x x
|
||||
#' @templateVar env env
|
||||
@@ -2301,7 +2304,7 @@ observeEvent <- function(eventExpr, handlerExpr,
|
||||
priority = priority,
|
||||
domain = domain,
|
||||
autoDestroy = TRUE,
|
||||
..stacktraceon = FALSE # TODO: Does this go in the bindEvent?
|
||||
..stacktraceon = TRUE
|
||||
))
|
||||
|
||||
o <- inject(bindEvent(
|
||||
|
||||
@@ -445,7 +445,9 @@ stopApp <- function(returnValue = invisible()) {
|
||||
#' @param host The IPv4 address that the application should listen on. Defaults
|
||||
#' to the `shiny.host` option, if set, or `"127.0.0.1"` if not.
|
||||
#' @param display.mode The mode in which to display the example. Defaults to
|
||||
#' `showcase`, but may be set to `normal` to see the example without
|
||||
#' `"auto"`, which uses the value of `DisplayMode` in the example's
|
||||
#' `DESCRIPTION` file. Set to `"showcase"` to show the app code and
|
||||
#' description with the running app, or `"normal"` to see the example without
|
||||
#' code or commentary.
|
||||
#' @param package The package in which to find the example (defaults to
|
||||
#' `"shiny"`).
|
||||
|
||||
@@ -2024,7 +2024,7 @@ ShinySession <- R6Class(
|
||||
tmpdata <- tempfile(fileext = ext)
|
||||
return(Context$new(getDefaultReactiveDomain(), '[download]')$run(function() {
|
||||
promises::with_promise_domain(reactivePromiseDomain(), {
|
||||
promises::with_promise_domain(createStackTracePromiseDomain(), {
|
||||
captureStackTraces({
|
||||
self$incrementBusyCount()
|
||||
hybrid_chain(
|
||||
# ..stacktraceon matches with the top-level ..stacktraceoff..
|
||||
|
||||
@@ -53,8 +53,8 @@ formalsAndBody <- function(x) {
|
||||
|
||||
#' @describeIn createRenderFunction convert a quosure to a function.
|
||||
#' @param q Quosure of the expression `x`. When capturing expressions to create
|
||||
#' your quosure, it is recommended to use [`enquo0()`] to not unquote the
|
||||
#' object too early. See [`enquo0()`] for more details.
|
||||
#' your quosure, it is recommended to use [`rlang::enquo0()`] to not unquote
|
||||
#' the object too early. See [`rlang::enquo0()`] for more details.
|
||||
#' @inheritParams installExprFunction
|
||||
#' @export
|
||||
quoToFunction <- function(
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
## revdepcheck results
|
||||
|
||||
We checked 1278 reverse dependencies (1277 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of shiny.
|
||||
|
||||
* We saw 2 new problems (NOTEs only)
|
||||
* We failed to check 19 packages due to installation issues
|
||||
|
||||
Issues with CRAN packages are summarised below.
|
||||
|
||||
### New problems
|
||||
|
||||
R CMD check displayed NOTEs for two packages, unrelated to changes in shiny.
|
||||
|
||||
* HH
|
||||
checking installed package size ... NOTE
|
||||
|
||||
* PopED
|
||||
checking installed package size ... NOTE
|
||||
|
||||
### Failed to check
|
||||
|
||||
* animalEKF
|
||||
* AovBay
|
||||
* Certara.VPCResults
|
||||
* chipPCR
|
||||
* ctsem
|
||||
* dartR.sim
|
||||
* diveR
|
||||
* gap
|
||||
* jsmodule
|
||||
* loon.shiny
|
||||
* robmedExtra
|
||||
* rstanarm
|
||||
* SensMap
|
||||
* Seurat
|
||||
* shinyTempSignal
|
||||
* Signac
|
||||
* statsr
|
||||
* TestAnaAPP
|
||||
* tidyvpc
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
|
||||
:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating{opacity:1}[data-shiny-busy-spinners] .recalculating:after{position:absolute;content:"";--_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg));--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));--_shiny-spinner-size: var(--shiny-spinner-size, 32px);--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);background:var(--_shiny-spinner-color);width:var(--_shiny-spinner-size);height:var(--_shiny-spinner-size);inset:calc(50% - var(--_shiny-spinner-size) / 2);mask-image:var(--_shiny-spinner-url);-webkit-mask-image:var(--_shiny-spinner-url);opacity:0;animation-delay:var(--_shiny-spinner-delay);animation-name:fade-in;animation-duration:.25s;animation-fill-mode:forwards}[data-shiny-busy-spinners] .recalculating>*:not(.recalculating){opacity:.2;transition:opacity .25s ease var(--shiny-spinner-delay, 1s)}[data-shiny-busy-spinners] .recalculating.shiny-html-output:after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, var(--bs-body-bg, #fff), var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), var(--bs-body-bg, #fff) ) );--_shiny-pulse-height: var(--shiny-pulse-height, 5px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(#shiny-disconnected-overlay):after{display:none}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, var(--bs-body-bg, #fff), var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), var(--bs-body-bg, #fff) ) );--_shiny-pulse-height: var(--shiny-pulse-height, 5px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:has(#shiny-disconnected-overlay):after{display:none}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes busy-page-pulse{0%{left:-75%;width:75%}50%{left:100%;width:75%}to{left:-75%;width:75%}}
|
||||
/*! shiny 1.10.0.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */
|
||||
:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating:after{position:absolute;content:"";--_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg));--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));--_shiny-spinner-size: var(--shiny-spinner-size, 32px);--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);background:var(--_shiny-spinner-color);width:var(--_shiny-spinner-size);height:var(--_shiny-spinner-size);inset:calc(50% - var(--_shiny-spinner-size) / 2);mask-image:var(--_shiny-spinner-url);-webkit-mask-image:var(--_shiny-spinner-url);opacity:0;animation-delay:var(--_shiny-spinner-delay);animation-name:fade-in;animation-duration:.25s;animation-fill-mode:forwards}[data-shiny-busy-spinners] .recalculating:has(>*),[data-shiny-busy-spinners] .recalculating:empty{opacity:1}[data-shiny-busy-spinners] .recalculating>*:not(.recalculating){opacity:var(--_shiny-fade-opacity);transition:opacity .25s ease var(--shiny-spinner-delay, 1s)}[data-shiny-busy-spinners] .recalculating.shiny-html-output:after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating:not(.shiny-html-output)):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(#shiny-disconnected-overlay):after{display:none}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:has(#shiny-disconnected-overlay):after{display:none}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes busy-page-pulse{0%{left:-14%;right:97%}45%{left:0%;right:14%}55%{left:14%;right:0%}to{left:97%;right:-14%}}.shiny-spinner-output-container{--shiny-spinner-size: 0px}
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.type_last {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -143,6 +143,11 @@ $font-family: $font-family-base !default;
|
||||
border-radius: $handle_width;
|
||||
z-index: 2;
|
||||
|
||||
&.type_last {
|
||||
// Ensure last-used handle is on top if it overlaps with another handle
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&.state_hover,
|
||||
&:hover {
|
||||
background: $handle_color_hover;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
/*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.10.0.9000 | (c) 2012-2025 Posit Software, 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
@@ -1,3 +1,3 @@
|
||||
/*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.10.0.9000 | (c) 2012-2025 Posit Software, 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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
4
inst/www/shared/shiny.min.css
vendored
4
inst/www/shared/shiny.min.css
vendored
File diff suppressed because one or more lines are too long
4
inst/www/shared/shiny.min.js
vendored
4
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -44,9 +44,9 @@ div:where(.shiny-html-output) {
|
||||
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
|
||||
&:has(> *) {
|
||||
display: contents;
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
/* Pass along styles that no longer impact the pass-through container */
|
||||
&.recalculating > * {
|
||||
opacity: 0.3;
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,8 @@ html.autoreload-enabled #shiny-disconnected-overlay.reloading {
|
||||
}
|
||||
|
||||
.recalculating {
|
||||
opacity: 0.3;
|
||||
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease 500ms;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,56 @@ is, a function that quickly returns a promise) and allows even that very
|
||||
session to immediately unblock and carry on with other user interactions.
|
||||
}
|
||||
|
||||
\examples{
|
||||
\dontshow{if (rlang::is_interactive() && rlang::is_installed("future")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
|
||||
|
||||
library(shiny)
|
||||
library(bslib)
|
||||
library(future)
|
||||
plan(multisession)
|
||||
|
||||
ui <- page_fluid(
|
||||
titlePanel("Extended Task Demo"),
|
||||
p(
|
||||
'Click the button below to perform a "calculation"',
|
||||
"that takes a while to perform."
|
||||
),
|
||||
input_task_button("recalculate", "Recalculate"),
|
||||
p(textOutput("result"))
|
||||
)
|
||||
|
||||
server <- function(input, output) {
|
||||
rand_task <- ExtendedTask$new(function() {
|
||||
future(
|
||||
{
|
||||
# Slow operation goes here
|
||||
Sys.sleep(2)
|
||||
sample(1:100, 1)
|
||||
},
|
||||
seed = TRUE
|
||||
)
|
||||
})
|
||||
|
||||
# Make button state reflect task.
|
||||
# If using R >=4.1, you can do this instead:
|
||||
# rand_task <- ExtendedTask$new(...) |> bind_task_button("recalculate")
|
||||
bind_task_button(rand_task, "recalculate")
|
||||
|
||||
observeEvent(input$recalculate, {
|
||||
# Invoke the extended in an observer
|
||||
rand_task$invoke()
|
||||
})
|
||||
|
||||
output$result <- renderText({
|
||||
# React to updated results when the task completes
|
||||
number <- rand_task$result()
|
||||
paste0("Your number is ", number, ".")
|
||||
})
|
||||
}
|
||||
|
||||
shinyApp(ui, server)
|
||||
\dontshow{\}) # examplesIf}
|
||||
}
|
||||
\section{Methods}{
|
||||
\subsection{Public methods}{
|
||||
\itemize{
|
||||
|
||||
@@ -182,7 +182,7 @@ If you want to use a cache that is shared across multiple R processes, you
|
||||
can use a \code{\link[cachem:cache_disk]{cachem::cache_disk()}}. You can create a application-level shared
|
||||
cache by putting this at the top of your app.R, server.R, or global.R:
|
||||
|
||||
\if{html}{\out{<div class="sourceCode">}}\preformatted{shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
|
||||
\if{html}{\out{<div class="sourceCode">}}\preformatted{shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache")))
|
||||
}\if{html}{\out{</div>}}
|
||||
|
||||
This will create a subdirectory in your system temp directory named
|
||||
|
||||
@@ -11,6 +11,8 @@ busyIndicatorOptions(
|
||||
spinner_size = NULL,
|
||||
spinner_delay = NULL,
|
||||
spinner_selector = NULL,
|
||||
fade_opacity = NULL,
|
||||
fade_selector = NULL,
|
||||
pulse_background = NULL,
|
||||
pulse_height = NULL,
|
||||
pulse_speed = NULL
|
||||
@@ -45,6 +47,13 @@ if the computation finishes quickly.}
|
||||
scoping the spinner customization. The default (\code{NULL}) will apply the
|
||||
spinner customization to the parent element of the spinner.}
|
||||
|
||||
\item{fade_opacity}{The opacity (a number between 0 and 1) for recalculating
|
||||
output. Set to 1 to "disable" the fade.}
|
||||
|
||||
\item{fade_selector}{A character string containing a CSS selector for
|
||||
scoping the spinner customization. The default (\code{NULL}) will apply the
|
||||
spinner customization to the parent element of the spinner.}
|
||||
|
||||
\item{pulse_background}{A CSS background definition for the pulse. The
|
||||
default uses a
|
||||
\href{https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient}{linear-gradient}
|
||||
@@ -57,11 +66,21 @@ CSS size.}
|
||||
time.}
|
||||
}
|
||||
\description{
|
||||
When busy indicators are enabled (see \code{\link[=useBusyIndicators]{useBusyIndicators()}}), a spinner is
|
||||
shown on each calculating/recalculating output, and a pulsing banner is shown
|
||||
at the top of the page when the app is otherwise busy. This function allows
|
||||
you to customize the appearance of those busy indicators. To apply the
|
||||
customization, include the result of this function inside the app's UI.
|
||||
Shiny automatically includes busy indicators, which more specifically means:
|
||||
\enumerate{
|
||||
\item Calculating/recalculating outputs have a spinner overlay.
|
||||
\item Outputs fade out/in when recalculating.
|
||||
\item When no outputs are calculating/recalculating, but Shiny is busy
|
||||
doing something else (e.g., a download, side-effect, etc), a page-level
|
||||
pulsing banner is shown.
|
||||
}
|
||||
|
||||
This function allows you to customize the appearance of these busy indicators
|
||||
by including the result of this function inside the app's UI. Note that,
|
||||
unless \code{spinner_selector} (or \code{fade_selector}) is specified, the spinner/fade
|
||||
customization applies to the parent element. If the customization should
|
||||
instead apply to the entire page, set \code{spinner_selector = 'html'} and
|
||||
\code{fade_selector = 'html'}.
|
||||
}
|
||||
\examples{
|
||||
\dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
|
||||
@@ -113,5 +132,5 @@ shinyApp(ui, server)
|
||||
\dontshow{\}) # examplesIf}
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=useBusyIndicators]{useBusyIndicators()}} for enabling/disabling busy indicators.
|
||||
\code{\link[=useBusyIndicators]{useBusyIndicators()}} to disable/enable busy indicators.
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ example, some render functions call \code{\link[=createWebDependency]{createWebD
|
||||
is able to serve JS and CSS resources.}
|
||||
|
||||
\item{q}{Quosure of the expression \code{x}. When capturing expressions to create
|
||||
your quosure, it is recommended to use \code{\link[=enquo0]{enquo0()}} to not unquote the
|
||||
object too early. See \code{\link[=enquo0]{enquo0()}} for more details.}
|
||||
your quosure, it is recommended to use \code{\link[rlang:defusing-advanced]{rlang::enquo0()}} to not unquote
|
||||
the object too early. See \code{\link[rlang:defusing-advanced]{rlang::enquo0()}} for more details.}
|
||||
|
||||
\item{label}{A label for the object to be shown in the debugger. Defaults to
|
||||
the name of the calling function.}
|
||||
|
||||
@@ -18,7 +18,10 @@ reactive(
|
||||
is.reactive(x)
|
||||
}
|
||||
\arguments{
|
||||
\item{x}{For \code{is.reactive()}, an object to test. For \code{reactive()}, an expression. When passing in a \code{\link[=quo]{quo()}}sure with \code{reactive()}, remember to use \code{\link[rlang:inject]{rlang::inject()}} to distinguish that you are passing in the content of your quosure, not the expression of the quosure.}
|
||||
\item{x}{For \code{is.reactive()}, an object to test. For \code{reactive()}, an
|
||||
expression. When passing in a \code{\link[rlang:defusing-advanced]{rlang::quo()}}sure with \code{reactive()},
|
||||
remember to use \code{\link[rlang:inject]{rlang::inject()}} to distinguish that you are passing in
|
||||
the content of your quosure, not the expression of the quosure.}
|
||||
|
||||
\item{env}{The parent environment for the reactive expression. By default,
|
||||
this is the calling environment, the same as when defining an ordinary
|
||||
|
||||
@@ -12,9 +12,10 @@ registerThemeDependency(func)
|
||||
of them.}
|
||||
}
|
||||
\description{
|
||||
This function registers a function that returns an \code{\link[=htmlDependency]{htmlDependency()}} or list
|
||||
of such objects. If \code{session$setCurrentTheme()} is called, the function will
|
||||
be re-executed, and the resulting html dependency will be sent to the client.
|
||||
This function registers a function that returns an
|
||||
\code{\link[htmltools:htmlDependency]{htmltools::htmlDependency()}} or list of such objects. If
|
||||
\code{session$setCurrentTheme()} is called, the function will be re-executed, and
|
||||
the resulting html dependency will be sent to the client.
|
||||
}
|
||||
\details{
|
||||
Note that \code{func} should \strong{not} be an anonymous function, or a function which
|
||||
|
||||
@@ -33,7 +33,9 @@ interactive sessions only.}
|
||||
to the \code{shiny.host} option, if set, or \code{"127.0.0.1"} if not.}
|
||||
|
||||
\item{display.mode}{The mode in which to display the example. Defaults to
|
||||
\code{showcase}, but may be set to \code{normal} to see the example without
|
||||
\code{"auto"}, which uses the value of \code{DisplayMode} in the example's
|
||||
\code{DESCRIPTION} file. Set to \code{"showcase"} to show the app code and
|
||||
description with the running app, or \code{"normal"} to see the example without
|
||||
code or commentary.}
|
||||
|
||||
\item{package}{The package in which to find the example (defaults to
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
\alias{useBusyIndicators}
|
||||
\title{Enable/disable busy indication}
|
||||
\usage{
|
||||
useBusyIndicators(..., spinners = TRUE, pulse = TRUE)
|
||||
useBusyIndicators(..., spinners = TRUE, pulse = TRUE, fade = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{Currently ignored.}
|
||||
@@ -14,6 +14,9 @@ output.}
|
||||
|
||||
\item{pulse}{Whether to show a pulsing banner at the top of the page when the
|
||||
app is busy.}
|
||||
|
||||
\item{fade}{Whether to fade recalculating outputs. A value of \code{FALSE} is
|
||||
equivalent to \code{busyIndicatorOptions(fade_opacity=1)}.}
|
||||
}
|
||||
\description{
|
||||
Busy indicators provide a visual cue to users when the server is busy
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"homepage": "https://shiny.rstudio.com",
|
||||
"repository": "github:rstudio/shiny",
|
||||
"name": "@types/rstudio-shiny",
|
||||
"version": "1.8.1-alpha.9001",
|
||||
"version": "1.10.0-alpha.9000",
|
||||
"license": "GPL-3.0-only",
|
||||
"main": "",
|
||||
"browser": "",
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
# Revdeps
|
||||
|
||||
## Failed to check (18)
|
||||
## Failed to check (20)
|
||||
|
||||
|package |version |error |warning |note |
|
||||
|:------------------|:-------|:-----|:-------|:----|
|
||||
|bigPint |? | | | |
|
||||
|bioCancer |? | | | |
|
||||
|ctsem |3.9.1 |1 | | |
|
||||
|animalEKF |1.2 |1 | | |
|
||||
|AovBay |0.1.0 |1 | | |
|
||||
|Certara.VPCResults |3.0.2 |1 | | |
|
||||
|chipPCR |1.0-2 |1 | | |
|
||||
|ctsem |3.10.1 |1 | | |
|
||||
|dartR.sim |? | | | |
|
||||
|diveR |? | | | |
|
||||
|EBImage |? | | | |
|
||||
|g3viz |? | | | |
|
||||
|GeneNetworkBuilder |? | | | |
|
||||
|grandR |? | | | |
|
||||
|InterCellar |? | | | |
|
||||
|LACE |? | | | |
|
||||
|gap |? | | | |
|
||||
|jsmodule |? | | | |
|
||||
|loon.shiny |? | | | |
|
||||
|MatrixQCvis |? | | | |
|
||||
|modchart |? | | | |
|
||||
|multilevelcoda |1.2.3 |1 | | |
|
||||
|omicsViewer |? | | | |
|
||||
|RQuantLib |0.4.21 |1 | | |
|
||||
|robmedExtra |0.1.1 |1 | | |
|
||||
|rstanarm |2.32.1 |1 | | |
|
||||
|Seurat |? | | | |
|
||||
|SensMap |0.7 |1 | | |
|
||||
|Seurat |5.1.0 |1 | |1 |
|
||||
|shinyTempSignal |0.0.8 |1 | | |
|
||||
|Signac |1.14.0 |1 | | |
|
||||
|statsr |0.3.0 |1 | | |
|
||||
|TestAnaAPP |1.1.2 |1 | | |
|
||||
|tidyvpc |1.5.2 |1 | | |
|
||||
|visR |? | | | |
|
||||
|
||||
## New problems (2)
|
||||
|
||||
|package |version |error |warning |note |
|
||||
|:-------|:-------|:-----|:-------|:------|
|
||||
|[HH](problems.md#hh)|3.1-52 | | |__+1__ |
|
||||
|[PopED](problems.md#poped)|0.7.0 | | |__+1__ |
|
||||
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
## revdepcheck results
|
||||
|
||||
We checked 1201 reverse dependencies (1191 from CRAN + 10 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package.
|
||||
We checked 1278 reverse dependencies (1277 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package.
|
||||
|
||||
* We saw 0 new problems
|
||||
* We failed to check 8 packages
|
||||
* We saw 2 new problems
|
||||
* We failed to check 19 packages
|
||||
|
||||
Issues with CRAN packages are summarised below.
|
||||
|
||||
### New problems
|
||||
(This reports the first line of each new failure)
|
||||
|
||||
* HH
|
||||
checking installed package size ... NOTE
|
||||
|
||||
* PopED
|
||||
checking installed package size ... NOTE
|
||||
|
||||
### Failed to check
|
||||
|
||||
* ctsem (NA)
|
||||
* diveR (NA)
|
||||
* grandR (NA)
|
||||
* loon.shiny (NA)
|
||||
* multilevelcoda (NA)
|
||||
* RQuantLib (NA)
|
||||
* rstanarm (NA)
|
||||
* Seurat (NA)
|
||||
* animalEKF (NA)
|
||||
* AovBay (NA)
|
||||
* Certara.VPCResults (NA)
|
||||
* chipPCR (NA)
|
||||
* ctsem (NA)
|
||||
* dartR.sim (NA)
|
||||
* diveR (NA)
|
||||
* gap (NA)
|
||||
* jsmodule (NA)
|
||||
* loon.shiny (NA)
|
||||
* robmedExtra (NA)
|
||||
* rstanarm (NA)
|
||||
* SensMap (NA)
|
||||
* Seurat (NA)
|
||||
* shinyTempSignal (NA)
|
||||
* Signac (NA)
|
||||
* statsr (NA)
|
||||
* TestAnaAPP (NA)
|
||||
* tidyvpc (NA)
|
||||
|
||||
@@ -1 +1,49 @@
|
||||
*Wow, no problems at all. :)*
|
||||
# HH
|
||||
|
||||
<details>
|
||||
|
||||
* Version: 3.1-52
|
||||
* GitHub: NA
|
||||
* Source code: https://github.com/cran/HH
|
||||
* Date/Publication: 2024-02-11 00:00:02 UTC
|
||||
* Number of recursive dependencies: 165
|
||||
|
||||
Run `revdepcheck::cloud_details(, "HH")` for more info
|
||||
|
||||
</details>
|
||||
|
||||
## Newly broken
|
||||
|
||||
* checking installed package size ... NOTE
|
||||
```
|
||||
installed size is 5.1Mb
|
||||
sub-directories of 1Mb or more:
|
||||
R 1.5Mb
|
||||
help 1.5Mb
|
||||
```
|
||||
|
||||
# PopED
|
||||
|
||||
<details>
|
||||
|
||||
* Version: 0.7.0
|
||||
* GitHub: https://github.com/andrewhooker/PopED
|
||||
* Source code: https://github.com/cran/PopED
|
||||
* Date/Publication: 2024-10-07 19:30:02 UTC
|
||||
* Number of recursive dependencies: 140
|
||||
|
||||
Run `revdepcheck::cloud_details(, "PopED")` for more info
|
||||
|
||||
</details>
|
||||
|
||||
## Newly broken
|
||||
|
||||
* checking installed package size ... NOTE
|
||||
```
|
||||
installed size is 5.5Mb
|
||||
sub-directories of 1Mb or more:
|
||||
R 1.5Mb
|
||||
doc 1.4Mb
|
||||
test 1.1Mb
|
||||
```
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import type {
|
||||
} from "esbuild";
|
||||
import { build as esbuildBuild } from "esbuild";
|
||||
|
||||
import process from "process";
|
||||
import { basename } from "path";
|
||||
import process from "process";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Type definitions are not found. This occurs when `strict: true` in tsconfig.json
|
||||
@@ -25,7 +25,7 @@ const shinyDesc = readcontrol.readSync("./DESCRIPTION") as ShinyDesc;
|
||||
|
||||
const bannerTxt = [
|
||||
`/*! ${shinyDesc.package} ${shinyDesc.version}`,
|
||||
`(c) 2012-${new Date().getFullYear()} RStudio, PBC.`,
|
||||
`(c) 2012-${new Date().getFullYear()} Posit Software, PBC.`,
|
||||
`License: ${shinyDesc.license} */`,
|
||||
].join(" | ");
|
||||
const banner = {
|
||||
|
||||
@@ -37,9 +37,11 @@
|
||||
the spinner. Undo that, but still apply (smaller) opacity to immediate children
|
||||
that aren't recalculating.
|
||||
*/
|
||||
opacity: 1;
|
||||
&:has(> *), &:empty {
|
||||
opacity: 1;
|
||||
}
|
||||
> *:not(.recalculating) {
|
||||
opacity: 0.2;
|
||||
opacity: var(--_shiny-fade-opacity);
|
||||
transition: opacity 250ms ease var(--shiny-spinner-delay, 1s);
|
||||
}
|
||||
|
||||
@@ -62,15 +64,15 @@
|
||||
--shiny-pulse-background,
|
||||
linear-gradient(
|
||||
120deg,
|
||||
var(--bs-body-bg, #fff),
|
||||
transparent,
|
||||
var(--bs-indigo, #4b00c1),
|
||||
var(--bs-purple, #74149c),
|
||||
var(--bs-pink, #bf007f),
|
||||
var(--bs-body-bg, #fff)
|
||||
transparent
|
||||
)
|
||||
);
|
||||
--_shiny-pulse-height: var(--shiny-pulse-height, 5px);
|
||||
--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);
|
||||
--_shiny-pulse-height: var(--shiny-pulse-height, 3px);
|
||||
--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);
|
||||
|
||||
/* Color, sizing, & positioning */
|
||||
position: fixed;
|
||||
@@ -83,6 +85,7 @@
|
||||
/* Animation */
|
||||
animation-name: busy-page-pulse;
|
||||
animation-duration: var(--_shiny-pulse-speed);
|
||||
animation-direction: alternate;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: ease-in-out;
|
||||
|
||||
@@ -97,7 +100,9 @@
|
||||
&.shiny-busy::after {
|
||||
@include shiny-page-busy;
|
||||
}
|
||||
&.shiny-busy:has(.recalculating)::after {
|
||||
// Hide the pulse if there are spinners on the page
|
||||
// (Note: UI outputs don't get spinners)
|
||||
&.shiny-busy:has(.recalculating:not(.shiny-html-output))::after {
|
||||
display: none;
|
||||
}
|
||||
&.shiny-busy:has(#shiny-disconnected-overlay)::after {
|
||||
@@ -128,16 +133,31 @@
|
||||
/* Keyframes for the pulsing banner */
|
||||
@keyframes busy-page-pulse {
|
||||
0% {
|
||||
left: -75%;
|
||||
width: 75%;
|
||||
left: -14%;
|
||||
right: 97%;
|
||||
}
|
||||
50% {
|
||||
left: 100%;
|
||||
width: 75%;
|
||||
|
||||
45% {
|
||||
left: 0%;
|
||||
right: 14%;
|
||||
}
|
||||
/* Go back */
|
||||
100% {
|
||||
left: -75%;
|
||||
width: 75%;
|
||||
|
||||
55% {
|
||||
left: 14%;
|
||||
right: 0%;
|
||||
}
|
||||
|
||||
to {
|
||||
left: 97%;
|
||||
right: -14%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Effectively disable the spinner when it's wrapped in shinycssloader::withSpinner()
|
||||
// since that's a sign our spinner isn't needed.
|
||||
// The reason this sets size to 0px instead of display:none is so, if someone
|
||||
// really wants to show the spinner, they can override this with a custom size.
|
||||
.shiny-spinner-output-container {
|
||||
--shiny-spinner-size: 0px;
|
||||
}
|
||||
@@ -2,19 +2,12 @@
|
||||
// Project: Shiny <https://shiny.rstudio.com/>
|
||||
// Definitions by: RStudio <https://www.rstudio.com/>
|
||||
|
||||
import type { Shiny as RStudioShiny } from "../src/shiny/index";
|
||||
import type { ShinyClass } from "../src/shiny/index";
|
||||
|
||||
declare global {
|
||||
// Tell Shiny variable globally exists
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const Shiny: RStudioShiny;
|
||||
|
||||
// Tell window.Shiny exists
|
||||
interface Window {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
Shiny: RStudioShiny;
|
||||
Shiny: ShinyClass;
|
||||
}
|
||||
|
||||
// Make `Shiny` a globally available type definition. (No need to import the type)
|
||||
type Shiny = RStudioShiny;
|
||||
}
|
||||
|
||||
@@ -40,19 +40,15 @@ class DateInputBindingBase extends InputBinding {
|
||||
el;
|
||||
}
|
||||
subscribe(el: HTMLElement, callback: (x: boolean) => void): void {
|
||||
$(el).on(
|
||||
"keyup.dateInputBinding input.dateInputBinding",
|
||||
// event: Event
|
||||
function () {
|
||||
// Use normal debouncing policy when typing
|
||||
callback(true);
|
||||
}
|
||||
);
|
||||
// Don't update when in the middle of typing; listening on keyup or input
|
||||
// tends to send spurious values to the server, based on unpredictable
|
||||
// browser-dependant interpretation of partially-typed date strings.
|
||||
$(el).on(
|
||||
"changeDate.dateInputBinding change.dateInputBinding",
|
||||
// event: Event
|
||||
function () {
|
||||
// Send immediately when clicked
|
||||
// Or if typing, when enter pressed or focus lost
|
||||
callback(false);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -161,19 +161,15 @@ class DateRangeInputBinding extends DateInputBindingBase {
|
||||
this._setMax($endinput[0], $endinput.data("max-date"));
|
||||
}
|
||||
subscribe(el: HTMLElement, callback: (x: boolean) => void): void {
|
||||
$(el).on(
|
||||
"keyup.dateRangeInputBinding input.dateRangeInputBinding",
|
||||
// event: Event
|
||||
function () {
|
||||
// Use normal debouncing policy when typing
|
||||
callback(true);
|
||||
}
|
||||
);
|
||||
// Don't update when in the middle of typing; listening on keyup or input
|
||||
// tends to send spurious values to the server, based on unpredictable
|
||||
// browser-dependant interpretation of partially-typed date strings.
|
||||
$(el).on(
|
||||
"changeDate.dateRangeInputBinding change.dateRangeInputBinding",
|
||||
// event: Event
|
||||
function () {
|
||||
// Send immediately when clicked
|
||||
// Or if typing, when enter pressed or focus lost
|
||||
callback(false);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,11 +18,10 @@ import { TextInputBinding } from "./text";
|
||||
import { TextareaInputBinding } from "./textarea";
|
||||
|
||||
// TODO-barret make this an init method
|
||||
type InitInputBindings = {
|
||||
function initInputBindings(): {
|
||||
inputBindings: BindingRegistry<InputBinding>;
|
||||
fileInputBinding: FileInputBinding;
|
||||
};
|
||||
function initInputBindings(): InitInputBindings {
|
||||
} {
|
||||
const inputBindings = new BindingRegistry<InputBinding>();
|
||||
|
||||
inputBindings.register(new TextInputBinding(), "shiny.textInput");
|
||||
|
||||
@@ -142,13 +142,13 @@ class SelectInputBinding extends InputBinding {
|
||||
};
|
||||
};
|
||||
|
||||
// Calling selectize.clear() first works around https://github.com/selectize/selectize.js/issues/2146
|
||||
// As of selectize.js >= v0.13.1, .clearOptions() clears the selection,
|
||||
// but does NOT remove the previously-selected options. So unless we call
|
||||
// .clear() first, the current selection(s) will remain as (deselected)
|
||||
// options. See #3966 #4142
|
||||
selectize.clear();
|
||||
selectize.clearOptions();
|
||||
// If a new `selected` value is provided, also clear the current selection (otherwise it gets added as an option).
|
||||
// Note: although the selectize docs suggest otherwise, as of selectize.js >v0.15.2,
|
||||
// .clearOptions() no longer implicitly .clear()s (see #3967)
|
||||
if (hasDefinedProperty(data, "value")) {
|
||||
selectize.clear();
|
||||
}
|
||||
let loaded = false;
|
||||
|
||||
selectize.settings.load = function (query: string, callback: CallbackFn) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { Shiny } from "..";
|
||||
import { ShinyClientError } from "../shiny/error";
|
||||
|
||||
const buttonStyles = css`
|
||||
@@ -306,6 +307,7 @@ export class ShinyErrorMessage extends LitElement {
|
||||
|
||||
.error-message {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.decoration-container {
|
||||
@@ -488,6 +490,52 @@ export class ShinyErrorMessage extends LitElement {
|
||||
|
||||
customElements.define("shiny-error-message", ShinyErrorMessage);
|
||||
|
||||
export type ShinyClientMessage = {
|
||||
message: string;
|
||||
headline?: string;
|
||||
status?: "error" | "info" | "warning";
|
||||
};
|
||||
|
||||
function showShinyClientMessage({
|
||||
headline = "",
|
||||
message,
|
||||
status = "warning",
|
||||
}: ShinyClientMessage): void {
|
||||
const consoleMessage = `[shiny] ${headline}${
|
||||
headline ? " - " : ""
|
||||
}${message}`;
|
||||
|
||||
switch (status) {
|
||||
case "error":
|
||||
console.error(consoleMessage);
|
||||
break;
|
||||
case "warning":
|
||||
console.warn(consoleMessage);
|
||||
break;
|
||||
default:
|
||||
console.log(consoleMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!Shiny.inDevMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if an Error Console Container element already exists. If it
|
||||
// doesn't we need to add it before putting an error on the screen
|
||||
let errorConsoleContainer = document.querySelector("shiny-error-console");
|
||||
if (!errorConsoleContainer) {
|
||||
errorConsoleContainer = document.createElement("shiny-error-console");
|
||||
document.body.appendChild(errorConsoleContainer);
|
||||
}
|
||||
|
||||
const errorConsole = document.createElement("shiny-error-message");
|
||||
errorConsole.setAttribute("headline", headline);
|
||||
errorConsole.setAttribute("message", message);
|
||||
|
||||
errorConsoleContainer.appendChild(errorConsole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to show an error message to user in shiny-error-message web
|
||||
* component. Only shows the error if we're in development mode.
|
||||
@@ -496,11 +544,6 @@ customElements.define("shiny-error-message", ShinyErrorMessage);
|
||||
* object.
|
||||
*/
|
||||
export function showErrorInClientConsole(e: unknown): void {
|
||||
if (!Shiny.inDevMode()) {
|
||||
// If we're in production, don't show the error to the user
|
||||
return;
|
||||
}
|
||||
|
||||
let errorMsg: string | null = null;
|
||||
let headline = "Error on client while running Shiny app";
|
||||
|
||||
@@ -515,17 +558,24 @@ export function showErrorInClientConsole(e: unknown): void {
|
||||
errorMsg = "Unknown error";
|
||||
}
|
||||
|
||||
// Check to see if an Error Console Container element already exists. If it
|
||||
// doesn't we need to add it before putting an error on the screen
|
||||
let errorConsoleContainer = document.querySelector("shiny-error-console");
|
||||
if (!errorConsoleContainer) {
|
||||
errorConsoleContainer = document.createElement("shiny-error-console");
|
||||
document.body.appendChild(errorConsoleContainer);
|
||||
}
|
||||
|
||||
const errorConsole = document.createElement("shiny-error-message");
|
||||
errorConsole.setAttribute("headline", headline || "");
|
||||
errorConsole.setAttribute("message", errorMsg);
|
||||
|
||||
errorConsoleContainer.appendChild(errorConsole);
|
||||
showShinyClientMessage({ headline, message: errorMsg, status: "error" });
|
||||
}
|
||||
|
||||
export class ShinyClientMessageEvent extends CustomEvent<ShinyClientMessage> {
|
||||
constructor(detail: ShinyClientMessage) {
|
||||
super("shiny:client-message", { detail, bubbles: true, cancelable: true });
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("shiny:client-message", (ev: Event) => {
|
||||
if (!(ev instanceof CustomEvent)) {
|
||||
throw new Error("[shiny] shiny:client-message expected a CustomEvent");
|
||||
}
|
||||
const { headline, message, status } = ev.detail;
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
"[shiny] shiny:client-message expected a `message` property in `event.detail`."
|
||||
);
|
||||
}
|
||||
showShinyClientMessage({ headline, message, status });
|
||||
});
|
||||
|
||||
@@ -162,8 +162,8 @@ function initCoordmap(
|
||||
const bounds = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: img.clientWidth - 1,
|
||||
bottom: img.clientHeight - 1,
|
||||
right: img.naturalWidth - 1,
|
||||
bottom: img.naturalHeight - 1,
|
||||
};
|
||||
|
||||
coordmap_.panels[0] = {
|
||||
@@ -290,11 +290,10 @@ function initCoordmap(
|
||||
|
||||
const matches = []; // Panels that match
|
||||
const dists = []; // Distance of offset to each matching panel
|
||||
let b;
|
||||
let i;
|
||||
|
||||
for (i = 0; i < coordmap.panels.length; i++) {
|
||||
b = coordmap.panels[i].range;
|
||||
const b = coordmap.panels[i].range;
|
||||
|
||||
if (
|
||||
x <= b.right + expandImg.x &&
|
||||
@@ -413,5 +412,5 @@ function initCoordmap(
|
||||
return coordmap;
|
||||
}
|
||||
|
||||
export { findOrigin, initCoordmap };
|
||||
export type { Coordmap, CoordmapInit };
|
||||
export { initCoordmap, findOrigin };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { init } from "./initialize";
|
||||
export { Shiny, type ShinyClass } from "./initialize";
|
||||
|
||||
init();
|
||||
|
||||
@@ -2,15 +2,20 @@ import { determineBrowserInfo } from "./browser";
|
||||
import { disableFormSubmission } from "./disableForm";
|
||||
import { trackHistory } from "./history";
|
||||
|
||||
import { setShiny } from "../shiny";
|
||||
import { ShinyClass } from "../shiny";
|
||||
import { setUserAgent } from "../utils/userAgent";
|
||||
import { windowShiny } from "../window/libraries";
|
||||
import { windowUserAgent } from "../window/userAgent";
|
||||
|
||||
import { initReactlog } from "../shiny/reactlog";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let Shiny: ShinyClass;
|
||||
|
||||
function init(): void {
|
||||
setShiny(windowShiny());
|
||||
if (window.Shiny) {
|
||||
throw new Error("Trying to create window.Shiny, but it already exists!");
|
||||
}
|
||||
Shiny = window.Shiny = new ShinyClass();
|
||||
setUserAgent(windowUserAgent()); // before determineBrowserInfo()
|
||||
|
||||
determineBrowserInfo();
|
||||
@@ -21,4 +26,4 @@ function init(): void {
|
||||
initReactlog();
|
||||
}
|
||||
|
||||
export { init };
|
||||
export { init, Shiny, type ShinyClass };
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import $ from "jquery";
|
||||
import { Shiny } from "..";
|
||||
import type { InputBinding, OutputBinding } from "../bindings";
|
||||
import { OutputBindingAdapter } from "../bindings/outputAdapter";
|
||||
import type { BindingRegistry } from "../bindings/registry";
|
||||
import { ShinyClientMessageEvent } from "../components/errorConsole";
|
||||
import type {
|
||||
InputRateDecorator,
|
||||
InputValidateDecorator,
|
||||
} from "../inputPolicies";
|
||||
import { ShinyClientError } from "./error";
|
||||
import { shinyAppBindOutput, shinyAppUnbindOutput } from "./initedMethods";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
|
||||
type BindScope = HTMLElement | JQuery<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a jQuery object containing HTMLElements
|
||||
* @param value The value to check
|
||||
* @returns A type predicate indicating if the value is a jQuery<HTMLElement>
|
||||
*/
|
||||
function isJQuery<T = HTMLElement>(value: unknown): value is JQuery<T> {
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(value, "jquery") &&
|
||||
typeof (value as any).jquery === "string"
|
||||
);
|
||||
}
|
||||
|
||||
// todo make sure allowDeferred can NOT be supplied and still work
|
||||
function valueChangeCallback(
|
||||
inputs: InputValidateDecorator,
|
||||
@@ -74,14 +87,17 @@ const bindingsRegistry = (() => {
|
||||
* accessibility and other reasons. However, in practice our bindings still
|
||||
* work as long as inputs the IDs within a binding type don't overlap.
|
||||
*
|
||||
* @returns ShinyClientError if current ID bindings are invalid, otherwise
|
||||
* returns an ok status.
|
||||
* @returns ShinyClientMessageEvent if current ID bindings are invalid,
|
||||
* otherwise returns an ok status.
|
||||
*/
|
||||
function checkValidity():
|
||||
| { status: "error"; error: ShinyClientError }
|
||||
| { status: "ok" } {
|
||||
function checkValidity(scope: BindScope): void {
|
||||
if (!isJQuery(scope) && !(scope instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
type BindingCounts = { [T in BindingTypes]: number };
|
||||
const duplicateIds = new Map<string, BindingCounts>();
|
||||
const problems: Set<string> = new Set();
|
||||
|
||||
// count duplicate IDs of each binding type
|
||||
bindings.forEach((idTypes, id) => {
|
||||
@@ -89,22 +105,30 @@ const bindingsRegistry = (() => {
|
||||
|
||||
idTypes.forEach((type) => (counts[type] += 1));
|
||||
|
||||
// If there's a single duplication of ids across both binding types, then
|
||||
// when we're not in devmode, we allow this to pass because a good amount of
|
||||
// existing applications use this pattern even though its invalid. Eventually
|
||||
// this behavior should be removed.
|
||||
if (counts.input === 1 && counts.output === 1 && !Shiny.inDevMode()) {
|
||||
if (counts.input + counts.output < 2) {
|
||||
return;
|
||||
}
|
||||
// We have duplicated IDs, add them to the set of duplicated IDs to be
|
||||
// reported to the user.
|
||||
duplicateIds.set(id, counts);
|
||||
|
||||
// If we have duplicated IDs, then add them to the set of duplicated IDs
|
||||
// to be reported to the user.
|
||||
if (counts.input + counts.output > 1) {
|
||||
duplicateIds.set(id, counts);
|
||||
if (counts.input > 1) {
|
||||
problems.add("input");
|
||||
}
|
||||
if (counts.output > 1) {
|
||||
problems.add("output");
|
||||
}
|
||||
if (counts.input >= 1 && counts.output >= 1) {
|
||||
problems.add("shared");
|
||||
}
|
||||
});
|
||||
|
||||
if (duplicateIds.size === 0) return { status: "ok" };
|
||||
if (duplicateIds.size === 0) return;
|
||||
// Duplicated IDs are now always a warning. Before the ShinyClient console
|
||||
// was added duplicate output IDs were errors in "production" mode. After
|
||||
// the Shiny Client console was introduced, duplicate IDs were no longer
|
||||
// production errors but *would* break apps in dev mode. Now, in v1.10+,
|
||||
// duplicate IDs are always warnings in all modes for consistency.
|
||||
|
||||
const duplicateIdMsg = Array.from(duplicateIds.entries())
|
||||
.map(([id, counts]) => {
|
||||
@@ -119,15 +143,27 @@ const bindingsRegistry = (() => {
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
status: "error",
|
||||
error: new ShinyClientError({
|
||||
headline: "Duplicate input/output IDs found",
|
||||
message: `The following ${
|
||||
duplicateIds.size === 1 ? "ID was" : "IDs were"
|
||||
} repeated:\n${duplicateIdMsg}`,
|
||||
}),
|
||||
};
|
||||
let txtVerb = "Duplicate";
|
||||
let txtNoun = "input/output";
|
||||
if (problems.has("input") && problems.has("output")) {
|
||||
// base case
|
||||
} else if (problems.has("input")) {
|
||||
txtNoun = "input";
|
||||
} else if (problems.has("output")) {
|
||||
txtNoun = "output";
|
||||
} else if (problems.has("shared")) {
|
||||
txtVerb = "Shared";
|
||||
}
|
||||
|
||||
const txtIdsWere = duplicateIds.size == 1 ? "ID was" : "IDs were";
|
||||
const headline = `${txtVerb} ${txtNoun} ${txtIdsWere} found`;
|
||||
const message = `The following ${txtIdsWere} used for more than one ${
|
||||
problems.has("shared") ? "input/output" : txtNoun
|
||||
}:\n${duplicateIdMsg}`;
|
||||
|
||||
const event = new ShinyClientMessageEvent({ headline, message });
|
||||
const scopeElement = isJQuery(scope) ? scope.get(0) : scope;
|
||||
(scopeElement || window).dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,15 +458,7 @@ async function _bindAll(
|
||||
// complete error message that contains everything they will need to fix. If
|
||||
// we threw as we saw collisions then the user would fix the first collision,
|
||||
// re-run, and then see the next collision, etc.
|
||||
const bindingValidity = bindingsRegistry.checkValidity();
|
||||
if (bindingValidity.status === "error") {
|
||||
// Only throw if we're in dev mode. Otherwise, just log a warning.
|
||||
if (Shiny.inDevMode()) {
|
||||
throw bindingValidity.error;
|
||||
} else {
|
||||
console.warn("[shiny] " + bindingValidity.error.message);
|
||||
}
|
||||
}
|
||||
bindingsRegistry.checkValidity(scope);
|
||||
|
||||
return currentInputs;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,34 @@ import $ from "jquery";
|
||||
import { InputBinding, OutputBinding } from "../bindings";
|
||||
import { initInputBindings } from "../bindings/input";
|
||||
import { initOutputBindings } from "../bindings/output";
|
||||
import type { BindingRegistry } from "../bindings/registry";
|
||||
import { showErrorInClientConsole } from "../components/errorConsole";
|
||||
import { resetBrush } from "../imageutils/resetBrush";
|
||||
import { $escape, compareVersion } from "../utils";
|
||||
import { initShiny } from "./init";
|
||||
import type { InputPolicy } from "../inputPolicies";
|
||||
import {
|
||||
InputBatchSender,
|
||||
InputDeferDecorator,
|
||||
InputEventDecorator,
|
||||
InputNoResendDecorator,
|
||||
InputRateDecorator,
|
||||
InputValidateDecorator,
|
||||
} from "../inputPolicies";
|
||||
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
|
||||
import { addDefaultInputOpts } from "../inputPolicies/inputValidateDecorator";
|
||||
import { debounce, Debouncer } from "../time";
|
||||
import {
|
||||
$escape,
|
||||
compareVersion,
|
||||
getBoundingClientSizeBeforeZoom,
|
||||
getComputedLinkColor,
|
||||
getStyle,
|
||||
hasDefinedProperty,
|
||||
mapValues,
|
||||
pixelRatio,
|
||||
} from "../utils";
|
||||
import { createInitStatus, type InitStatusPromise } from "../utils/promise";
|
||||
import type { BindInputsCtx, BindScope } from "./bind";
|
||||
import { bindAll, unbindAll, _bindAll } from "./bind";
|
||||
import type {
|
||||
shinyBindAll,
|
||||
shinyForgetLastInputValue,
|
||||
@@ -14,11 +38,12 @@ import type {
|
||||
shinySetInputValue,
|
||||
shinyUnbindAll,
|
||||
} from "./initedMethods";
|
||||
import { setFileInputBinding } from "./initedMethods";
|
||||
import { setFileInputBinding, setShinyObj } from "./initedMethods";
|
||||
import { removeModal, showModal } from "./modal";
|
||||
import { removeNotification, showNotification } from "./notifications";
|
||||
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
|
||||
import {
|
||||
registerDependency,
|
||||
renderContent,
|
||||
renderContentAsync,
|
||||
renderDependencies,
|
||||
@@ -26,17 +51,18 @@ import {
|
||||
renderHtml,
|
||||
renderHtmlAsync,
|
||||
} from "./render";
|
||||
import type { Handler, ShinyApp } from "./shinyapp";
|
||||
import { addCustomMessageHandler } from "./shinyapp";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
import { addCustomMessageHandler, ShinyApp, type Handler } from "./shinyapp";
|
||||
import { registerNames as singletonsRegisterNames } from "./singletons";
|
||||
|
||||
interface Shiny {
|
||||
class ShinyClass {
|
||||
version: string;
|
||||
$escape: typeof $escape;
|
||||
compareVersion: typeof compareVersion;
|
||||
inputBindings: ReturnType<typeof initInputBindings>["inputBindings"];
|
||||
inputBindings: BindingRegistry<InputBinding>;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
InputBinding: typeof InputBinding;
|
||||
outputBindings: ReturnType<typeof initOutputBindings>["outputBindings"];
|
||||
outputBindings: BindingRegistry<OutputBinding>;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
OutputBinding: typeof OutputBinding;
|
||||
resetBrush: typeof resetBrush;
|
||||
@@ -45,7 +71,6 @@ interface Shiny {
|
||||
remove: typeof removeNotification;
|
||||
};
|
||||
modal: { show: typeof showModal; remove: typeof removeModal };
|
||||
createSocket?: () => WebSocket;
|
||||
showReconnectDialog: typeof showReconnectDialog;
|
||||
hideReconnectDialog: typeof hideReconnectDialog;
|
||||
renderDependenciesAsync: typeof renderDependenciesAsync;
|
||||
@@ -54,9 +79,12 @@ interface Shiny {
|
||||
renderContent: typeof renderContent;
|
||||
renderHtmlAsync: typeof renderHtmlAsync;
|
||||
renderHtml: typeof renderHtml;
|
||||
user: string;
|
||||
progressHandlers?: ShinyApp["progressHandlers"];
|
||||
addCustomMessageHandler: typeof addCustomMessageHandler;
|
||||
|
||||
// The following are added in the initialization, by initShiny()
|
||||
createSocket?: () => WebSocket;
|
||||
user?: string;
|
||||
progressHandlers?: ShinyApp["progressHandlers"];
|
||||
shinyapp?: ShinyApp;
|
||||
setInputValue?: typeof shinySetInputValue;
|
||||
onInputChange?: typeof shinySetInputValue;
|
||||
@@ -65,77 +93,629 @@ interface Shiny {
|
||||
unbindAll?: typeof shinyUnbindAll;
|
||||
initializeInputs?: typeof shinyInitializeInputs;
|
||||
|
||||
// Promise-like object that is resolved after initialization.
|
||||
initializedPromise: InitStatusPromise<void>;
|
||||
|
||||
// Eventually deprecate
|
||||
// For old-style custom messages - should deprecate and migrate to new
|
||||
oncustommessage?: Handler;
|
||||
|
||||
constructor() {
|
||||
// `process.env.SHINY_VERSION` is overwritten to the Shiny version at build time.
|
||||
// During testing, the `Shiny.version` will be `"development"`
|
||||
this.version = process.env.SHINY_VERSION || "development";
|
||||
|
||||
const { inputBindings, fileInputBinding } = initInputBindings();
|
||||
const { outputBindings } = initOutputBindings();
|
||||
|
||||
setFileInputBinding(fileInputBinding);
|
||||
|
||||
this.$escape = $escape;
|
||||
this.compareVersion = compareVersion;
|
||||
this.inputBindings = inputBindings;
|
||||
this.InputBinding = InputBinding;
|
||||
this.outputBindings = outputBindings;
|
||||
this.OutputBinding = OutputBinding;
|
||||
this.resetBrush = resetBrush;
|
||||
this.notifications = {
|
||||
show: showNotification,
|
||||
remove: removeNotification,
|
||||
};
|
||||
this.modal = { show: showModal, remove: removeModal };
|
||||
|
||||
this.addCustomMessageHandler = addCustomMessageHandler;
|
||||
this.showReconnectDialog = showReconnectDialog;
|
||||
this.hideReconnectDialog = hideReconnectDialog;
|
||||
this.renderDependenciesAsync = renderDependenciesAsync;
|
||||
this.renderDependencies = renderDependencies;
|
||||
this.renderContentAsync = renderContentAsync;
|
||||
this.renderContent = renderContent;
|
||||
this.renderHtmlAsync = renderHtmlAsync;
|
||||
this.renderHtml = renderHtml;
|
||||
|
||||
this.initializedPromise = createInitStatus<void>();
|
||||
|
||||
$(() => {
|
||||
// Init Shiny a little later than document ready, so user code can
|
||||
// run first (i.e. to register bindings)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.initialize();
|
||||
} catch (e) {
|
||||
showErrorInClientConsole(e);
|
||||
throw e;
|
||||
}
|
||||
}, 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to check if Shiny is running in development mode. By packaging as a
|
||||
* method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__`
|
||||
* variable in the global scope.
|
||||
* @returns `true` if Shiny is running in development mode, `false` otherwise.
|
||||
*/
|
||||
inDevMode: () => boolean;
|
||||
}
|
||||
|
||||
let windowShiny: Shiny;
|
||||
|
||||
function setShiny(windowShiny_: Shiny): void {
|
||||
windowShiny = windowShiny_;
|
||||
|
||||
// `process.env.SHINY_VERSION` is overwritten to the Shiny version at build time.
|
||||
// During testing, the `Shiny.version` will be `"development"`
|
||||
windowShiny.version = process.env.SHINY_VERSION || "development";
|
||||
|
||||
const { inputBindings, fileInputBinding } = initInputBindings();
|
||||
const { outputBindings } = initOutputBindings();
|
||||
|
||||
// set variable to be retrieved later
|
||||
setFileInputBinding(fileInputBinding);
|
||||
|
||||
windowShiny.$escape = $escape;
|
||||
windowShiny.compareVersion = compareVersion;
|
||||
windowShiny.inputBindings = inputBindings;
|
||||
windowShiny.InputBinding = InputBinding;
|
||||
windowShiny.outputBindings = outputBindings;
|
||||
windowShiny.OutputBinding = OutputBinding;
|
||||
windowShiny.resetBrush = resetBrush;
|
||||
windowShiny.notifications = {
|
||||
show: showNotification,
|
||||
remove: removeNotification,
|
||||
};
|
||||
windowShiny.modal = { show: showModal, remove: removeModal };
|
||||
|
||||
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;
|
||||
|
||||
windowShiny.inDevMode = () => {
|
||||
inDevMode(): boolean {
|
||||
if ("__SHINY_DEV_MODE__" in window)
|
||||
return Boolean(window.__SHINY_DEV_MODE__);
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
$(function () {
|
||||
// Init Shiny a little later than document ready, so user code can
|
||||
// run first (i.e. to register bindings)
|
||||
setTimeout(async function () {
|
||||
try {
|
||||
await initShiny(windowShiny);
|
||||
} catch (e) {
|
||||
showErrorInClientConsole(e);
|
||||
throw e;
|
||||
async initialize(): Promise<void> {
|
||||
setShinyObj(this);
|
||||
this.shinyapp = new ShinyApp();
|
||||
const shinyapp = this.shinyapp;
|
||||
|
||||
this.progressHandlers = shinyapp.progressHandlers;
|
||||
|
||||
const inputBatchSender = new InputBatchSender(shinyapp);
|
||||
const inputsNoResend = new InputNoResendDecorator(inputBatchSender);
|
||||
const inputsEvent = new InputEventDecorator(inputsNoResend);
|
||||
const inputsRate = new InputRateDecorator(inputsEvent);
|
||||
const inputsDefer = new InputDeferDecorator(inputsEvent);
|
||||
|
||||
let target: InputPolicy;
|
||||
|
||||
if ($('input[type="submit"], button[type="submit"]').length > 0) {
|
||||
// If there is a submit button on the page, use defer decorator
|
||||
target = inputsDefer;
|
||||
|
||||
$('input[type="submit"], button[type="submit"]').each(function () {
|
||||
$(this).click(function (event) {
|
||||
event.preventDefault();
|
||||
inputsDefer.submit();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// By default, use rate decorator
|
||||
target = inputsRate;
|
||||
}
|
||||
|
||||
const inputs = new InputValidateDecorator(target);
|
||||
|
||||
this.setInputValue = this.onInputChange = function (
|
||||
name: string,
|
||||
value: unknown,
|
||||
opts: Partial<InputPolicyOpts> = {}
|
||||
): void {
|
||||
const newOpts = addDefaultInputOpts(opts);
|
||||
|
||||
inputs.setInput(name, value, newOpts);
|
||||
};
|
||||
|
||||
// By default, Shiny deduplicates input value changes; that is, if
|
||||
// `setInputValue` is called with the same value as the input already
|
||||
// has, the call is ignored (unless opts.priority = "event"). Calling
|
||||
// `forgetLastInputValue` tells Shiny that the very next call to
|
||||
// `setInputValue` for this input id shouldn't be ignored, even if it
|
||||
// is a dupe of the existing value.
|
||||
this.forgetLastInputValue = function (name) {
|
||||
inputsNoResend.forget(name);
|
||||
};
|
||||
|
||||
// MUST be called after `setShiny()`
|
||||
const inputBindings = this.inputBindings;
|
||||
const outputBindings = this.outputBindings;
|
||||
|
||||
function shinyBindCtx(): BindInputsCtx {
|
||||
return {
|
||||
inputs,
|
||||
inputsRate,
|
||||
sendOutputHiddenState,
|
||||
maybeAddThemeObserver,
|
||||
inputBindings,
|
||||
outputBindings,
|
||||
initDeferredIframes,
|
||||
};
|
||||
}
|
||||
|
||||
this.bindAll = async function (scope: BindScope) {
|
||||
await bindAll(shinyBindCtx(), scope);
|
||||
};
|
||||
this.unbindAll = function (scope: BindScope, includeSelf = false) {
|
||||
unbindAll(shinyBindCtx(), scope, includeSelf);
|
||||
};
|
||||
|
||||
// Calls .initialize() for all of the input objects in all input bindings,
|
||||
// in the given scope.
|
||||
function initializeInputs(scope: BindScope = document.documentElement) {
|
||||
const bindings = inputBindings.getBindings();
|
||||
|
||||
// Iterate over all bindings
|
||||
for (let i = 0; i < bindings.length; i++) {
|
||||
const binding = bindings[i].binding;
|
||||
const inputObjects = binding.find(scope);
|
||||
|
||||
if (inputObjects) {
|
||||
// Iterate over all input objects for this binding
|
||||
for (let j = 0; j < inputObjects.length; j++) {
|
||||
const $inputObjectJ = $(inputObjects[j]);
|
||||
|
||||
if (!$inputObjectJ.data("_shiny_initialized")) {
|
||||
$inputObjectJ.data("_shiny_initialized", true);
|
||||
binding.initialize(inputObjects[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
this.initializeInputs = initializeInputs;
|
||||
|
||||
function getIdFromEl(el: HTMLElement) {
|
||||
const $el = $(el);
|
||||
const bindingAdapter = $el.data("shiny-output-binding");
|
||||
|
||||
if (!bindingAdapter) return null;
|
||||
else return bindingAdapter.getId();
|
||||
}
|
||||
|
||||
// Initialize all input objects in the document, before binding
|
||||
initializeInputs(document.documentElement);
|
||||
|
||||
// The input values returned by _bindAll() each have a structure like this:
|
||||
// { value: 123, opts: { ... } }
|
||||
// We want to only keep the value. This is because when the initialValues is
|
||||
// passed to ShinyApp.connect(), the ShinyApp object stores the
|
||||
// initialValues object for the duration of the session, and the opts may
|
||||
// have a reference to the DOM element, which would prevent it from being
|
||||
// GC'd.
|
||||
const initialValues = mapValues(
|
||||
await _bindAll(shinyBindCtx(), document.documentElement),
|
||||
(x) => x.value
|
||||
);
|
||||
|
||||
// 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(
|
||||
function () {
|
||||
const id = getIdFromEl(this),
|
||||
rect = getBoundingClientSizeBeforeZoom(this);
|
||||
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
initialValues[".clientdata_output_" + id + "_width"] = rect.width;
|
||||
initialValues[".clientdata_output_" + id + "_height"] = rect.height;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function getComputedBgColor(
|
||||
el: HTMLElement | null
|
||||
): string | null | undefined {
|
||||
if (!el) {
|
||||
// Top of document, can't recurse further
|
||||
return null;
|
||||
}
|
||||
|
||||
const bgColor = getStyle(el, "background-color");
|
||||
|
||||
if (!bgColor) return bgColor;
|
||||
const m = bgColor.match(
|
||||
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/
|
||||
);
|
||||
|
||||
if (bgColor === "transparent" || (m && parseFloat(m[4]) === 0)) {
|
||||
// No background color on this element. See if it has a background image.
|
||||
const bgImage = getStyle(el, "background-image");
|
||||
|
||||
if (bgImage && bgImage !== "none") {
|
||||
// Failed to detect background color, since it has a background image
|
||||
return null;
|
||||
} else {
|
||||
// Recurse
|
||||
return getComputedBgColor(el.parentElement);
|
||||
}
|
||||
}
|
||||
return bgColor;
|
||||
}
|
||||
|
||||
function getComputedFont(el: HTMLElement) {
|
||||
const fontFamily = getStyle(el, "font-family");
|
||||
const fontSize = getStyle(el, "font-size");
|
||||
|
||||
return {
|
||||
families: fontFamily?.replace(/"/g, "").split(", "),
|
||||
size: fontSize,
|
||||
};
|
||||
}
|
||||
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
|
||||
function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const el = this;
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
initialValues[".clientdata_output_" + id + "_bg"] =
|
||||
getComputedBgColor(el);
|
||||
initialValues[".clientdata_output_" + id + "_fg"] = getStyle(
|
||||
el,
|
||||
"color"
|
||||
);
|
||||
initialValues[".clientdata_output_" + id + "_accent"] =
|
||||
getComputedLinkColor(el);
|
||||
initialValues[".clientdata_output_" + id + "_font"] =
|
||||
getComputedFont(el);
|
||||
maybeAddThemeObserver(el);
|
||||
}
|
||||
);
|
||||
|
||||
// Resend computed styles if *an output element's* class or style attribute changes.
|
||||
// This gives us some level of confidence that getCurrentOutputInfo() will be
|
||||
// properly invalidated if output container is mutated; but unfortunately,
|
||||
// we don't have a reasonable way to detect change in *inherited* styles
|
||||
// (other than session$setCurrentTheme())
|
||||
// https://github.com/rstudio/shiny/issues/3196
|
||||
// https://github.com/rstudio/shiny/issues/2998
|
||||
function maybeAddThemeObserver(el: HTMLElement): void {
|
||||
if (!window.MutationObserver) {
|
||||
return; // IE10 and lower
|
||||
}
|
||||
|
||||
const cl = el.classList;
|
||||
const reportTheme =
|
||||
cl.contains("shiny-image-output") ||
|
||||
cl.contains("shiny-plot-output") ||
|
||||
cl.contains("shiny-report-theme");
|
||||
|
||||
if (!reportTheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $el = $(el);
|
||||
|
||||
if ($el.data("shiny-theme-observer")) {
|
||||
return; // i.e., observer is already observing
|
||||
}
|
||||
|
||||
const observerCallback = new Debouncer(null, () => doSendTheme(el), 100);
|
||||
const observer = new MutationObserver(() =>
|
||||
observerCallback.normalCall()
|
||||
);
|
||||
const config = { attributes: true, attributeFilter: ["style", "class"] };
|
||||
|
||||
observer.observe(el, config);
|
||||
$el.data("shiny-theme-observer", observer);
|
||||
}
|
||||
|
||||
function doSendTheme(el: HTMLElement): void {
|
||||
// Sending theme info on error isn't necessary (it'd add an unnecessary additional round-trip)
|
||||
if (el.classList.contains("shiny-output-error")) {
|
||||
return;
|
||||
}
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_bg",
|
||||
getComputedBgColor(el)
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_fg",
|
||||
getStyle(el, "color")
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_accent",
|
||||
getComputedLinkColor(el)
|
||||
);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_font",
|
||||
getComputedFont(el)
|
||||
);
|
||||
}
|
||||
|
||||
function doSendImageSize() {
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
|
||||
function () {
|
||||
const id = getIdFromEl(this),
|
||||
rect = getBoundingClientSizeBeforeZoom(this);
|
||||
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
inputs.setInput(".clientdata_output_" + id + "_width", rect.width);
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_height",
|
||||
rect.height
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
|
||||
function () {
|
||||
doSendTheme(this);
|
||||
}
|
||||
);
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const $this = $(this),
|
||||
binding = $this.data("shiny-output-binding");
|
||||
|
||||
$this.trigger({
|
||||
type: "shiny:visualchange",
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
visible: !isHidden(this),
|
||||
binding: binding,
|
||||
});
|
||||
binding.onResize();
|
||||
});
|
||||
}
|
||||
|
||||
sendImageSizeFns.setImageSend(inputBatchSender, doSendImageSize);
|
||||
|
||||
// Return true if the object or one of its ancestors in the DOM tree has
|
||||
// style='display:none'; otherwise return false.
|
||||
function isHidden(obj: HTMLElement | null): boolean {
|
||||
// null means we've hit the top of the tree. If width or height is
|
||||
// non-zero, then we know that no ancestor has display:none.
|
||||
if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
|
||||
return false;
|
||||
} else if (getStyle(obj, "display") === "none") {
|
||||
return true;
|
||||
} else {
|
||||
return isHidden(obj.parentNode as HTMLElement | null);
|
||||
}
|
||||
}
|
||||
let lastKnownVisibleOutputs: { [key: string]: boolean } = {};
|
||||
// Set initial state of outputs to hidden, if needed
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const id = getIdFromEl(this);
|
||||
|
||||
if (isHidden(this)) {
|
||||
initialValues[".clientdata_output_" + id + "_hidden"] = true;
|
||||
} else {
|
||||
lastKnownVisibleOutputs[id] = true;
|
||||
initialValues[".clientdata_output_" + id + "_hidden"] = false;
|
||||
}
|
||||
});
|
||||
// Send update when hidden state changes
|
||||
function doSendOutputHiddenState() {
|
||||
const visibleOutputs: { [key: string]: boolean } = {};
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const id = getIdFromEl(this);
|
||||
|
||||
delete lastKnownVisibleOutputs[id];
|
||||
// Assume that the object is hidden when width and height are 0
|
||||
const hidden = isHidden(this),
|
||||
evt = {
|
||||
type: "shiny:visualchange",
|
||||
visible: !hidden,
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
inputs.setInput(".clientdata_output_" + id + "_hidden", true);
|
||||
} else {
|
||||
visibleOutputs[id] = true;
|
||||
inputs.setInput(".clientdata_output_" + id + "_hidden", false);
|
||||
}
|
||||
const $this = $(this);
|
||||
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
evt.binding = $this.data("shiny-output-binding");
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
$this.trigger(evt);
|
||||
});
|
||||
// Anything left in lastKnownVisibleOutputs is orphaned
|
||||
for (const name in lastKnownVisibleOutputs) {
|
||||
if (hasDefinedProperty(lastKnownVisibleOutputs, name))
|
||||
inputs.setInput(".clientdata_output_" + name + "_hidden", true);
|
||||
}
|
||||
// Update the visible outputs for next time
|
||||
lastKnownVisibleOutputs = visibleOutputs;
|
||||
}
|
||||
// sendOutputHiddenState gets called each time DOM elements are shown or
|
||||
// hidden. This can be in the hundreds or thousands of times at startup.
|
||||
// We'll debounce it, so that we do the actual work once per tick.
|
||||
const sendOutputHiddenStateDebouncer = new Debouncer(
|
||||
null,
|
||||
doSendOutputHiddenState,
|
||||
0
|
||||
);
|
||||
|
||||
function sendOutputHiddenState() {
|
||||
sendOutputHiddenStateDebouncer.normalCall();
|
||||
}
|
||||
// We need to make sure doSendOutputHiddenState actually gets called before
|
||||
// the inputBatchSender sends data to the server. The lastChanceCallback
|
||||
// here does that - if the debouncer has a pending call, flush it.
|
||||
inputBatchSender.lastChanceCallback.push(function () {
|
||||
if (sendOutputHiddenStateDebouncer.isPending())
|
||||
sendOutputHiddenStateDebouncer.immediateCall();
|
||||
});
|
||||
|
||||
// Given a namespace and a handler function, return a function that invokes
|
||||
// the handler only when e's namespace matches. For example, if the
|
||||
// namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
|
||||
// If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
|
||||
function filterEventsByNamespace(
|
||||
namespace: string,
|
||||
handler: (...handlerArgs: any[]) => void,
|
||||
...args: any[]
|
||||
) {
|
||||
const namespaceArr = namespace.split(".");
|
||||
|
||||
return function (this: HTMLElement, e: JQuery.TriggeredEvent) {
|
||||
const eventNamespace = e.namespace?.split(".") ?? [];
|
||||
|
||||
// If any of the namespace strings aren't present in this event, quit.
|
||||
for (let i = 0; i < namespaceArr.length; i++) {
|
||||
if (eventNamespace.indexOf(namespaceArr[i]) === -1) return;
|
||||
}
|
||||
|
||||
handler.apply(this, [namespaceArr, handler, ...args]);
|
||||
};
|
||||
}
|
||||
|
||||
// The size of each image may change either because the browser window was
|
||||
// resized, or because a tab was shown/hidden (hidden elements report size
|
||||
// of 0x0). It's OK to over-report sizes because the input pipeline will
|
||||
// filter out values that haven't changed.
|
||||
$(window).resize(debounce(500, sendImageSizeFns.regular));
|
||||
// Need to register callbacks for each Bootstrap 3 class.
|
||||
const bs3classes = [
|
||||
"modal",
|
||||
"dropdown",
|
||||
"tab",
|
||||
"tooltip",
|
||||
"popover",
|
||||
"collapse",
|
||||
];
|
||||
|
||||
$.each(bs3classes, function (idx, classname) {
|
||||
$(document.body).on(
|
||||
"shown.bs." + classname + ".sendImageSize",
|
||||
"*",
|
||||
filterEventsByNamespace("bs", sendImageSizeFns.regular)
|
||||
);
|
||||
$(document.body).on(
|
||||
"shown.bs." +
|
||||
classname +
|
||||
".sendOutputHiddenState " +
|
||||
"hidden.bs." +
|
||||
classname +
|
||||
".sendOutputHiddenState",
|
||||
"*",
|
||||
filterEventsByNamespace("bs", sendOutputHiddenState)
|
||||
);
|
||||
});
|
||||
|
||||
// This is needed for Bootstrap 2 compatibility and for non-Bootstrap
|
||||
// related shown/hidden events (like conditionalPanel)
|
||||
$(document.body).on("shown.sendImageSize", "*", sendImageSizeFns.regular);
|
||||
$(document.body).on(
|
||||
"shown.sendOutputHiddenState hidden.sendOutputHiddenState",
|
||||
"*",
|
||||
sendOutputHiddenState
|
||||
);
|
||||
|
||||
// Send initial pixel ratio, and update it if it changes
|
||||
initialValues[".clientdata_pixelratio"] = pixelRatio();
|
||||
$(window).resize(function () {
|
||||
inputs.setInput(".clientdata_pixelratio", pixelRatio());
|
||||
});
|
||||
|
||||
// Send initial URL
|
||||
initialValues[".clientdata_url_protocol"] = window.location.protocol;
|
||||
initialValues[".clientdata_url_hostname"] = window.location.hostname;
|
||||
initialValues[".clientdata_url_port"] = window.location.port;
|
||||
initialValues[".clientdata_url_pathname"] = window.location.pathname;
|
||||
|
||||
// Send initial URL search (query string) and update it if it changes
|
||||
initialValues[".clientdata_url_search"] = window.location.search;
|
||||
|
||||
$(window).on("pushstate", function (e) {
|
||||
inputs.setInput(".clientdata_url_search", window.location.search);
|
||||
return;
|
||||
e;
|
||||
});
|
||||
|
||||
$(window).on("popstate", function (e) {
|
||||
inputs.setInput(".clientdata_url_search", window.location.search);
|
||||
return;
|
||||
e;
|
||||
});
|
||||
|
||||
// This is only the initial value of the hash. The hash can change, but
|
||||
// a reactive version of this isn't sent because watching for changes can
|
||||
// require polling on some browsers. The JQuery hashchange plugin can be
|
||||
// used if this capability is important.
|
||||
initialValues[".clientdata_url_hash_initial"] = window.location.hash;
|
||||
initialValues[".clientdata_url_hash"] = window.location.hash;
|
||||
|
||||
$(window).on("hashchange", function (e) {
|
||||
inputs.setInput(".clientdata_url_hash", window.location.hash);
|
||||
return;
|
||||
e;
|
||||
});
|
||||
|
||||
// The server needs to know what singletons were rendered as part of
|
||||
// the page loading
|
||||
const singletonText = (initialValues[".clientdata_singletons"] = $(
|
||||
'script[type="application/shiny-singletons"]'
|
||||
).text());
|
||||
|
||||
singletonsRegisterNames(singletonText.split(/,/));
|
||||
|
||||
const dependencyText = $(
|
||||
'script[type="application/html-dependencies"]'
|
||||
).text();
|
||||
|
||||
$.each(dependencyText.split(/;/), function (i, depStr) {
|
||||
const match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr);
|
||||
|
||||
if (match) {
|
||||
registerDependency(match[1], match[2]);
|
||||
}
|
||||
});
|
||||
|
||||
// We've collected all the initial values--start the server process!
|
||||
inputsNoResend.reset(initialValues);
|
||||
shinyapp.connect(initialValues);
|
||||
$(document).one("shiny:connected", () => {
|
||||
initDeferredIframes();
|
||||
});
|
||||
|
||||
$(document).one("shiny:sessioninitialized", () => {
|
||||
this.initializedPromise.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Give any deferred iframes a chance to load.
|
||||
function initDeferredIframes(): void {
|
||||
// TODO-barret; This method uses `window.Shiny`. Could be replaced with `fullShinyObj_.shinyapp?.isConnected()`,
|
||||
// but that would not use `window.Shiny`. Is it a problem???
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
|
||||
// Can not expect error when combining with window available Shiny definition
|
||||
!window.Shiny ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
|
||||
// Can not expect error when combining with window available Shiny definition
|
||||
!window.Shiny.shinyapp ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
|
||||
// Can not expect error when combining with window available Shiny definition
|
||||
!window.Shiny.shinyapp.isConnected()
|
||||
) {
|
||||
// If somehow we accidentally call this before the server connection is
|
||||
// established, just ignore the call. At the time of this writing it
|
||||
// doesn't happen, but it's easy to imagine a later refactoring putting
|
||||
// us in this situation and it'd be hard to notice with either manual
|
||||
// testing or automated tests, because the only effect is on HTTP request
|
||||
// timing. (Update: Actually Aron saw this being called without even
|
||||
// window.Shiny being defined, but it was hard to repro.)
|
||||
return;
|
||||
}
|
||||
|
||||
$(".shiny-frame-deferred").each(function (i, el) {
|
||||
const $el = $(el);
|
||||
|
||||
$el.removeClass("shiny-frame-deferred");
|
||||
// @ts-expect-error; If it is undefined, set using the undefined value
|
||||
$el.attr("src", $el.attr("data-deferred-src"));
|
||||
$el.attr("data-deferred-src", null);
|
||||
});
|
||||
}
|
||||
|
||||
export { windowShiny, setShiny };
|
||||
export type { Shiny };
|
||||
export { ShinyClass };
|
||||
|
||||
@@ -1,565 +0,0 @@
|
||||
import $ from "jquery";
|
||||
import type { Shiny } from ".";
|
||||
import type { InputPolicy } from "../inputPolicies";
|
||||
import {
|
||||
InputBatchSender,
|
||||
InputDeferDecorator,
|
||||
InputEventDecorator,
|
||||
InputNoResendDecorator,
|
||||
InputRateDecorator,
|
||||
InputValidateDecorator,
|
||||
} from "../inputPolicies";
|
||||
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
|
||||
import { addDefaultInputOpts } from "../inputPolicies/inputValidateDecorator";
|
||||
import { debounce, Debouncer } from "../time";
|
||||
import {
|
||||
getComputedLinkColor,
|
||||
getStyle,
|
||||
hasDefinedProperty,
|
||||
mapValues,
|
||||
pixelRatio,
|
||||
} from "../utils";
|
||||
import type { BindInputsCtx, BindScope } from "./bind";
|
||||
import { bindAll, unbindAll, _bindAll } from "./bind";
|
||||
import { setShinyObj } from "./initedMethods";
|
||||
import { registerDependency } from "./render";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
import { ShinyApp } from "./shinyapp";
|
||||
import { registerNames as singletonsRegisterNames } from "./singletons";
|
||||
|
||||
// "init_shiny.js"
|
||||
async function initShiny(windowShiny: Shiny): Promise<void> {
|
||||
setShinyObj(windowShiny);
|
||||
const shinyapp = (windowShiny.shinyapp = new ShinyApp());
|
||||
|
||||
windowShiny.progressHandlers = shinyapp.progressHandlers;
|
||||
|
||||
const inputBatchSender = new InputBatchSender(shinyapp);
|
||||
const inputsNoResend = new InputNoResendDecorator(inputBatchSender);
|
||||
const inputsEvent = new InputEventDecorator(inputsNoResend);
|
||||
const inputsRate = new InputRateDecorator(inputsEvent);
|
||||
const inputsDefer = new InputDeferDecorator(inputsEvent);
|
||||
|
||||
let target: InputPolicy;
|
||||
|
||||
if ($('input[type="submit"], button[type="submit"]').length > 0) {
|
||||
// If there is a submit button on the page, use defer decorator
|
||||
target = inputsDefer;
|
||||
|
||||
$('input[type="submit"], button[type="submit"]').each(function () {
|
||||
$(this).click(function (event) {
|
||||
event.preventDefault();
|
||||
inputsDefer.submit();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// By default, use rate decorator
|
||||
target = inputsRate;
|
||||
}
|
||||
|
||||
const inputs = new InputValidateDecorator(target);
|
||||
|
||||
windowShiny.setInputValue = windowShiny.onInputChange = function (
|
||||
name: string,
|
||||
value: unknown,
|
||||
opts: Partial<InputPolicyOpts> = {}
|
||||
): void {
|
||||
const newOpts = addDefaultInputOpts(opts);
|
||||
|
||||
inputs.setInput(name, value, newOpts);
|
||||
};
|
||||
|
||||
// By default, Shiny deduplicates input value changes; that is, if
|
||||
// `setInputValue` is called with the same value as the input already
|
||||
// has, the call is ignored (unless opts.priority = "event"). Calling
|
||||
// `forgetLastInputValue` tells Shiny that the very next call to
|
||||
// `setInputValue` for this input id shouldn't be ignored, even if it
|
||||
// is a dupe of the existing value.
|
||||
windowShiny.forgetLastInputValue = function (name) {
|
||||
inputsNoResend.forget(name);
|
||||
};
|
||||
|
||||
// MUST be called after `setShiny()`
|
||||
const inputBindings = windowShiny.inputBindings;
|
||||
const outputBindings = windowShiny.outputBindings;
|
||||
|
||||
function shinyBindCtx(): BindInputsCtx {
|
||||
return {
|
||||
inputs,
|
||||
inputsRate,
|
||||
sendOutputHiddenState,
|
||||
maybeAddThemeObserver,
|
||||
inputBindings,
|
||||
outputBindings,
|
||||
initDeferredIframes,
|
||||
};
|
||||
}
|
||||
|
||||
windowShiny.bindAll = async function (scope: BindScope) {
|
||||
await bindAll(shinyBindCtx(), scope);
|
||||
};
|
||||
windowShiny.unbindAll = function (scope: BindScope, includeSelf = false) {
|
||||
unbindAll(shinyBindCtx(), scope, includeSelf);
|
||||
};
|
||||
|
||||
// Calls .initialize() for all of the input objects in all input bindings,
|
||||
// in the given scope.
|
||||
function initializeInputs(scope: BindScope = document.documentElement) {
|
||||
const bindings = inputBindings.getBindings();
|
||||
|
||||
// Iterate over all bindings
|
||||
for (let i = 0; i < bindings.length; i++) {
|
||||
const binding = bindings[i].binding;
|
||||
const inputObjects = binding.find(scope);
|
||||
|
||||
if (inputObjects) {
|
||||
// Iterate over all input objects for this binding
|
||||
for (let j = 0; j < inputObjects.length; j++) {
|
||||
const $inputObjectJ = $(inputObjects[j]);
|
||||
|
||||
if (!$inputObjectJ.data("_shiny_initialized")) {
|
||||
$inputObjectJ.data("_shiny_initialized", true);
|
||||
binding.initialize(inputObjects[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
windowShiny.initializeInputs = initializeInputs;
|
||||
|
||||
function getIdFromEl(el: HTMLElement) {
|
||||
const $el = $(el);
|
||||
const bindingAdapter = $el.data("shiny-output-binding");
|
||||
|
||||
if (!bindingAdapter) return null;
|
||||
else return bindingAdapter.getId();
|
||||
}
|
||||
|
||||
// Initialize all input objects in the document, before binding
|
||||
initializeInputs(document.documentElement);
|
||||
|
||||
// The input values returned by _bindAll() each have a structure like this:
|
||||
// { value: 123, opts: { ... } }
|
||||
// We want to only keep the value. This is because when the initialValues is
|
||||
// passed to ShinyApp.connect(), the ShinyApp object stores the
|
||||
// initialValues object for the duration of the session, and the opts may
|
||||
// have a reference to the DOM element, which would prevent it from being
|
||||
// GC'd.
|
||||
const initialValues = mapValues(
|
||||
await _bindAll(shinyBindCtx(), document.documentElement),
|
||||
(x) => x.value
|
||||
);
|
||||
|
||||
// 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(
|
||||
function () {
|
||||
const id = getIdFromEl(this),
|
||||
rect = this.getBoundingClientRect();
|
||||
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
initialValues[".clientdata_output_" + id + "_width"] = rect.width;
|
||||
initialValues[".clientdata_output_" + id + "_height"] = rect.height;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function getComputedBgColor(
|
||||
el: HTMLElement | null
|
||||
): string | null | undefined {
|
||||
if (!el) {
|
||||
// Top of document, can't recurse further
|
||||
return null;
|
||||
}
|
||||
|
||||
const bgColor = getStyle(el, "background-color");
|
||||
|
||||
if (!bgColor) return bgColor;
|
||||
const m = bgColor.match(
|
||||
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/
|
||||
);
|
||||
|
||||
if (bgColor === "transparent" || (m && parseFloat(m[4]) === 0)) {
|
||||
// No background color on this element. See if it has a background image.
|
||||
const bgImage = getStyle(el, "background-image");
|
||||
|
||||
if (bgImage && bgImage !== "none") {
|
||||
// Failed to detect background color, since it has a background image
|
||||
return null;
|
||||
} else {
|
||||
// Recurse
|
||||
return getComputedBgColor(el.parentElement);
|
||||
}
|
||||
}
|
||||
return bgColor;
|
||||
}
|
||||
|
||||
function getComputedFont(el: HTMLElement) {
|
||||
const fontFamily = getStyle(el, "font-family");
|
||||
const fontSize = getStyle(el, "font-size");
|
||||
|
||||
return {
|
||||
families: fontFamily?.replace(/"/g, "").split(", "),
|
||||
size: fontSize,
|
||||
};
|
||||
}
|
||||
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
|
||||
function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const el = this;
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
initialValues[".clientdata_output_" + id + "_bg"] =
|
||||
getComputedBgColor(el);
|
||||
initialValues[".clientdata_output_" + id + "_fg"] = getStyle(el, "color");
|
||||
initialValues[".clientdata_output_" + id + "_accent"] =
|
||||
getComputedLinkColor(el);
|
||||
initialValues[".clientdata_output_" + id + "_font"] = getComputedFont(el);
|
||||
maybeAddThemeObserver(el);
|
||||
}
|
||||
);
|
||||
|
||||
// Resend computed styles if *an output element's* class or style attribute changes.
|
||||
// This gives us some level of confidence that getCurrentOutputInfo() will be
|
||||
// properly invalidated if output container is mutated; but unfortunately,
|
||||
// we don't have a reasonable way to detect change in *inherited* styles
|
||||
// (other than session$setCurrentTheme())
|
||||
// https://github.com/rstudio/shiny/issues/3196
|
||||
// https://github.com/rstudio/shiny/issues/2998
|
||||
function maybeAddThemeObserver(el: HTMLElement): void {
|
||||
if (!window.MutationObserver) {
|
||||
return; // IE10 and lower
|
||||
}
|
||||
|
||||
const cl = el.classList;
|
||||
const reportTheme =
|
||||
cl.contains("shiny-image-output") ||
|
||||
cl.contains("shiny-plot-output") ||
|
||||
cl.contains("shiny-report-theme");
|
||||
|
||||
if (!reportTheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $el = $(el);
|
||||
|
||||
if ($el.data("shiny-theme-observer")) {
|
||||
return; // i.e., observer is already observing
|
||||
}
|
||||
|
||||
const observerCallback = new Debouncer(null, () => doSendTheme(el), 100);
|
||||
const observer = new MutationObserver(() => observerCallback.normalCall());
|
||||
const config = { attributes: true, attributeFilter: ["style", "class"] };
|
||||
|
||||
observer.observe(el, config);
|
||||
$el.data("shiny-theme-observer", observer);
|
||||
}
|
||||
|
||||
function doSendTheme(el: HTMLElement): void {
|
||||
// Sending theme info on error isn't necessary (it'd add an unnecessary additional round-trip)
|
||||
if (el.classList.contains("shiny-output-error")) {
|
||||
return;
|
||||
}
|
||||
const id = getIdFromEl(el);
|
||||
|
||||
inputs.setInput(".clientdata_output_" + id + "_bg", getComputedBgColor(el));
|
||||
inputs.setInput(".clientdata_output_" + id + "_fg", getStyle(el, "color"));
|
||||
inputs.setInput(
|
||||
".clientdata_output_" + id + "_accent",
|
||||
getComputedLinkColor(el)
|
||||
);
|
||||
inputs.setInput(".clientdata_output_" + id + "_font", getComputedFont(el));
|
||||
}
|
||||
|
||||
function doSendImageSize() {
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
|
||||
function () {
|
||||
const id = getIdFromEl(this),
|
||||
rect = this.getBoundingClientRect();
|
||||
|
||||
if (rect.width !== 0 || rect.height !== 0) {
|
||||
inputs.setInput(".clientdata_output_" + id + "_width", rect.width);
|
||||
inputs.setInput(".clientdata_output_" + id + "_height", rect.height);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
|
||||
function () {
|
||||
doSendTheme(this);
|
||||
}
|
||||
);
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const $this = $(this),
|
||||
binding = $this.data("shiny-output-binding");
|
||||
|
||||
$this.trigger({
|
||||
type: "shiny:visualchange",
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
visible: !isHidden(this),
|
||||
binding: binding,
|
||||
});
|
||||
binding.onResize();
|
||||
});
|
||||
}
|
||||
|
||||
sendImageSizeFns.setImageSend(inputBatchSender, doSendImageSize);
|
||||
|
||||
// Return true if the object or one of its ancestors in the DOM tree has
|
||||
// style='display:none'; otherwise return false.
|
||||
function isHidden(obj: HTMLElement | null): boolean {
|
||||
// null means we've hit the top of the tree. If width or height is
|
||||
// non-zero, then we know that no ancestor has display:none.
|
||||
if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
|
||||
return false;
|
||||
} else if (getStyle(obj, "display") === "none") {
|
||||
return true;
|
||||
} else {
|
||||
return isHidden(obj.parentNode as HTMLElement | null);
|
||||
}
|
||||
}
|
||||
let lastKnownVisibleOutputs: { [key: string]: boolean } = {};
|
||||
// Set initial state of outputs to hidden, if needed
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const id = getIdFromEl(this);
|
||||
|
||||
if (isHidden(this)) {
|
||||
initialValues[".clientdata_output_" + id + "_hidden"] = true;
|
||||
} else {
|
||||
lastKnownVisibleOutputs[id] = true;
|
||||
initialValues[".clientdata_output_" + id + "_hidden"] = false;
|
||||
}
|
||||
});
|
||||
// Send update when hidden state changes
|
||||
function doSendOutputHiddenState() {
|
||||
const visibleOutputs: { [key: string]: boolean } = {};
|
||||
|
||||
$(".shiny-bound-output").each(function () {
|
||||
const id = getIdFromEl(this);
|
||||
|
||||
delete lastKnownVisibleOutputs[id];
|
||||
// Assume that the object is hidden when width and height are 0
|
||||
const hidden = isHidden(this),
|
||||
evt = {
|
||||
type: "shiny:visualchange",
|
||||
visible: !hidden,
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
inputs.setInput(".clientdata_output_" + id + "_hidden", true);
|
||||
} else {
|
||||
visibleOutputs[id] = true;
|
||||
inputs.setInput(".clientdata_output_" + id + "_hidden", false);
|
||||
}
|
||||
const $this = $(this);
|
||||
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
evt.binding = $this.data("shiny-output-binding");
|
||||
// @ts-expect-error; Can not remove info on a established, malformed Event object
|
||||
$this.trigger(evt);
|
||||
});
|
||||
// Anything left in lastKnownVisibleOutputs is orphaned
|
||||
for (const name in lastKnownVisibleOutputs) {
|
||||
if (hasDefinedProperty(lastKnownVisibleOutputs, name))
|
||||
inputs.setInput(".clientdata_output_" + name + "_hidden", true);
|
||||
}
|
||||
// Update the visible outputs for next time
|
||||
lastKnownVisibleOutputs = visibleOutputs;
|
||||
}
|
||||
// sendOutputHiddenState gets called each time DOM elements are shown or
|
||||
// hidden. This can be in the hundreds or thousands of times at startup.
|
||||
// We'll debounce it, so that we do the actual work once per tick.
|
||||
const sendOutputHiddenStateDebouncer = new Debouncer(
|
||||
null,
|
||||
doSendOutputHiddenState,
|
||||
0
|
||||
);
|
||||
|
||||
function sendOutputHiddenState() {
|
||||
sendOutputHiddenStateDebouncer.normalCall();
|
||||
}
|
||||
// We need to make sure doSendOutputHiddenState actually gets called before
|
||||
// the inputBatchSender sends data to the server. The lastChanceCallback
|
||||
// here does that - if the debouncer has a pending call, flush it.
|
||||
inputBatchSender.lastChanceCallback.push(function () {
|
||||
if (sendOutputHiddenStateDebouncer.isPending())
|
||||
sendOutputHiddenStateDebouncer.immediateCall();
|
||||
});
|
||||
|
||||
// Given a namespace and a handler function, return a function that invokes
|
||||
// the handler only when e's namespace matches. For example, if the
|
||||
// namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
|
||||
// If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
|
||||
function filterEventsByNamespace(
|
||||
namespace: string,
|
||||
handler: (...handlerArgs: any[]) => void,
|
||||
...args: any[]
|
||||
) {
|
||||
const namespaceArr = namespace.split(".");
|
||||
|
||||
return function (this: HTMLElement, e: JQuery.TriggeredEvent) {
|
||||
const eventNamespace = e.namespace?.split(".") ?? [];
|
||||
|
||||
// If any of the namespace strings aren't present in this event, quit.
|
||||
for (let i = 0; i < namespaceArr.length; i++) {
|
||||
if (eventNamespace.indexOf(namespaceArr[i]) === -1) return;
|
||||
}
|
||||
|
||||
handler.apply(this, [namespaceArr, handler, ...args]);
|
||||
};
|
||||
}
|
||||
|
||||
// The size of each image may change either because the browser window was
|
||||
// resized, or because a tab was shown/hidden (hidden elements report size
|
||||
// of 0x0). It's OK to over-report sizes because the input pipeline will
|
||||
// filter out values that haven't changed.
|
||||
$(window).resize(debounce(500, sendImageSizeFns.regular));
|
||||
// Need to register callbacks for each Bootstrap 3 class.
|
||||
const bs3classes = [
|
||||
"modal",
|
||||
"dropdown",
|
||||
"tab",
|
||||
"tooltip",
|
||||
"popover",
|
||||
"collapse",
|
||||
];
|
||||
|
||||
$.each(bs3classes, function (idx, classname) {
|
||||
$(document.body).on(
|
||||
"shown.bs." + classname + ".sendImageSize",
|
||||
"*",
|
||||
filterEventsByNamespace("bs", sendImageSizeFns.regular)
|
||||
);
|
||||
$(document.body).on(
|
||||
"shown.bs." +
|
||||
classname +
|
||||
".sendOutputHiddenState " +
|
||||
"hidden.bs." +
|
||||
classname +
|
||||
".sendOutputHiddenState",
|
||||
"*",
|
||||
filterEventsByNamespace("bs", sendOutputHiddenState)
|
||||
);
|
||||
});
|
||||
|
||||
// This is needed for Bootstrap 2 compatibility and for non-Bootstrap
|
||||
// related shown/hidden events (like conditionalPanel)
|
||||
$(document.body).on("shown.sendImageSize", "*", sendImageSizeFns.regular);
|
||||
$(document.body).on(
|
||||
"shown.sendOutputHiddenState hidden.sendOutputHiddenState",
|
||||
"*",
|
||||
sendOutputHiddenState
|
||||
);
|
||||
|
||||
// Send initial pixel ratio, and update it if it changes
|
||||
initialValues[".clientdata_pixelratio"] = pixelRatio();
|
||||
$(window).resize(function () {
|
||||
inputs.setInput(".clientdata_pixelratio", pixelRatio());
|
||||
});
|
||||
|
||||
// Send initial URL
|
||||
initialValues[".clientdata_url_protocol"] = window.location.protocol;
|
||||
initialValues[".clientdata_url_hostname"] = window.location.hostname;
|
||||
initialValues[".clientdata_url_port"] = window.location.port;
|
||||
initialValues[".clientdata_url_pathname"] = window.location.pathname;
|
||||
|
||||
// Send initial URL search (query string) and update it if it changes
|
||||
initialValues[".clientdata_url_search"] = window.location.search;
|
||||
|
||||
$(window).on("pushstate", function (e) {
|
||||
inputs.setInput(".clientdata_url_search", window.location.search);
|
||||
return;
|
||||
e;
|
||||
});
|
||||
|
||||
$(window).on("popstate", function (e) {
|
||||
inputs.setInput(".clientdata_url_search", window.location.search);
|
||||
return;
|
||||
e;
|
||||
});
|
||||
|
||||
// This is only the initial value of the hash. The hash can change, but
|
||||
// a reactive version of this isn't sent because watching for changes can
|
||||
// require polling on some browsers. The JQuery hashchange plugin can be
|
||||
// used if this capability is important.
|
||||
initialValues[".clientdata_url_hash_initial"] = window.location.hash;
|
||||
initialValues[".clientdata_url_hash"] = window.location.hash;
|
||||
|
||||
$(window).on("hashchange", function (e) {
|
||||
inputs.setInput(".clientdata_url_hash", window.location.hash);
|
||||
return;
|
||||
e;
|
||||
});
|
||||
|
||||
// The server needs to know what singletons were rendered as part of
|
||||
// the page loading
|
||||
const singletonText = (initialValues[".clientdata_singletons"] = $(
|
||||
'script[type="application/shiny-singletons"]'
|
||||
).text());
|
||||
|
||||
singletonsRegisterNames(singletonText.split(/,/));
|
||||
|
||||
const dependencyText = $(
|
||||
'script[type="application/html-dependencies"]'
|
||||
).text();
|
||||
|
||||
$.each(dependencyText.split(/;/), function (i, depStr) {
|
||||
const match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr);
|
||||
|
||||
if (match) {
|
||||
registerDependency(match[1], match[2]);
|
||||
}
|
||||
});
|
||||
|
||||
// We've collected all the initial values--start the server process!
|
||||
inputsNoResend.reset(initialValues);
|
||||
shinyapp.connect(initialValues);
|
||||
$(document).one("shiny:connected", function () {
|
||||
initDeferredIframes();
|
||||
});
|
||||
|
||||
// window.console.log("Shiny version: ", windowShiny.version);
|
||||
} // function initShiny()
|
||||
|
||||
// Give any deferred iframes a chance to load.
|
||||
function initDeferredIframes(): void {
|
||||
// TODO-barret; This method uses `window.Shiny`. Could be replaced with `fullShinyObj_.shinyapp?.isConnected()`,
|
||||
// but that would not use `window.Shiny`. Is it a problem???
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
|
||||
// Can not expect error when combining with window available Shiny definition
|
||||
!window.Shiny ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
|
||||
// Can not expect error when combining with window available Shiny definition
|
||||
!window.Shiny.shinyapp ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
|
||||
// Can not expect error when combining with window available Shiny definition
|
||||
!window.Shiny.shinyapp.isConnected()
|
||||
) {
|
||||
// If somehow we accidentally call this before the server connection is
|
||||
// established, just ignore the call. At the time of this writing it
|
||||
// doesn't happen, but it's easy to imagine a later refactoring putting
|
||||
// us in this situation and it'd be hard to notice with either manual
|
||||
// testing or automated tests, because the only effect is on HTTP request
|
||||
// timing. (Update: Actually Aron saw this being called without even
|
||||
// window.Shiny being defined, but it was hard to repro.)
|
||||
return;
|
||||
}
|
||||
|
||||
$(".shiny-frame-deferred").each(function (i, el) {
|
||||
const $el = $(el);
|
||||
|
||||
$el.removeClass("shiny-frame-deferred");
|
||||
// @ts-expect-error; If it is undefined, set using the undefined value
|
||||
$el.attr("src", $el.attr("data-deferred-src"));
|
||||
$el.attr("data-deferred-src", null);
|
||||
});
|
||||
}
|
||||
|
||||
export { initShiny };
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Shiny } from ".";
|
||||
import type { ShinyClass } from ".";
|
||||
import type { FileInputBinding } from "../bindings/input/fileinput";
|
||||
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
|
||||
import type { EventPriority } from "../inputPolicies";
|
||||
@@ -10,7 +10,7 @@ let fullShinyObj: FullShinyDef;
|
||||
// TODO-future; It would be nice to have a way to export this type value instead of / in addition to `Shiny`
|
||||
type FullShinyDef = Required<
|
||||
Pick<
|
||||
Shiny,
|
||||
ShinyClass,
|
||||
| "bindAll"
|
||||
| "forgetLastInputValue"
|
||||
| "initializeInputs"
|
||||
@@ -21,9 +21,9 @@ type FullShinyDef = Required<
|
||||
| "user"
|
||||
>
|
||||
> &
|
||||
Shiny;
|
||||
ShinyClass;
|
||||
|
||||
function setShinyObj(shiny: Shiny): void {
|
||||
function setShinyObj(shiny: ShinyClass): void {
|
||||
fullShinyObj = shiny as FullShinyDef;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,8 +71,12 @@ function remove(): void {
|
||||
// Look for a Bootstrap modal and if present, trigger hide event. This will
|
||||
// trigger the hidden.bs.modal callback that we set in show(), which unbinds
|
||||
// and removes the element.
|
||||
if ($modal.find(".modal").length > 0) {
|
||||
$modal.find(".modal").modal("hide");
|
||||
const $bsModal = $modal.find(".modal");
|
||||
if ($bsModal.length > 0) {
|
||||
// We both hide the modal when its shown and also immediately; the immediate
|
||||
// version is a no-op in Bootstrap if called before the modal is fully shown
|
||||
$bsModal.on("shown.bs.modal", () => $bsModal.modal("hide"));
|
||||
$bsModal.modal("hide");
|
||||
} else {
|
||||
// If not a Bootstrap modal dialog, simply unbind and remove it.
|
||||
shinyUnbindAll($modal);
|
||||
|
||||
@@ -109,6 +109,9 @@ function addCustomMessageHandler(type: string, handler: Handler): void {
|
||||
|
||||
//// End message handler variables
|
||||
|
||||
/**
|
||||
* The ShinyApp class handles the communication with the Shiny Server.
|
||||
*/
|
||||
class ShinyApp {
|
||||
$socket: ShinyWebSocket | null = null;
|
||||
|
||||
@@ -611,7 +614,7 @@ class ShinyApp {
|
||||
|
||||
const nsPrefix = el.attr("data-ns-prefix") as string;
|
||||
const nsScope = this._narrowScope(scope, nsPrefix);
|
||||
const show = condFunc(nsScope);
|
||||
const show = Boolean(condFunc(nsScope));
|
||||
const showing = el.css("display") !== "none";
|
||||
|
||||
if (show !== showing) {
|
||||
@@ -1050,6 +1053,12 @@ class ShinyApp {
|
||||
const $tabContent = getTabContent($tabset);
|
||||
let tabsetId = $parentTabset.attr("data-tabsetid");
|
||||
|
||||
// TODO: Only render tab content HTML once
|
||||
// The following lines turn the content/nav control HTML into DOM nodes,
|
||||
// but we don't insert these directly, instead we take the HTML from
|
||||
// these nodes and pass it through `renderContentAsync()`. This means
|
||||
// the inserted HTML may not perfectly match the message HTML, esp. if
|
||||
// the content uses web components that alter their HTML when loaded.
|
||||
const $divTag = $(message.divTag.html);
|
||||
const $liTag = $(message.liTag.html);
|
||||
const $aTag = $liTag.find("> a");
|
||||
@@ -1163,12 +1172,14 @@ class ShinyApp {
|
||||
// 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);
|
||||
// If `el` itself is a script tag, this approach won't work (the script
|
||||
// won't be run), since we're only sending innerHTML through renderContent
|
||||
// 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.
|
||||
await renderContentAsync(el, el.innerHTML || el.textContent);
|
||||
if (el.nodeType === Node.ELEMENT_NODE) {
|
||||
// If `el` itself is a script tag, this approach won't work (the script
|
||||
// won't be run), since we're only sending innerHTML through renderContent
|
||||
// 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.
|
||||
await renderContentAsync(el, el.innerHTML || el.textContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.select) {
|
||||
@@ -1610,4 +1621,4 @@ class ShinyApp {
|
||||
}
|
||||
|
||||
export { ShinyApp, addCustomMessageHandler };
|
||||
export type { Handler, ErrorsMessageValue };
|
||||
export type { Handler, ErrorsMessageValue, ShinyWebSocket };
|
||||
|
||||
@@ -144,11 +144,25 @@ function pixelRatio(): number {
|
||||
}
|
||||
}
|
||||
|
||||
function getBoundingClientSizeBeforeZoom(el: HTMLElement): {
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
const rect = el.getBoundingClientRect();
|
||||
// Cast to any because currentCSSZoom isn't in the type def of HTMLElement
|
||||
// TODO: typescript >= 5.5.2 added this property to the type definition
|
||||
const zoom = (el as any).currentCSSZoom || 1;
|
||||
return {
|
||||
width: rect.width / zoom,
|
||||
height: rect.height / zoom,
|
||||
};
|
||||
}
|
||||
|
||||
// Takes a string expression and returns a function that takes an argument.
|
||||
//
|
||||
// When the function is executed, it will evaluate that expression using
|
||||
// "with" on the argument value, and return the result.
|
||||
function scopeExprToFunc(expr: string): (scope: unknown) => boolean {
|
||||
function scopeExprToFunc(expr: string): (scope: unknown) => unknown {
|
||||
/*jshint evil: true */
|
||||
const exprEscaped = expr
|
||||
.replace(/[\\"']/g, "\\$&")
|
||||
@@ -159,7 +173,7 @@ function scopeExprToFunc(expr: string): (scope: unknown) => boolean {
|
||||
// \b has a special meaning; need [\b] to match backspace char.
|
||||
.replace(/[\b]/g, "\\b");
|
||||
|
||||
let func: () => boolean;
|
||||
let func: () => unknown;
|
||||
|
||||
try {
|
||||
// @ts-expect-error; Do not know how to type this _dangerous_ situation
|
||||
@@ -178,7 +192,7 @@ function scopeExprToFunc(expr: string): (scope: unknown) => boolean {
|
||||
throw e;
|
||||
}
|
||||
|
||||
return function (scope: unknown): boolean {
|
||||
return function (scope: unknown): unknown {
|
||||
return func.call(scope);
|
||||
};
|
||||
}
|
||||
@@ -398,6 +412,7 @@ export {
|
||||
formatDateUTC,
|
||||
makeResizeFilter,
|
||||
pixelRatio,
|
||||
getBoundingClientSizeBeforeZoom,
|
||||
scopeExprToFunc,
|
||||
asArray,
|
||||
mergeSort,
|
||||
|
||||
46
srcts/src/utils/promise.ts
Normal file
46
srcts/src/utils/promise.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// A shim for Promise.withResolvers. Once browser support is widespread, we can
|
||||
// remove this.
|
||||
export function promiseWithResolvers<T>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: PromiseLike<T> | T) => void;
|
||||
reject: (reason?: any) => void;
|
||||
} {
|
||||
let resolve: (value: PromiseLike<T> | T) => void;
|
||||
let reject: (reason?: any) => void;
|
||||
const promise = new Promise(
|
||||
(res: (value: PromiseLike<T> | T) => void, rej: (reason?: any) => void) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return { promise, resolve: resolve!, reject: reject! };
|
||||
}
|
||||
|
||||
export interface InitStatusPromise<T> extends Promise<T> {
|
||||
promise: Promise<T>;
|
||||
resolve(x: T): void;
|
||||
resolved(): boolean;
|
||||
}
|
||||
|
||||
export function createInitStatus<T>(): InitStatusPromise<T> {
|
||||
const { promise, resolve } = promiseWithResolvers<T>();
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let _resolved = false;
|
||||
|
||||
return {
|
||||
promise,
|
||||
resolve(x: T) {
|
||||
_resolved = true;
|
||||
resolve(x);
|
||||
},
|
||||
then: promise.then.bind(promise),
|
||||
catch: promise.catch.bind(promise),
|
||||
finally: promise.finally.bind(promise),
|
||||
[Symbol.toStringTag]: "InitStatus",
|
||||
resolved() {
|
||||
return _resolved;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { Shiny } from "../shiny";
|
||||
|
||||
function windowShiny(): Shiny {
|
||||
// Use `any` type as we know what we are doing is _dangerous_
|
||||
// Immediately init shiny on the window
|
||||
if (!(window as any)["Shiny"]) {
|
||||
(window as any)["Shiny"] = {};
|
||||
}
|
||||
return (window as any)["Shiny"];
|
||||
}
|
||||
|
||||
export { windowShiny };
|
||||
6
srcts/types/extras/globalShiny.d.ts
vendored
6
srcts/types/extras/globalShiny.d.ts
vendored
@@ -1,8 +1,6 @@
|
||||
import type { Shiny as RStudioShiny } from "../src/shiny/index";
|
||||
import type { ShinyClass } from "../src/shiny/index";
|
||||
declare global {
|
||||
const Shiny: RStudioShiny;
|
||||
interface Window {
|
||||
Shiny: RStudioShiny;
|
||||
Shiny: ShinyClass;
|
||||
}
|
||||
type Shiny = RStudioShiny;
|
||||
}
|
||||
|
||||
3
srcts/types/src/bindings/input/index.d.ts
vendored
3
srcts/types/src/bindings/input/index.d.ts
vendored
@@ -1,9 +1,8 @@
|
||||
import { BindingRegistry } from "../registry";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
import { FileInputBinding } from "./fileinput";
|
||||
type InitInputBindings = {
|
||||
declare function initInputBindings(): {
|
||||
inputBindings: BindingRegistry<InputBinding>;
|
||||
fileInputBinding: FileInputBinding;
|
||||
};
|
||||
declare function initInputBindings(): InitInputBindings;
|
||||
export { initInputBindings, InputBinding };
|
||||
|
||||
8
srcts/types/src/components/errorConsole.d.ts
vendored
8
srcts/types/src/components/errorConsole.d.ts
vendored
@@ -10,6 +10,11 @@ export declare class ShinyErrorMessage extends LitElement {
|
||||
copyErrorToClipboard(): Promise<void>;
|
||||
render(): import("lit-html").TemplateResult<1>;
|
||||
}
|
||||
export type ShinyClientMessage = {
|
||||
message: string;
|
||||
headline?: string;
|
||||
status?: "error" | "info" | "warning";
|
||||
};
|
||||
/**
|
||||
* Function to show an error message to user in shiny-error-message web
|
||||
* component. Only shows the error if we're in development mode.
|
||||
@@ -18,3 +23,6 @@ export declare class ShinyErrorMessage extends LitElement {
|
||||
* object.
|
||||
*/
|
||||
export declare function showErrorInClientConsole(e: unknown): void;
|
||||
export declare class ShinyClientMessageEvent extends CustomEvent<ShinyClientMessage> {
|
||||
constructor(detail: ShinyClientMessage);
|
||||
}
|
||||
|
||||
2
srcts/types/src/imageutils/initCoordmap.d.ts
vendored
2
srcts/types/src/imageutils/initCoordmap.d.ts
vendored
@@ -48,5 +48,5 @@ type Coordmap = {
|
||||
mouseCoordinateSender: (inputId: string, clip?: boolean, nullOutside?: boolean) => (e: JQuery.MouseDownEvent | JQuery.MouseMoveEvent | null) => void;
|
||||
};
|
||||
declare function initCoordmap($el: JQuery<HTMLElement>, coordmap_: CoordmapInit): Coordmap;
|
||||
export { findOrigin, initCoordmap };
|
||||
export type { Coordmap, CoordmapInit };
|
||||
export { initCoordmap, findOrigin };
|
||||
|
||||
2
srcts/types/src/index.d.ts
vendored
2
srcts/types/src/index.d.ts
vendored
@@ -1 +1 @@
|
||||
export {};
|
||||
export { Shiny, type ShinyClass } from "./initialize";
|
||||
|
||||
4
srcts/types/src/initialize/index.d.ts
vendored
4
srcts/types/src/initialize/index.d.ts
vendored
@@ -1,2 +1,4 @@
|
||||
import { ShinyClass } from "../shiny";
|
||||
declare let Shiny: ShinyClass;
|
||||
declare function init(): void;
|
||||
export { init };
|
||||
export { init, Shiny, type ShinyClass };
|
||||
|
||||
29
srcts/types/src/shiny/index.d.ts
vendored
29
srcts/types/src/shiny/index.d.ts
vendored
@@ -1,22 +1,21 @@
|
||||
import { InputBinding, OutputBinding } from "../bindings";
|
||||
import { initInputBindings } from "../bindings/input";
|
||||
import { initOutputBindings } from "../bindings/output";
|
||||
import type { BindingRegistry } from "../bindings/registry";
|
||||
import { resetBrush } from "../imageutils/resetBrush";
|
||||
import { $escape, compareVersion } from "../utils";
|
||||
import { type InitStatusPromise } from "../utils/promise";
|
||||
import type { shinyBindAll, shinyForgetLastInputValue, shinyInitializeInputs, shinySetInputValue, shinyUnbindAll } from "./initedMethods";
|
||||
import { removeModal, showModal } from "./modal";
|
||||
import { removeNotification, showNotification } from "./notifications";
|
||||
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
|
||||
import { renderContent, renderContentAsync, renderDependencies, renderDependenciesAsync, renderHtml, renderHtmlAsync } from "./render";
|
||||
import type { Handler, ShinyApp } from "./shinyapp";
|
||||
import { addCustomMessageHandler } from "./shinyapp";
|
||||
interface Shiny {
|
||||
import { addCustomMessageHandler, ShinyApp, type Handler } from "./shinyapp";
|
||||
declare class ShinyClass {
|
||||
version: string;
|
||||
$escape: typeof $escape;
|
||||
compareVersion: typeof compareVersion;
|
||||
inputBindings: ReturnType<typeof initInputBindings>["inputBindings"];
|
||||
inputBindings: BindingRegistry<InputBinding>;
|
||||
InputBinding: typeof InputBinding;
|
||||
outputBindings: ReturnType<typeof initOutputBindings>["outputBindings"];
|
||||
outputBindings: BindingRegistry<OutputBinding>;
|
||||
OutputBinding: typeof OutputBinding;
|
||||
resetBrush: typeof resetBrush;
|
||||
notifications: {
|
||||
@@ -27,7 +26,6 @@ interface Shiny {
|
||||
show: typeof showModal;
|
||||
remove: typeof removeModal;
|
||||
};
|
||||
createSocket?: () => WebSocket;
|
||||
showReconnectDialog: typeof showReconnectDialog;
|
||||
hideReconnectDialog: typeof hideReconnectDialog;
|
||||
renderDependenciesAsync: typeof renderDependenciesAsync;
|
||||
@@ -36,9 +34,10 @@ interface Shiny {
|
||||
renderContent: typeof renderContent;
|
||||
renderHtmlAsync: typeof renderHtmlAsync;
|
||||
renderHtml: typeof renderHtml;
|
||||
user: string;
|
||||
progressHandlers?: ShinyApp["progressHandlers"];
|
||||
addCustomMessageHandler: typeof addCustomMessageHandler;
|
||||
createSocket?: () => WebSocket;
|
||||
user?: string;
|
||||
progressHandlers?: ShinyApp["progressHandlers"];
|
||||
shinyapp?: ShinyApp;
|
||||
setInputValue?: typeof shinySetInputValue;
|
||||
onInputChange?: typeof shinySetInputValue;
|
||||
@@ -46,16 +45,16 @@ interface Shiny {
|
||||
bindAll?: typeof shinyBindAll;
|
||||
unbindAll?: typeof shinyUnbindAll;
|
||||
initializeInputs?: typeof shinyInitializeInputs;
|
||||
initializedPromise: InitStatusPromise<void>;
|
||||
oncustommessage?: Handler;
|
||||
constructor();
|
||||
/**
|
||||
* Method to check if Shiny is running in development mode. By packaging as a
|
||||
* method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__`
|
||||
* variable in the global scope.
|
||||
* @returns `true` if Shiny is running in development mode, `false` otherwise.
|
||||
*/
|
||||
inDevMode: () => boolean;
|
||||
inDevMode(): boolean;
|
||||
initialize(): Promise<void>;
|
||||
}
|
||||
declare let windowShiny: Shiny;
|
||||
declare function setShiny(windowShiny_: Shiny): void;
|
||||
export { windowShiny, setShiny };
|
||||
export type { Shiny };
|
||||
export { ShinyClass };
|
||||
|
||||
3
srcts/types/src/shiny/init.d.ts
vendored
3
srcts/types/src/shiny/init.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
import type { Shiny } from ".";
|
||||
declare function initShiny(windowShiny: Shiny): Promise<void>;
|
||||
export { initShiny };
|
||||
4
srcts/types/src/shiny/initedMethods.d.ts
vendored
4
srcts/types/src/shiny/initedMethods.d.ts
vendored
@@ -1,10 +1,10 @@
|
||||
import type { Shiny } from ".";
|
||||
import type { ShinyClass } from ".";
|
||||
import type { FileInputBinding } from "../bindings/input/fileinput";
|
||||
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
|
||||
import type { EventPriority } from "../inputPolicies";
|
||||
import type { BindScope } from "./bind";
|
||||
import type { Handler, ShinyApp } from "./shinyapp";
|
||||
declare function setShinyObj(shiny: Shiny): void;
|
||||
declare function setShinyObj(shiny: ShinyClass): void;
|
||||
declare function shinySetInputValue(name: string, value: unknown, opts?: {
|
||||
priority?: EventPriority;
|
||||
}): void;
|
||||
|
||||
5
srcts/types/src/shiny/shinyapp.d.ts
vendored
5
srcts/types/src/shiny/shinyapp.d.ts
vendored
@@ -19,6 +19,9 @@ type InputValues = {
|
||||
};
|
||||
type MessageValue = Parameters<WebSocket["send"]>[0];
|
||||
declare function addCustomMessageHandler(type: string, handler: Handler): void;
|
||||
/**
|
||||
* The ShinyApp class handles the communication with the Shiny Server.
|
||||
*/
|
||||
declare class ShinyApp {
|
||||
$socket: ShinyWebSocket | null;
|
||||
taskQueue: AsyncQueue<() => Promise<void> | void>;
|
||||
@@ -104,4 +107,4 @@ declare class ShinyApp {
|
||||
}): string;
|
||||
}
|
||||
export { ShinyApp, addCustomMessageHandler };
|
||||
export type { Handler, ErrorsMessageValue };
|
||||
export type { Handler, ErrorsMessageValue, ShinyWebSocket };
|
||||
|
||||
8
srcts/types/src/utils/index.d.ts
vendored
8
srcts/types/src/utils/index.d.ts
vendored
@@ -10,7 +10,11 @@ declare function parseDate(dateString: string): Date;
|
||||
declare function formatDateUTC(x: Date): string;
|
||||
declare function makeResizeFilter(el: HTMLElement, func: (width: HTMLElement["offsetWidth"], height: HTMLElement["offsetHeight"]) => void): () => void;
|
||||
declare function pixelRatio(): number;
|
||||
declare function scopeExprToFunc(expr: string): (scope: unknown) => boolean;
|
||||
declare function getBoundingClientSizeBeforeZoom(el: HTMLElement): {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
declare function scopeExprToFunc(expr: string): (scope: unknown) => unknown;
|
||||
declare function asArray<T>(value: T | T[] | null | undefined): T[];
|
||||
declare function mergeSort<Item>(list: Item[], sortfunc: (a: Item, b: Item) => boolean | number): Item[];
|
||||
declare function $escape(val: undefined): undefined;
|
||||
@@ -26,4 +30,4 @@ declare function updateLabel(labelTxt: string | undefined, labelNode: JQuery<HTM
|
||||
declare function getComputedLinkColor(el: HTMLElement): string;
|
||||
declare function isBS3(): boolean;
|
||||
declare function toLowerCase<T extends string>(str: T): Lowercase<T>;
|
||||
export { escapeHTML, randomId, strToBool, getStyle, padZeros, roundSignif, parseDate, formatDateUTC, makeResizeFilter, pixelRatio, scopeExprToFunc, asArray, mergeSort, $escape, mapValues, isnan, _equal, equal, compareVersion, updateLabel, getComputedLinkColor, hasOwnProperty, hasDefinedProperty, isBS3, toLowerCase, };
|
||||
export { escapeHTML, randomId, strToBool, getStyle, padZeros, roundSignif, parseDate, formatDateUTC, makeResizeFilter, pixelRatio, getBoundingClientSizeBeforeZoom, scopeExprToFunc, asArray, mergeSort, $escape, mapValues, isnan, _equal, equal, compareVersion, updateLabel, getComputedLinkColor, hasOwnProperty, hasDefinedProperty, isBS3, toLowerCase, };
|
||||
|
||||
11
srcts/types/src/utils/promise.d.ts
vendored
Normal file
11
srcts/types/src/utils/promise.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export declare function promiseWithResolvers<T>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: PromiseLike<T> | T) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
export interface InitStatusPromise<T> extends Promise<T> {
|
||||
promise: Promise<T>;
|
||||
resolve(x: T): void;
|
||||
resolved(): boolean;
|
||||
}
|
||||
export declare function createInitStatus<T>(): InitStatusPromise<T>;
|
||||
3
srcts/types/src/window/libraries.d.ts
vendored
3
srcts/types/src/window/libraries.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
import type { Shiny } from "../shiny";
|
||||
declare function windowShiny(): Shiny;
|
||||
export { windowShiny };
|
||||
685
tests/testthat/_snaps/stacks-deep.md
Normal file
685
tests/testthat/_snaps/stacks-deep.md
Normal file
@@ -0,0 +1,685 @@
|
||||
# deep stack capturing
|
||||
|
||||
Code
|
||||
cat(sep = "\n", formatError(err))
|
||||
Output
|
||||
Error in onFinally: boom
|
||||
: stop
|
||||
: onFinally [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: self$then
|
||||
: promise$finally
|
||||
: finally
|
||||
: onRejected [test-stacks-deep.R#XXX]
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onRejected
|
||||
: handleReject
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnRejected
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: self$then
|
||||
: promise$catch
|
||||
: catch
|
||||
: %...!%
|
||||
: onFulfilled [test-stacks-deep.R#XXX]
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
|
||||
---
|
||||
|
||||
Code
|
||||
cat(sep = "\n", formatError(err, full = TRUE))
|
||||
Output
|
||||
Error in onFinally: boom
|
||||
: h
|
||||
: .handleSimpleError
|
||||
: stop
|
||||
: onFinally [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: withCallingHandlers
|
||||
: callback
|
||||
: force
|
||||
: reenter_promise_domain
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: withVisible
|
||||
: private$doResolve
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: base::tryCatch
|
||||
: tryCatch
|
||||
: resolve
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: with_reporter
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: base::tryCatch
|
||||
: tryCatch
|
||||
: promise
|
||||
: self$then
|
||||
: promise$finally
|
||||
: finally
|
||||
: onRejected [test-stacks-deep.R#XXX]
|
||||
: withCallingHandlers
|
||||
: callback
|
||||
: force
|
||||
: reenter_promise_domain
|
||||
: <Anonymous>
|
||||
: onRejected
|
||||
: withVisible
|
||||
: private$doResolve
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: base::tryCatch
|
||||
: tryCatch
|
||||
: resolve
|
||||
: handleReject
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: with_reporter
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnRejected
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: base::tryCatch
|
||||
: tryCatch
|
||||
: promise
|
||||
: self$then
|
||||
: promise$catch
|
||||
: catch
|
||||
: %...!%
|
||||
: onFulfilled [test-stacks-deep.R#XXX]
|
||||
: withCallingHandlers
|
||||
: callback
|
||||
: force
|
||||
: reenter_promise_domain
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: withVisible
|
||||
: private$doResolve
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: base::tryCatch
|
||||
: tryCatch
|
||||
: resolve
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: with_reporter
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: base::tryCatch
|
||||
: tryCatch
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: withCallingHandlers [test-stacks-deep.R#XXX]
|
||||
: domain$wrapSync
|
||||
: promises::with_promise_domain
|
||||
: captureStackTraces
|
||||
: as.promise
|
||||
: catch
|
||||
: %...!%
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: withCallingHandlers
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: doTryCatch
|
||||
: tryCatchOne
|
||||
: tryCatchList
|
||||
: tryCatch
|
||||
: with_reporter
|
||||
: test_files_serial
|
||||
: test_files
|
||||
|
||||
# deep stacks long chain
|
||||
|
||||
Code
|
||||
cat(sep = "\n", stacktrace <- formatError(dserr))
|
||||
Output
|
||||
Error in onFulfilled: boom
|
||||
: stop
|
||||
: onFulfilled [test-stacks-deep.R#XXX]
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: J__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: I__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: H__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: G__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: F__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: E__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: D__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: C__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: B__ [test-stacks-deep.R#XXX]
|
||||
: onFulfilled
|
||||
: callback
|
||||
: <Anonymous>
|
||||
: onFulfilled
|
||||
: handleFulfill
|
||||
: <Anonymous>
|
||||
: execCallbacks
|
||||
: later::run_now
|
||||
: wait_for_it
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
From earlier call:
|
||||
: domain$wrapOnFulfilled
|
||||
: promiseDomain$onThen
|
||||
: action
|
||||
: promise
|
||||
: promise$then
|
||||
: then
|
||||
: %...>%
|
||||
: A__ [test-stacks-deep.R#XXX]
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: test_that
|
||||
: eval [test-stacks-deep.R#XXX]
|
||||
: eval
|
||||
: test_code
|
||||
: source_file
|
||||
: FUN
|
||||
: lapply
|
||||
: test_files_serial
|
||||
: test_files
|
||||
|
||||
90
tests/testthat/_snaps/stacks.md
Normal file
90
tests/testthat/_snaps/stacks.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# integration tests
|
||||
|
||||
Code
|
||||
df
|
||||
Output
|
||||
num call loc
|
||||
1 64 A [test-stacks.R#3]
|
||||
2 63 B [test-stacks.R#7]
|
||||
3 62 <reactive:C> [test-stacks.R#11]
|
||||
4 42 C
|
||||
5 41 renderTable [test-stacks.R#18]
|
||||
6 40 func
|
||||
7 39 force
|
||||
8 38 withVisible
|
||||
9 37 withCallingHandlers
|
||||
|
||||
---
|
||||
|
||||
Code
|
||||
df
|
||||
Output
|
||||
num call loc
|
||||
1 67 h
|
||||
2 66 .handleSimpleError
|
||||
3 65 stop
|
||||
4 64 A [test-stacks.R#3]
|
||||
5 63 B [test-stacks.R#7]
|
||||
6 62 <reactive:C> [test-stacks.R#11]
|
||||
7 61 ..stacktraceon..
|
||||
8 60 .func
|
||||
9 59 withVisible
|
||||
10 58 withCallingHandlers
|
||||
11 57 contextFunc
|
||||
12 56 env$runWith
|
||||
13 55 withCallingHandlers
|
||||
14 54 domain$wrapSync
|
||||
15 53 promises::with_promise_domain
|
||||
16 52 captureStackTraces
|
||||
17 51 force
|
||||
18 50 domain$wrapSync
|
||||
19 49 promises::with_promise_domain
|
||||
20 48 withReactiveDomain
|
||||
21 47 domain$wrapSync
|
||||
22 46 promises::with_promise_domain
|
||||
23 45 ctx$run
|
||||
24 44 self$.updateValue
|
||||
25 43 ..stacktraceoff..
|
||||
26 42 C
|
||||
27 41 renderTable [test-stacks.R#18]
|
||||
28 40 func
|
||||
29 39 force
|
||||
30 38 withVisible
|
||||
31 37 withCallingHandlers
|
||||
32 36 domain$wrapSync
|
||||
33 35 promises::with_promise_domain
|
||||
34 34 captureStackTraces
|
||||
35 33 doTryCatch
|
||||
36 32 tryCatchOne
|
||||
37 31 tryCatchList
|
||||
38 30 tryCatch
|
||||
39 29 do
|
||||
40 28 hybrid_chain
|
||||
41 27 renderFunc
|
||||
42 26 renderTable({ C() }, server = FALSE)
|
||||
43 25 ..stacktraceon.. [test-stacks.R#17]
|
||||
44 24 contextFunc
|
||||
45 23 env$runWith
|
||||
46 22 withCallingHandlers
|
||||
47 21 domain$wrapSync
|
||||
48 20 promises::with_promise_domain
|
||||
49 19 captureStackTraces
|
||||
50 18 force
|
||||
51 17 domain$wrapSync
|
||||
52 16 promises::with_promise_domain
|
||||
53 15 withReactiveDomain
|
||||
54 14 domain$wrapSync
|
||||
55 13 promises::with_promise_domain
|
||||
56 12 ctx$run
|
||||
57 11 ..stacktraceoff..
|
||||
58 10 isolate
|
||||
59 9 withCallingHandlers [test-stacks.R#16]
|
||||
60 8 domain$wrapSync
|
||||
61 7 promises::with_promise_domain
|
||||
62 6 captureStackTraces
|
||||
63 5 doTryCatch [test-stacks.R#15]
|
||||
64 4 tryCatchOne
|
||||
65 3 tryCatchList
|
||||
66 2 tryCatch
|
||||
67 1 try
|
||||
|
||||
@@ -11,18 +11,18 @@ test_that("HTML has correct attributes", {
|
||||
})
|
||||
|
||||
test_that("Github extensions are on by default", {
|
||||
html <- markdown("a ~paragraph~ with a link: https://example.com")
|
||||
html <- markdown("a ~~paragraph~~ with a link: https://example.com")
|
||||
expect_equal(html, HTML("<p>a <del>paragraph</del> with a link: <a href=\"https://example.com\">https://example.com</a></p>\n"))
|
||||
})
|
||||
|
||||
test_that("Github extensions can be disabled", {
|
||||
html <- markdown("a ~paragraph~", extensions = FALSE)
|
||||
expect_equal(html, HTML("<p>a ~paragraph~</p>\n"))
|
||||
html <- markdown("a ~~paragraph~~", extensions = FALSE)
|
||||
expect_equal(html, HTML("<p>a ~~paragraph~~</p>\n"))
|
||||
})
|
||||
|
||||
test_that("Additional options are respected", {
|
||||
html <- markdown("a ~paragraph~", extensions = FALSE, sourcepos = TRUE)
|
||||
expect_equal(html, HTML("<p data-sourcepos=\"1:1-1:13\">a ~paragraph~</p>\n"))
|
||||
html <- markdown("a ~~paragraph~~", extensions = FALSE, sourcepos = TRUE)
|
||||
expect_equal(html, HTML("<p data-sourcepos=\"1:1-1:15\">a ~~paragraph~~</p>\n"))
|
||||
})
|
||||
|
||||
test_that("Multiline markdown works properly", {
|
||||
|
||||
49
tests/testthat/test-promise-domains.R
Normal file
49
tests/testthat/test-promise-domains.R
Normal file
@@ -0,0 +1,49 @@
|
||||
with_several_promise_domains <- function(expr) {
|
||||
withReactiveDomain(MockShinySession$new(), {
|
||||
promises::with_promise_domain(reactivePromiseDomain(), {
|
||||
captureStackTraces({
|
||||
expr
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
recursive_promise <- function(n, callback = identity) {
|
||||
if (n <= 0) {
|
||||
return(promise_resolve(0))
|
||||
}
|
||||
|
||||
p <- promises::promise_resolve(TRUE)
|
||||
promises::then(p, ~{
|
||||
callback(n)
|
||||
recursive_promise(n - 1, callback = callback)
|
||||
})
|
||||
}
|
||||
|
||||
test_that("Stack trace doesn't grow (resolution within domain)", {
|
||||
|
||||
depths <- list()
|
||||
with_several_promise_domains({
|
||||
recursive_promise(10, function(n) {
|
||||
depths <<- c(depths, list(length(sys.calls())))
|
||||
})
|
||||
while (!later::loop_empty()) {
|
||||
later::run_now()
|
||||
}
|
||||
})
|
||||
expect_equal(diff(range(depths)), 0)
|
||||
})
|
||||
|
||||
test_that("Stack trace doesn't grow (resolution outside domain)", {
|
||||
|
||||
depths <- list()
|
||||
with_several_promise_domains({
|
||||
recursive_promise(10, function(n) {
|
||||
depths <<- c(depths, list(length(sys.calls())))
|
||||
})
|
||||
})
|
||||
while (!later::loop_empty()) {
|
||||
later::run_now()
|
||||
}
|
||||
expect_equal(diff(range(depths)), 0)
|
||||
})
|
||||
@@ -1232,162 +1232,85 @@ test_that("event handling helpers take correct dependencies", {
|
||||
})
|
||||
|
||||
|
||||
test_that("debounce/throttle work properly (with priming)", {
|
||||
do_priming <- TRUE
|
||||
# Some of the CRAN test machines are heavily loaded and so the timing for
|
||||
# these tests isn't reliable. https://github.com/rstudio/shiny/pull/2789
|
||||
skip_on_cran()
|
||||
for (do_priming in c(TRUE, FALSE)) {
|
||||
label <- if (do_priming) "with priming" else "without priming"
|
||||
test_that(sprintf("debounce/throttle work properly (%s)", label), {
|
||||
# Some of the CRAN test machines are heavily loaded and so the timing for
|
||||
# these tests isn't reliable. https://github.com/rstudio/shiny/pull/2789
|
||||
skip_on_cran()
|
||||
|
||||
# The changing of rv$a will be the (chatty) source of reactivity.
|
||||
rv <- reactiveValues(a = 0)
|
||||
# The changing of rv$a will be the (chatty) source of reactivity.
|
||||
rv <- reactiveValues(a = 0)
|
||||
|
||||
# This observer will be what changes rv$a.
|
||||
src <- observe({
|
||||
invalidateLater(100)
|
||||
rv$a <- isolate(rv$a) + 1
|
||||
})
|
||||
on.exit(src$destroy(), add = TRUE)
|
||||
# This observer will be what changes rv$a.
|
||||
src <- observe({
|
||||
invalidateLater(300)
|
||||
rv$a <- isolate(rv$a) + 1
|
||||
})
|
||||
on.exit(src$destroy(), add = TRUE)
|
||||
|
||||
# Make a debounced reactive to test.
|
||||
dr <- debounce(reactive(rv$a), 500)
|
||||
# Make a debounced reactive to test.
|
||||
dr <- debounce(reactive(rv$a), 500)
|
||||
|
||||
# Make a throttled reactive to test.
|
||||
tr <- throttle(reactive(rv$a), 500)
|
||||
# Make a throttled reactive to test.
|
||||
tr <- throttle(reactive(rv$a), 500)
|
||||
|
||||
# Keep track of how often dr/tr are fired
|
||||
dr_fired <- 0
|
||||
dr_monitor <- observeEvent(dr(), {
|
||||
dr_fired <<- dr_fired + 1
|
||||
})
|
||||
on.exit(dr_monitor$destroy(), add = TRUE)
|
||||
# Keep track of how often dr/tr are fired
|
||||
dr_fired <- 0
|
||||
dr_monitor <- observeEvent(dr(), {
|
||||
dr_fired <<- dr_fired + 1
|
||||
})
|
||||
on.exit(dr_monitor$destroy(), add = TRUE)
|
||||
|
||||
tr_fired <- 0
|
||||
tr_monitor <- observeEvent(tr(), {
|
||||
tr_fired <<- tr_fired + 1
|
||||
})
|
||||
on.exit(tr_monitor$destroy(), add = TRUE)
|
||||
tr_fired <- 0
|
||||
tr_monitor <- observeEvent(tr(), {
|
||||
tr_fired <<- tr_fired + 1
|
||||
})
|
||||
on.exit(tr_monitor$destroy(), add = TRUE)
|
||||
|
||||
# Starting values are both 0. Earlier I found that the tests behaved
|
||||
# differently if I accessed the values of dr/tr before the first call to
|
||||
# flushReact(). That bug was fixed, but to ensure that similar bugs don't
|
||||
# appear undetected, we run this test with and without do_priming.
|
||||
if (do_priming) {
|
||||
# Starting values are both 0. Earlier I found that the tests behaved
|
||||
# differently if I accessed the values of dr/tr before the first call to
|
||||
# flushReact(). That bug was fixed, but to ensure that similar bugs don't
|
||||
# appear undetected, we run this test with and without do_priming.
|
||||
if (do_priming) {
|
||||
expect_identical(isolate(dr()), 0)
|
||||
expect_identical(isolate(tr()), 0)
|
||||
}
|
||||
|
||||
# Pump timer and reactives for about 1.3 seconds
|
||||
stopAt <- Sys.time() + 1.3
|
||||
while (Sys.time() < stopAt) {
|
||||
timerCallbacks$executeElapsed()
|
||||
flushReact()
|
||||
Sys.sleep(0.001)
|
||||
}
|
||||
|
||||
# dr() should not have had time to fire, other than the initial run, since
|
||||
# there haven't been long enough gaps between invalidations.
|
||||
expect_identical(dr_fired, 1)
|
||||
# The value of dr() should not have updated either.
|
||||
expect_identical(isolate(dr()), 0)
|
||||
expect_identical(isolate(tr()), 0)
|
||||
}
|
||||
|
||||
# Pump timer and reactives for about 1.3 seconds
|
||||
stopAt <- Sys.time() + 1.3
|
||||
while (Sys.time() < stopAt) {
|
||||
timerCallbacks$executeElapsed()
|
||||
flushReact()
|
||||
Sys.sleep(0.001)
|
||||
}
|
||||
# tr() however, has had time to fire multiple times and update its value.
|
||||
expect_identical(tr_fired, 3)
|
||||
expect_identical(isolate(tr()), 4)
|
||||
|
||||
# dr() should not have had time to fire, other than the initial run, since
|
||||
# there haven't been long enough gaps between invalidations.
|
||||
expect_identical(dr_fired, 1)
|
||||
# The value of dr() should not have updated either.
|
||||
expect_identical(isolate(dr()), 0)
|
||||
# Now let some time pass without any more updates.
|
||||
src$destroy() # No more updates
|
||||
stopAt <- Sys.time() + 1
|
||||
while (Sys.time() < stopAt) {
|
||||
timerCallbacks$executeElapsed()
|
||||
flushReact()
|
||||
Sys.sleep(0.001)
|
||||
}
|
||||
|
||||
# tr() however, has had time to fire multiple times and update its value.
|
||||
expect_identical(tr_fired, 3)
|
||||
expect_identical(isolate(tr()), 10)
|
||||
|
||||
# Now let some time pass without any more updates.
|
||||
src$destroy() # No more updates
|
||||
stopAt <- Sys.time() + 1
|
||||
while (Sys.time() < stopAt) {
|
||||
timerCallbacks$executeElapsed()
|
||||
flushReact()
|
||||
Sys.sleep(0.001)
|
||||
}
|
||||
|
||||
# dr should've fired, and we should have converged on the right answer.
|
||||
expect_identical(dr_fired, 2)
|
||||
isolate(expect_identical(rv$a, dr()))
|
||||
expect_identical(tr_fired, 4)
|
||||
isolate(expect_identical(rv$a, tr()))
|
||||
})
|
||||
|
||||
# Identical to test block above, but with do_priming set to FALSE.
|
||||
test_that("debounce/throttle work properly (without priming)", {
|
||||
do_priming <- FALSE
|
||||
# Some of the CRAN test machines are heavily loaded and so the timing for
|
||||
# these tests isn't reliable. https://github.com/rstudio/shiny/pull/2789
|
||||
skip_on_cran()
|
||||
|
||||
# The changing of rv$a will be the (chatty) source of reactivity.
|
||||
rv <- reactiveValues(a = 0)
|
||||
|
||||
# This observer will be what changes rv$a.
|
||||
src <- observe({
|
||||
invalidateLater(100)
|
||||
rv$a <- isolate(rv$a) + 1
|
||||
# dr should've fired, and we should have converged on the right answer.
|
||||
expect_identical(dr_fired, 2)
|
||||
isolate(expect_identical(rv$a, dr()))
|
||||
expect_identical(tr_fired, 4)
|
||||
isolate(expect_identical(rv$a, tr()))
|
||||
})
|
||||
on.exit(src$destroy(), add = TRUE)
|
||||
|
||||
# Make a debounced reactive to test.
|
||||
dr <- debounce(reactive(rv$a), 500)
|
||||
|
||||
# Make a throttled reactive to test.
|
||||
tr <- throttle(reactive(rv$a), 500)
|
||||
|
||||
# Keep track of how often dr/tr are fired
|
||||
dr_fired <- 0
|
||||
dr_monitor <- observeEvent(dr(), {
|
||||
dr_fired <<- dr_fired + 1
|
||||
})
|
||||
on.exit(dr_monitor$destroy(), add = TRUE)
|
||||
|
||||
tr_fired <- 0
|
||||
tr_monitor <- observeEvent(tr(), {
|
||||
tr_fired <<- tr_fired + 1
|
||||
})
|
||||
on.exit(tr_monitor$destroy(), add = TRUE)
|
||||
|
||||
# Starting values are both 0. Earlier I found that the tests behaved
|
||||
# differently if I accessed the values of dr/tr before the first call to
|
||||
# flushReact(). That bug was fixed, but to ensure that similar bugs don't
|
||||
# appear undetected, we run this test with and without do_priming.
|
||||
if (do_priming) {
|
||||
expect_identical(isolate(dr()), 0)
|
||||
expect_identical(isolate(tr()), 0)
|
||||
}
|
||||
|
||||
# Pump timer and reactives for about 1.3 seconds
|
||||
stopAt <- Sys.time() + 1.3
|
||||
while (Sys.time() < stopAt) {
|
||||
timerCallbacks$executeElapsed()
|
||||
flushReact()
|
||||
Sys.sleep(0.001)
|
||||
}
|
||||
|
||||
# dr() should not have had time to fire, other than the initial run, since
|
||||
# there haven't been long enough gaps between invalidations.
|
||||
expect_identical(dr_fired, 1)
|
||||
# The value of dr() should not have updated either.
|
||||
expect_identical(isolate(dr()), 0)
|
||||
|
||||
# tr() however, has had time to fire multiple times and update its value.
|
||||
expect_identical(tr_fired, 3)
|
||||
expect_identical(isolate(tr()), 10)
|
||||
|
||||
# Now let some time pass without any more updates.
|
||||
src$destroy() # No more updates
|
||||
stopAt <- Sys.time() + 1
|
||||
while (Sys.time() < stopAt) {
|
||||
timerCallbacks$executeElapsed()
|
||||
flushReact()
|
||||
Sys.sleep(0.001)
|
||||
}
|
||||
|
||||
# dr should've fired, and we should have converged on the right answer.
|
||||
expect_identical(dr_fired, 2)
|
||||
isolate(expect_identical(rv$a, dr()))
|
||||
expect_identical(tr_fired, 4)
|
||||
isolate(expect_identical(rv$a, tr()))
|
||||
})
|
||||
}
|
||||
|
||||
test_that("reactive domain works across async handlers", {
|
||||
obj <- new.env()
|
||||
|
||||
@@ -129,13 +129,9 @@ test_that("message logger appears", {
|
||||
|
||||
|
||||
test_that("reactlog_version is as expected", {
|
||||
suggests <- strsplit(packageDescription("shiny")$Suggests, ",")[[1]]
|
||||
reactlog <- trimws(
|
||||
grep("reactlog", suggests, value = TRUE)
|
||||
)
|
||||
expect_length(reactlog, 1)
|
||||
expect_equal(
|
||||
reactlog,
|
||||
sprintf("reactlog (>= %s)", reactlog_min_version)
|
||||
expect_match(
|
||||
packageDescription("shiny")$Suggests,
|
||||
# The space between reactlog and the version number can include \n
|
||||
sprintf("\\breactlog\\s+\\Q(>= %s)\\E", reactlog_min_version)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,3 +1,47 @@
|
||||
formatError <- function(err, full = FALSE, offset = TRUE, cleanPaths = TRUE) {
|
||||
# This complicated capturing code is necessary because printStackTrace uses a
|
||||
# combination of `message()` and `cat(file=stderr())` to print the error,
|
||||
# stack traces, and stack trace boundaries ("From earlier call:"). We want to
|
||||
# treat all of it as part of the same string.
|
||||
|
||||
str <- noquote(capture.output(
|
||||
suppressWarnings(
|
||||
suppressMessages(
|
||||
withCallingHandlers(
|
||||
printError(err, full = full, offset = offset),
|
||||
warning = function(cnd) {
|
||||
cat(conditionMessage(cnd), "\n", sep = "", file = stderr())
|
||||
},
|
||||
message = function(cnd) {
|
||||
cat(conditionMessage(cnd), file = stderr())
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
type = "message"
|
||||
))
|
||||
|
||||
# Remove directories and line numbers from file/line references, e.g.
|
||||
# 53: callback [/Users/jcheng/Development/rstudio/shiny/R/conditions.R#155]
|
||||
# becomes
|
||||
# 53: callback [conditions.R#XXX]
|
||||
#
|
||||
# This is to make the snapshot tests more stable across different machines and
|
||||
# ignores benign code movement within a file.
|
||||
str <- sub("#\\d+\\]$", "#XXX]", str, perl = TRUE)
|
||||
# Remove any file/line number reference that's not test-stacks-deep.R. These
|
||||
# are just too inconsistent across different ways of invoking testthat--not
|
||||
# relative vs. absolute paths, but whether the file/line number is included at
|
||||
# all!
|
||||
str <- sub(" \\[(?!test-stacks-deep.R)[^[]+#XXX\\]", "", str, perl = TRUE)
|
||||
# The frame numbers vary too much between different ways of invoking testthat
|
||||
# ("Run Tests" editor toolbar button and "Test" Build tab button in RStudio,
|
||||
# devtools::test(), etc.) so we blank them out.
|
||||
str <- sub("^[ \\d]+:", " :", str, perl = TRUE)
|
||||
str
|
||||
}
|
||||
|
||||
|
||||
describe("deep stack trace filtering", {
|
||||
it("passes smoke test", {
|
||||
st <- list(
|
||||
@@ -43,3 +87,170 @@ describe("deep stack trace filtering", {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test_that("deep stack capturing", {
|
||||
`%...>%` <- promises::`%...>%`
|
||||
`%...!%` <- promises::`%...!%`
|
||||
finally <- promises::finally
|
||||
|
||||
err <- NULL
|
||||
captureStackTraces({
|
||||
promise_resolve("one") %...>% {
|
||||
promise_reject("error") %...!% {
|
||||
finally(promise_resolve("two"), ~{
|
||||
stop("boom")
|
||||
})
|
||||
}
|
||||
}
|
||||
}) %...!% (function(err) {
|
||||
err <<- err
|
||||
})
|
||||
|
||||
wait_for_it()
|
||||
|
||||
expect_s3_class(err, "error", exact = FALSE)
|
||||
expect_snapshot(cat(sep="\n", formatError(err)))
|
||||
expect_snapshot(cat(sep="\n", formatError(err, full = TRUE)))
|
||||
})
|
||||
|
||||
test_that("deep stack capturing within reactives", {
|
||||
rerr <- NULL
|
||||
observe({
|
||||
promise_resolve("one") %...>% {
|
||||
promise_resolve("two") %...>% {
|
||||
stop("boom")
|
||||
}
|
||||
} %...!% (function(err) {
|
||||
rerr <<- err
|
||||
})
|
||||
})
|
||||
|
||||
flushReact()
|
||||
wait_for_it()
|
||||
|
||||
expect_s3_class(rerr, "error", exact = FALSE)
|
||||
expect_length(attr(rerr, "deep.stack.trace"), 2)
|
||||
})
|
||||
|
||||
test_that("deep stacks long chain", {
|
||||
op <- options(shiny.deepstacktrace = 3L)
|
||||
on.exit(options(op), add = TRUE, after = FALSE)
|
||||
|
||||
# Without deep stack traces, the stack trace would give no clue that the error
|
||||
# originally started from a call to `A__()`. With deep stack traces, we can
|
||||
# see that the error originated from `A__` and passed through `I__` and `J__`.
|
||||
# But due to culling, we don't see `B__` through `H__`--these are omitted for
|
||||
# brevity and to prevent unbounded growth of the accounting we do.
|
||||
|
||||
A__ <- function() promise_resolve(TRUE) %...>% B__()
|
||||
B__ <- function(x) promise_resolve(TRUE) %...>% C__()
|
||||
C__ <- function(x) promise_resolve(TRUE) %...>% D__()
|
||||
D__ <- function(x) promise_resolve(TRUE) %...>% E__()
|
||||
E__ <- function(x) promise_resolve(TRUE) %...>% F__()
|
||||
F__ <- function(x) promise_resolve(TRUE) %...>% G__()
|
||||
G__ <- function(x) promise_resolve(TRUE) %...>% H__()
|
||||
H__ <- function(x) promise_resolve(TRUE) %...>% I__()
|
||||
I__ <- function(x) promise_resolve(TRUE) %...>% J__()
|
||||
J__ <- function(x) promise_resolve(TRUE) %...>% { stop("boom") }
|
||||
|
||||
dserr <- NULL
|
||||
captureStackTraces(
|
||||
A__()
|
||||
) %...!% (function(err) {
|
||||
dserr <<- err
|
||||
})
|
||||
|
||||
wait_for_it()
|
||||
|
||||
expect_s3_class(dserr, "error", exact = FALSE)
|
||||
expect_snapshot(cat(sep="\n", stacktrace <- formatError(dserr)))
|
||||
# Ensure we dropTrivialTestFrames only when snapshotting
|
||||
expect_false(length(stacktrace) == length(formatError(dserr)))
|
||||
# Ensure that A__ through J__ are present in the traces
|
||||
for (letter in LETTERS[1:10]) {
|
||||
expect_length(which(grepl(paste0(letter, "__"), stacktrace)), 1L)
|
||||
}
|
||||
})
|
||||
|
||||
test_that("Deep stack deduplication", {
|
||||
recursive_promise <- function(n) {
|
||||
if (n <= 0) {
|
||||
stop("boom")
|
||||
}
|
||||
|
||||
p <- promises::promise_resolve(TRUE)
|
||||
promises::then(p, ~{
|
||||
recursive_promise(n - 1)
|
||||
})
|
||||
}
|
||||
|
||||
op <- options(shiny.deepstacktrace = TRUE)
|
||||
on.exit(options(op), add = TRUE, after = FALSE)
|
||||
|
||||
uerr <- NULL
|
||||
captureStackTraces(recursive_promise(100)) %...!% (function(err) {
|
||||
uerr <<- err
|
||||
})
|
||||
|
||||
wait_for_it()
|
||||
|
||||
expect_s3_class(uerr, "error", exact = FALSE)
|
||||
# Even though we traveled through 100 promises recursively, we only retained
|
||||
# the unique ones
|
||||
expect_identical(length(attr(uerr, "deep.stack.trace", exact = TRUE)), 2L)
|
||||
})
|
||||
|
||||
test_that("stack trace stripping works", {
|
||||
A__ <- function() promise_resolve(TRUE) %...>% B__()
|
||||
B__ <- function(x) promise_resolve(TRUE) %...>% { ..stacktraceoff..(C__()) }
|
||||
C__ <- function(x) promise_resolve(TRUE) %...>% D__()
|
||||
D__ <- function(x) promise_resolve(TRUE) %...>% { ..stacktraceon..(E__()) }
|
||||
E__ <- function(x) promise_resolve(TRUE) %...>% { stop("boom") }
|
||||
|
||||
strperr <- NULL
|
||||
captureStackTraces(A__()) %...!% (function(err) {
|
||||
strperr <<- err
|
||||
})
|
||||
|
||||
..stacktracefloor..(
|
||||
wait_for_it()
|
||||
)
|
||||
|
||||
expect_s3_class(strperr, "error", exact = FALSE)
|
||||
|
||||
str <- formatError(strperr)
|
||||
expect_length(which(grepl("A__", str)), 1L)
|
||||
expect_length(which(grepl("B__", str)), 1L)
|
||||
expect_length(which(grepl("C__", str)), 0L)
|
||||
expect_length(which(grepl("D__", str)), 0L)
|
||||
expect_length(which(grepl("E__", str)), 1L)
|
||||
|
||||
str_full <- formatError(strperr, full = TRUE)
|
||||
expect_length(which(grepl("A__", str_full)), 1L)
|
||||
expect_length(which(grepl("B__", str_full)), 1L)
|
||||
expect_length(which(grepl("C__", str_full)), 1L)
|
||||
expect_length(which(grepl("D__", str_full)), 1L)
|
||||
expect_length(which(grepl("E__", str_full)), 1L)
|
||||
})
|
||||
|
||||
test_that("coro async generator deep stack count is low", {
|
||||
gen <- coro::async_generator(function() {
|
||||
for (i in 1:50) {
|
||||
await(coro::async_sleep(0.001))
|
||||
yield(i)
|
||||
}
|
||||
stop("boom")
|
||||
})
|
||||
|
||||
cgerr <- NULL
|
||||
captureStackTraces(
|
||||
coro::async_collect(gen()) %...!% (function(err) {
|
||||
cgerr <<- err
|
||||
})
|
||||
)
|
||||
|
||||
wait_for_it()
|
||||
|
||||
expect_s3_class(cgerr, "error", exact = FALSE)
|
||||
expect_length(attr(cgerr, "deep.stack.trace"), 2L)
|
||||
})
|
||||
|
||||
@@ -98,14 +98,15 @@ extractStackTrace <- function(calls,
|
||||
num = index,
|
||||
call = getCallNames(calls),
|
||||
loc = getLocs(calls),
|
||||
category = getCallCategories(calls),
|
||||
# category = getCallCategories(calls),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
}
|
||||
|
||||
cleanLocs <- function(locs) {
|
||||
locs[!grepl("test-stacks\\.R", locs, perl = TRUE)] <- ""
|
||||
sub("^.*#", "", locs)
|
||||
# sub("^.*#", "", locs)
|
||||
locs
|
||||
}
|
||||
|
||||
dumpTests <- function(df) {
|
||||
@@ -129,46 +130,12 @@ test_that("integration tests", {
|
||||
df <- causeError(full = FALSE)
|
||||
# dumpTests(df)
|
||||
|
||||
expect_equal(df$num, c(56L, 55L, 54L, 38L, 37L, 36L, 35L, 34L, 33L))
|
||||
expect_equal(df$call, c("A", "B", "<reactive:C>", "C", "renderTable",
|
||||
"func", "force", "withVisible", "withCallingHandlers"))
|
||||
expect_equal(nzchar(df$loc), c(TRUE, TRUE, TRUE, FALSE, TRUE,
|
||||
FALSE, FALSE, FALSE, FALSE))
|
||||
expect_snapshot(df)
|
||||
|
||||
df <- causeError(full = TRUE)
|
||||
# dumpTests(df)
|
||||
|
||||
expect_equal(df$num, c(59L, 58L, 57L, 56L, 55L, 54L, 53L,
|
||||
52L, 51L, 50L, 49L, 48L, 47L, 46L, 45L, 44L, 43L, 42L, 41L,
|
||||
40L, 39L, 38L, 37L, 36L, 35L, 34L, 33L, 32L, 31L, 30L, 29L,
|
||||
28L, 27L, 26L, 25L, 24L, 23L, 22L, 21L, 20L, 19L, 18L, 17L,
|
||||
16L, 15L, 14L, 13L, 12L, 11L, 10L, 9L, 8L, 7L, 6L, 5L, 4L,
|
||||
3L, 2L, 1L))
|
||||
expect_equal(df$call, c("h", ".handleSimpleError", "stop",
|
||||
"A", "B", "<reactive:C>", "..stacktraceon..", ".func", "withVisible",
|
||||
"withCallingHandlers", "contextFunc", "env$runWith", "force",
|
||||
"domain$wrapSync", "promises::with_promise_domain",
|
||||
"withReactiveDomain", "domain$wrapSync", "promises::with_promise_domain",
|
||||
"ctx$run", "self$.updateValue", "..stacktraceoff..", "C",
|
||||
"renderTable", "func", "force", "withVisible", "withCallingHandlers",
|
||||
"domain$wrapSync", "promises::with_promise_domain",
|
||||
"captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
|
||||
"tryCatch", "do", "hybrid_chain", "renderFunc", "renderTable({ C() }, server = FALSE)",
|
||||
"..stacktraceon..", "contextFunc", "env$runWith", "force",
|
||||
"domain$wrapSync", "promises::with_promise_domain",
|
||||
"withReactiveDomain", "domain$wrapSync", "promises::with_promise_domain",
|
||||
"ctx$run", "..stacktraceoff..", "isolate", "withCallingHandlers",
|
||||
"domain$wrapSync", "promises::with_promise_domain",
|
||||
"captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
|
||||
"tryCatch", "try"))
|
||||
expect_equal(nzchar(df$loc), c(FALSE, FALSE, FALSE, TRUE,
|
||||
TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
|
||||
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
|
||||
TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
|
||||
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FALSE,
|
||||
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
|
||||
FALSE, TRUE, FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE,
|
||||
FALSE))
|
||||
expect_snapshot(df)
|
||||
# dumpTests(df)
|
||||
})
|
||||
|
||||
test_that("shiny.error", {
|
||||
@@ -239,6 +206,57 @@ test_that("validation error logging", {
|
||||
captureErrorLog(validate("boom"))
|
||||
expect_null(caught)
|
||||
|
||||
caught <- NULL
|
||||
captureErrorLog(stop("boom"))
|
||||
expect_true(!is.null(caught))
|
||||
})
|
||||
|
||||
test_that("observeEvent is not overly stripped (#4162)", {
|
||||
caught <- NULL
|
||||
..stacktraceoff..(
|
||||
..stacktracefloor..({
|
||||
observeEvent(1, {
|
||||
tryCatch(
|
||||
captureStackTraces(stop("boom")),
|
||||
error = function(cond) {
|
||||
caught <<- cond
|
||||
}
|
||||
)
|
||||
})
|
||||
flushReact()
|
||||
})
|
||||
)
|
||||
st_str <- capture.output(printStackTrace(caught), type = "message")
|
||||
expect_true(any(grepl("observeEvent\\(1\\)", st_str)))
|
||||
|
||||
# Now same thing, but deep stack trace version
|
||||
|
||||
A__ <- function() {
|
||||
promises::then(promises::promise_resolve(TRUE), ~{
|
||||
stop("boom")
|
||||
})
|
||||
}
|
||||
|
||||
B__ <- function() {
|
||||
promises::then(promises::promise_resolve(TRUE), ~{
|
||||
A__()
|
||||
})
|
||||
}
|
||||
|
||||
caught <- NULL
|
||||
..stacktraceoff..(
|
||||
..stacktracefloor..({
|
||||
observeEvent(1, {
|
||||
captureStackTraces(promises::catch(B__(), ~{
|
||||
caught <<- .
|
||||
}))
|
||||
})
|
||||
flushReact()
|
||||
wait_for_it()
|
||||
})
|
||||
)
|
||||
st_str <- capture.output(printStackTrace(caught), type = "message")
|
||||
# cat(st_str, sep = "\n")
|
||||
expect_true(any(grepl("A__", st_str)))
|
||||
expect_true(any(grepl("B__", st_str)))
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ new file mode 100644
|
||||
index 00000000..ba052f8b
|
||||
--- /dev/null
|
||||
+++ b/inst/www/shared/ionrangeslider/scss/shiny.scss
|
||||
@@ -0,0 +1,201 @@
|
||||
@@ -0,0 +1,206 @@
|
||||
+/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
|
||||
+© Posit, PBC, 2023
|
||||
+© RStudio, Inc, 2014
|
||||
@@ -149,6 +149,11 @@ index 00000000..ba052f8b
|
||||
+ border-radius: $handle_width;
|
||||
+ z-index: 2;
|
||||
+
|
||||
+ &.type_last {
|
||||
+ // Ensure last-used handle is on top if it overlaps with another handle
|
||||
+ z-index: 3;
|
||||
+ }
|
||||
+
|
||||
+ &.state_hover,
|
||||
+ &:hover {
|
||||
+ background: $handle_color_hover;
|
||||
|
||||
Reference in New Issue
Block a user