Compare commits
46 Commits
spinners
...
init-promi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da68eb6161 | ||
|
|
dedaf6808e | ||
|
|
3b926fd0c9 | ||
|
|
88a8fa70c7 | ||
|
|
d1e5e3c58f | ||
|
|
7575a92a76 | ||
|
|
cb1bc10461 | ||
|
|
25c40967da | ||
|
|
068b232e75 | ||
|
|
0b7fda707e | ||
|
|
9fd4ba199e | ||
|
|
43e40c7969 | ||
|
|
248f19333c | ||
|
|
306c4f847b | ||
|
|
9c7937b3df | ||
|
|
83d9cae6cc | ||
|
|
def783fe3e | ||
|
|
d141faa18d | ||
|
|
8240ebea26 | ||
|
|
0c3af7b8bf | ||
|
|
a2b7207816 | ||
|
|
18660c39c8 | ||
|
|
c54ce06b75 | ||
|
|
8de3737a87 | ||
|
|
98b76b5fa7 | ||
|
|
97f25443ef | ||
|
|
bacdb3204a | ||
|
|
faa0edb899 | ||
|
|
aa973c2d64 | ||
|
|
dcecef2841 | ||
|
|
f18b598ed7 | ||
|
|
13138ffa98 | ||
|
|
e689cdc522 | ||
|
|
3e0efd8484 | ||
|
|
4a8400d2a5 | ||
|
|
e432bb0592 | ||
|
|
d002734afe | ||
|
|
54e7377f24 | ||
|
|
a49d24108f | ||
|
|
733a4e8983 | ||
|
|
6309a6fca3 | ||
|
|
3d66940402 | ||
|
|
2872c87e32 | ||
|
|
ecb591f2e1 | ||
|
|
8e37d45948 | ||
|
|
c11f120bb9 |
@@ -35,10 +35,6 @@ rules:
|
||||
|
||||
default-case:
|
||||
- error
|
||||
indent:
|
||||
- error
|
||||
- 2
|
||||
- SwitchCase: 1
|
||||
linebreak-style:
|
||||
- error
|
||||
- unix
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name : Ask a Question
|
||||
about : The issue tracker is not for questions -- please ask questions at https://community.rstudio.com/c/shiny.
|
||||
about : The issue tracker is not for questions -- please ask questions at https://forum.posit.co/tags/shiny.
|
||||
---
|
||||
|
||||
The issue tracker is not for questions. If you have a question, please feel free to ask it on our community site, at https://community.rstudio.com/c/shiny.
|
||||
The issue tracker is not for questions. If you have a question, please feel free to ask it on our community site, at https://forum.posit.co/c/shiny.
|
||||
|
||||
|
||||
1
.github/shiny-workflows/routine.sh
vendored
@@ -5,6 +5,7 @@ echo "Updating package.json version to match DESCRIPTION Version"
|
||||
Rscript ./tools/updatePackageJsonVersion.R
|
||||
if [ -n "$(git status --porcelain package.json)" ]
|
||||
then
|
||||
echo "package.json has changed after running ./tools/updatePackageJsonVersion.R. Re-running 'yarn build'"
|
||||
yarn build
|
||||
git add ./inst package.json && git commit -m 'Sync package version (GitHub Actions)' || echo "No package version to commit"
|
||||
else
|
||||
|
||||
6
.vscode/settings.json
vendored
@@ -15,4 +15,10 @@
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
},
|
||||
"[json]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Package: shiny
|
||||
Type: Package
|
||||
Title: Web Application Framework for R
|
||||
Version: 1.8.1.9000
|
||||
Version: 1.8.1.9001
|
||||
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"),
|
||||
@@ -91,8 +91,8 @@ Imports:
|
||||
withr,
|
||||
commonmark (>= 1.7),
|
||||
glue (>= 1.3.2),
|
||||
bslib (>= 0.3.0),
|
||||
cachem,
|
||||
bslib (>= 0.6.0),
|
||||
cachem (>= 1.1.0),
|
||||
lifecycle (>= 0.2.0)
|
||||
Suggests:
|
||||
datasets,
|
||||
@@ -128,6 +128,7 @@ Collate:
|
||||
'map.R'
|
||||
'utils.R'
|
||||
'bootstrap.R'
|
||||
'busy-indicators-spinners.R'
|
||||
'busy-indicators.R'
|
||||
'cache-utils.R'
|
||||
'deprecated.R'
|
||||
@@ -205,7 +206,7 @@ Collate:
|
||||
'version_selectize.R'
|
||||
'version_strftime.R'
|
||||
'viewer.R'
|
||||
RoxygenNote: 7.3.1
|
||||
RoxygenNote: 7.3.2
|
||||
Encoding: UTF-8
|
||||
Roxygen: list(markdown = TRUE)
|
||||
RdMacros: lifecycle
|
||||
|
||||
@@ -78,6 +78,7 @@ export(br)
|
||||
export(browserViewer)
|
||||
export(brushOpts)
|
||||
export(brushedPoints)
|
||||
export(busyIndicatorOptions)
|
||||
export(callModule)
|
||||
export(captureStackTraces)
|
||||
export(checkboxGroupInput)
|
||||
@@ -204,7 +205,6 @@ export(pre)
|
||||
export(prependTab)
|
||||
export(printError)
|
||||
export(printStackTrace)
|
||||
export(pulseOptions)
|
||||
export(quoToFunction)
|
||||
export(radioButtons)
|
||||
export(reactive)
|
||||
@@ -216,6 +216,7 @@ export(reactiveVal)
|
||||
export(reactiveValues)
|
||||
export(reactiveValuesToList)
|
||||
export(reactlog)
|
||||
export(reactlogAddMark)
|
||||
export(reactlogReset)
|
||||
export(reactlogShow)
|
||||
export(registerInputHandler)
|
||||
@@ -273,7 +274,6 @@ export(snapshotExclude)
|
||||
export(snapshotPreprocessInput)
|
||||
export(snapshotPreprocessOutput)
|
||||
export(span)
|
||||
export(spinnerOptions)
|
||||
export(splitLayout)
|
||||
export(stopApp)
|
||||
export(strong)
|
||||
|
||||
20
NEWS.md
@@ -1,7 +1,25 @@
|
||||
# shiny (development version)
|
||||
|
||||
## 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)
|
||||
|
||||
* 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)
|
||||
|
||||
* 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)
|
||||
|
||||
* A new `shiny.client_devmode` option controls client-side devmode features, in particular the client-side error console introduced in shiny 1.8.1, independently of the R-side features of `shiny::devmode()`. This usage is primarily intended for automatic use in Shinylive. (#4073)
|
||||
|
||||
* Added function `reactlogAddMark()` to programmatically add _mark_ed locations in the reactlog log without the requirement of keyboard bindings during an idle reactive moment. (#4103)
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* `downloadButton()` and `downloadLink()` are now disabled up until they are fully initialized. This prevents the user from clicking the button/link before the download is ready. (#4041)
|
||||
|
||||
* Output bindings that are removed, invalidated, then inserted again (while invalidated) now correctly include the `.recalculating` CSS class. (#4039)
|
||||
|
||||
* Fixed a recent issue with `uiOutput()` and `conditionalPanel()` not properly lower opacity when recalculation (in a Bootstrap 5 context). (#4027)
|
||||
|
||||
# shiny 1.8.1.1
|
||||
@@ -16,7 +34,7 @@
|
||||
|
||||
* Added a JavaScript error dialog, reporting errors that previously were only discoverable by opening the browser's devtools open. Since this dialog is mainly useful for debugging and development, it must be enabled with `shiny::devmode()`. (#3931)
|
||||
|
||||
* `runExamples()` now uses the `{bslib}` package to generate a better looking result. It also gains a `package` argument so that other packages can leverage this same function to run Shiny app examples. For more, see `?runExamples`. (#3963, #4005)
|
||||
* `runExample()` now uses the `{bslib}` package to generate a better looking result. It also gains a `package` argument so that other packages can leverage this same function to run Shiny app examples. For more, see `?runExample`. (#3963, #4005)
|
||||
|
||||
* Added `onUnhandledError()` to register a function that will be called when an unhandled error occurs in a Shiny app. Note that this handler doesn't stop the error or prevent the session from closing, but it can be used to log the error or to clean up session-specific resources. (thanks @JohnCoene, #3993)
|
||||
|
||||
|
||||
@@ -1276,10 +1276,13 @@ downloadButton <- function(outputId,
|
||||
...,
|
||||
icon = shiny::icon("download")) {
|
||||
tags$a(id=outputId,
|
||||
class=paste('btn btn-default shiny-download-link', class),
|
||||
class='btn btn-default shiny-download-link disabled',
|
||||
class=class,
|
||||
href='',
|
||||
target='_blank',
|
||||
download=NA,
|
||||
"aria-disabled"="true",
|
||||
tabindex="-1",
|
||||
validateIcon(icon),
|
||||
label, ...)
|
||||
}
|
||||
@@ -1288,10 +1291,13 @@ downloadButton <- function(outputId,
|
||||
#' @export
|
||||
downloadLink <- function(outputId, label="Download", class=NULL, ...) {
|
||||
tags$a(id=outputId,
|
||||
class=paste(c('shiny-download-link', class), collapse=" "),
|
||||
class='shiny-download-link disabled',
|
||||
class=class,
|
||||
href='',
|
||||
target='_blank',
|
||||
download=NA,
|
||||
"aria-disabled"="true",
|
||||
tabindex="-1",
|
||||
label, ...)
|
||||
}
|
||||
|
||||
|
||||
4
R/busy-indicators-spinners.R
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by tools/updateSpinnerTypes.R: do not edit by hand
|
||||
.busySpinnerTypes <-
|
||||
c("ring", "ring2", "ring3", "bars", "bars2", "bars3", "pulse",
|
||||
"pulse2", "pulse3", "dots", "dots2", "dots3")
|
||||
@@ -1,17 +1,29 @@
|
||||
#' Use and customize busy indicator types.
|
||||
#' Enable/disable busy indication
|
||||
#'
|
||||
#' To enable busy indicators, include the result of this function in the app's UI.
|
||||
#' Busy indicators provide a visual cue to users when the server is busy
|
||||
#' calculating outputs or otherwise performing tasks (e.g., producing
|
||||
#' downloads). 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. Busy indication is enabled by
|
||||
#' default for UI created with \pkg{bslib}, but must be enabled otherwise. To
|
||||
#' enable/disable, include the result of this function in anywhere in the app's
|
||||
#' UI.
|
||||
#'
|
||||
#' When both `spinners` and `pulse` are set to `TRUE`, the pulse is disabled
|
||||
#' when spinner(s) are active. When both `spinners` and `pulse` are set to
|
||||
#' `FALSE`, no busy indication is shown (other than the gray-ing out of
|
||||
#' recalculating outputs).
|
||||
#' When both `spinners` and `pulse` are set to `TRUE`, the pulse is
|
||||
#' automatically disabled when spinner(s) are active. When both `spinners` and
|
||||
#' `pulse` are set to `FALSE`, no busy indication is shown (other than the
|
||||
#' graying out of recalculating outputs).
|
||||
#'
|
||||
#' @param ... Currently ignored.
|
||||
#' @param spinners Whether to show a spinner on each calculating/recalculating
|
||||
#' output.
|
||||
#' @param pulse Whether to show a pulsing banner at the top of the page when the
|
||||
#' app is busy.
|
||||
#'
|
||||
#' @param spinners Overlay a spinner on each calculating/recalculating output.
|
||||
#' @param pulse Show a pulsing banner at the top of the window when the server is busy.
|
||||
#' @export
|
||||
#' @seealso [spinnerOptions()] [pulseOptions()]
|
||||
#' @examplesIf interactive()
|
||||
#' @seealso [busyIndicatorOptions()] for customizing the appearance of the busy
|
||||
#' indicators.
|
||||
#' @examplesIf rlang::is_interactive()
|
||||
#'
|
||||
#' library(bslib)
|
||||
#'
|
||||
@@ -36,16 +48,19 @@
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
useBusyIndicators <- function(spinners = TRUE, pulse = TRUE) {
|
||||
useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
|
||||
|
||||
rlang::check_dots_empty()
|
||||
|
||||
attrs <- list("shinyBusySpinners" = spinners, "shinyBusyPulse" = pulse)
|
||||
|
||||
js <- Map(function(key, value) {
|
||||
if (value) {
|
||||
js <- vapply(names(attrs), character(1), FUN = function(key) {
|
||||
if (attrs[[key]]) {
|
||||
sprintf("document.documentElement.dataset.%s = 'true';", key)
|
||||
} else {
|
||||
sprintf("delete document.documentElement.dataset.%s;", key)
|
||||
}
|
||||
}, names(attrs), attrs)
|
||||
})
|
||||
|
||||
js <- HTML(paste(js, collapse = "\n"))
|
||||
|
||||
@@ -54,122 +69,171 @@ useBusyIndicators <- function(spinners = TRUE, pulse = TRUE) {
|
||||
tags$script(js)
|
||||
}
|
||||
|
||||
|
||||
#' Customize spinning busy indicators.
|
||||
#' Customize busy indicator options
|
||||
#'
|
||||
#' Include the result of this function in the app's UI to customize spinner
|
||||
#' appearance.
|
||||
#' 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.
|
||||
#'
|
||||
#' @details To effectively disable spinners, set the `size` to "0px".
|
||||
#' @param ... Currently ignored.
|
||||
#' @param spinner_type The type of spinner. Pre-bundled types include:
|
||||
#' '`r paste0(.busySpinnerTypes, collapse = "', '")`'.
|
||||
#'
|
||||
#' @param type The type of spinner to use. Builtin options include: tadpole,
|
||||
#' disc, dots, dot-track, and bounce. A custom type may also provided, which
|
||||
#' should be a valid value for the CSS
|
||||
#' [mask-image](https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image)
|
||||
#' property.
|
||||
#' @param color The color of the spinner. This can be any valid CSS color.
|
||||
#' Defaults to the app's "primary" color (if Bootstrap is on the page) or
|
||||
#' light-blue if not.
|
||||
#' @param size The size of the spinner. This can be any valid CSS size.
|
||||
#' @param easing The easing function to use for the spinner animation. This can
|
||||
#' be any valid CSS [easing
|
||||
#' function](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function).
|
||||
#' @param speed The amount of time for the spinner to complete a single
|
||||
#' revolution. This can be any valid CSS time.
|
||||
#' @param delay The amount of time to wait before showing the spinner. This can
|
||||
#' be any valid CSS time and can useful for not showing the spinner
|
||||
#' A path to a local SVG file can also be provided. The SVG should adhere to
|
||||
#' the following rules:
|
||||
#' * The SVG itself should contain the animation.
|
||||
#' * It should avoid absolute sizes (the spinner's containing DOM element
|
||||
#' size is set in CSS by `spinner_size`, so it should fill that container).
|
||||
#' * It should avoid setting absolute colors (the spinner's containing DOM element
|
||||
#' color is set in CSS by `spinner_color`, so it should inherit that color).
|
||||
#' @param spinner_color The color of the spinner. This can be any valid CSS
|
||||
#' color. Defaults to the app's "primary" color if Bootstrap is on the page.
|
||||
#' @param spinner_size The size of the spinner. This can be any valid CSS size.
|
||||
#' @param spinner_delay The amount of time to wait before showing the spinner.
|
||||
#' This can be any valid CSS time and can be useful for not showing the spinner
|
||||
#' if the computation finishes quickly.
|
||||
#' @param css_selector A CSS selector for scoping the spinner customization.
|
||||
#' Defaults to the root element.
|
||||
#' @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 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)
|
||||
#' of the theme's indigo, purple, and pink colors.
|
||||
#' @param pulse_height The height of the pulsing banner. This can be any valid
|
||||
#' CSS size.
|
||||
#' @param pulse_speed The speed of the pulsing banner. This can be any valid CSS
|
||||
#' time.
|
||||
#'
|
||||
#' @export
|
||||
#' @seealso [useBusyIndicators()] [pulseOptions()]
|
||||
#' @examplesIf interactive()
|
||||
#' @seealso [useBusyIndicators()] for enabling/disabling busy indicators.
|
||||
#' @examplesIf rlang::is_interactive()
|
||||
#'
|
||||
#' library(bslib)
|
||||
#'
|
||||
#' card_ui <- function(id, spinner_type = id) {
|
||||
#' card(
|
||||
#' busyIndicatorOptions(spinner_type = spinner_type),
|
||||
#' card_header(paste("Spinner:", spinner_type)),
|
||||
#' plotOutput(shiny::NS(id, "plot"))
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' card_server <- function(id, simulate = reactive()) {
|
||||
#' moduleServer(
|
||||
#' id = id,
|
||||
#' function(input, output, session) {
|
||||
#' output$plot <- renderPlot({
|
||||
#' Sys.sleep(1)
|
||||
#' simulate()
|
||||
#' plot(x = rnorm(100), y = rnorm(100))
|
||||
#' })
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
#'
|
||||
#' ui <- page_fillable(
|
||||
#' useBusyIndicators(),
|
||||
#' spinnerOptions(color = "orange"),
|
||||
#' card(
|
||||
#' card_header(
|
||||
#' "A plot",
|
||||
#' input_task_button("simulate", "Simulate"),
|
||||
#' class = "d-flex justify-content-between align-items-center"
|
||||
#' ),
|
||||
#' plotOutput("p"),
|
||||
#' input_task_button("simulate", "Simulate", icon = icon("refresh")),
|
||||
#' layout_columns(
|
||||
#' card_ui("ring"),
|
||||
#' card_ui("bars"),
|
||||
#' card_ui("dots"),
|
||||
#' card_ui("pulse"),
|
||||
#' col_widths = 6
|
||||
#' )
|
||||
#' )
|
||||
#'
|
||||
#' server <- function(input, output) {
|
||||
#' output$p <- renderPlot({
|
||||
#' input$simulate
|
||||
#' Sys.sleep(4)
|
||||
#' plot(x = rnorm(100), y = rnorm(100))
|
||||
#' })
|
||||
#' server <- function(input, output, session) {
|
||||
#' simulate <- reactive(input$simulate)
|
||||
#' card_server("ring", simulate)
|
||||
#' card_server("bars", simulate)
|
||||
#' card_server("dots", simulate)
|
||||
#' card_server("pulse", simulate)
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
spinnerOptions <- function(
|
||||
type = NULL,
|
||||
#'
|
||||
busyIndicatorOptions <- function(
|
||||
...,
|
||||
color = NULL,
|
||||
size = NULL,
|
||||
easing = NULL,
|
||||
speed = NULL,
|
||||
delay = NULL,
|
||||
css_selector = ":root"
|
||||
spinner_type = NULL,
|
||||
spinner_color = NULL,
|
||||
spinner_size = NULL,
|
||||
spinner_delay = NULL,
|
||||
spinner_selector = NULL,
|
||||
pulse_background = NULL,
|
||||
pulse_height = NULL,
|
||||
pulse_speed = NULL
|
||||
) {
|
||||
|
||||
# bounce requires a different animation than the others
|
||||
if (isTRUE(type == "bounce")) {
|
||||
animation <- "shiny-busy-spinner-bounce"
|
||||
speed <- speed %||% "0.8s"
|
||||
} else {
|
||||
animation <- NULL
|
||||
}
|
||||
rlang::check_dots_empty()
|
||||
|
||||
# Supported types have a CSS var already defined with their SVG data
|
||||
if (isTRUE(type %in% c("tadpole", "disc", "dots", "dot-track", "bounce"))) {
|
||||
type <- sprintf("var(--_shiny-spinner-type-%s)", type)
|
||||
}
|
||||
res <- tagList(
|
||||
spinnerOptions(
|
||||
type = spinner_type,
|
||||
color = spinner_color,
|
||||
size = spinner_size,
|
||||
delay = spinner_delay,
|
||||
selector = spinner_selector
|
||||
),
|
||||
pulseOptions(
|
||||
background = pulse_background,
|
||||
height = pulse_height,
|
||||
speed = pulse_speed
|
||||
)
|
||||
)
|
||||
|
||||
# Options are controlled via CSS variables.
|
||||
css_vars <- paste0(c(
|
||||
if (!is.null(type)) sprintf("--shiny-spinner-mask-img: %s", type),
|
||||
if (!is.null(easing)) sprintf("--shiny-spinner-easing: %s", easing),
|
||||
if (!is.null(animation)) sprintf("--shiny-spinner-animation: %s", animation),
|
||||
if (!is.null(color)) sprintf("--shiny-spinner-color: %s", color),
|
||||
if (!is.null(size)) sprintf("--shiny-spinner-size: %s", size),
|
||||
if (!is.null(speed)) sprintf("--shiny-spinner-speed: %s", speed),
|
||||
if (!is.null(delay)) sprintf("--shiny-spinner-delay: %s", delay)
|
||||
), collapse = ";")
|
||||
|
||||
# The CSS cascade allows this to be called multiple times, and as long as the CSS
|
||||
# selector is the same, the last call takes precedence. Also, css_selector allows
|
||||
# for scoping of the spinner customization.
|
||||
tags$style(HTML(paste0(css_selector, " {", css_vars, "}")))
|
||||
bslib::as.card_item(dropNulls(res))
|
||||
}
|
||||
|
||||
|
||||
#' Customize the pulsing busy indicator.
|
||||
#'
|
||||
#' Include the result of this function in the app's UI to customize the pulsing
|
||||
#' banner.
|
||||
#'
|
||||
#' @param color The color of the pulsing banner. This can be any valid CSS
|
||||
#' color. Defaults to the app's "primary" color (if Bootstrap is on the page)
|
||||
#' or light-blue if not.
|
||||
#' @param height The height of the pulsing banner. This can be any valid CSS
|
||||
#' size. Defaults to "3.5px".
|
||||
#' @export
|
||||
#' @seealso [useBusyIndicators()] [spinnerOptions()]
|
||||
pulseOptions <- function(color = NULL, height = NULL, speed = NULL) {
|
||||
css_vars <- paste0(c(
|
||||
if (!is.null(color)) sprintf("--shiny-pulse-color: %s", color),
|
||||
if (!is.null(height)) sprintf("--shiny-pulse-height: %s", height),
|
||||
if (!is.null(speed)) sprintf("--shiny-pulse-speed: %s", speed)
|
||||
), collapse = ";")
|
||||
spinnerOptions <- function(type = NULL, color = NULL, size = NULL, delay = NULL, selector = NULL) {
|
||||
if (is.null(type) && is.null(color) && is.null(size) && is.null(delay) && is.null(selector)) {
|
||||
return(NULL)
|
||||
}
|
||||
|
||||
url <- NULL
|
||||
if (!is.null(type)) {
|
||||
stopifnot(is.character(type) && length(type) == 1)
|
||||
if (file.exists(type) && grepl("\\.svg$", type)) {
|
||||
typeRaw <- readBin(type, "raw", n = file.info(type)$size)
|
||||
url <- sprintf("url('data:image/svg+xml;base64,%s')", rawToBase64(typeRaw))
|
||||
} else {
|
||||
type <- rlang::arg_match(type, .busySpinnerTypes)
|
||||
url <- sprintf("url('spinners/%s.svg')", type)
|
||||
}
|
||||
}
|
||||
|
||||
# Options controlled via CSS variables.
|
||||
css_vars <- htmltools::css(
|
||||
"--shiny-spinner-url" = url,
|
||||
"--shiny-spinner-color" = htmltools::parseCssColors(color),
|
||||
"--shiny-spinner-size" = htmltools::validateCssUnit(size),
|
||||
"--shiny-spinner-delay" = delay
|
||||
)
|
||||
|
||||
id <- NULL
|
||||
if (is.null(selector)) {
|
||||
id <- paste0("spinner-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)
|
||||
}
|
||||
|
||||
css_vars <- htmltools::css(
|
||||
"--shiny-pulse-background" = background,
|
||||
"--shiny-pulse-height" = htmltools::validateCssUnit(height),
|
||||
"--shiny-pulse-speed" = speed
|
||||
)
|
||||
|
||||
tags$style(HTML(paste0(":root {", css_vars, "}")))
|
||||
}
|
||||
@@ -180,7 +244,6 @@ busyIndicatorDependency <- function() {
|
||||
version = get_package_version("shiny"),
|
||||
src = "www/shared/busy-indicators",
|
||||
package = "shiny",
|
||||
stylesheet = "busy-indicators.css",
|
||||
script = "busy-indicators.js"
|
||||
stylesheet = "busy-indicators.css"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -128,6 +128,12 @@ in_devmode <- function() {
|
||||
!identical(Sys.getenv("TESTTHAT"), "true")
|
||||
}
|
||||
|
||||
in_client_devmode <- function() {
|
||||
# Client-side devmode enables client-side only dev features without local
|
||||
# devmode. Currently, the main feature is the client-side error console.
|
||||
isTRUE(getOption("shiny.client_devmode", FALSE))
|
||||
}
|
||||
|
||||
#' @describeIn devmode Temporarily set Shiny Developer Mode and Developer
|
||||
#' message verbosity
|
||||
#' @param code Code to execute with the temporary Dev Mode options set
|
||||
|
||||
122
R/graph.R
@@ -1,4 +1,3 @@
|
||||
|
||||
# domain is like session
|
||||
|
||||
|
||||
@@ -20,7 +19,7 @@ reactIdStr <- function(num) {
|
||||
#' dependencies and execution in your application.
|
||||
#'
|
||||
#' To use the reactive log visualizer, start with a fresh R session and
|
||||
#' run the command `options(shiny.reactlog=TRUE)`; then launch your
|
||||
#' run the command `reactlog::reactlog_enable()`; then launch your
|
||||
#' application in the usual way (e.g. using [runApp()]). At
|
||||
#' any time you can hit Ctrl+F3 (or for Mac users, Command+F3) in your
|
||||
#' web browser to launch the reactive log visualization.
|
||||
@@ -43,16 +42,20 @@ reactIdStr <- function(num) {
|
||||
#' call `reactlogShow()` explicitly.
|
||||
#'
|
||||
#' For security and performance reasons, do not enable
|
||||
#' `shiny.reactlog` in production environments. When the option is
|
||||
#' enabled, it's possible for any user of your app to see at least some
|
||||
#' of the source code of your reactive expressions and observers.
|
||||
#' `options(shiny.reactlog=TRUE)` (or `reactlog::reactlog_enable()`) in
|
||||
#' production environments. When the option is enabled, it's possible
|
||||
#' for any user of your app to see at least some of the source code of
|
||||
#' your reactive expressions and observers. In addition, reactlog
|
||||
#' should be considered a memory leak as it will constantly grow and
|
||||
#' will never reset until the R session is restarted.
|
||||
#'
|
||||
#' @name reactlog
|
||||
NULL
|
||||
|
||||
|
||||
#' @describeIn reactlog Return a list of reactive information. Can be used in conjunction with
|
||||
#' [reactlog::reactlog_show] to later display the reactlog graph.
|
||||
#' @describeIn reactlog Return a list of reactive information. Can be used in
|
||||
#' conjunction with [reactlog::reactlog_show] to later display the reactlog
|
||||
#' graph.
|
||||
#' @export
|
||||
reactlog <- function() {
|
||||
rLog$asList()
|
||||
@@ -67,12 +70,34 @@ reactlogShow <- function(time = TRUE) {
|
||||
reactlog::reactlog_show(reactlog(), time = time)
|
||||
}
|
||||
|
||||
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
|
||||
#' @describeIn reactlog Resets the entire reactlog stack. Useful for debugging
|
||||
#' and removing all prior reactive history.
|
||||
#' @export
|
||||
reactlogReset <- function() {
|
||||
rLog$reset()
|
||||
}
|
||||
|
||||
#' @describeIn reactlog Adds "mark" entry into the reactlog stack. This is
|
||||
#' useful for programmatically adding a marked entry in the reactlog, rather
|
||||
#' than using your keyboard's key combination.
|
||||
#'
|
||||
#' For example, we can _mark_ the reactlog at the beginning of an
|
||||
#' `observeEvent`'s calculation:
|
||||
#' ```r
|
||||
#' observeEvent(input$my_event_trigger, {
|
||||
#' # Add a mark in the reactlog
|
||||
#' reactlogAddMark()
|
||||
#' # Run your regular event reaction code here...
|
||||
#' ....
|
||||
#' })
|
||||
#' ```
|
||||
#' @param session The Shiny session to assign the mark to. Defaults to the
|
||||
#' current session.
|
||||
#' @export
|
||||
reactlogAddMark <- function(session = getDefaultReactiveDomain()) {
|
||||
rLog$userMark(session)
|
||||
}
|
||||
|
||||
# called in "/reactlog" middleware
|
||||
renderReactlog <- function(sessionToken = NULL, time = TRUE) {
|
||||
check_reactlog()
|
||||
@@ -98,7 +123,6 @@ RLog <- R6Class(
|
||||
private = list(
|
||||
option = "shiny.reactlog",
|
||||
msgOption = "shiny.reactlog.console",
|
||||
|
||||
appendEntry = function(domain, logEntry) {
|
||||
if (self$isLogging()) {
|
||||
sessionToken <- if (is.null(domain)) NULL else domain$token
|
||||
@@ -113,20 +137,19 @@ RLog <- R6Class(
|
||||
public = list(
|
||||
msg = "<MessageLogger>",
|
||||
logStack = "<Stack>",
|
||||
|
||||
noReactIdLabel = "NoCtxReactId",
|
||||
noReactId = reactIdStr("NoCtxReactId"),
|
||||
dummyReactIdLabel = "DummyReactId",
|
||||
dummyReactId = reactIdStr("DummyReactId"),
|
||||
|
||||
asList = function() {
|
||||
ret <- self$logStack$as_list()
|
||||
attr(ret, "version") <- "1"
|
||||
ret
|
||||
},
|
||||
|
||||
ctxIdStr = function(ctxId) {
|
||||
if (is.null(ctxId) || identical(ctxId, "")) return(NULL)
|
||||
if (is.null(ctxId) || identical(ctxId, "")) {
|
||||
return(NULL)
|
||||
}
|
||||
paste0("ctx", ctxId)
|
||||
},
|
||||
namesIdStr = function(reactId) {
|
||||
@@ -141,7 +164,6 @@ RLog <- R6Class(
|
||||
keyIdStr = function(reactId, key) {
|
||||
paste0(reactId, "$", key)
|
||||
},
|
||||
|
||||
valueStr = function(value, n = 200) {
|
||||
if (!self$isLogging()) {
|
||||
# return a placeholder string to avoid calling str
|
||||
@@ -151,10 +173,9 @@ RLog <- R6Class(
|
||||
# only capture the first level of the object
|
||||
utils::capture.output(utils::str(value, max.level = 1))
|
||||
})
|
||||
outputTxt <- paste0(output, collapse="\n")
|
||||
outputTxt <- paste0(output, collapse = "\n")
|
||||
msg$shortenString(outputTxt, n = n)
|
||||
},
|
||||
|
||||
initialize = function(rlogOption = "shiny.reactlog", msgOption = "shiny.reactlog.console") {
|
||||
private$option <- rlogOption
|
||||
private$msgOption <- msgOption
|
||||
@@ -174,7 +195,6 @@ RLog <- R6Class(
|
||||
isLogging = function() {
|
||||
isTRUE(getOption(private$option, FALSE))
|
||||
},
|
||||
|
||||
define = function(reactId, value, label, type, domain) {
|
||||
valueStr <- self$valueStr(value)
|
||||
if (msg$hasReact(reactId)) {
|
||||
@@ -205,9 +225,10 @@ RLog <- R6Class(
|
||||
defineObserver = function(reactId, label, domain) {
|
||||
self$define(reactId, value = NULL, label, "observer", domain)
|
||||
},
|
||||
|
||||
dependsOn = function(reactId, depOnReactId, ctxId, domain) {
|
||||
if (is.null(reactId)) return()
|
||||
if (is.null(reactId)) {
|
||||
return()
|
||||
}
|
||||
ctxId <- ctxIdStr(ctxId)
|
||||
msg$log("dependsOn:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
|
||||
private$appendEntry(domain, list(
|
||||
@@ -220,7 +241,6 @@ RLog <- R6Class(
|
||||
dependsOnKey = function(reactId, depOnReactId, key, ctxId, domain) {
|
||||
self$dependsOn(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
|
||||
},
|
||||
|
||||
dependsOnRemove = function(reactId, depOnReactId, ctxId, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
msg$log("dependsOnRemove:", msg$reactStr(reactId), " on", msg$reactStr(depOnReactId), msg$ctxStr(ctxId))
|
||||
@@ -234,7 +254,6 @@ RLog <- R6Class(
|
||||
dependsOnKeyRemove = function(reactId, depOnReactId, key, ctxId, domain) {
|
||||
self$dependsOnRemove(reactId, self$keyIdStr(depOnReactId, key), ctxId, domain)
|
||||
},
|
||||
|
||||
createContext = function(ctxId, label, type, prevCtxId, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
prevCtxId <- self$ctxIdStr(prevCtxId)
|
||||
@@ -245,10 +264,9 @@ RLog <- R6Class(
|
||||
label = msg$shortenString(label),
|
||||
type = type,
|
||||
prevCtxId = prevCtxId,
|
||||
srcref = as.vector(attr(label, "srcref")), srcfile=attr(label, "srcfile")
|
||||
srcref = as.vector(attr(label, "srcref")), srcfile = attr(label, "srcfile")
|
||||
))
|
||||
},
|
||||
|
||||
enter = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
@@ -291,7 +309,6 @@ RLog <- R6Class(
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
valueChange = function(reactId, value, domain) {
|
||||
valueStr <- self$valueStr(value)
|
||||
msg$log("valueChange:", msg$reactStr(reactId), msg$valueStr(valueStr))
|
||||
@@ -313,8 +330,6 @@ RLog <- R6Class(
|
||||
valueChangeKey = function(reactId, key, value, domain) {
|
||||
self$valueChange(self$keyIdStr(reactId, key), value, domain)
|
||||
},
|
||||
|
||||
|
||||
invalidateStart = function(reactId, ctxId, type, domain) {
|
||||
ctxId <- self$ctxIdStr(ctxId)
|
||||
if (identical(type, "isolate")) {
|
||||
@@ -357,7 +372,6 @@ RLog <- R6Class(
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
invalidateLater = function(reactId, runningCtx, millis, domain) {
|
||||
msg$log("invalidateLater: ", millis, "ms", msg$reactStr(reactId), msg$ctxStr(runningCtx))
|
||||
private$appendEntry(domain, list(
|
||||
@@ -367,14 +381,12 @@ RLog <- R6Class(
|
||||
millis = millis
|
||||
))
|
||||
},
|
||||
|
||||
idle = function(domain = NULL) {
|
||||
msg$log("idle")
|
||||
private$appendEntry(domain, list(
|
||||
action = "idle"
|
||||
))
|
||||
},
|
||||
|
||||
asyncStart = function(domain = NULL) {
|
||||
msg$log("asyncStart")
|
||||
private$appendEntry(domain, list(
|
||||
@@ -387,7 +399,6 @@ RLog <- R6Class(
|
||||
action = "asyncStop"
|
||||
))
|
||||
},
|
||||
|
||||
freezeReactiveVal = function(reactId, domain) {
|
||||
msg$log("freeze:", msg$reactStr(reactId))
|
||||
private$appendEntry(domain, list(
|
||||
@@ -398,7 +409,6 @@ RLog <- R6Class(
|
||||
freezeReactiveKey = function(reactId, key, domain) {
|
||||
self$freezeReactiveVal(self$keyIdStr(reactId, key), domain)
|
||||
},
|
||||
|
||||
thawReactiveVal = function(reactId, domain) {
|
||||
msg$log("thaw:", msg$reactStr(reactId))
|
||||
private$appendEntry(domain, list(
|
||||
@@ -409,54 +419,60 @@ RLog <- R6Class(
|
||||
thawReactiveKey = function(reactId, key, domain) {
|
||||
self$thawReactiveVal(self$keyIdStr(reactId, key), domain)
|
||||
},
|
||||
|
||||
userMark = function(domain = NULL) {
|
||||
msg$log("userMark")
|
||||
private$appendEntry(domain, list(
|
||||
action = "userMark"
|
||||
))
|
||||
}
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
MessageLogger = R6Class(
|
||||
MessageLogger <- R6Class(
|
||||
"MessageLogger",
|
||||
portable = FALSE,
|
||||
public = list(
|
||||
depth = 0L,
|
||||
reactCache = list(),
|
||||
option = "shiny.reactlog.console",
|
||||
|
||||
initialize = function(option = "shiny.reactlog.console", depth = 0L) {
|
||||
if (!missing(depth)) self$depth <- depth
|
||||
if (!missing(option)) self$option <- option
|
||||
},
|
||||
|
||||
isLogging = function() {
|
||||
isTRUE(getOption(self$option))
|
||||
},
|
||||
isNotLogging = function() {
|
||||
! isTRUE(getOption(self$option))
|
||||
!isTRUE(getOption(self$option))
|
||||
},
|
||||
depthIncrement = function() {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$depth <- self$depth + 1L
|
||||
},
|
||||
depthDecrement = function() {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$depth <- self$depth - 1L
|
||||
},
|
||||
hasReact = function(reactId) {
|
||||
if (self$isNotLogging()) return(FALSE)
|
||||
if (self$isNotLogging()) {
|
||||
return(FALSE)
|
||||
}
|
||||
!is.null(self$getReact(reactId))
|
||||
},
|
||||
getReact = function(reactId, force = FALSE) {
|
||||
if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
|
||||
if (identical(force, FALSE) && self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$reactCache[[reactId]]
|
||||
},
|
||||
setReact = function(reactObj, force = FALSE) {
|
||||
if (identical(force, FALSE) && self$isNotLogging()) return(NULL)
|
||||
if (identical(force, FALSE) && self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$reactCache[[reactObj$reactId]] <- reactObj
|
||||
},
|
||||
shortenString = function(txt, n = 250) {
|
||||
@@ -475,13 +491,17 @@ MessageLogger = R6Class(
|
||||
},
|
||||
valueStr = function(valueStr) {
|
||||
paste0(
|
||||
" '", self$shortenString(self$singleLine(valueStr)), "'"
|
||||
" '", self$shortenString(self$singleLine(valueStr)), "'"
|
||||
)
|
||||
},
|
||||
reactStr = function(reactId) {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
reactInfo <- self$getReact(reactId)
|
||||
if (is.null(reactInfo)) return(" <UNKNOWN_REACTID>")
|
||||
if (is.null(reactInfo)) {
|
||||
return(" <UNKNOWN_REACTID>")
|
||||
}
|
||||
paste0(
|
||||
" ", reactInfo$reactId, ":'", self$shortenString(self$singleLine(reactInfo$label)), "'"
|
||||
)
|
||||
@@ -490,11 +510,15 @@ MessageLogger = R6Class(
|
||||
self$ctxStr(ctxId = NULL, type = type)
|
||||
},
|
||||
ctxStr = function(ctxId = NULL, type = NULL) {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
self$ctxPrevCtxStr(ctxId = ctxId, prevCtxId = NULL, type = type)
|
||||
},
|
||||
ctxPrevCtxStr = function(ctxId = NULL, prevCtxId = NULL, type = NULL, preCtxIdTxt = " in ") {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
paste0(
|
||||
if (!is.null(ctxId)) paste0(preCtxIdTxt, ctxId),
|
||||
if (!is.null(prevCtxId)) paste0(" from ", prevCtxId),
|
||||
@@ -502,7 +526,9 @@ MessageLogger = R6Class(
|
||||
)
|
||||
},
|
||||
log = function(...) {
|
||||
if (self$isNotLogging()) return(NULL)
|
||||
if (self$isNotLogging()) {
|
||||
return(NULL)
|
||||
}
|
||||
msg <- paste0(
|
||||
paste0(rep("= ", depth), collapse = ""), "- ", paste0(..., collapse = ""),
|
||||
collapse = ""
|
||||
|
||||
@@ -153,6 +153,12 @@ datePickerDependency <- function(theme) {
|
||||
)
|
||||
}
|
||||
|
||||
datePickerSass <- function() {
|
||||
sass::sass_file(
|
||||
system_file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
|
||||
)
|
||||
}
|
||||
|
||||
datePickerCSS <- function(theme) {
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(htmlDependency(
|
||||
@@ -164,10 +170,8 @@ datePickerCSS <- function(theme) {
|
||||
))
|
||||
}
|
||||
|
||||
scss_file <- system_file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
|
||||
|
||||
bslib::bs_dependency(
|
||||
input = sass::sass_file(scss_file),
|
||||
input = datePickerSass(),
|
||||
theme = theme,
|
||||
name = "bootstrap-datepicker",
|
||||
version = version_bs_date_picker,
|
||||
|
||||
@@ -241,11 +241,8 @@ selectizeDependencyFunc <- function(theme) {
|
||||
return(selectizeStaticDependency(version_selectize))
|
||||
}
|
||||
|
||||
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/")
|
||||
bs_version <- bslib::theme_version(theme)
|
||||
stylesheet <- file.path(
|
||||
selectizeDir, "scss", paste0("selectize.bootstrap", bs_version, ".scss")
|
||||
)
|
||||
|
||||
# It'd be cleaner to ship the JS in a separate, href-based,
|
||||
# HTML dependency (which we currently do for other themable widgets),
|
||||
# but DT, crosstalk, and maybe other pkgs include selectize JS/CSS
|
||||
@@ -253,10 +250,11 @@ selectizeDependencyFunc <- function(theme) {
|
||||
# name, the JS/CSS would be loaded/included twice, which leads to
|
||||
# strange issues, especially since we now include a 3rd party
|
||||
# accessibility plugin https://github.com/rstudio/shiny/pull/3153
|
||||
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/")
|
||||
script <- file.path(selectizeDir, selectizeScripts())
|
||||
|
||||
bslib::bs_dependency(
|
||||
input = sass::sass_file(stylesheet),
|
||||
input = selectizeSass(bs_version),
|
||||
theme = theme,
|
||||
name = "selectize",
|
||||
version = version_selectize,
|
||||
@@ -265,6 +263,14 @@ selectizeDependencyFunc <- function(theme) {
|
||||
)
|
||||
}
|
||||
|
||||
selectizeSass <- function(bs_version) {
|
||||
selectizeDir <- system_file(package = "shiny", "www/shared/selectize/")
|
||||
stylesheet <- file.path(
|
||||
selectizeDir, "scss", paste0("selectize.bootstrap", bs_version, ".scss")
|
||||
)
|
||||
sass::sass_file(stylesheet)
|
||||
}
|
||||
|
||||
selectizeStaticDependency <- function(version) {
|
||||
htmlDependency(
|
||||
"selectize",
|
||||
|
||||
@@ -222,6 +222,15 @@ ionRangeSliderDependency <- function() {
|
||||
)
|
||||
}
|
||||
|
||||
ionRangeSliderDependencySass <- function() {
|
||||
list(
|
||||
list(accent = "$component-active-bg"),
|
||||
sass::sass_file(
|
||||
system_file(package = "shiny", "www/shared/ionrangeslider/scss/shiny.scss")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ionRangeSliderDependencyCSS <- function(theme) {
|
||||
if (!is_bs_theme(theme)) {
|
||||
return(htmlDependency(
|
||||
@@ -234,12 +243,7 @@ ionRangeSliderDependencyCSS <- function(theme) {
|
||||
}
|
||||
|
||||
bslib::bs_dependency(
|
||||
input = list(
|
||||
list(accent = "$component-active-bg"),
|
||||
sass::sass_file(
|
||||
system_file(package = "shiny", "www/shared/ionrangeslider/scss/shiny.scss")
|
||||
)
|
||||
),
|
||||
input = ionRangeSliderDependencySass(),
|
||||
theme = theme,
|
||||
name = "ionRangeSlider",
|
||||
version = version_ion_range_slider,
|
||||
|
||||
@@ -151,6 +151,11 @@ getShinyOption <- function(name, default = NULL) {
|
||||
# ' \item{shiny.devmode.verbose (defaults to `TRUE`)}{If `TRUE`, will display messages printed
|
||||
# ' about which options are being set. See [devmode()] for more details. }
|
||||
### (end not documenting 'shiny.devmode.verbose')
|
||||
### start shiny.client_devmode is primarily for niche, internal shinylive usage
|
||||
# ' \item{shiny.client_devmode (defaults to `FALSE`)}{If `TRUE`, enables client-
|
||||
# ' side devmode features. Currently the primary feature is the client-side
|
||||
# ' error console.}
|
||||
### end shiny.client_devmode
|
||||
#' }
|
||||
#'
|
||||
#'
|
||||
|
||||
17
R/shinyui.R
@@ -69,7 +69,7 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
|
||||
)
|
||||
}
|
||||
|
||||
if (in_devmode()) {
|
||||
if (in_devmode() || in_client_devmode()) {
|
||||
# If we're in dev mode, add a simple script to the head that injects a
|
||||
# global variable for the client to use to detect dev mode.
|
||||
shiny_deps[[length(shiny_deps) + 1]] <-
|
||||
@@ -135,6 +135,14 @@ shinyDependencies <- function() {
|
||||
)
|
||||
}
|
||||
|
||||
shinyDependencySass <- function(bs_version) {
|
||||
bootstrap_scss <- paste0("shiny.bootstrap", bs_version, ".scss")
|
||||
|
||||
scss_home <- system_file("www/shared/shiny_scss", package = "shiny")
|
||||
scss_files <- file.path(scss_home, c(bootstrap_scss, "shiny.scss"))
|
||||
lapply(scss_files, sass::sass_file)
|
||||
}
|
||||
|
||||
shinyDependencyCSS <- function(theme) {
|
||||
version <- get_package_version("shiny")
|
||||
|
||||
@@ -150,14 +158,9 @@ shinyDependencyCSS <- function(theme) {
|
||||
}
|
||||
|
||||
bs_version <- bslib::theme_version(theme)
|
||||
bootstrap_scss <- paste0("shiny.bootstrap", bs_version, ".scss")
|
||||
|
||||
scss_home <- system_file("www/shared/shiny_scss", package = "shiny")
|
||||
scss_files <- file.path(scss_home, c(bootstrap_scss, "shiny.scss"))
|
||||
scss_files <- lapply(scss_files, sass::sass_file)
|
||||
|
||||
bslib::bs_dependency(
|
||||
input = scss_files,
|
||||
input = shinyDependencySass(bs_version),
|
||||
theme = theme,
|
||||
name = "shiny-sass",
|
||||
version = version,
|
||||
|
||||
154
inst/diagrams/outputProgressStateMachine.drawio
Normal file
@@ -0,0 +1,154 @@
|
||||
<mxfile host="app.diagrams.net" modified="2024-05-07T22:40:15.581Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" etag="Zsitjb4PT-sW3A63SWd7" version="24.3.1" type="device">
|
||||
<diagram name="Page-1" id="zz6aoPEyabkTD7ESu8ts">
|
||||
<mxGraphModel dx="595" dy="889" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-1" value="Initial" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="120" y="270" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-2" value="Running" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="270" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-3" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-1" target="DS1AFzV_2DL1v2c9v1jZ-2" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="260" y="480" as="sourcePoint" />
|
||||
<mxPoint x="310" y="270" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-4" value="Recalculating" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="210" y="250" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-6" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-2" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="320" y="220" as="sourcePoint" />
|
||||
<mxPoint x="310" y="350" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-7" value="Idle" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="350" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-8" value="Recalculated" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="330" y="310" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-9" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-7" target="DS1AFzV_2DL1v2c9v1jZ-10" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="320" y="320" as="sourcePoint" />
|
||||
<mxPoint x="310" y="440" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="320" y="410" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-10" value="Value" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="280" y="440" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-11" value="Error" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="370" y="440" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-12" value="Persistent" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="440" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-13" value="Cancel" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="180" y="440" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-14" value="<span style="text-align: start; font-size: 10pt; font-family: Arial;" data-sheets-userformat="{&quot;2&quot;:513,&quot;3&quot;:{&quot;1&quot;:0},&quot;12&quot;:0}" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;{progress: {type: \&quot;binding\&quot;, message: {persistent: true}}}&quot;}" data-sheets-root="1">{progress: {type: "binding", message: {persistent: true}}}</span>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="45" y="340" width="170" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-15" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-10" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="320" y="400" as="sourcePoint" />
|
||||
<mxPoint x="310" y="550" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="320" y="520" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-16" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-11" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="320" y="490" as="sourcePoint" />
|
||||
<mxPoint x="320" y="550" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-17" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-12" target="DS1AFzV_2DL1v2c9v1jZ-18" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="330" y="500" as="sourcePoint" />
|
||||
<mxPoint x="290" y="540" as="targetPoint" />
|
||||
<Array as="points" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-18" value="Invalidated" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="260" y="550" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-20" value="" style="curved=1;endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-18" target="DS1AFzV_2DL1v2c9v1jZ-2" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="260" y="480" as="sourcePoint" />
|
||||
<mxPoint x="310" y="430" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="420" y="610" />
|
||||
<mxPoint x="550" y="470" />
|
||||
<mxPoint x="440" y="320" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-23" value="Recalculating" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="450" y="340" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-24" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-2" target="DS1AFzV_2DL1v2c9v1jZ-12" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="320" y="400" as="sourcePoint" />
|
||||
<mxPoint x="320" y="450" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-25" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0.395;entryY=-0.025;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-7" target="DS1AFzV_2DL1v2c9v1jZ-11" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="330" y="410" as="sourcePoint" />
|
||||
<mxPoint x="330" y="460" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="380" y="410" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-26" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-7" target="DS1AFzV_2DL1v2c9v1jZ-13" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="340" y="420" as="sourcePoint" />
|
||||
<mxPoint x="340" y="470" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-27" value="Value" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="400" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-28" value="Error" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="330" y="400" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-29" value="No message" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="200" y="400" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-30" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0;entryDx=0;entryDy=0;" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-13" target="DS1AFzV_2DL1v2c9v1jZ-18" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="230" y="490" as="sourcePoint" />
|
||||
<mxPoint x="300" y="558" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="240" y="520" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-31" value="<span style="font-family: Arial; font-size: 13px; text-align: left; white-space: pre-wrap; background-color: rgb(255, 255, 255);">{progress: {type: "binding"}}</span>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
|
||||
<mxGeometry x="190" y="490" width="180" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="DS1AFzV_2DL1v2c9v1jZ-35" value="<h1 style="margin-top: 0px;">Shiny output progress states</h1><p>This diagram depicts a state machine of output binding progress state. Each node represents a possible state and each edge represents a server-&gt;client message that moves outputs from one state to another. <b>If a node is highlighted in blue</b>, then the output should be showing a busy state when visible (i.e., <font face="Courier New">binding.showProgress(true)</font>)</p>" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="85" y="120" width="465" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="J9lKobNiy15ndT9nfcn--1" value="" style="curved=1;endArrow=classic;html=1;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="DS1AFzV_2DL1v2c9v1jZ-7" target="DS1AFzV_2DL1v2c9v1jZ-18">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="280" y="480" as="sourcePoint" />
|
||||
<mxPoint x="220" y="510" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="610" y="420" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
4
inst/diagrams/outputProgressStateMachine.svg
Normal file
|
After Width: | Height: | Size: 312 KiB |
@@ -1,3 +0,0 @@
|
||||
/*! shiny 1.8.1.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
|
||||
"use strict";(function(){document.documentElement.classList.add("shiny-not-yet-idle");$(document).one("shiny:idle",function(){document.documentElement.classList.remove("shiny-not-yet-idle")});})();
|
||||
//# sourceMappingURL=busy-indicators.js.map
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../../../srcts/extras/busy-indicators/busy-indicators.ts"],
|
||||
"sourcesContent": ["// This of this like the .shiny-busy class that shiny.js puts on the root element,\n// except it's added before shiny.js is initialized, connected, etc.\n// TODO: maybe shiny.js should be doing something like this?\ndocument.documentElement.classList.add(\"shiny-not-yet-idle\");\n$(document).one(\"shiny:idle\", function () {\n document.documentElement.classList.remove(\"shiny-not-yet-idle\");\n});"],
|
||||
"mappings": ";yBAGA,SAAS,gBAAgB,UAAU,IAAI,oBAAoB,EAC3D,EAAE,QAAQ,EAAE,IAAI,aAAc,UAAY,CACxC,SAAS,gBAAgB,UAAU,OAAO,oBAAoB,CAChE,CAAC",
|
||||
"names": []
|
||||
}
|
||||
20
inst/www/shared/busy-indicators/spinners/LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Utkarsh Verma
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
inst/www/shared/busy-indicators/spinners/bars.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_hzlK{animation:spinner_vc4H .8s linear infinite;animation-delay:-.8s}.spinner_koGT{animation-delay:-.65s}.spinner_YF1u{animation-delay:-.5s}@keyframes spinner_vc4H{0%{y:1px;height:22px}93.75%{y:5px;height:14px;opacity:.2}}</style><rect class="spinner_hzlK" x="1" y="1" width="6" height="22"/><rect class="spinner_hzlK spinner_koGT" x="9" y="1" width="6" height="22"/><rect class="spinner_hzlK spinner_YF1u" x="17" y="1" width="6" height="22"/></svg>
|
||||
|
After Width: | Height: | Size: 526 B |
1
inst/www/shared/busy-indicators/spinners/bars2.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_jCIR{animation:spinner_B8Vq .9s linear infinite;animation-delay:-.9s}.spinner_upm8{animation-delay:-.8s}.spinner_2eL5{animation-delay:-.7s}.spinner_Rp9l{animation-delay:-.6s}.spinner_dy3W{animation-delay:-.5s}@keyframes spinner_B8Vq{0%,66.66%{animation-timing-function:cubic-bezier(0.36,.61,.3,.98);y:6px;height:12px}33.33%{animation-timing-function:cubic-bezier(0.36,.61,.3,.98);y:1px;height:22px}}</style><rect class="spinner_jCIR" x="1" y="6" width="2.8" height="12"/><rect class="spinner_jCIR spinner_upm8" x="5.8" y="6" width="2.8" height="12"/><rect class="spinner_jCIR spinner_2eL5" x="10.6" y="6" width="2.8" height="12"/><rect class="spinner_jCIR spinner_Rp9l" x="15.4" y="6" width="2.8" height="12"/><rect class="spinner_jCIR spinner_dy3W" x="20.2" y="6" width="2.8" height="12"/></svg>
|
||||
|
After Width: | Height: | Size: 873 B |
1
inst/www/shared/busy-indicators/spinners/bars3.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_OSmW{transform-origin:center;animation:spinner_T6mA .75s step-end infinite}@keyframes spinner_T6mA{8.3%{transform:rotate(30deg)}16.6%{transform:rotate(60deg)}25%{transform:rotate(90deg)}33.3%{transform:rotate(120deg)}41.6%{transform:rotate(150deg)}50%{transform:rotate(180deg)}58.3%{transform:rotate(210deg)}66.6%{transform:rotate(240deg)}75%{transform:rotate(270deg)}83.3%{transform:rotate(300deg)}91.6%{transform:rotate(330deg)}100%{transform:rotate(360deg)}}</style><g class="spinner_OSmW"><rect x="11" y="1" width="2" height="5" opacity=".14"/><rect x="11" y="1" width="2" height="5" transform="rotate(30 12 12)" opacity=".29"/><rect x="11" y="1" width="2" height="5" transform="rotate(60 12 12)" opacity=".43"/><rect x="11" y="1" width="2" height="5" transform="rotate(90 12 12)" opacity=".57"/><rect x="11" y="1" width="2" height="5" transform="rotate(120 12 12)" opacity=".71"/><rect x="11" y="1" width="2" height="5" transform="rotate(150 12 12)" opacity=".86"/><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
inst/www/shared/busy-indicators/spinners/dots.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_b2T7{animation:spinner_xe7Q .8s linear infinite}.spinner_YRVV{animation-delay:-.65s}.spinner_c9oY{animation-delay:-.5s}@keyframes spinner_xe7Q{93.75%,100%{r:3px}46.875%{r:.2px}}</style><circle class="spinner_b2T7" cx="4" cy="12" r="3"/><circle class="spinner_b2T7 spinner_YRVV" cx="12" cy="12" r="3"/><circle class="spinner_b2T7 spinner_c9oY" cx="20" cy="12" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 449 B |
1
inst/www/shared/busy-indicators/spinners/dots2.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_Wezc{transform-origin:center;animation:spinner_Oiah .75s step-end infinite}@keyframes spinner_Oiah{8.3%{transform:rotate(30deg)}16.6%{transform:rotate(60deg)}25%{transform:rotate(90deg)}33.3%{transform:rotate(120deg)}41.6%{transform:rotate(150deg)}50%{transform:rotate(180deg)}58.3%{transform:rotate(210deg)}66.6%{transform:rotate(240deg)}75%{transform:rotate(270deg)}83.3%{transform:rotate(300deg)}91.6%{transform:rotate(330deg)}100%{transform:rotate(360deg)}}</style><g class="spinner_Wezc"><circle cx="12" cy="2.5" r="1.5" opacity=".14"/><circle cx="16.75" cy="3.77" r="1.5" opacity=".29"/><circle cx="20.23" cy="7.25" r="1.5" opacity=".43"/><circle cx="21.50" cy="12.00" r="1.5" opacity=".57"/><circle cx="20.23" cy="16.75" r="1.5" opacity=".71"/><circle cx="16.75" cy="20.23" r="1.5" opacity=".86"/><circle cx="12" cy="21.5" r="1.5"/></g></svg>
|
||||
|
After Width: | Height: | Size: 926 B |
1
inst/www/shared/busy-indicators/spinners/dots3.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_DupU{animation:spinner_sM3D 1.2s infinite}.spinner_GWtZ{animation-delay:.1s}.spinner_dwN6{animation-delay:.2s}.spinner_46QP{animation-delay:.3s}.spinner_PD82{animation-delay:.4s}.spinner_eUgh{animation-delay:.5s}.spinner_eUaP{animation-delay:.6s}.spinner_j38H{animation-delay:.7s}.spinner_tVmX{animation-delay:.8s}.spinner_DQhX{animation-delay:.9s}.spinner_GIL4{animation-delay:1s}.spinner_n0Yb{animation-delay:1.1s}@keyframes spinner_sM3D{0%,50%{animation-timing-function:cubic-bezier(0,1,0,1);r:0}10%{animation-timing-function:cubic-bezier(.53,0,.61,.73);r:2px}}</style><circle class="spinner_DupU" cx="12" cy="3" r="0"/><circle class="spinner_DupU spinner_GWtZ" cx="16.50" cy="4.21" r="0"/><circle class="spinner_DupU spinner_n0Yb" cx="7.50" cy="4.21" r="0"/><circle class="spinner_DupU spinner_dwN6" cx="19.79" cy="7.50" r="0"/><circle class="spinner_DupU spinner_GIL4" cx="4.21" cy="7.50" r="0"/><circle class="spinner_DupU spinner_46QP" cx="21.00" cy="12.00" r="0"/><circle class="spinner_DupU spinner_DQhX" cx="3.00" cy="12.00" r="0"/><circle class="spinner_DupU spinner_PD82" cx="19.79" cy="16.50" r="0"/><circle class="spinner_DupU spinner_tVmX" cx="4.21" cy="16.50" r="0"/><circle class="spinner_DupU spinner_eUgh" cx="16.50" cy="19.79" r="0"/><circle class="spinner_DupU spinner_j38H" cx="7.50" cy="19.79" r="0"/><circle class="spinner_DupU spinner_eUaP" cx="12" cy="21" r="0"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
inst/www/shared/busy-indicators/spinners/pulse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_ZCsl{animation:spinner_qV4G 1.2s cubic-bezier(0.52,.6,.25,.99) infinite}.spinner_gaIW{animation-delay:.6s}@keyframes spinner_qV4G{0%{r:0;opacity:1}100%{r:11px;opacity:0}}</style><circle class="spinner_ZCsl" cx="12" cy="12" r="0"/><circle class="spinner_ZCsl spinner_gaIW" cx="12" cy="12" r="0"/></svg>
|
||||
|
After Width: | Height: | Size: 378 B |
1
inst/www/shared/busy-indicators/spinners/pulse2.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_ngNb{animation:spinner_ZRWK 1.2s cubic-bezier(0.52,.6,.25,.99) infinite}.spinner_6TBP{animation-delay:.6s}@keyframes spinner_ZRWK{0%{transform:translate(12px,12px) scale(0);opacity:1}100%{transform:translate(0,0) scale(1);opacity:0}}</style><path class="spinner_ngNb" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z" transform="translate(12, 12) scale(0)"/><path class="spinner_ngNb spinner_6TBP" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z" transform="translate(12, 12) scale(0)"/></svg>
|
||||
|
After Width: | Height: | Size: 635 B |
1
inst/www/shared/busy-indicators/spinners/pulse3.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_Uvk8{animation:spinner_otJF 1.6s cubic-bezier(.52,.6,.25,.99) infinite}.spinner_ypeD{animation-delay:.2s}.spinner_y0Rj{animation-delay:.4s}@keyframes spinner_otJF{0%{transform:translate(12px,12px) scale(0);opacity:1}75%,100%{transform:translate(0,0) scale(1);opacity:0}}</style><path class="spinner_Uvk8" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z" transform="translate(12, 12) scale(0)"/><path class="spinner_Uvk8 spinner_ypeD" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z" transform="translate(12, 12) scale(0)"/><path class="spinner_Uvk8 spinner_y0Rj" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z" transform="translate(12, 12) scale(0)"/></svg>
|
||||
|
After Width: | Height: | Size: 834 B |
1
inst/www/shared/busy-indicators/spinners/ring.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg stroke="#000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_V8m1{transform-origin:center;animation:spinner_zKoa 2s linear infinite}.spinner_V8m1 circle{stroke-linecap:round;animation:spinner_YpZS 1.5s ease-in-out infinite}@keyframes spinner_zKoa{100%{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%{stroke-dasharray:42 150;stroke-dashoffset:-16}95%,100%{stroke-dasharray:42 150;stroke-dashoffset:-59}}</style><g class="spinner_V8m1"><circle cx="12" cy="12" r="9.5" fill="none" stroke-width="3"></circle></g></svg>
|
||||
|
After Width: | Height: | Size: 598 B |
1
inst/www/shared/busy-indicators/spinners/ring2.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_ajPY{transform-origin:center;animation:spinner_AtaB .75s infinite linear}@keyframes spinner_AtaB{100%{transform:rotate(360deg)}}</style><path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/><path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" class="spinner_ajPY"/></svg>
|
||||
|
After Width: | Height: | Size: 509 B |
1
inst/www/shared/busy-indicators/spinners/ring3.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_aj0A{transform-origin:center;animation:spinner_KYSC .75s infinite linear}@keyframes spinner_KYSC{100%{transform:rotate(360deg)}}</style><path d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z" class="spinner_aj0A"/></svg>
|
||||
|
After Width: | Height: | Size: 412 B |
@@ -1,2 +1,2 @@
|
||||
/*! shiny 1.8.1.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
|
||||
#showcase-well{border-radius:0}.shiny-code{background-color:#fff;margin-bottom:0}.shiny-code code{font-family:Menlo,Consolas,Courier New,monospace}.shiny-code-container{margin-top:20px;clear:both}.shiny-code-container h3{display:inline;margin-right:15px}.showcase-header{font-size:16px;font-weight:400}.showcase-code-link{text-align:right;padding:15px}#showcase-app-container{vertical-align:top}#showcase-code-tabs{margin-right:15px}#showcase-code-tabs pre{border:none;line-height:1em}#showcase-code-tabs .nav,#showcase-code-tabs ul{margin-bottom:0}#showcase-code-tabs .tab-content{border-style:solid;border-color:#e5e5e5;border-width:0px 1px 1px 1px;overflow:auto;border-bottom-right-radius:4px;border-bottom-left-radius:4px}#showcase-app-code{width:100%}#showcase-code-position-toggle{float:right}#showcase-sxs-code{padding-top:20px;vertical-align:top}.showcase-code-license{display:block;text-align:right}#showcase-code-content pre{background-color:#fff}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/*! shiny 1.8.1.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
|
||||
/*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
|
||||
"use strict";(function(){var a=eval;window.addEventListener("message",function(i){var e=i.data;e.code&&a(e.code)});})();
|
||||
//# sourceMappingURL=shiny-testmode.js.map
|
||||
|
||||
24835
inst/www/shared/shiny.js
4
inst/www/shared/shiny.min.css
vendored
4
inst/www/shared/shiny.min.js
vendored
117
man/busyIndicatorOptions.Rd
Normal file
@@ -0,0 +1,117 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/busy-indicators.R
|
||||
\name{busyIndicatorOptions}
|
||||
\alias{busyIndicatorOptions}
|
||||
\title{Customize busy indicator options}
|
||||
\usage{
|
||||
busyIndicatorOptions(
|
||||
...,
|
||||
spinner_type = NULL,
|
||||
spinner_color = NULL,
|
||||
spinner_size = NULL,
|
||||
spinner_delay = NULL,
|
||||
spinner_selector = NULL,
|
||||
pulse_background = NULL,
|
||||
pulse_height = NULL,
|
||||
pulse_speed = NULL
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{...}{Currently ignored.}
|
||||
|
||||
\item{spinner_type}{The type of spinner. Pre-bundled types include:
|
||||
'ring', 'ring2', 'ring3', 'bars', 'bars2', 'bars3', 'pulse', 'pulse2', 'pulse3', 'dots', 'dots2', 'dots3'.
|
||||
|
||||
A path to a local SVG file can also be provided. The SVG should adhere to
|
||||
the following rules:
|
||||
\itemize{
|
||||
\item The SVG itself should contain the animation.
|
||||
\item It should avoid absolute sizes (the spinner's containing DOM element
|
||||
size is set in CSS by \code{spinner_size}, so it should fill that container).
|
||||
\item It should avoid setting absolute colors (the spinner's containing DOM element
|
||||
color is set in CSS by \code{spinner_color}, so it should inherit that color).
|
||||
}}
|
||||
|
||||
\item{spinner_color}{The color of the spinner. This can be any valid CSS
|
||||
color. Defaults to the app's "primary" color if Bootstrap is on the page.}
|
||||
|
||||
\item{spinner_size}{The size of the spinner. This can be any valid CSS size.}
|
||||
|
||||
\item{spinner_delay}{The amount of time to wait before showing the spinner.
|
||||
This can be any valid CSS time and can be useful for not showing the spinner
|
||||
if the computation finishes quickly.}
|
||||
|
||||
\item{spinner_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}
|
||||
of the theme's indigo, purple, and pink colors.}
|
||||
|
||||
\item{pulse_height}{The height of the pulsing banner. This can be any valid
|
||||
CSS size.}
|
||||
|
||||
\item{pulse_speed}{The speed of the pulsing banner. This can be any valid CSS
|
||||
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.
|
||||
}
|
||||
\examples{
|
||||
\dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
|
||||
|
||||
library(bslib)
|
||||
|
||||
card_ui <- function(id, spinner_type = id) {
|
||||
card(
|
||||
busyIndicatorOptions(spinner_type = spinner_type),
|
||||
card_header(paste("Spinner:", spinner_type)),
|
||||
plotOutput(shiny::NS(id, "plot"))
|
||||
)
|
||||
}
|
||||
|
||||
card_server <- function(id, simulate = reactive()) {
|
||||
moduleServer(
|
||||
id = id,
|
||||
function(input, output, session) {
|
||||
output$plot <- renderPlot({
|
||||
Sys.sleep(1)
|
||||
simulate()
|
||||
plot(x = rnorm(100), y = rnorm(100))
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ui <- page_fillable(
|
||||
useBusyIndicators(),
|
||||
input_task_button("simulate", "Simulate", icon = icon("refresh")),
|
||||
layout_columns(
|
||||
card_ui("ring"),
|
||||
card_ui("bars"),
|
||||
card_ui("dots"),
|
||||
card_ui("pulse"),
|
||||
col_widths = 6
|
||||
)
|
||||
)
|
||||
|
||||
server <- function(input, output, session) {
|
||||
simulate <- reactive(input$simulate)
|
||||
card_server("ring", simulate)
|
||||
card_server("bars", simulate)
|
||||
card_server("dots", simulate)
|
||||
card_server("pulse", simulate)
|
||||
}
|
||||
|
||||
shinyApp(ui, server)
|
||||
\dontshow{\}) # examplesIf}
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=useBusyIndicators]{useBusyIndicators()}} for enabling/disabling busy indicators.
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/busy-indicators.R
|
||||
\name{pulseOptions}
|
||||
\alias{pulseOptions}
|
||||
\title{Customize the pulsing busy indicator.}
|
||||
\usage{
|
||||
pulseOptions(color = NULL, height = NULL, speed = NULL)
|
||||
}
|
||||
\arguments{
|
||||
\item{color}{The color of the pulsing banner. This can be any valid CSS
|
||||
color. Defaults to the app's "primary" color (if Bootstrap is on the page)
|
||||
or light-blue if not.}
|
||||
|
||||
\item{height}{The height of the pulsing banner. This can be any valid CSS
|
||||
size. Defaults to "3.5px".}
|
||||
}
|
||||
\description{
|
||||
Include the result of this function in the app's UI to customize the pulsing
|
||||
banner.
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=useBusyIndicators]{useBusyIndicators()}} \code{\link[=spinnerOptions]{spinnerOptions()}}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
\alias{reactlog}
|
||||
\alias{reactlogShow}
|
||||
\alias{reactlogReset}
|
||||
\alias{reactlogAddMark}
|
||||
\title{Reactive Log Visualizer}
|
||||
\usage{
|
||||
reactlog()
|
||||
@@ -11,10 +12,15 @@ reactlog()
|
||||
reactlogShow(time = TRUE)
|
||||
|
||||
reactlogReset()
|
||||
|
||||
reactlogAddMark(session = getDefaultReactiveDomain())
|
||||
}
|
||||
\arguments{
|
||||
\item{time}{A boolean that specifies whether or not to display the
|
||||
time that each reactive takes to calculate a result.}
|
||||
|
||||
\item{session}{The Shiny session to assign the mark to. Defaults to the
|
||||
current session.}
|
||||
}
|
||||
\description{
|
||||
Provides an interactive browser-based tool for visualizing reactive
|
||||
@@ -22,7 +28,7 @@ dependencies and execution in your application.
|
||||
}
|
||||
\details{
|
||||
To use the reactive log visualizer, start with a fresh R session and
|
||||
run the command \code{options(shiny.reactlog=TRUE)}; then launch your
|
||||
run the command \code{reactlog::reactlog_enable()}; then launch your
|
||||
application in the usual way (e.g. using \code{\link[=runApp]{runApp()}}). At
|
||||
any time you can hit Ctrl+F3 (or for Mac users, Command+F3) in your
|
||||
web browser to launch the reactive log visualization.
|
||||
@@ -45,17 +51,37 @@ browser will not load new activity into the report; you will need to
|
||||
call \code{reactlogShow()} explicitly.
|
||||
|
||||
For security and performance reasons, do not enable
|
||||
\code{shiny.reactlog} in production environments. When the option is
|
||||
enabled, it's possible for any user of your app to see at least some
|
||||
of the source code of your reactive expressions and observers.
|
||||
\code{options(shiny.reactlog=TRUE)} (or \code{reactlog::reactlog_enable()}) in
|
||||
production environments. When the option is enabled, it's possible
|
||||
for any user of your app to see at least some of the source code of
|
||||
your reactive expressions and observers. In addition, reactlog
|
||||
should be considered a memory leak as it will constantly grow and
|
||||
will never reset until the R session is restarted.
|
||||
}
|
||||
\section{Functions}{
|
||||
\itemize{
|
||||
\item \code{reactlog()}: Return a list of reactive information. Can be used in conjunction with
|
||||
\link[reactlog:reactlog_show]{reactlog::reactlog_show} to later display the reactlog graph.
|
||||
\item \code{reactlog()}: Return a list of reactive information. Can be used in
|
||||
conjunction with \link[reactlog:reactlog_show]{reactlog::reactlog_show} to later display the reactlog
|
||||
graph.
|
||||
|
||||
\item \code{reactlogShow()}: Display a full reactlog graph for all sessions.
|
||||
|
||||
\item \code{reactlogReset()}: Resets the entire reactlog stack. Useful for debugging and removing all prior reactive history.
|
||||
\item \code{reactlogReset()}: Resets the entire reactlog stack. Useful for debugging
|
||||
and removing all prior reactive history.
|
||||
|
||||
\item \code{reactlogAddMark()}: Adds "mark" entry into the reactlog stack. This is
|
||||
useful for programmatically adding a marked entry in the reactlog, rather
|
||||
than using your keyboard's key combination.
|
||||
|
||||
For example, we can \emph{mark} the reactlog at the beginning of an
|
||||
\code{observeEvent}'s calculation:
|
||||
|
||||
\if{html}{\out{<div class="sourceCode r">}}\preformatted{observeEvent(input$my_event_trigger, \{
|
||||
# Add a mark in the reactlog
|
||||
reactlogAddMark()
|
||||
# Run your regular event reaction code here...
|
||||
....
|
||||
\})
|
||||
}\if{html}{\out{</div>}}
|
||||
|
||||
}}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/busy-indicators.R
|
||||
\name{spinnerOptions}
|
||||
\alias{spinnerOptions}
|
||||
\title{Customize spinning busy indicators.}
|
||||
\usage{
|
||||
spinnerOptions(
|
||||
type = NULL,
|
||||
...,
|
||||
color = NULL,
|
||||
size = NULL,
|
||||
easing = NULL,
|
||||
speed = NULL,
|
||||
delay = NULL,
|
||||
css_selector = ":root"
|
||||
)
|
||||
}
|
||||
\arguments{
|
||||
\item{type}{The type of spinner to use. Builtin options include: tadpole,
|
||||
disc, dots, dot-track, and bounce. A custom type may also provided, which
|
||||
should be a valid value for the CSS
|
||||
\href{https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image}{mask-image}
|
||||
property.}
|
||||
|
||||
\item{color}{The color of the spinner. This can be any valid CSS color.
|
||||
Defaults to the app's "primary" color (if Bootstrap is on the page) or
|
||||
light-blue if not.}
|
||||
|
||||
\item{size}{The size of the spinner. This can be any valid CSS size.}
|
||||
|
||||
\item{easing}{The easing function to use for the spinner animation. This can
|
||||
be any valid CSS \href{https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function}{easing function}.}
|
||||
|
||||
\item{speed}{The amount of time for the spinner to complete a single
|
||||
revolution. This can be any valid CSS time.}
|
||||
|
||||
\item{delay}{The amount of time to wait before showing the spinner. This can
|
||||
be any valid CSS time and can useful for not showing the spinner
|
||||
if the computation finishes quickly.}
|
||||
|
||||
\item{css_selector}{A CSS selector for scoping the spinner customization.
|
||||
Defaults to the root element.}
|
||||
}
|
||||
\description{
|
||||
Include the result of this function in the app's UI to customize spinner
|
||||
appearance.
|
||||
}
|
||||
\details{
|
||||
To effectively disable spinners, set the \code{size} to "0px".
|
||||
}
|
||||
\examples{
|
||||
\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
|
||||
|
||||
library(bslib)
|
||||
|
||||
ui <- page_fillable(
|
||||
useBusyIndicators(),
|
||||
spinnerOptions(color = "orange"),
|
||||
card(
|
||||
card_header(
|
||||
"A plot",
|
||||
input_task_button("simulate", "Simulate"),
|
||||
class = "d-flex justify-content-between align-items-center"
|
||||
),
|
||||
plotOutput("p"),
|
||||
)
|
||||
)
|
||||
|
||||
server <- function(input, output) {
|
||||
output$p <- renderPlot({
|
||||
input$simulate
|
||||
Sys.sleep(4)
|
||||
plot(x = rnorm(100), y = rnorm(100))
|
||||
})
|
||||
}
|
||||
|
||||
shinyApp(ui, server)
|
||||
\dontshow{\}) # examplesIf}
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=useBusyIndicators]{useBusyIndicators()}} \code{\link[=pulseOptions]{pulseOptions()}}
|
||||
}
|
||||
@@ -2,26 +2,37 @@
|
||||
% Please edit documentation in R/busy-indicators.R
|
||||
\name{useBusyIndicators}
|
||||
\alias{useBusyIndicators}
|
||||
\title{Use and customize busy indicator types.}
|
||||
\title{Enable/disable busy indication}
|
||||
\usage{
|
||||
useBusyIndicators(spinners = TRUE, pulse = TRUE)
|
||||
useBusyIndicators(..., spinners = TRUE, pulse = TRUE)
|
||||
}
|
||||
\arguments{
|
||||
\item{spinners}{Overlay a spinner on each calculating/recalculating output.}
|
||||
\item{...}{Currently ignored.}
|
||||
|
||||
\item{pulse}{Show a pulsing banner at the top of the window when the server is busy.}
|
||||
\item{spinners}{Whether to show a spinner on each calculating/recalculating
|
||||
output.}
|
||||
|
||||
\item{pulse}{Whether to show a pulsing banner at the top of the page when the
|
||||
app is busy.}
|
||||
}
|
||||
\description{
|
||||
To enable busy indicators, include the result of this function in the app's UI.
|
||||
Busy indicators provide a visual cue to users when the server is busy
|
||||
calculating outputs or otherwise performing tasks (e.g., producing
|
||||
downloads). 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. Busy indication is enabled by
|
||||
default for UI created with \pkg{bslib}, but must be enabled otherwise. To
|
||||
enable/disable, include the result of this function in anywhere in the app's
|
||||
UI.
|
||||
}
|
||||
\details{
|
||||
When both \code{spinners} and \code{pulse} are set to \code{TRUE}, the pulse is disabled
|
||||
when spinner(s) are active. When both \code{spinners} and \code{pulse} are set to
|
||||
\code{FALSE}, no busy indication is shown (other than the gray-ing out of
|
||||
recalculating outputs).
|
||||
When both \code{spinners} and \code{pulse} are set to \code{TRUE}, the pulse is
|
||||
automatically disabled when spinner(s) are active. When both \code{spinners} and
|
||||
\code{pulse} are set to \code{FALSE}, no busy indication is shown (other than the
|
||||
graying out of recalculating outputs).
|
||||
}
|
||||
\examples{
|
||||
\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
|
||||
\dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
|
||||
|
||||
library(bslib)
|
||||
|
||||
@@ -49,5 +60,6 @@ shinyApp(ui, server)
|
||||
\dontshow{\}) # examplesIf}
|
||||
}
|
||||
\seealso{
|
||||
\code{\link[=spinnerOptions]{spinnerOptions()}} \code{\link[=pulseOptions]{pulseOptions()}}
|
||||
\code{\link[=busyIndicatorOptions]{busyIndicatorOptions()}} for customizing the appearance of the busy
|
||||
indicators.
|
||||
}
|
||||
|
||||
11
package.json
@@ -3,7 +3,7 @@
|
||||
"homepage": "https://shiny.rstudio.com",
|
||||
"repository": "github:rstudio/shiny",
|
||||
"name": "@types/rstudio-shiny",
|
||||
"version": "1.8.1-alpha.9000",
|
||||
"version": "1.8.1-alpha.9001",
|
||||
"license": "GPL-3.0-only",
|
||||
"main": "",
|
||||
"browser": "",
|
||||
@@ -53,7 +53,7 @@
|
||||
"esbuild": "^0.15.10",
|
||||
"esbuild-plugin-babel": "https://github.com/schloerke/esbuild-plugin-babel#patch-2",
|
||||
"esbuild-plugin-globals": "^0.1.1",
|
||||
"esbuild-sass-plugin": "^2.9.0",
|
||||
"esbuild-plugin-sass": "^1.0.1",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest": "^27.0.4",
|
||||
@@ -69,6 +69,7 @@
|
||||
"phantomjs-prebuilt": "^2.1.16",
|
||||
"postcss": "^8.3.5",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"readcontrol": "^1.0.0",
|
||||
"replace": "^1.2.1",
|
||||
"ts-jest": "^26",
|
||||
@@ -94,5 +95,11 @@
|
||||
"coverage": "type-coverage -p tsconfig.json --at-least 90",
|
||||
"circular": "madge --circular --extensions ts srcts/src",
|
||||
"circular_image": "madge --circular --extensions ts --image madge.svg srcts/src"
|
||||
},
|
||||
"prettier": {
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports"
|
||||
],
|
||||
"organizeImportsSkipDestructiveCodeActions": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ build({
|
||||
"srcts/extras/shiny-autoreload.ts",
|
||||
"srcts/extras/shiny-showcase.ts",
|
||||
"srcts/extras/shiny-testmode.ts",
|
||||
"srcts/extras/busy-indicators/busy-indicators.ts",
|
||||
],
|
||||
outdir: outDir,
|
||||
});
|
||||
@@ -25,7 +24,7 @@ build({
|
||||
// - Sass -----------------------------------------------------------
|
||||
|
||||
import autoprefixer from "autoprefixer";
|
||||
import sassPlugin from "esbuild-sass-plugin";
|
||||
import sassPlugin from "esbuild-plugin-sass";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore; Type definitions are not found. This occurs when `strict: true` in tsconfig.json
|
||||
import postCssPlugin from "@deanc/esbuild-plugin-postcss";
|
||||
@@ -58,8 +57,7 @@ build({
|
||||
...sassOpts,
|
||||
entryPoints: ["srcts/extras/busy-indicators/busy-indicators.scss"],
|
||||
outfile: outDir + "busy-indicators/busy-indicators.css",
|
||||
plugins: [sassPlugin({ type: "css", sourceMap: false })],
|
||||
loader: { ".svg": "dataurl" },
|
||||
bundle: true,
|
||||
plugins: [sassPlugin()],
|
||||
bundle: false,
|
||||
metafile: true,
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse class="spinner_rXNP" cx="12" cy="5" rx="4" ry="4" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 132 B |
@@ -1,78 +1,53 @@
|
||||
/*
|
||||
"Built-in" spinner types
|
||||
|
||||
It's important to list out all the types/svgs here so that esbuild can bundle them
|
||||
with the CSS (see js/build.ts).
|
||||
*/
|
||||
:root {
|
||||
--_shiny-spinner-type-tadpole: url(tadpole-spinner.svg);
|
||||
--_shiny-spinner-type-disc: url(disc-spinner.svg);
|
||||
--_shiny-spinner-type-dots: url(dots-spinner.svg);
|
||||
--_shiny-spinner-type-dot-track: url(dot-track-spinner.svg);
|
||||
--_shiny-spinner-type-bounce: url(ball.svg);
|
||||
:where([data-shiny-busy-spinners] .recalculating) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* This data atttribute is set by ui.busy_indicators.use() */
|
||||
[data-shiny-busy-spinners] {
|
||||
/* This class gets set by busy_indicators.ts */
|
||||
|
||||
.recalculating {
|
||||
position: relative;
|
||||
overflow: visible; /* overflow:hidden can, in some cases, clip the spinner */
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
|
||||
/* ui.busy_indicators.spinner_options() */
|
||||
--_shiny-spinner-mask-img: var(--shiny-spinner-mask-img, var(--_shiny-spinner-type-tadpole));
|
||||
--_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-easing: var(--shiny-spinner-easing, ease-in-out);
|
||||
--_shiny-spinner-speed: var(--shiny-spinner-speed, 2s);
|
||||
--_shiny-spinner-delay: var(--shiny-spinner-delay, 0.5s);
|
||||
--_shiny-spinner-animation: var(--shiny-spinner-animation, shiny-busy-spinner-spin);
|
||||
--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);
|
||||
|
||||
/* Apply the spinner type as a mask */
|
||||
mask-image: var(--_shiny-spinner-mask-img);
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-image: var(--_shiny-spinner-mask-img);
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
|
||||
/* Color, sizing, & positioning */
|
||||
background: var(--_shiny-spinner-color);
|
||||
width: var(--_shiny-spinner-size);
|
||||
height: var(--_shiny-spinner-size);
|
||||
inset: calc(50% - var(--_shiny-spinner-size) / 2);
|
||||
|
||||
/* Animation */
|
||||
animation-name: var(--_shiny-spinner-animation);
|
||||
animation-duration: var(--_shiny-spinner-speed);
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: var(--_shiny-spinner-easing);
|
||||
animation-delay: var(--_shiny-spinner-delay);
|
||||
mask-image: var(--_shiny-spinner-url);
|
||||
-webkit-mask-image: var(--_shiny-spinner-url);
|
||||
|
||||
content: "";
|
||||
scale: 0;
|
||||
opacity: 0;
|
||||
animation-delay: var(--_shiny-spinner-delay);
|
||||
animation-name: fade-in;
|
||||
animation-duration: 250ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
/*
|
||||
shiny.css puts `opacity: 0.3` on .recalculating, which unfortunately applies to
|
||||
the spinner. Undo that, but still apply (smaller) opacity to immediate children
|
||||
that aren't shiny-output-busy.
|
||||
that aren't recalculating.
|
||||
*/
|
||||
opacity: 1;
|
||||
> *:not(.recalculating) {
|
||||
opacity: 0.2;
|
||||
transition: opacity 250ms ease var(--shiny-spinner-delay, 1s);
|
||||
}
|
||||
|
||||
/*
|
||||
Disable spinner on uiOutput() mainly because (for other reasons) it has
|
||||
`display:contents`, which breaks the ::after positioning.
|
||||
Note that, even if we could position it, we'd probably want to disable it
|
||||
if it has shiny-output-busy children.
|
||||
if it has recalculating children.
|
||||
*/
|
||||
&.shiny-html-output::after {
|
||||
display: none;
|
||||
@@ -103,7 +78,6 @@
|
||||
left: 0;
|
||||
height: var(--_shiny-pulse-height);
|
||||
background: var(--_shiny-pulse-background);
|
||||
border-radius: 50%;
|
||||
z-index: 9999;
|
||||
|
||||
/* Animation */
|
||||
@@ -117,14 +91,17 @@
|
||||
|
||||
/*
|
||||
In spinners+pulse mode (the recommended default), show a page-level banner if the
|
||||
page is busy, but there are no shiny-output-busy elements.
|
||||
page is busy, but there are no recalculating elements.
|
||||
*/
|
||||
[data-shiny-busy-spinners][data-shiny-busy-pulse] {
|
||||
&.shiny-busy:not(:has(.recalculating))::after {
|
||||
&.shiny-busy::after {
|
||||
@include shiny-page-busy;
|
||||
}
|
||||
&.shiny-not-yet-idle:not(:has(.recalculating))::after {
|
||||
@include shiny-page-busy;
|
||||
&.shiny-busy:has(.recalculating)::after {
|
||||
display: none;
|
||||
}
|
||||
&.shiny-busy:has(#shiny-disconnected-overlay)::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,45 +110,18 @@
|
||||
&.shiny-busy::after {
|
||||
@include shiny-page-busy;
|
||||
}
|
||||
&.shiny-not-yet-idle::after {
|
||||
@include shiny-page-busy;
|
||||
&.shiny-busy:has(#shiny-disconnected-overlay)::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframes behind most spinner types */
|
||||
@keyframes shiny-busy-spinner-spin {
|
||||
/* Keyframes for the fading spinner */
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
scale: 1;
|
||||
rotate: 0deg;
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
scale: 1;
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
/* For busy_indicators.spinner_options(type="bounce") */
|
||||
@keyframes shiny-busy-spinner-bounce {
|
||||
0% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
translate: 0 calc(var(--_shiny-spinner-size) * (5 / 24));
|
||||
scale: 1 1;
|
||||
}
|
||||
46.875% {
|
||||
translate: 0 calc(var(--_shiny-spinner-size) * (20 / 24));
|
||||
scale: 1 1;
|
||||
}
|
||||
50% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
translate: 0 calc(var(--_shiny-spinner-size) * (20.5 / 24));
|
||||
scale: 1.2 0.85;
|
||||
}
|
||||
53.125% {
|
||||
scale: 1 1;
|
||||
}
|
||||
100% {
|
||||
translate: 0 calc(var(--_shiny-spinner-size) * (5 / 24));
|
||||
scale: 1 1;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// This of this like the .shiny-busy class that shiny.js puts on the root element,
|
||||
// except it's added before shiny.js is initialized, connected, etc.
|
||||
// TODO: maybe shiny.js should be doing something like this?
|
||||
document.documentElement.classList.add("shiny-not-yet-idle");
|
||||
$(document).one("shiny:idle", function () {
|
||||
document.documentElement.classList.remove("shiny-not-yet-idle");
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M9 16.7833C13.2986 16.7833 17.3579 13.2986 17.3579 9C17.3579 4.70142 13.2986 1.21672 9 1.21672C4.70141 1.21672 1.21672 4.70142 1.21672 9C1.21672 13.2986 4.70141 16.7833 9 16.7833ZM9 17.8678C13.8976 17.8678 17.3579 13.8976 17.3579 9C17.3579 4.10245 13.8976 0.132202 9 0.132202C4.10245 0.132202 0.132198 4.10245 0.132198 9C0.132198 13.8976 4.10245 17.8678 9 17.8678Z"
|
||||
fill="black" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 521 B |
@@ -1,5 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25" />
|
||||
<circle class="spinner_7WDj" cx="12" cy="2.5" r="1.5" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 238 B |
@@ -1,11 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="spinner_Wezc">
|
||||
<circle cx="12" cy="2.5" r="1.5" opacity=".14" />
|
||||
<circle cx="16.75" cy="3.77" r="1.5" opacity=".29" />
|
||||
<circle cx="20.23" cy="7.25" r="1.5" opacity=".43" />
|
||||
<circle cx="21.50" cy="12.00" r="1.5" opacity=".57" />
|
||||
<circle cx="20.23" cy="16.75" r="1.5" opacity=".71" />
|
||||
<circle cx="16.75" cy="20.23" r="1.5" opacity=".86" />
|
||||
<circle cx="12" cy="21.5" r="1.5" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 522 B |
@@ -1,5 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<path class="spinner_0XTQ"
|
||||
d="M12,23a9.63,9.63,0,0,1-8-9.5,9.51,9.51,0,0,1,6.79-9.1A1.66,1.66,0,0,0,12,2.81h0a1.67,1.67,0,0,0-1.94-1.64A11,11,0,0,0,12,23Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 234 B |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
import { hasDefinedProperty } from "../../utils";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
type CheckedHTMLElement = HTMLInputElement;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import $ from "jquery";
|
||||
|
||||
import { InputBinding } from "./inputBinding";
|
||||
import { $escape, updateLabel, hasDefinedProperty } from "../../utils";
|
||||
import { $escape, hasDefinedProperty, updateLabel } from "../../utils";
|
||||
import type { CheckedHTMLElement } from "./checkbox";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
type CheckboxGroupHTMLElement = CheckedHTMLElement;
|
||||
type ValueLabelObject = {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import $ from "jquery";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
import {
|
||||
formatDateUTC,
|
||||
updateLabel,
|
||||
$escape,
|
||||
parseDate,
|
||||
formatDateUTC,
|
||||
hasDefinedProperty,
|
||||
parseDate,
|
||||
updateLabel,
|
||||
} from "../../utils";
|
||||
import type { NotUndefined } from "../../utils/extraTypes";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
declare global {
|
||||
interface JQuery {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $ from "jquery";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
import { FileUploader } from "../../file/fileProcessor";
|
||||
import { shinyShinyApp } from "../../shiny/initedMethods";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
const zoneActive = "shiny-file-input-active";
|
||||
const zoneOver = "shiny-file-input-over";
|
||||
|
||||
@@ -2,27 +2,26 @@ import { BindingRegistry } from "../registry";
|
||||
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
import { ActionButtonInputBinding } from "./actionbutton";
|
||||
import { CheckboxInputBinding } from "./checkbox";
|
||||
import { CheckboxGroupInputBinding } from "./checkboxgroup";
|
||||
import { DateInputBinding } from "./date";
|
||||
import { DateRangeInputBinding } from "./daterange";
|
||||
import { FileInputBinding } from "./fileinput";
|
||||
import { NumberInputBinding } from "./number";
|
||||
import { PasswordInputBinding } from "./password";
|
||||
import { RadioInputBinding } from "./radio";
|
||||
import { SelectInputBinding } from "./selectInput";
|
||||
import { SliderInputBinding } from "./slider";
|
||||
import { BootstrapTabInputBinding } from "./tabinput";
|
||||
import { TextInputBinding } from "./text";
|
||||
import { TextareaInputBinding } from "./textarea";
|
||||
import { RadioInputBinding } from "./radio";
|
||||
import { DateInputBinding } from "./date";
|
||||
import { SliderInputBinding } from "./slider";
|
||||
import { DateRangeInputBinding } from "./daterange";
|
||||
import { SelectInputBinding } from "./selectInput";
|
||||
import { ActionButtonInputBinding } from "./actionbutton";
|
||||
import { BootstrapTabInputBinding } from "./tabinput";
|
||||
import { FileInputBinding } from "./fileinput";
|
||||
|
||||
// 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");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
import { $escape, hasDefinedProperty, updateLabel } from "../../utils";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
type RadioHTMLElement = HTMLInputElement;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $ from "jquery";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
import { $escape, hasDefinedProperty, updateLabel } from "../../utils";
|
||||
import { indirectEval } from "../../utils/eval";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
type SelectHTMLElement = HTMLSelectElement & { nonempty: boolean };
|
||||
|
||||
@@ -154,7 +154,7 @@ class SelectInputBinding extends InputBinding {
|
||||
selectize.settings.load = function (query: string, callback: CallbackFn) {
|
||||
const settings = selectize.settings;
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
/* eslint-disable-next-line @typescript-eslint/no-floating-promises */
|
||||
$.ajax({
|
||||
url: data.url,
|
||||
data: {
|
||||
|
||||
@@ -5,10 +5,10 @@ import type {
|
||||
import $ from "jquery";
|
||||
// import { NameValueHTMLElement } from ".";
|
||||
import {
|
||||
formatDateUTC,
|
||||
updateLabel,
|
||||
$escape,
|
||||
formatDateUTC,
|
||||
hasDefinedProperty,
|
||||
updateLabel,
|
||||
} from "../../utils";
|
||||
|
||||
import type { TextHTMLElement } from "./text";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
import { hasDefinedProperty, isBS3 } from "../../utils";
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
type TabInputReceiveMessageData = { value?: string };
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import $ from "jquery";
|
||||
import { $escape, updateLabel, hasDefinedProperty } from "../../utils";
|
||||
import { $escape, hasDefinedProperty, updateLabel } from "../../utils";
|
||||
|
||||
import { InputBinding } from "./inputBinding";
|
||||
|
||||
@@ -122,5 +122,4 @@ class TextInputBinding extends TextInputBindingBase {
|
||||
}
|
||||
|
||||
export { TextInputBinding, TextInputBindingBase };
|
||||
|
||||
export type { TextHTMLElement, TextReceiveMessageData };
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import $ from "jquery";
|
||||
|
||||
import { OutputBinding } from "./outputBinding";
|
||||
import { shinyUnbindAll } from "../../shiny/initedMethods";
|
||||
import type { ErrorsMessageValue } from "../../shiny/shinyapp";
|
||||
import { debounce } from "../../time";
|
||||
import { escapeHTML } from "../../utils";
|
||||
import { indirectEval } from "../../utils/eval";
|
||||
import type { ErrorsMessageValue } from "../../shiny/shinyapp";
|
||||
import { OutputBinding } from "./outputBinding";
|
||||
|
||||
class DatatableOutputBinding extends OutputBinding {
|
||||
find(scope: HTMLElement): JQuery<HTMLElement> {
|
||||
|
||||
@@ -7,7 +7,10 @@ class DownloadLinkOutputBinding extends OutputBinding {
|
||||
return $(scope).find("a.shiny-download-link");
|
||||
}
|
||||
renderValue(el: HTMLElement, data: string): void {
|
||||
$(el).attr("href", data);
|
||||
el.setAttribute("href", data);
|
||||
el.classList.remove("disabled");
|
||||
el.removeAttribute("aria-disabled");
|
||||
el.removeAttribute("tabindex");
|
||||
}
|
||||
// Progress shouldn't be shown on the download button
|
||||
// (progress will be shown as a page level pulse instead)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import $ from "jquery";
|
||||
|
||||
import { OutputBinding } from "./outputBinding";
|
||||
import { shinyUnbindAll } from "../../shiny/initedMethods";
|
||||
import { renderContentAsync } from "../../shiny/render";
|
||||
import type { ErrorsMessageValue } from "../../shiny/shinyapp";
|
||||
import { OutputBinding } from "./outputBinding";
|
||||
|
||||
class HtmlOutputBinding extends OutputBinding {
|
||||
find(scope: HTMLElement): JQuery<HTMLElement> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import $ from "jquery";
|
||||
import { OutputBinding } from "./outputBinding";
|
||||
import {
|
||||
createBrushHandler,
|
||||
createClickHandler,
|
||||
@@ -8,16 +7,17 @@ import {
|
||||
disableDrag,
|
||||
initCoordmap,
|
||||
} from "../../imageutils";
|
||||
import type { CoordmapInit } from "../../imageutils/initCoordmap";
|
||||
import type { ErrorsMessageValue } from "../../shiny/shinyapp";
|
||||
import {
|
||||
strToBool,
|
||||
getComputedLinkColor,
|
||||
getStyle,
|
||||
hasOwnProperty,
|
||||
strToBool,
|
||||
} from "../../utils";
|
||||
import { isIE, IEVersion } from "../../utils/browser";
|
||||
import type { CoordmapInit } from "../../imageutils/initCoordmap";
|
||||
import type { ErrorsMessageValue } from "../../shiny/shinyapp";
|
||||
import { IEVersion, isIE } from "../../utils/browser";
|
||||
import { ifUndefined } from "../../utils/object";
|
||||
import { OutputBinding } from "./outputBinding";
|
||||
|
||||
class ImageOutputBinding extends OutputBinding {
|
||||
find(scope: HTMLElement): JQuery<HTMLElement> {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TextOutputBinding } from "./text";
|
||||
import { BindingRegistry } from "../registry";
|
||||
import { DownloadLinkOutputBinding } from "./downloadlink";
|
||||
import { DatatableOutputBinding } from "./datatable";
|
||||
import { DownloadLinkOutputBinding } from "./downloadlink";
|
||||
import { HtmlOutputBinding } from "./html";
|
||||
import { imageOutputBinding } from "./image";
|
||||
import { TextOutputBinding } from "./text";
|
||||
|
||||
import { OutputBinding } from "./outputBinding";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import { asArray } from "../../utils";
|
||||
import type { ErrorsMessageValue } from "../../shiny/shinyapp";
|
||||
import { asArray } from "../../utils";
|
||||
|
||||
class OutputBinding {
|
||||
name!: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { Shiny } from "..";
|
||||
import { ShinyClientError } from "../shiny/error";
|
||||
|
||||
const buttonStyles = css`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import $ from "jquery";
|
||||
import { triggerFileInputChanged } from "../events/inputChanged";
|
||||
import { $escape } from "../utils";
|
||||
import type { ShinyApp } from "../shiny/shinyapp";
|
||||
import { getFileInputBinding } from "../shiny/initedMethods";
|
||||
import type { ShinyApp } from "../shiny/shinyapp";
|
||||
import { $escape } from "../utils";
|
||||
|
||||
type JobId = string;
|
||||
type UploadUrl = string;
|
||||
@@ -180,7 +180,7 @@ class FileUploader extends FileProcessor {
|
||||
onFile(file: File, cont: () => void): void {
|
||||
this.onProgress(file, 0);
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
/* eslint-disable-next-line @typescript-eslint/no-floating-promises */
|
||||
$.ajax(this.uploadUrl, {
|
||||
type: "POST",
|
||||
cache: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $ from "jquery";
|
||||
import { equal, isnan, mapValues, roundSignif } from "../utils";
|
||||
import type { Coordmap } from "./initCoordmap";
|
||||
import { findOrigin } from "./initCoordmap";
|
||||
import { equal, isnan, mapValues, roundSignif } from "../utils";
|
||||
import type { Panel } from "./initPanelScales";
|
||||
|
||||
import type { Offset } from "./findbox";
|
||||
@@ -656,5 +656,4 @@ function createBrush(
|
||||
}
|
||||
|
||||
export { createBrush };
|
||||
|
||||
export type { Bounds, BrushOpts, BoundsCss };
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import $ from "jquery";
|
||||
import { imageOutputBinding } from "../bindings/output/image";
|
||||
import type { InputRatePolicy } from "../inputPolicies";
|
||||
import { shinySetInputValue } from "../shiny/initedMethods";
|
||||
import { Debouncer, Throttler } from "../time";
|
||||
import type { Bounds, BoundsCss, BrushOpts } from "./createBrush";
|
||||
import { createBrush } from "./createBrush";
|
||||
import type { BoundsCss, Bounds, BrushOpts } from "./createBrush";
|
||||
import type { Offset } from "./findbox";
|
||||
import type { Coordmap } from "./initCoordmap";
|
||||
import type { Panel } from "./initPanelScales";
|
||||
import type { InputRatePolicy } from "../inputPolicies";
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Handler creators for click, hover, brush.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createBrush } from "./createBrush";
|
||||
import { createClickInfo } from "./createClickInfo";
|
||||
import {
|
||||
createBrushHandler,
|
||||
createClickHandler,
|
||||
createHoverHandler,
|
||||
createBrushHandler,
|
||||
} from "./createHandlers";
|
||||
import { disableDrag } from "./disableDrag";
|
||||
import { findBox } from "./findbox";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import $ from "jquery";
|
||||
import { shinySetInputValue } from "../shiny/initedMethods";
|
||||
import { mapValues } from "../utils";
|
||||
import type { Offset } from "./findbox";
|
||||
import type { Bounds } from "./createBrush";
|
||||
import type { Offset } from "./findbox";
|
||||
import type { Panel, PanelInit } from "./initPanelScales";
|
||||
import { initPanelScales } from "./initPanelScales";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Map a value x from a domain to a range. If clip is true, clip it to the
|
||||
|
||||
import type { Offset } from "./findbox";
|
||||
import { mapValues } from "../utils";
|
||||
import type { Bounds } from "./createBrush";
|
||||
import type { Offset } from "./findbox";
|
||||
|
||||
// range.
|
||||
function mapLinear(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { init } from "./initialize";
|
||||
export { Shiny, type ShinyClass } from "./initialize";
|
||||
|
||||
init();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
|
||||
import { isIE, setIsQt, setIsIE, setIEVersion } from "../utils/browser";
|
||||
import { isIE, setIEVersion, setIsIE, setIsQt } from "../utils/browser";
|
||||
import { userAgent } from "../utils/userAgent";
|
||||
|
||||
function getIEVersion() {
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { determineBrowserInfo } from "./browser";
|
||||
import { disableFormSubmission } from "./disableForm";
|
||||
import { trackHistory } from "./history";
|
||||
import { determineBrowserInfo } from "./browser";
|
||||
|
||||
import { windowShiny } from "../window/libraries";
|
||||
import { setShiny } from "../shiny";
|
||||
import { ShinyClass } from "../shiny";
|
||||
import { setUserAgent } from "../utils/userAgent";
|
||||
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,12 +1,11 @@
|
||||
import { InputBatchSender } from "./inputBatchSender";
|
||||
import { InputNoResendDecorator } from "./inputNoResendDecorator";
|
||||
import { InputEventDecorator } from "./inputEventDecorator";
|
||||
import { InputRateDecorator } from "./inputRateDecorator";
|
||||
import { InputDeferDecorator } from "./inputDeferDecorator";
|
||||
import { InputEventDecorator } from "./inputEventDecorator";
|
||||
import { InputNoResendDecorator } from "./inputNoResendDecorator";
|
||||
import { InputRateDecorator } from "./inputRateDecorator";
|
||||
import { InputValidateDecorator } from "./inputValidateDecorator";
|
||||
|
||||
import type { InputPolicy } from "./inputPolicy";
|
||||
import type { EventPriority } from "./inputPolicy";
|
||||
import type { EventPriority, InputPolicy } from "./inputPolicy";
|
||||
import type { InputRatePolicy } from "./inputRatePolicy";
|
||||
|
||||
export {
|
||||
@@ -17,5 +16,4 @@ export {
|
||||
InputDeferDecorator,
|
||||
InputValidateDecorator,
|
||||
};
|
||||
|
||||
export type { InputPolicy, EventPriority, InputRatePolicy };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
import type { ShinyApp } from "../shiny/shinyapp";
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
|
||||
// Schedules data to be sent to shinyapp at the next setTimeout(0).
|
||||
// Batches multiple input calls into one websocket message.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { EventPriority } from "./inputPolicy";
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
import { hasDefinedProperty } from "../utils";
|
||||
import type {
|
||||
EventPriority,
|
||||
InputPolicy,
|
||||
InputPolicyOpts,
|
||||
} from "./inputPolicy";
|
||||
|
||||
class InputDeferDecorator implements InputPolicy {
|
||||
pendingInput: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
import type { ShinyEventInputChanged } from "../events/shinyEvents";
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
import { splitInputNameType } from "./splitInputNameType";
|
||||
|
||||
class InputEventDecorator implements InputPolicy {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
import { hasDefinedProperty } from "../utils";
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
import { splitInputNameType } from "./splitInputNameType";
|
||||
|
||||
type LastSentValues = { [key: string]: { [key: string]: string } };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
import { Debouncer, Invoker, Throttler } from "../time";
|
||||
import { splitInputNameType } from "./splitInputNameType";
|
||||
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
|
||||
import type { InputRatePolicy } from "./inputRatePolicy";
|
||||
import { splitInputNameType } from "./splitInputNameType";
|
||||
|
||||
type RatePolicyModes = "debounce" | "direct" | "throttle";
|
||||
|
||||
@@ -66,5 +66,4 @@ class InputRateDecorator implements InputPolicy {
|
||||
}
|
||||
|
||||
export { InputRateDecorator };
|
||||
|
||||
export type { RatePolicyModes };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import $ from "jquery";
|
||||
import { Shiny } from "..";
|
||||
import type { InputBinding, OutputBinding } from "../bindings";
|
||||
import { OutputBindingAdapter } from "../bindings/outputAdapter";
|
||||
import type { BindingRegistry } from "../bindings/registry";
|
||||
@@ -6,9 +7,9 @@ import type {
|
||||
InputRateDecorator,
|
||||
InputValidateDecorator,
|
||||
} from "../inputPolicies";
|
||||
import { ShinyClientError } from "./error";
|
||||
import { shinyAppBindOutput, shinyAppUnbindOutput } from "./initedMethods";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
import { ShinyClientError } from "./error";
|
||||
|
||||
type BindScope = HTMLElement | JQuery<HTMLElement>;
|
||||
|
||||
@@ -318,11 +319,12 @@ async function bindOutputs(
|
||||
await shinyAppBindOutput(id, bindingAdapter);
|
||||
$el.data("shiny-output-binding", bindingAdapter);
|
||||
$el.addClass("shiny-bound-output");
|
||||
if (Shiny.shinyapp?.isRecalculating(id)) {
|
||||
if (binding.showProgress) binding.showProgress(el, true);
|
||||
}
|
||||
if (!$el.attr("aria-live")) $el.attr("aria-live", "polite");
|
||||
|
||||
if (Shiny.shinyapp?.$outputProgress.isRecalculating(id)) {
|
||||
bindingAdapter.showProgress(true);
|
||||
}
|
||||
|
||||
bindingsRegistry.addBinding(id, "output");
|
||||
$el.trigger({
|
||||
type: "shiny:bound",
|
||||
@@ -463,5 +465,4 @@ async function bindAll(
|
||||
}
|
||||
|
||||
export { unbindAll, bindAll, _bindAll };
|
||||
|
||||
export type { BindScope, BindInputsCtx };
|
||||
|
||||
@@ -1,42 +1,67 @@
|
||||
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 { showNotification, removeNotification } from "./notifications";
|
||||
import { showModal, removeModal } from "./modal";
|
||||
import { showReconnectDialog, hideReconnectDialog } from "./reconnectDialog";
|
||||
import type { InputPolicy } from "../inputPolicies";
|
||||
import {
|
||||
renderContentAsync,
|
||||
renderContent,
|
||||
renderDependenciesAsync,
|
||||
renderDependencies,
|
||||
renderHtmlAsync,
|
||||
renderHtml,
|
||||
} from "./render";
|
||||
import { initShiny } from "./init";
|
||||
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,
|
||||
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,
|
||||
shinySetInputValue,
|
||||
shinyInitializeInputs,
|
||||
shinySetInputValue,
|
||||
shinyUnbindAll,
|
||||
} from "./initedMethods";
|
||||
import { setFileInputBinding } from "./initedMethods";
|
||||
import type { Handler, ShinyApp } from "./shinyapp";
|
||||
import { addCustomMessageHandler } from "./shinyapp";
|
||||
import { initInputBindings } from "../bindings/input";
|
||||
import { initOutputBindings } from "../bindings/output";
|
||||
import { showErrorInClientConsole } from "../components/errorConsole";
|
||||
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,
|
||||
renderDependenciesAsync,
|
||||
renderHtml,
|
||||
renderHtmlAsync,
|
||||
} from "./render";
|
||||
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 +70,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 +78,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,78 +92,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 = 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", () => {
|
||||
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 {
|
||||
InputBatchSender,
|
||||
InputDeferDecorator,
|
||||
InputEventDecorator,
|
||||
InputNoResendDecorator,
|
||||
InputRateDecorator,
|
||||
InputValidateDecorator,
|
||||
} from "../inputPolicies";
|
||||
import type { InputPolicy } from "../inputPolicies";
|
||||
import { addDefaultInputOpts } from "../inputPolicies/inputValidateDecorator";
|
||||
import { debounce, Debouncer } from "../time";
|
||||
import {
|
||||
getComputedLinkColor,
|
||||
getStyle,
|
||||
hasDefinedProperty,
|
||||
mapValues,
|
||||
pixelRatio,
|
||||
} from "../utils";
|
||||
import { bindAll, unbindAll, _bindAll } from "./bind";
|
||||
import type { BindInputsCtx, BindScope } from "./bind";
|
||||
import { setShinyObj } from "./initedMethods";
|
||||
import { registerDependency } from "./render";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
import { ShinyApp } from "./shinyapp";
|
||||
import { registerNames as singletonsRegisterNames } from "./singletons";
|
||||
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
|
||||
|
||||
// "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;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import $ from "jquery";
|
||||
import { $escape, randomId } from "../utils";
|
||||
import { shinyUnbindAll } from "./initedMethods";
|
||||
import type { HtmlDep } from "./render";
|
||||
import { renderDependenciesAsync } from "./render";
|
||||
import { renderContentAsync } from "./render";
|
||||
import { renderContentAsync, renderDependenciesAsync } from "./render";
|
||||
|
||||
// Milliseconds to fade in or out
|
||||
const fadeDuration = 250;
|
||||
|
||||
280
srcts/src/shiny/outputProgress.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* A state machine for tracking Shiny output progress.
|
||||
*
|
||||
* Although there are a handful of possible states, the ultimate goal here is to
|
||||
* be able to say whether a given output `isRecalculating()` or not (and thus,
|
||||
* whether we should show progress indication or not).
|
||||
*
|
||||
* The diagram below depicts the state machine. Each node represents a possible
|
||||
* state and each edge represents a server-->client message that moves outputs
|
||||
* from one state to another. If a node name is all caps, then the output should
|
||||
* be showing a busy state when visible (i.e., `binding.showProgress(true)`).
|
||||
*
|
||||
* A more polished SVG version of this diagram can be found here:
|
||||
* https://github.com/rstudio/shiny/blob/main/inst/diagrams/outputProgressStateMachine.svg
|
||||
*
|
||||
* +---------+ recalculating +---------+
|
||||
* | INITIAL +--------------->| RUNNING |<----------------+
|
||||
* +---------+ +---+-----+ |
|
||||
* +-----------/ | |
|
||||
* | recalculated |
|
||||
* | | |
|
||||
* | +--v---+---------------+ |
|
||||
* | +--+ IDLE +--+-------+ | |
|
||||
* | | +------+ | | | |
|
||||
* 1 | 2| 3| 4| | |
|
||||
* v v v v | |
|
||||
* +------------+ +------+ +-----+ +-----+ | |
|
||||
* | PERSISTENT | |cancel| |value| |error| | |
|
||||
* +-------+----+ +---+--+ +-+---+ +-+---+ | |
|
||||
* | 5| 5| | | |
|
||||
* 5| v v 5| | |
|
||||
* | +-------------+ | | |
|
||||
* +------>| INVALIDATED |<-----+ | |
|
||||
* +-----+-------+<-----------+ |
|
||||
* | |
|
||||
* | recalculating |
|
||||
* +-----------------------+
|
||||
*
|
||||
* 1. {progress: {type: "binding", message: {persistent: true}}}
|
||||
* 2. No message
|
||||
* 3. Value
|
||||
* 4. Error
|
||||
* 5. {progress: {type: "binding"}}
|
||||
*/
|
||||
|
||||
// The possible states of a given output.
|
||||
enum OutputStates {
|
||||
Initial = "initial",
|
||||
Running = "running",
|
||||
Idle = "idle",
|
||||
Value = "value",
|
||||
Error = "error",
|
||||
Cancel = "cancel",
|
||||
Persistent = "persistent",
|
||||
Invalidated = "invalidated",
|
||||
}
|
||||
|
||||
// Server->client messages that have an impact on output progress state.
|
||||
// These derive from the Session's defineOutput() method.
|
||||
type RecalculatingMessage = {
|
||||
recalculating: {
|
||||
name: string;
|
||||
status: "recalculated" | "recalculating";
|
||||
};
|
||||
};
|
||||
|
||||
type FlushMessage = {
|
||||
errors: { [key: string]: unknown };
|
||||
values: { [key: string]: unknown };
|
||||
inputMessages: [];
|
||||
};
|
||||
|
||||
type ProgressMessage = {
|
||||
progress: {
|
||||
type: "binding";
|
||||
message: {
|
||||
id: string;
|
||||
persistent: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// A generic server->client message type.
|
||||
type Message = { [key: string]: unknown };
|
||||
|
||||
// The state machine that tracks the progress of outputs in a Shiny app.
|
||||
class OutputProgressReporter {
|
||||
private outputStates: Map<string, OutputStates> = new Map();
|
||||
// A map of outputs that have changed their progress status since the last call to takeChanges().
|
||||
// The value is true if the output is recalculating, and false otherwise.
|
||||
private changedOutputs: Map<string, boolean> = new Map();
|
||||
|
||||
takeChanges(): Map<string, boolean> {
|
||||
const result = this.changedOutputs;
|
||||
this.changedOutputs = new Map();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns whether the output is recalculating or not.
|
||||
isRecalculating(name: string): boolean {
|
||||
const state = this.#getState(name);
|
||||
|
||||
// The blue nodes in the state machine diagram
|
||||
const recalculatingStates = [
|
||||
OutputStates.Initial,
|
||||
OutputStates.Running,
|
||||
OutputStates.Idle,
|
||||
OutputStates.Persistent,
|
||||
OutputStates.Invalidated,
|
||||
];
|
||||
|
||||
return recalculatingStates.includes(state);
|
||||
}
|
||||
|
||||
// Update output state based on the message received from the server.
|
||||
// Note that any message can be passed to this method, but only the
|
||||
// messages that are relevant to output progress do anything to the state.
|
||||
updateStateFromMessage(message: Message): void {
|
||||
if (isRecalculatingMessage(message)) {
|
||||
// The "1st level" of the state machine diagram
|
||||
this.#updateStateFromRecalculating(message);
|
||||
}
|
||||
|
||||
if (isFlushMessage(message)) {
|
||||
// The "2nd level" of the state machine diagram
|
||||
this.#updateStateFromFlush(message);
|
||||
}
|
||||
|
||||
if (isProgressMessage(message)) {
|
||||
// The "3rd level" of the state machine diagram
|
||||
this.#updateStateFromProgress(message);
|
||||
}
|
||||
}
|
||||
|
||||
#updateStateFromRecalculating(message: RecalculatingMessage): void {
|
||||
const { name, status } = message.recalculating;
|
||||
|
||||
const state = this.#getState(name);
|
||||
|
||||
if (status === "recalculating") {
|
||||
switch (state) {
|
||||
case OutputStates.Initial:
|
||||
case OutputStates.Invalidated:
|
||||
this.#setState(name, OutputStates.Running);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Shiny server sent a message that the output '${name}' is recalculating,
|
||||
but the output is in an unexpected state of: '${state}'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "recalculated") {
|
||||
switch (state) {
|
||||
case OutputStates.Running:
|
||||
this.#setState(name, OutputStates.Idle);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Shiny server sent a message that the output '${name}' has been recalculated,
|
||||
but the output is in an unexpected state of: '${state}'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#updateStateFromFlush(message: FlushMessage): void {
|
||||
for (const name in message.values) {
|
||||
this.#updateStateFromValueOrError(name, OutputStates.Value);
|
||||
}
|
||||
|
||||
for (const name in message.errors) {
|
||||
this.#updateStateFromValueOrError(name, OutputStates.Error);
|
||||
}
|
||||
|
||||
// Since req(F, cancelOutput = TRUE) doesn't send a message, we need to identify
|
||||
// the situation by looking for outputs that are still idle and move them to cancel.
|
||||
for (const [name, state] of this.outputStates) {
|
||||
switch (state) {
|
||||
case OutputStates.Idle:
|
||||
this.#setState(name, OutputStates.Cancel);
|
||||
break;
|
||||
case OutputStates.Value:
|
||||
case OutputStates.Error:
|
||||
case OutputStates.Cancel:
|
||||
case OutputStates.Persistent:
|
||||
case OutputStates.Invalidated: // If the output is suspended
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Shiny server sent a flush message, and after processing the values and errors,
|
||||
the output '${name}' has an unexpected ending state of: '${state}'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#updateStateFromProgress(message: ProgressMessage): void {
|
||||
const { id, persistent } = message.progress.message;
|
||||
const state = this.#getState(id);
|
||||
if (persistent) {
|
||||
switch (state) {
|
||||
case OutputStates.Running:
|
||||
this.#setState(id, OutputStates.Persistent);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Shiny server has sent a 'persistent progress' message for ${id},
|
||||
but the output is in an unexpected state of: ${state}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
switch (state) {
|
||||
case OutputStates.Value:
|
||||
case OutputStates.Error:
|
||||
case OutputStates.Cancel:
|
||||
case OutputStates.Persistent:
|
||||
case OutputStates.Idle:
|
||||
this.#setState(id, OutputStates.Invalidated);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Shiny server has sent a progress message for ${id},
|
||||
but the output is in an unexpected state of: ${state}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When receiving values/errors as part of a flush message, outputs should generally
|
||||
// be moving from Idle to Value/Error state.
|
||||
#updateStateFromValueOrError(
|
||||
name: string,
|
||||
type: OutputStates.Error | OutputStates.Value
|
||||
): void {
|
||||
const state = this.#getState(name);
|
||||
switch (state) {
|
||||
case OutputStates.Idle:
|
||||
this.#setState(name, type);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Shiny server has sent a ${type} for the output '${name}',
|
||||
but the output is in an unexpected state of: '${state}'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#getState(name: string): OutputStates {
|
||||
return this.outputStates.get(name) ?? OutputStates.Initial;
|
||||
}
|
||||
|
||||
#setState(name: string, state: OutputStates): void {
|
||||
const oldRecalc = this.isRecalculating(name);
|
||||
this.outputStates.set(name, state);
|
||||
const newRecalc = this.isRecalculating(name);
|
||||
if (oldRecalc !== newRecalc) {
|
||||
this.changedOutputs.set(name, newRecalc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type guards
|
||||
function isRecalculatingMessage(x: Message): x is RecalculatingMessage {
|
||||
const m = x as RecalculatingMessage;
|
||||
return m.recalculating !== undefined;
|
||||
}
|
||||
|
||||
function isFlushMessage(x: Message): x is FlushMessage {
|
||||
const m = x as FlushMessage;
|
||||
return m.values !== undefined && m.errors !== undefined;
|
||||
}
|
||||
|
||||
function isProgressMessage(x: Message): x is ProgressMessage {
|
||||
const m = x as ProgressMessage;
|
||||
return m.progress !== undefined && m.progress.type === "binding";
|
||||
}
|
||||
|
||||
export { OutputProgressReporter };
|
||||
@@ -50,13 +50,14 @@ function initReactlog(): void {
|
||||
window.escape(shinyAppConfig().sessionId);
|
||||
|
||||
// send notification
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
/* eslint-disable-next-line @typescript-eslint/no-floating-promises */
|
||||
$.get(url, function (result: "marked" | void) {
|
||||
if (result !== "marked") return;
|
||||
|
||||
const html =
|
||||
'<span id="shiny-reactlog-mark-text">Marked time point in reactlog</span>';
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-floating-promises */
|
||||
showNotification({
|
||||
html: html,
|
||||
closeButton: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
|
||||
import { showNotification, removeNotification } from "./notifications";
|
||||
import { removeNotification, showNotification } from "./notifications";
|
||||
|
||||
function updateTime(reconnectTime: number): void {
|
||||
const $time = $("#shiny-reconnect-time");
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from "./initedMethods";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
|
||||
import { renderHtml as singletonsRenderHtml } from "./singletons";
|
||||
import type { WherePosition } from "./singletons";
|
||||
import { renderHtml as singletonsRenderHtml } from "./singletons";
|
||||
|
||||
// There are synchronous and asynchronous versions of the exported functions
|
||||
// renderContent(), renderHtml(), and renderDependencies(). This is because they
|
||||
|
||||