Compare commits

...

38 Commits

Author SHA1 Message Date
karangattu
5f9176ac3b yarn build (GitHub Actions) 2024-10-31 16:48:08 +00:00
Karan Gathani
8f6ccd6bc4 remove comment 2024-10-31 09:43:06 -07:00
karangattu
380dfe0904 yarn build (GitHub Actions) 2024-10-31 16:22:50 +00:00
Karan Gathani
774def26de Add entry in NEWS.md 2024-10-31 09:18:40 -07:00
Karan
b24cbcb7b8 Merge branch 'main' into add-clientdata-height-width 2024-10-30 21:12:03 -07:00
Carson
4d3ec8e7bd Add to initial values; re-send window sizes in a debounced and decoupled fashion on resize; remove user-agent and mobile values 2024-10-30 17:10:06 -05:00
karangattu
1018f7cc62 yarn build (GitHub Actions) 2024-10-29 01:29:43 +00:00
Karan Gathani
d87bcc5184 Merge branch 'add-clientdata-height-width' of https://github.com/rstudio/shiny into add-clientdata-height-width 2024-10-28 18:25:39 -07:00
Karan Gathani
ca590f3854 use existing sendImageSizeFns instead of new SendWindowSize 2024-10-28 18:25:37 -07:00
karangattu
a56cd962f0 yarn build (GitHub Actions) 2024-10-28 23:56:39 +00:00
Karan Gathani
078aeb23a8 use debouncer when fetching window sizes 2024-10-28 16:52:05 -07:00
Adam Foryś
79af1d6c92 Fix url bookmarking with possibility to modify excludes (#3762)
* Fix url bookmarking with possibility to modify excludes

* Update NEWS.md
2024-10-28 09:36:42 -05:00
karangattu
f0bbc1db56 yarn build (GitHub Actions) 2024-10-22 20:02:09 +00:00
Karan Gathani
930040b66c Add viewport and scrollable size values to clientdata 2024-10-22 12:50:48 -07:00
Yihui Xie
a145add5d4 Use double-tilde for strikethrough in Markdown tests (#4144)
* Use double-tilde for strikethrough in Markdown tests

The current dev version of commonmark has disabled single tilde   for strikethrough: https://github.com/r-lib/commonmark/pull/33 Using double-tilde will make it consistent with Pandoc's Markdown (where single-tilde is for subscripts).

* Skip tests if commonmark is outdated

* Revert "Skip tests if commonmark is outdated"

This reverts commit 97bee20863.

---------

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2024-10-15 11:21:53 -05:00
Dan Gealow
abf71389be Fix sporadic dates (#3664) (#3665)
* Remove dateInput and dateRangeInput handlers for keyup and input events

This prevents spurious updates while typing, but still sends when enter is pressed, focus is lost, or the GUI is clicked (due to the remaining `changeDate` and `change` handlers).

* chore: small edits to comments and NEWS item

---------

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-09-30 17:26:43 -04:00
Garrick Aden-Buie
2e2114f99d fix(busy): Show pulse if only UI are recalculating (#4137)
* fix(busy): Show pulse if only UI are recalculating

Because UI elements don't get spinners
2024-09-30 13:55:47 -04:00
Garrick Aden-Buie
09d415502f docs(NEWS): Fix name of sliderInput() function (#4136) 2024-09-27 11:14:40 -04:00
Garrick Aden-Buie
c489fef4ff fix(input_slider): Make sure last used handle is always above others (#4131) 2024-09-27 10:09:43 -04:00
Garrick Aden-Buie
9d12b0fca7 fix(conditionalPanel): Coerce condition result to boolean (#4127)
Co-authored-by: Kamil Zyla <kamil@appsilon.com>
2024-09-27 09:38:15 -04:00
Garrick Aden-Buie
cc9b9d4e6a feat(pulse): Tweak pulse animation and height (#4122) 2024-09-27 09:31:44 -04:00
Joe Cheng
34f9e4484d Merge pull request #4134 from rstudio/test/fix-reactivity-timing
fix: Timing of throttle/debounce reactivity test
2024-09-25 15:51:29 -07:00
Garrick Aden-Buie
03a3f8f886 test(reactivity): Consolidate identical tests into for loop 2024-09-25 10:09:17 -04:00
Garrick Aden-Buie
b900db0c74 test: Update the other test 2024-09-24 22:05:52 -04:00
Garrick Aden-Buie
5fb3ebc2d9 ci: run tests again 2024-09-24 22:00:19 -04:00
Garrick Aden-Buie
fbc6b2df57 chore: take out debugging code 2024-09-24 21:32:27 -04:00
Garrick Aden-Buie
6208225354 fix: Tweak updates to avoid overlapping events 2024-09-24 21:22:40 -04:00
Garrick Aden-Buie
e22b693418 chore: show value in debug too 2024-09-24 21:01:29 -04:00
Garrick Aden-Buie
c7ca49c634 debug: Add debugging messages for debounce/throttle test 2024-09-24 20:44:31 -04:00
Carson Sievert
d84aa94762 Start new version (#4113)
* Start new version

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-08-01 10:28:25 -05:00
Carson Sievert
89e2c18531 v1.9.1 release candidate (#4112)
* v1.9.1 release candidate

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-08-01 09:42:31 -05:00
Joe Cheng
43d36c08dc Remove double-scaling in coordmap.getPanelCss() (#4111)
Fixes #4110
2024-07-31 11:31:31 -05:00
Carson Sievert
4bc330e5dd Start new version (#4108)
* Start new version

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-07-29 17:21:35 -05:00
Carson Sievert
56ab530d87 v1.9.0 release candidate (#4105)
* Start v1.9.0 release candidate

* Check-in revdep results

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* `yarn build` (GitHub Actions)

* ran revdepcheck on cloud. 2 errors reported. Both seem like false positives

* Fix R CMD check note about Rd links targets missing package anchors

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
Co-authored-by: Barret Schloerke <barret@posit.co>
Co-authored-by: schloerke <schloerke@users.noreply.github.com>
2024-07-29 17:10:38 -05:00
Garrick Aden-Buie
599209a036 chore: make pulse and spinner opt-in (for now) (#4107)
* chore: make pulse and spinner opt-in (for now)

* Reword busy indication NEWS section

---------

Co-authored-by: Carson <cpsievert1@gmail.com>
2024-07-29 11:21:28 -05:00
Joe Cheng
15b5fa6c01 Click handler on scaled image getting clipped (#4094)
* Fix #3234: Click handler on scaled image getting clipped

There were two related problems here, both happening in the same scenario:
when an imageOutput with click handlers is showing an image at less than
its natural size (e.g. a 1000x1000 px .png file, being displayed in the
web page at 500x500 due to max-width or for whatever other reason), any
click where the image coordinate (1000x1000) exceeds the display size
(500x500).

In the example above, a user clicks at 300x300 in the 500x500 displayed
image. We call 300x300 the "CSS coordinates". This gets scaled up into
the position in the PNG's own coordinate system, "image coordinates":
in this case, 600x600. Since the 600x600 image coordinate is greater
than the 500x500 CSS coordinate limit, the following issues were
triggered.

1. When imageOutput(click=clickOpts(clip=TRUE)) (the default), these
   clicks weren't registering at all. There was code that detected
   clicks that were inside the imageOutput but outside the actual image,
   but this code didn't take scaling into account.

2. Even with clip=FALSE, the click would be triggered BUT the `x` and `y`
   values on the click event were incorrect--they would max out at the
   CSS coordinate limit. This because plot and image output divide the
   world into "panels" and clicks snap to the nearest panel. In the case
   of image outputs, the server doesn't provide any panels, so the
   client makes one big panel that covers the whole image--but that code
   was erroneously using CSS sizes, not image sizes.

* Update NEWS
2024-07-26 11:09:39 -05:00
Carson Sievert
3f4676d9a6 Enable busy indicators by default, add ability to disable/customize fade (#4104)
* Follow up to #4040: enable busy indicators by default

* Make our spinner invisible when wrapped inside a shinycssloaders::withSpinner() container

* Add the ability to disable/customize recalculating opacity (i.e., fade)

* Fix bug with fade not being applied correctly when the output container has no children

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

* `yarn build` (GitHub Actions)

* Update NEWS.md

* Follow up to b7e7af: need to also rest opacity for :empty case (for initial calculation)

* Rd docs fixes/improvements

---------

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2024-07-24 12:57:42 -05:00
Winston Chang
bb89cf9235 Add Shiny.initializedPromise (#4063)
* Convert Shiny from interface to class

* Remove unused global Shiny type

* Add prettier plugin for organizing imports

* Disable eslint indentation rule

* Simplify types

* Add Shiny.connectedPromise and Shiny.sessionInitPromise

* Fix typing issue

* Move prettier plugin to devDependencies

* Rename Shiny class to ShinyClass, and export type

* Remove global Shiny type; use internal imports

* Small code cleanup

* Move initShiny() function into ShinyClass

* Rebuild type files

* Raise error if window.Shiny already exists

* Rename promises

* Add InitStatusPromise class

* `yarn build` (GitHub Actions)

* Update news

* Remove isConnected

* Update yarn.lock

* Rename isInitialized to initializedPromise

* Rebuild shiny.js

* `yarn build` (GitHub Actions)

* Update NEWS

---------

Co-authored-by: wch <wch@users.noreply.github.com>
2024-07-23 22:11:20 -05:00
63 changed files with 4513 additions and 4358 deletions

View File

@@ -35,10 +35,6 @@ rules:
default-case:
- error
indent:
- error
- 2
- SwitchCase: 1
linebreak-style:
- error
- unix

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.8.1.9001
Version: 1.9.1.9000
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@posit.co", comment = c(ORCID = "0000-0002-1576-2126")),
person("Joe", "Cheng", role = "aut", email = "joe@posit.co"),

42
NEWS.md
View File

@@ -2,7 +2,45 @@
## New features and improvements
* Added new functions, `useBusyIndicators()` and `busyIndicatorOptions()`, for enabling and customizing busy indication. Busy indicators provide a visual cue to users when the server is busy calculating outputs or otherwise serving requests to the client. When enabled, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. (#4040)
* Small improvements to the default pulse busy indicator to better blend with any background. It's also now slightly smaller by default. (#4122)
* When spinners and the pulse busy indicators are enabled, Shiny now shows the pulse indicator when dynamic UI elements are recalculating if no other spinners are present in the app. (#4137)
* Capture and send client window size and scroll dimensions to the server which can be accessed via `session$clientData`. (#4147)
## Bug fixes
* Fixed a bug in `conditionalPanel()` that would cause the panel to repeatedly show/hide itself when the provided condition was not boolean. (@kamilzyla, #4127)
* Fixed a bug with `sliderInput()` when used as a range slider that made it impossible to change the slider value when both handles were at the maximum value. (#4131)
* `dateInput` and `dateRangeInput` no longer send immediate updates to the server when the user is typing a date input. Instead, it waits until the user presses Enter or clicks out of the field to send the update, avoiding spurious and incorrect date values. Note that an update is still sent immediately when the field is cleared. (#3664)
* Fixed a bug in `onBookmark` hook that caused elements to not be excluded from URL bookmarking. (#3762)
# shiny 1.9.1
## Bug fixes
* Fixed a bug introduced in v1.9.0 where the boundaries of hover/click/brush regions on plots were being incorrectly scaled when browser zoom was used. (#4111)
# shiny 1.9.0
## New busy indication feature
Add the new `useBusyIndicators()` function to any UI definition to:
1. Add a spinner overlay on calculating/recalculating outputs.
2. Show a page-level pulsing banner when Shiny is busy calculating something (e.g., a download, side-effect, etc), but no calculating/recalculating outputs are visible.
In a future version of Shiny, busy indication will be enabled by default, so we encourage you to try it out now, provide feedback, and report any issues.
In addition, various properties of the spinners and pulse can be customized with `busyIndicatorOptions()`. For more details, see `?busyIndicatorOptions`. (#4040, #4104)
## New features and improvements
* The client-side TypeScript code for Shiny has been refactored so that the `Shiny` object is now an instance of class `ShinyClass`. (#4063)
* In TypeScript, the `Shiny` object has a new property `initializedPromise`, which is a Promise-like object that can be `await`ed or chained with `.then()`. This Promise-like object corresponds to the `shiny:sessioninitialized` JavaScript event, but is easier to use because it can be used both before and after the events have occurred. (#4063)
* Output bindings now include the `.recalculating` CSS class when they are first bound, up until the first render. This makes it possible/easier to show progress indication when the output is calculating for the first time. (#4039)
@@ -18,6 +56,8 @@
* Fixed a recent issue with `uiOutput()` and `conditionalPanel()` not properly lower opacity when recalculation (in a Bootstrap 5 context). (#4027)
* Image outputs that were scaled by CSS had certain regions that were unresponsive to hover/click/brush handlers. (#3234)
# shiny 1.8.1.1
* In v1.8.1, shiny.js starting throwing an error when input/output bindings have duplicate IDs. This error is now only thrown when `shiny::devmode(TRUE)` is enabled, so the issue is still made discoverable through the JS error console, but avoids unnecessarily breaking apps that happen to work with duplicate IDs. (#4019)

View File

@@ -99,13 +99,13 @@ saveShinySaveState <- function(state) {
# Encode the state to a URL. This does not save to disk.
encodeShinySaveState <- function(state) {
exclude <- c(state$exclude, "._bookmark_")
inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
# Allow user-supplied onSave function to do things like add state$values.
if (!is.null(state$onSave))
isolate(state$onSave(state))
exclude <- c(state$exclude, "._bookmark_")
inputVals <- serializeReactiveValues(state$input, exclude, stateDir = NULL)
inputVals <- vapply(inputVals,
function(x) toJSON(x, strict_atomic = FALSE),
character(1),

View File

@@ -172,9 +172,10 @@ setCurrentTheme <- function(theme) {
#' Register a theme dependency
#'
#' This function registers a function that returns an [htmlDependency()] or list
#' of such objects. If `session$setCurrentTheme()` is called, the function will
#' be re-executed, and the resulting html dependency will be sent to the client.
#' This function registers a function that returns an
#' [htmltools::htmlDependency()] or list of such objects. If
#' `session$setCurrentTheme()` is called, the function will be re-executed, and
#' the resulting html dependency will be sent to the client.
#'
#' Note that `func` should **not** be an anonymous function, or a function which
#' is defined within the calling function. This is so that,

View File

@@ -19,6 +19,8 @@
#' output.
#' @param pulse Whether to show a pulsing banner at the top of the page when the
#' app is busy.
#' @param fade Whether to fade recalculating outputs. A value of `FALSE` is
#' equivalent to `busyIndicatorOptions(fade_opacity=1)`.
#'
#' @export
#' @seealso [busyIndicatorOptions()] for customizing the appearance of the busy
@@ -48,7 +50,7 @@
#' }
#'
#' shinyApp(ui, server)
useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE, fade = TRUE) {
rlang::check_dots_empty()
@@ -62,20 +64,33 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
}
})
js <- HTML(paste(js, collapse = "\n"))
# TODO: it'd be nice if htmltools had something like a page_attrs() that allowed us
# to do this without needing to inject JS into the head.
tags$script(js)
res <- tags$script(HTML(paste(js, collapse = "\n")))
if (!fade) {
res <- tagList(res, fadeOptions(opacity = 1))
}
res
}
#' Customize busy indicator options
#'
#' When busy indicators are enabled (see [useBusyIndicators()]), a spinner is
#' shown on each calculating/recalculating output, and a pulsing banner is shown
#' at the top of the page when the app is otherwise busy. This function allows
#' you to customize the appearance of those busy indicators. To apply the
#' customization, include the result of this function inside the app's UI.
#' @description
#' Shiny automatically includes busy indicators, which more specifically means:
#' 1. Calculating/recalculating outputs have a spinner overlay.
#' 2. Outputs fade out/in when recalculating.
#' 3. When no outputs are calculating/recalculating, but Shiny is busy
#' doing something else (e.g., a download, side-effect, etc), a page-level
#' pulsing banner is shown.
#'
#' This function allows you to customize the appearance of these busy indicators
#' by including the result of this function inside the app's UI. Note that,
#' unless `spinner_selector` (or `fade_selector`) is specified, the spinner/fade
#' customization applies to the parent element. If the customization should
#' instead apply to the entire page, set `spinner_selector = 'html'` and
#' `fade_selector = 'html'`.
#'
#' @param ... Currently ignored.
#' @param spinner_type The type of spinner. Pre-bundled types include:
@@ -97,6 +112,11 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
#' @param spinner_selector A character string containing a CSS selector for
#' scoping the spinner customization. The default (`NULL`) will apply the
#' spinner customization to the parent element of the spinner.
#' @param fade_opacity The opacity (a number between 0 and 1) for recalculating
#' output. Set to 1 to "disable" the fade.
#' @param fade_selector A character string containing a CSS selector for
#' scoping the spinner customization. The default (`NULL`) will apply the
#' spinner customization to the parent element of the spinner.
#' @param pulse_background A CSS background definition for the pulse. The
#' default uses a
#' [linear-gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient)
@@ -107,7 +127,7 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) {
#' time.
#'
#' @export
#' @seealso [useBusyIndicators()] for enabling/disabling busy indicators.
#' @seealso [useBusyIndicators()] to disable/enable busy indicators.
#' @examplesIf rlang::is_interactive()
#'
#' library(bslib)
@@ -162,6 +182,8 @@ busyIndicatorOptions <- function(
spinner_size = NULL,
spinner_delay = NULL,
spinner_selector = NULL,
fade_opacity = NULL,
fade_selector = NULL,
pulse_background = NULL,
pulse_height = NULL,
pulse_speed = NULL
@@ -177,6 +199,7 @@ busyIndicatorOptions <- function(
delay = spinner_delay,
selector = spinner_selector
),
fadeOptions(opacity = fade_opacity, selector = fade_selector),
pulseOptions(
background = pulse_background,
height = pulse_height,
@@ -224,6 +247,26 @@ spinnerOptions <- function(type = NULL, color = NULL, size = NULL, delay = NULL,
tags$style(css, id = id)
}
fadeOptions <- function(opacity = NULL, selector = NULL) {
if (is.null(opacity) && is.null(selector)) {
return(NULL)
}
css_vars <- htmltools::css(
"--shiny-fade-opacity" = opacity
)
id <- NULL
if (is.null(selector)) {
id <- paste0("fade-options-", p_randomInt(100, 1000000))
selector <- sprintf(":has(> #%s)", id)
}
css <- HTML(paste0(selector, " {", css_vars, "}"))
tags$style(css, id = id)
}
pulseOptions <- function(background = NULL, height = NULL, speed = NULL) {
if (is.null(background) && is.null(height) && is.null(speed)) {
return(NULL)
@@ -244,6 +287,8 @@ busyIndicatorDependency <- function() {
version = get_package_version("shiny"),
src = "www/shared/busy-indicators",
package = "shiny",
stylesheet = "busy-indicators.css"
stylesheet = "busy-indicators.css",
# TODO-future: In next release make spinners and pulse opt-out
# head = as.character(useBusyIndicators())
)
}

View File

@@ -951,7 +951,10 @@ Observable <- R6Class(
#' See the [Shiny tutorial](https://shiny.rstudio.com/tutorial/) for
#' more information about reactive expressions.
#'
#' @param x For `is.reactive()`, an object to test. For `reactive()`, an expression. When passing in a [`quo()`]sure with `reactive()`, remember to use [`rlang::inject()`] to distinguish that you are passing in the content of your quosure, not the expression of the quosure.
#' @param x For `is.reactive()`, an object to test. For `reactive()`, an
#' expression. When passing in a [`rlang::quo()`]sure with `reactive()`,
#' remember to use [`rlang::inject()`] to distinguish that you are passing in
#' the content of your quosure, not the expression of the quosure.
#' @template param-env
#' @templateVar x x
#' @templateVar env env

View File

@@ -53,8 +53,8 @@ formalsAndBody <- function(x) {
#' @describeIn createRenderFunction convert a quosure to a function.
#' @param q Quosure of the expression `x`. When capturing expressions to create
#' your quosure, it is recommended to use [`enquo0()`] to not unquote the
#' object too early. See [`enquo0()`] for more details.
#' your quosure, it is recommended to use [`rlang::enquo0()`] to not unquote
#' the object too early. See [`rlang::enquo0()`] for more details.
#' @inheritParams installExprFunction
#' @export
quoToFunction <- function(

View File

@@ -1,2 +1,2 @@
/*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating{opacity:1}[data-shiny-busy-spinners] .recalculating:after{position:absolute;content:"";--_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg));--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));--_shiny-spinner-size: var(--shiny-spinner-size, 32px);--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);background:var(--_shiny-spinner-color);width:var(--_shiny-spinner-size);height:var(--_shiny-spinner-size);inset:calc(50% - var(--_shiny-spinner-size) / 2);mask-image:var(--_shiny-spinner-url);-webkit-mask-image:var(--_shiny-spinner-url);opacity:0;animation-delay:var(--_shiny-spinner-delay);animation-name:fade-in;animation-duration:.25s;animation-fill-mode:forwards}[data-shiny-busy-spinners] .recalculating>*:not(.recalculating){opacity:.2;transition:opacity .25s ease var(--shiny-spinner-delay, 1s)}[data-shiny-busy-spinners] .recalculating.shiny-html-output:after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, var(--bs-body-bg, #fff), var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), var(--bs-body-bg, #fff) ) );--_shiny-pulse-height: var(--shiny-pulse-height, 5px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(#shiny-disconnected-overlay):after{display:none}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, var(--bs-body-bg, #fff), var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), var(--bs-body-bg, #fff) ) );--_shiny-pulse-height: var(--shiny-pulse-height, 5px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:has(#shiny-disconnected-overlay):after{display:none}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes busy-page-pulse{0%{left:-75%;width:75%}50%{left:100%;width:75%}to{left:-75%;width:75%}}
/*! shiny 1.9.1.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating:after{position:absolute;content:"";--_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg));--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));--_shiny-spinner-size: var(--shiny-spinner-size, 32px);--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);background:var(--_shiny-spinner-color);width:var(--_shiny-spinner-size);height:var(--_shiny-spinner-size);inset:calc(50% - var(--_shiny-spinner-size) / 2);mask-image:var(--_shiny-spinner-url);-webkit-mask-image:var(--_shiny-spinner-url);opacity:0;animation-delay:var(--_shiny-spinner-delay);animation-name:fade-in;animation-duration:.25s;animation-fill-mode:forwards}[data-shiny-busy-spinners] .recalculating:has(>*),[data-shiny-busy-spinners] .recalculating:empty{opacity:1}[data-shiny-busy-spinners] .recalculating>*:not(.recalculating){opacity:var(--_shiny-fade-opacity);transition:opacity .25s ease var(--shiny-spinner-delay, 1s)}[data-shiny-busy-spinners] .recalculating.shiny-html-output:after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating:not(.shiny-html-output)):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(#shiny-disconnected-overlay):after{display:none}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:has(#shiny-disconnected-overlay):after{display:none}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes busy-page-pulse{0%{left:-14%;right:97%}45%{left:0%;right:14%}55%{left:14%;right:0%}to{left:97%;right:-14%}}.shiny-spinner-output-container{--shiny-spinner-size: 0px}

View File

@@ -235,6 +235,10 @@
z-index: 2;
}
.irs--shiny .irs-handle.type_last {
z-index: 3;
}
.irs--shiny .irs-handle.state_hover, .irs--shiny .irs-handle:hover {
background: #fff;
}

View File

@@ -143,6 +143,11 @@ $font-family: $font-family-base !default;
border-radius: $handle_width;
z-index: 2;
&.type_last {
// Ensure last-used handle is on top if it overlaps with another handle
z-index: 3;
}
&.state_hover,
&:hover {
background: $handle_color_hover;

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
/*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.9.1.9000 | (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}

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
/*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.9.1.9000 | (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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -44,9 +44,9 @@ div:where(.shiny-html-output) {
/* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */
&:has(> *) {
display: contents;
/* Pass along styles that no longer impact the pass-through container */
/* Pass along styles that no longer impact the pass-through container */
&.recalculating > * {
opacity: 0.3;
opacity: var(--_shiny-fade-opacity);
}
}
}

View File

@@ -156,7 +156,8 @@ html.autoreload-enabled #shiny-disconnected-overlay.reloading {
}
.recalculating {
opacity: 0.3;
--_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3);
opacity: var(--_shiny-fade-opacity);
transition: opacity 250ms ease 500ms;
}

View File

@@ -11,6 +11,8 @@ busyIndicatorOptions(
spinner_size = NULL,
spinner_delay = NULL,
spinner_selector = NULL,
fade_opacity = NULL,
fade_selector = NULL,
pulse_background = NULL,
pulse_height = NULL,
pulse_speed = NULL
@@ -45,6 +47,13 @@ if the computation finishes quickly.}
scoping the spinner customization. The default (\code{NULL}) will apply the
spinner customization to the parent element of the spinner.}
\item{fade_opacity}{The opacity (a number between 0 and 1) for recalculating
output. Set to 1 to "disable" the fade.}
\item{fade_selector}{A character string containing a CSS selector for
scoping the spinner customization. The default (\code{NULL}) will apply the
spinner customization to the parent element of the spinner.}
\item{pulse_background}{A CSS background definition for the pulse. The
default uses a
\href{https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient}{linear-gradient}
@@ -57,11 +66,21 @@ CSS size.}
time.}
}
\description{
When busy indicators are enabled (see \code{\link[=useBusyIndicators]{useBusyIndicators()}}), a spinner is
shown on each calculating/recalculating output, and a pulsing banner is shown
at the top of the page when the app is otherwise busy. This function allows
you to customize the appearance of those busy indicators. To apply the
customization, include the result of this function inside the app's UI.
Shiny automatically includes busy indicators, which more specifically means:
\enumerate{
\item Calculating/recalculating outputs have a spinner overlay.
\item Outputs fade out/in when recalculating.
\item When no outputs are calculating/recalculating, but Shiny is busy
doing something else (e.g., a download, side-effect, etc), a page-level
pulsing banner is shown.
}
This function allows you to customize the appearance of these busy indicators
by including the result of this function inside the app's UI. Note that,
unless \code{spinner_selector} (or \code{fade_selector}) is specified, the spinner/fade
customization applies to the parent element. If the customization should
instead apply to the entire page, set \code{spinner_selector = 'html'} and
\code{fade_selector = 'html'}.
}
\examples{
\dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
@@ -113,5 +132,5 @@ shinyApp(ui, server)
\dontshow{\}) # examplesIf}
}
\seealso{
\code{\link[=useBusyIndicators]{useBusyIndicators()}} for enabling/disabling busy indicators.
\code{\link[=useBusyIndicators]{useBusyIndicators()}} to disable/enable busy indicators.
}

View File

@@ -72,8 +72,8 @@ example, some render functions call \code{\link[=createWebDependency]{createWebD
is able to serve JS and CSS resources.}
\item{q}{Quosure of the expression \code{x}. When capturing expressions to create
your quosure, it is recommended to use \code{\link[=enquo0]{enquo0()}} to not unquote the
object too early. See \code{\link[=enquo0]{enquo0()}} for more details.}
your quosure, it is recommended to use \code{\link[rlang:defusing-advanced]{rlang::enquo0()}} to not unquote
the object too early. See \code{\link[rlang:defusing-advanced]{rlang::enquo0()}} for more details.}
\item{label}{A label for the object to be shown in the debugger. Defaults to
the name of the calling function.}

View File

@@ -18,7 +18,10 @@ reactive(
is.reactive(x)
}
\arguments{
\item{x}{For \code{is.reactive()}, an object to test. For \code{reactive()}, an expression. When passing in a \code{\link[=quo]{quo()}}sure with \code{reactive()}, remember to use \code{\link[rlang:inject]{rlang::inject()}} to distinguish that you are passing in the content of your quosure, not the expression of the quosure.}
\item{x}{For \code{is.reactive()}, an object to test. For \code{reactive()}, an
expression. When passing in a \code{\link[rlang:defusing-advanced]{rlang::quo()}}sure with \code{reactive()},
remember to use \code{\link[rlang:inject]{rlang::inject()}} to distinguish that you are passing in
the content of your quosure, not the expression of the quosure.}
\item{env}{The parent environment for the reactive expression. By default,
this is the calling environment, the same as when defining an ordinary

View File

@@ -12,9 +12,10 @@ registerThemeDependency(func)
of them.}
}
\description{
This function registers a function that returns an \code{\link[=htmlDependency]{htmlDependency()}} or list
of such objects. If \code{session$setCurrentTheme()} is called, the function will
be re-executed, and the resulting html dependency will be sent to the client.
This function registers a function that returns an
\code{\link[htmltools:htmlDependency]{htmltools::htmlDependency()}} or list of such objects. If
\code{session$setCurrentTheme()} is called, the function will be re-executed, and
the resulting html dependency will be sent to the client.
}
\details{
Note that \code{func} should \strong{not} be an anonymous function, or a function which

View File

@@ -4,7 +4,7 @@
\alias{useBusyIndicators}
\title{Enable/disable busy indication}
\usage{
useBusyIndicators(..., spinners = TRUE, pulse = TRUE)
useBusyIndicators(..., spinners = TRUE, pulse = TRUE, fade = TRUE)
}
\arguments{
\item{...}{Currently ignored.}
@@ -14,6 +14,9 @@ output.}
\item{pulse}{Whether to show a pulsing banner at the top of the page when the
app is busy.}
\item{fade}{Whether to fade recalculating outputs. A value of \code{FALSE} is
equivalent to \code{busyIndicatorOptions(fade_opacity=1)}.}
}
\description{
Busy indicators provide a visual cue to users when the server is busy

View File

@@ -3,7 +3,7 @@
"homepage": "https://shiny.rstudio.com",
"repository": "github:rstudio/shiny",
"name": "@types/rstudio-shiny",
"version": "1.8.1-alpha.9001",
"version": "1.9.1-alpha.9000",
"license": "GPL-3.0-only",
"main": "",
"browser": "",

View File

@@ -1,25 +1,43 @@
# Revdeps
## Failed to check (18)
## Failed to check (29)
|package |version |error |warning |note |
|:------------------|:-------|:-----|:-------|:----|
|bigPint |? | | | |
|bioCancer |? | | | |
|ctsem |3.9.1 |1 | | |
|diveR |? | | | |
|EBImage |? | | | |
|g3viz |? | | | |
|GeneNetworkBuilder |? | | | |
|grandR |? | | | |
|InterCellar |? | | | |
|LACE |? | | | |
|loon.shiny |? | | | |
|MatrixQCvis |? | | | |
|modchart |? | | | |
|multilevelcoda |1.2.3 |1 | | |
|omicsViewer |? | | | |
|RQuantLib |0.4.21 |1 | | |
|rstanarm |2.32.1 |1 | | |
|Seurat |? | | | |
|package |version |error |warning |note |
|:---------------|:-------|:-----|:-------|:----|
|alevinQC |? | | | |
|animalEKF |1.2 |1 | | |
|animaltracker |? | | | |
|antaresViz |? | | | |
|AovBay |0.1.0 |1 | | |
|bdclean |? | | | |
|chipPCR |1.0-2 |1 | | |
|ctsem |3.10.0 |1 | | |
|diveR |? | | | |
|DynNom |5.1 |1 | | |
|hydflood |? | | | |
|loon.shiny |? | | | |
|modchart |? | | | |
|MODIStsp |? | | | |
|protGear |? | | | |
|robmedExtra |0.1.0 |1 | | |
|RQuantLib |0.4.23 |1 | | |
|rstanarm |2.32.1 |1 | | |
|scPipe |? | | | |
|sen2r |? | | | |
|SensMap |0.7 |1 | | |
|Seurat |5.1.0 |1 | | |
|shinyTempSignal |0.0.8 |1 | | |
|Signac |1.13.0 |1 | | |
|simplevis |? | | | |
|statsr |0.3.0 |1 | | |
|teachingApps |? | | | |
|tidyvpc |1.5.1 |1 | | |
|TVTB |? | | | |
## New problems (2)
|package |version |error |warning |note |
|:-------|:-------|:--------|:-------|:------|
|[EgoCor](problems.md#egocor)|1.2.0 |1 __+1__ | |-1 |
|[mxfda](problems.md#mxfda)|0.2.1 | | |__+1__ |

View File

@@ -1,19 +1,36 @@
## revdepcheck results
We checked 1201 reverse dependencies (1191 from CRAN + 10 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package.
We checked 1221 reverse dependencies (1208 from CRAN + 13 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package.
* We saw 0 new problems
* We failed to check 8 packages
* We saw 2 new problems
* We failed to check 16 packages
Issues with CRAN packages are summarised below.
### New problems
(This reports the first line of each new failure)
* EgoCor
checking running R code from vignettes ... ERROR
* mxfda
checking installed package size ... NOTE
### Failed to check
* ctsem (NA)
* diveR (NA)
* grandR (NA)
* loon.shiny (NA)
* multilevelcoda (NA)
* RQuantLib (NA)
* rstanarm (NA)
* Seurat (NA)
* animalEKF (NA)
* AovBay (NA)
* chipPCR (NA)
* ctsem (NA)
* diveR (NA)
* DynNom (NA)
* loon.shiny (NA)
* robmedExtra (NA)
* RQuantLib (NA)
* rstanarm (NA)
* SensMap (NA)
* Seurat (NA)
* shinyTempSignal (NA)
* Signac (NA)
* statsr (NA)
* tidyvpc (NA)

View File

@@ -1 +1,93 @@
*Wow, no problems at all. :)*
# EgoCor
<details>
* Version: 1.2.0
* GitHub: https://github.com/julia-dyck/EgoCor
* Source code: https://github.com/cran/EgoCor
* Date/Publication: 2024-03-28 18:10:02 UTC
* Number of recursive dependencies: 74
Run `revdepcheck::cloud_details(, "EgoCor")` for more info
</details>
## Newly broken
* checking running R code from vignettes ... ERROR
```
Errors in running code in vignettes:
when running code in Intro_to_EgoCor.Rmd
...
Warning in gstat::fit.variogram(emp.sv, model = v, fit.sills = TRUE, fit.ranges = TRUE, :
linear model has singular covariance matrix
Warning in gstat::fit.variogram(emp.sv, model = v, fit.sills = TRUE, fit.ranges = TRUE, :
linear model has singular covariance matrix
Warning in gstat::fit.variogram(emp.sv, model = v, fit.sills = TRUE, fit.ranges = TRUE, :
No convergence after 200 iterations: try different initial values?
When sourcing Intro_to_EgoCor.R:
Error: missing value where TRUE/FALSE needed
Execution halted
Intro_to_EgoCor.Rmd using UTF-8... failed
```
## Newly fixed
* checking re-building of vignette outputs ... NOTE
```
Error(s) in re-building vignettes:
--- re-building Intro_to_EgoCor.Rmd using rmarkdown
```
## In both
* checking examples ... ERROR
```
Running examples in EgoCor-Ex.R failed
The error most likely occurred in:
> ### Name: vario.reg.prep
> ### Title: Adjustment for covariates before semi-variogram model fitting
> ### Aliases: vario.reg.prep
>
> ### ** Examples
>
> ## Example 1
...
+ }
Warning in check_dep_version() :
ABI version mismatch:
lme4 was built with Matrix ABI version 1
Current Matrix ABI version is 0
Please re-install lme4 from source or restore original Matrix package
Error in initializePtr() :
function 'cholmod_factor_ldetA' not provided by package 'Matrix'
Calls: <Anonymous> ... initialize -> <Anonymous> -> initializePtr -> .Call
Execution halted
```
# mxfda
<details>
* Version: 0.2.1
* GitHub: https://github.com/julia-wrobel/mxfda
* Source code: https://github.com/cran/mxfda
* Date/Publication: 2024-05-08 11:00:02 UTC
* Number of recursive dependencies: 220
Run `revdepcheck::cloud_details(, "mxfda")` for more info
</details>
## Newly broken
* checking installed package size ... NOTE
```
installed size is 5.6Mb
sub-directories of 1Mb or more:
data 4.0Mb
```

View File

@@ -37,9 +37,11 @@
the spinner. Undo that, but still apply (smaller) opacity to immediate children
that aren't recalculating.
*/
opacity: 1;
&:has(> *), &:empty {
opacity: 1;
}
> *:not(.recalculating) {
opacity: 0.2;
opacity: var(--_shiny-fade-opacity);
transition: opacity 250ms ease var(--shiny-spinner-delay, 1s);
}
@@ -62,15 +64,15 @@
--shiny-pulse-background,
linear-gradient(
120deg,
var(--bs-body-bg, #fff),
transparent,
var(--bs-indigo, #4b00c1),
var(--bs-purple, #74149c),
var(--bs-pink, #bf007f),
var(--bs-body-bg, #fff)
transparent
)
);
--_shiny-pulse-height: var(--shiny-pulse-height, 5px);
--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);
--_shiny-pulse-height: var(--shiny-pulse-height, 3px);
--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);
/* Color, sizing, & positioning */
position: fixed;
@@ -83,6 +85,7 @@
/* Animation */
animation-name: busy-page-pulse;
animation-duration: var(--_shiny-pulse-speed);
animation-direction: alternate;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
@@ -97,7 +100,9 @@
&.shiny-busy::after {
@include shiny-page-busy;
}
&.shiny-busy:has(.recalculating)::after {
// Hide the pulse if there are spinners on the page
// (Note: UI outputs don't get spinners)
&.shiny-busy:has(.recalculating:not(.shiny-html-output))::after {
display: none;
}
&.shiny-busy:has(#shiny-disconnected-overlay)::after {
@@ -128,16 +133,31 @@
/* Keyframes for the pulsing banner */
@keyframes busy-page-pulse {
0% {
left: -75%;
width: 75%;
left: -14%;
right: 97%;
}
50% {
left: 100%;
width: 75%;
45% {
left: 0%;
right: 14%;
}
/* Go back */
100% {
left: -75%;
width: 75%;
55% {
left: 14%;
right: 0%;
}
to {
left: 97%;
right: -14%;
}
}
// Effectively disable the spinner when it's wrapped in shinycssloader::withSpinner()
// since that's a sign our spinner isn't needed.
// The reason this sets size to 0px instead of display:none is so, if someone
// really wants to show the spinner, they can override this with a custom size.
.shiny-spinner-output-container {
--shiny-spinner-size: 0px;
}

View File

@@ -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;
}

View File

@@ -40,19 +40,15 @@ class DateInputBindingBase extends InputBinding {
el;
}
subscribe(el: HTMLElement, callback: (x: boolean) => void): void {
$(el).on(
"keyup.dateInputBinding input.dateInputBinding",
// event: Event
function () {
// Use normal debouncing policy when typing
callback(true);
}
);
// Don't update when in the middle of typing; listening on keyup or input
// tends to send spurious values to the server, based on unpredictable
// browser-dependant interpretation of partially-typed date strings.
$(el).on(
"changeDate.dateInputBinding change.dateInputBinding",
// event: Event
function () {
// Send immediately when clicked
// Or if typing, when enter pressed or focus lost
callback(false);
}
);

View File

@@ -161,19 +161,15 @@ class DateRangeInputBinding extends DateInputBindingBase {
this._setMax($endinput[0], $endinput.data("max-date"));
}
subscribe(el: HTMLElement, callback: (x: boolean) => void): void {
$(el).on(
"keyup.dateRangeInputBinding input.dateRangeInputBinding",
// event: Event
function () {
// Use normal debouncing policy when typing
callback(true);
}
);
// Don't update when in the middle of typing; listening on keyup or input
// tends to send spurious values to the server, based on unpredictable
// browser-dependant interpretation of partially-typed date strings.
$(el).on(
"changeDate.dateRangeInputBinding change.dateRangeInputBinding",
// event: Event
function () {
// Send immediately when clicked
// Or if typing, when enter pressed or focus lost
callback(false);
}
);

View File

@@ -18,11 +18,10 @@ import { TextInputBinding } from "./text";
import { TextareaInputBinding } from "./textarea";
// TODO-barret make this an init method
type InitInputBindings = {
function initInputBindings(): {
inputBindings: BindingRegistry<InputBinding>;
fileInputBinding: FileInputBinding;
};
function initInputBindings(): InitInputBindings {
} {
const inputBindings = new BindingRegistry<InputBinding>();
inputBindings.register(new TextInputBinding(), "shiny.textInput");

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { css, html, LitElement } from "lit";
import { Shiny } from "..";
import { ShinyClientError } from "../shiny/error";
const buttonStyles = css`

View File

@@ -162,8 +162,8 @@ function initCoordmap(
const bounds = {
top: 0,
left: 0,
right: img.clientWidth - 1,
bottom: img.clientHeight - 1,
right: img.naturalWidth - 1,
bottom: img.naturalHeight - 1,
};
coordmap_.panels[0] = {
@@ -290,11 +290,10 @@ function initCoordmap(
const matches = []; // Panels that match
const dists = []; // Distance of offset to each matching panel
let b;
let i;
for (i = 0; i < coordmap.panels.length; i++) {
b = coordmap.panels[i].range;
const b = coordmap.panels[i].range;
if (
x <= b.right + expandImg.x &&
@@ -413,5 +412,5 @@ function initCoordmap(
return coordmap;
}
export { findOrigin, initCoordmap };
export type { Coordmap, CoordmapInit };
export { initCoordmap, findOrigin };

View File

@@ -1,3 +1,4 @@
import { init } from "./initialize";
export { Shiny, type ShinyClass } from "./initialize";
init();

View File

@@ -2,15 +2,20 @@ import { determineBrowserInfo } from "./browser";
import { disableFormSubmission } from "./disableForm";
import { trackHistory } from "./history";
import { setShiny } from "../shiny";
import { ShinyClass } from "../shiny";
import { setUserAgent } from "../utils/userAgent";
import { windowShiny } from "../window/libraries";
import { windowUserAgent } from "../window/userAgent";
import { initReactlog } from "../shiny/reactlog";
// eslint-disable-next-line @typescript-eslint/naming-convention
let Shiny: ShinyClass;
function init(): void {
setShiny(windowShiny());
if (window.Shiny) {
throw new Error("Trying to create window.Shiny, but it already exists!");
}
Shiny = window.Shiny = new ShinyClass();
setUserAgent(windowUserAgent()); // before determineBrowserInfo()
determineBrowserInfo();
@@ -21,4 +26,4 @@ function init(): void {
initReactlog();
}
export { init };
export { init, Shiny, type ShinyClass };

View File

@@ -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";

View File

@@ -3,10 +3,33 @@ import $ from "jquery";
import { InputBinding, OutputBinding } from "../bindings";
import { initInputBindings } from "../bindings/input";
import { initOutputBindings } from "../bindings/output";
import type { BindingRegistry } from "../bindings/registry";
import { showErrorInClientConsole } from "../components/errorConsole";
import { resetBrush } from "../imageutils/resetBrush";
import { $escape, compareVersion } from "../utils";
import { initShiny } from "./init";
import type { InputPolicy } from "../inputPolicies";
import {
InputBatchSender,
InputDeferDecorator,
InputEventDecorator,
InputNoResendDecorator,
InputRateDecorator,
InputValidateDecorator,
} from "../inputPolicies";
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
import { addDefaultInputOpts } from "../inputPolicies/inputValidateDecorator";
import { debounce, Debouncer } from "../time";
import {
$escape,
compareVersion,
getComputedLinkColor,
getStyle,
hasDefinedProperty,
mapValues,
pixelRatio,
} from "../utils";
import { createInitStatus, type InitStatusPromise } from "../utils/promise";
import type { BindInputsCtx, BindScope } from "./bind";
import { bindAll, unbindAll, _bindAll } from "./bind";
import type {
shinyBindAll,
shinyForgetLastInputValue,
@@ -14,11 +37,12 @@ import type {
shinySetInputValue,
shinyUnbindAll,
} from "./initedMethods";
import { setFileInputBinding } from "./initedMethods";
import { setFileInputBinding, setShinyObj } from "./initedMethods";
import { removeModal, showModal } from "./modal";
import { removeNotification, showNotification } from "./notifications";
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
import {
registerDependency,
renderContent,
renderContentAsync,
renderDependencies,
@@ -26,17 +50,18 @@ import {
renderHtml,
renderHtmlAsync,
} from "./render";
import type { Handler, ShinyApp } from "./shinyapp";
import { addCustomMessageHandler } from "./shinyapp";
import { sendImageSizeFns } from "./sendImageSize";
import { addCustomMessageHandler, ShinyApp, type Handler } from "./shinyapp";
import { registerNames as singletonsRegisterNames } from "./singletons";
interface Shiny {
class ShinyClass {
version: string;
$escape: typeof $escape;
compareVersion: typeof compareVersion;
inputBindings: ReturnType<typeof initInputBindings>["inputBindings"];
inputBindings: BindingRegistry<InputBinding>;
// eslint-disable-next-line @typescript-eslint/naming-convention
InputBinding: typeof InputBinding;
outputBindings: ReturnType<typeof initOutputBindings>["outputBindings"];
outputBindings: BindingRegistry<OutputBinding>;
// eslint-disable-next-line @typescript-eslint/naming-convention
OutputBinding: typeof OutputBinding;
resetBrush: typeof resetBrush;
@@ -45,7 +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,77 +92,650 @@ 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;
});
initialValues[".clientdata_window_width"] = window.innerWidth;
initialValues[".clientdata_window_height"] = window.innerHeight;
initialValues[".clientdata_scroll_width"] =
document.documentElement.scrollWidth;
initialValues[".clientdata_scroll_height"] =
document.documentElement.scrollHeight;
function doSendWindowSize() {
inputs.setInput(".clientdata_window_width", window.innerWidth);
inputs.setInput(".clientdata_window_height", window.innerHeight);
inputs.setInput(
".clientdata_scroll_width",
document.documentElement.scrollWidth
);
inputs.setInput(
".clientdata_scroll_height",
document.documentElement.scrollHeight
);
}
$(window).resize(debounce(500, doSendWindowSize));
// 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 };

View File

@@ -1,565 +0,0 @@
import $ from "jquery";
import type { Shiny } from ".";
import type { InputPolicy } from "../inputPolicies";
import {
InputBatchSender,
InputDeferDecorator,
InputEventDecorator,
InputNoResendDecorator,
InputRateDecorator,
InputValidateDecorator,
} from "../inputPolicies";
import type { InputPolicyOpts } from "../inputPolicies/inputPolicy";
import { addDefaultInputOpts } from "../inputPolicies/inputValidateDecorator";
import { debounce, Debouncer } from "../time";
import {
getComputedLinkColor,
getStyle,
hasDefinedProperty,
mapValues,
pixelRatio,
} from "../utils";
import type { BindInputsCtx, BindScope } from "./bind";
import { bindAll, unbindAll, _bindAll } from "./bind";
import { setShinyObj } from "./initedMethods";
import { registerDependency } from "./render";
import { sendImageSizeFns } from "./sendImageSize";
import { ShinyApp } from "./shinyapp";
import { registerNames as singletonsRegisterNames } from "./singletons";
// "init_shiny.js"
async function initShiny(windowShiny: Shiny): Promise<void> {
setShinyObj(windowShiny);
const shinyapp = (windowShiny.shinyapp = new ShinyApp());
windowShiny.progressHandlers = shinyapp.progressHandlers;
const inputBatchSender = new InputBatchSender(shinyapp);
const inputsNoResend = new InputNoResendDecorator(inputBatchSender);
const inputsEvent = new InputEventDecorator(inputsNoResend);
const inputsRate = new InputRateDecorator(inputsEvent);
const inputsDefer = new InputDeferDecorator(inputsEvent);
let target: InputPolicy;
if ($('input[type="submit"], button[type="submit"]').length > 0) {
// If there is a submit button on the page, use defer decorator
target = inputsDefer;
$('input[type="submit"], button[type="submit"]').each(function () {
$(this).click(function (event) {
event.preventDefault();
inputsDefer.submit();
});
});
} else {
// By default, use rate decorator
target = inputsRate;
}
const inputs = new InputValidateDecorator(target);
windowShiny.setInputValue = windowShiny.onInputChange = function (
name: string,
value: unknown,
opts: Partial<InputPolicyOpts> = {}
): void {
const newOpts = addDefaultInputOpts(opts);
inputs.setInput(name, value, newOpts);
};
// By default, Shiny deduplicates input value changes; that is, if
// `setInputValue` is called with the same value as the input already
// has, the call is ignored (unless opts.priority = "event"). Calling
// `forgetLastInputValue` tells Shiny that the very next call to
// `setInputValue` for this input id shouldn't be ignored, even if it
// is a dupe of the existing value.
windowShiny.forgetLastInputValue = function (name) {
inputsNoResend.forget(name);
};
// MUST be called after `setShiny()`
const inputBindings = windowShiny.inputBindings;
const outputBindings = windowShiny.outputBindings;
function shinyBindCtx(): BindInputsCtx {
return {
inputs,
inputsRate,
sendOutputHiddenState,
maybeAddThemeObserver,
inputBindings,
outputBindings,
initDeferredIframes,
};
}
windowShiny.bindAll = async function (scope: BindScope) {
await bindAll(shinyBindCtx(), scope);
};
windowShiny.unbindAll = function (scope: BindScope, includeSelf = false) {
unbindAll(shinyBindCtx(), scope, includeSelf);
};
// Calls .initialize() for all of the input objects in all input bindings,
// in the given scope.
function initializeInputs(scope: BindScope = document.documentElement) {
const bindings = inputBindings.getBindings();
// Iterate over all bindings
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i].binding;
const inputObjects = binding.find(scope);
if (inputObjects) {
// Iterate over all input objects for this binding
for (let j = 0; j < inputObjects.length; j++) {
const $inputObjectJ = $(inputObjects[j]);
if (!$inputObjectJ.data("_shiny_initialized")) {
$inputObjectJ.data("_shiny_initialized", true);
binding.initialize(inputObjects[j]);
}
}
}
}
}
windowShiny.initializeInputs = initializeInputs;
function getIdFromEl(el: HTMLElement) {
const $el = $(el);
const bindingAdapter = $el.data("shiny-output-binding");
if (!bindingAdapter) return null;
else return bindingAdapter.getId();
}
// Initialize all input objects in the document, before binding
initializeInputs(document.documentElement);
// The input values returned by _bindAll() each have a structure like this:
// { value: 123, opts: { ... } }
// We want to only keep the value. This is because when the initialValues is
// passed to ShinyApp.connect(), the ShinyApp object stores the
// initialValues object for the duration of the session, and the opts may
// have a reference to the DOM element, which would prevent it from being
// GC'd.
const initialValues = mapValues(
await _bindAll(shinyBindCtx(), document.documentElement),
(x) => x.value
);
// The server needs to know the size of each image and plot output element,
// in case it is auto-sizing
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
function () {
const id = getIdFromEl(this),
rect = this.getBoundingClientRect();
if (rect.width !== 0 || rect.height !== 0) {
initialValues[".clientdata_output_" + id + "_width"] = rect.width;
initialValues[".clientdata_output_" + id + "_height"] = rect.height;
}
}
);
function getComputedBgColor(
el: HTMLElement | null
): string | null | undefined {
if (!el) {
// Top of document, can't recurse further
return null;
}
const bgColor = getStyle(el, "background-color");
if (!bgColor) return bgColor;
const m = bgColor.match(
/^rgba\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/
);
if (bgColor === "transparent" || (m && parseFloat(m[4]) === 0)) {
// No background color on this element. See if it has a background image.
const bgImage = getStyle(el, "background-image");
if (bgImage && bgImage !== "none") {
// Failed to detect background color, since it has a background image
return null;
} else {
// Recurse
return getComputedBgColor(el.parentElement);
}
}
return bgColor;
}
function getComputedFont(el: HTMLElement) {
const fontFamily = getStyle(el, "font-family");
const fontSize = getStyle(el, "font-size");
return {
families: fontFamily?.replace(/"/g, "").split(", "),
size: fontSize,
};
}
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
function () {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const el = this;
const id = getIdFromEl(el);
initialValues[".clientdata_output_" + id + "_bg"] =
getComputedBgColor(el);
initialValues[".clientdata_output_" + id + "_fg"] = getStyle(el, "color");
initialValues[".clientdata_output_" + id + "_accent"] =
getComputedLinkColor(el);
initialValues[".clientdata_output_" + id + "_font"] = getComputedFont(el);
maybeAddThemeObserver(el);
}
);
// Resend computed styles if *an output element's* class or style attribute changes.
// This gives us some level of confidence that getCurrentOutputInfo() will be
// properly invalidated if output container is mutated; but unfortunately,
// we don't have a reasonable way to detect change in *inherited* styles
// (other than session$setCurrentTheme())
// https://github.com/rstudio/shiny/issues/3196
// https://github.com/rstudio/shiny/issues/2998
function maybeAddThemeObserver(el: HTMLElement): void {
if (!window.MutationObserver) {
return; // IE10 and lower
}
const cl = el.classList;
const reportTheme =
cl.contains("shiny-image-output") ||
cl.contains("shiny-plot-output") ||
cl.contains("shiny-report-theme");
if (!reportTheme) {
return;
}
const $el = $(el);
if ($el.data("shiny-theme-observer")) {
return; // i.e., observer is already observing
}
const observerCallback = new Debouncer(null, () => doSendTheme(el), 100);
const observer = new MutationObserver(() => observerCallback.normalCall());
const config = { attributes: true, attributeFilter: ["style", "class"] };
observer.observe(el, config);
$el.data("shiny-theme-observer", observer);
}
function doSendTheme(el: HTMLElement): void {
// Sending theme info on error isn't necessary (it'd add an unnecessary additional round-trip)
if (el.classList.contains("shiny-output-error")) {
return;
}
const id = getIdFromEl(el);
inputs.setInput(".clientdata_output_" + id + "_bg", getComputedBgColor(el));
inputs.setInput(".clientdata_output_" + id + "_fg", getStyle(el, "color"));
inputs.setInput(
".clientdata_output_" + id + "_accent",
getComputedLinkColor(el)
);
inputs.setInput(".clientdata_output_" + id + "_font", getComputedFont(el));
}
function doSendImageSize() {
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
function () {
const id = getIdFromEl(this),
rect = this.getBoundingClientRect();
if (rect.width !== 0 || rect.height !== 0) {
inputs.setInput(".clientdata_output_" + id + "_width", rect.width);
inputs.setInput(".clientdata_output_" + id + "_height", rect.height);
}
}
);
$(".shiny-image-output, .shiny-plot-output, .shiny-report-theme").each(
function () {
doSendTheme(this);
}
);
$(".shiny-bound-output").each(function () {
const $this = $(this),
binding = $this.data("shiny-output-binding");
$this.trigger({
type: "shiny:visualchange",
// @ts-expect-error; Can not remove info on a established, malformed Event object
visible: !isHidden(this),
binding: binding,
});
binding.onResize();
});
}
sendImageSizeFns.setImageSend(inputBatchSender, doSendImageSize);
// Return true if the object or one of its ancestors in the DOM tree has
// style='display:none'; otherwise return false.
function isHidden(obj: HTMLElement | null): boolean {
// null means we've hit the top of the tree. If width or height is
// non-zero, then we know that no ancestor has display:none.
if (obj === null || obj.offsetWidth !== 0 || obj.offsetHeight !== 0) {
return false;
} else if (getStyle(obj, "display") === "none") {
return true;
} else {
return isHidden(obj.parentNode as HTMLElement | null);
}
}
let lastKnownVisibleOutputs: { [key: string]: boolean } = {};
// Set initial state of outputs to hidden, if needed
$(".shiny-bound-output").each(function () {
const id = getIdFromEl(this);
if (isHidden(this)) {
initialValues[".clientdata_output_" + id + "_hidden"] = true;
} else {
lastKnownVisibleOutputs[id] = true;
initialValues[".clientdata_output_" + id + "_hidden"] = false;
}
});
// Send update when hidden state changes
function doSendOutputHiddenState() {
const visibleOutputs: { [key: string]: boolean } = {};
$(".shiny-bound-output").each(function () {
const id = getIdFromEl(this);
delete lastKnownVisibleOutputs[id];
// Assume that the object is hidden when width and height are 0
const hidden = isHidden(this),
evt = {
type: "shiny:visualchange",
visible: !hidden,
};
if (hidden) {
inputs.setInput(".clientdata_output_" + id + "_hidden", true);
} else {
visibleOutputs[id] = true;
inputs.setInput(".clientdata_output_" + id + "_hidden", false);
}
const $this = $(this);
// @ts-expect-error; Can not remove info on a established, malformed Event object
evt.binding = $this.data("shiny-output-binding");
// @ts-expect-error; Can not remove info on a established, malformed Event object
$this.trigger(evt);
});
// Anything left in lastKnownVisibleOutputs is orphaned
for (const name in lastKnownVisibleOutputs) {
if (hasDefinedProperty(lastKnownVisibleOutputs, name))
inputs.setInput(".clientdata_output_" + name + "_hidden", true);
}
// Update the visible outputs for next time
lastKnownVisibleOutputs = visibleOutputs;
}
// sendOutputHiddenState gets called each time DOM elements are shown or
// hidden. This can be in the hundreds or thousands of times at startup.
// We'll debounce it, so that we do the actual work once per tick.
const sendOutputHiddenStateDebouncer = new Debouncer(
null,
doSendOutputHiddenState,
0
);
function sendOutputHiddenState() {
sendOutputHiddenStateDebouncer.normalCall();
}
// We need to make sure doSendOutputHiddenState actually gets called before
// the inputBatchSender sends data to the server. The lastChanceCallback
// here does that - if the debouncer has a pending call, flush it.
inputBatchSender.lastChanceCallback.push(function () {
if (sendOutputHiddenStateDebouncer.isPending())
sendOutputHiddenStateDebouncer.immediateCall();
});
// Given a namespace and a handler function, return a function that invokes
// the handler only when e's namespace matches. For example, if the
// namespace is "bs", it would match when e.namespace is "bs" or "bs.tab".
// If the namespace is "bs.tab", it would match for "bs.tab", but not "bs".
function filterEventsByNamespace(
namespace: string,
handler: (...handlerArgs: any[]) => void,
...args: any[]
) {
const namespaceArr = namespace.split(".");
return function (this: HTMLElement, e: JQuery.TriggeredEvent) {
const eventNamespace = e.namespace?.split(".") ?? [];
// If any of the namespace strings aren't present in this event, quit.
for (let i = 0; i < namespaceArr.length; i++) {
if (eventNamespace.indexOf(namespaceArr[i]) === -1) return;
}
handler.apply(this, [namespaceArr, handler, ...args]);
};
}
// The size of each image may change either because the browser window was
// resized, or because a tab was shown/hidden (hidden elements report size
// of 0x0). It's OK to over-report sizes because the input pipeline will
// filter out values that haven't changed.
$(window).resize(debounce(500, sendImageSizeFns.regular));
// Need to register callbacks for each Bootstrap 3 class.
const bs3classes = [
"modal",
"dropdown",
"tab",
"tooltip",
"popover",
"collapse",
];
$.each(bs3classes, function (idx, classname) {
$(document.body).on(
"shown.bs." + classname + ".sendImageSize",
"*",
filterEventsByNamespace("bs", sendImageSizeFns.regular)
);
$(document.body).on(
"shown.bs." +
classname +
".sendOutputHiddenState " +
"hidden.bs." +
classname +
".sendOutputHiddenState",
"*",
filterEventsByNamespace("bs", sendOutputHiddenState)
);
});
// This is needed for Bootstrap 2 compatibility and for non-Bootstrap
// related shown/hidden events (like conditionalPanel)
$(document.body).on("shown.sendImageSize", "*", sendImageSizeFns.regular);
$(document.body).on(
"shown.sendOutputHiddenState hidden.sendOutputHiddenState",
"*",
sendOutputHiddenState
);
// Send initial pixel ratio, and update it if it changes
initialValues[".clientdata_pixelratio"] = pixelRatio();
$(window).resize(function () {
inputs.setInput(".clientdata_pixelratio", pixelRatio());
});
// Send initial URL
initialValues[".clientdata_url_protocol"] = window.location.protocol;
initialValues[".clientdata_url_hostname"] = window.location.hostname;
initialValues[".clientdata_url_port"] = window.location.port;
initialValues[".clientdata_url_pathname"] = window.location.pathname;
// Send initial URL search (query string) and update it if it changes
initialValues[".clientdata_url_search"] = window.location.search;
$(window).on("pushstate", function (e) {
inputs.setInput(".clientdata_url_search", window.location.search);
return;
e;
});
$(window).on("popstate", function (e) {
inputs.setInput(".clientdata_url_search", window.location.search);
return;
e;
});
// This is only the initial value of the hash. The hash can change, but
// a reactive version of this isn't sent because watching for changes can
// require polling on some browsers. The JQuery hashchange plugin can be
// used if this capability is important.
initialValues[".clientdata_url_hash_initial"] = window.location.hash;
initialValues[".clientdata_url_hash"] = window.location.hash;
$(window).on("hashchange", function (e) {
inputs.setInput(".clientdata_url_hash", window.location.hash);
return;
e;
});
// The server needs to know what singletons were rendered as part of
// the page loading
const singletonText = (initialValues[".clientdata_singletons"] = $(
'script[type="application/shiny-singletons"]'
).text());
singletonsRegisterNames(singletonText.split(/,/));
const dependencyText = $(
'script[type="application/html-dependencies"]'
).text();
$.each(dependencyText.split(/;/), function (i, depStr) {
const match = /\s*^(.+)\[(.+)\]\s*$/.exec(depStr);
if (match) {
registerDependency(match[1], match[2]);
}
});
// We've collected all the initial values--start the server process!
inputsNoResend.reset(initialValues);
shinyapp.connect(initialValues);
$(document).one("shiny:connected", function () {
initDeferredIframes();
});
// window.console.log("Shiny version: ", windowShiny.version);
} // function initShiny()
// Give any deferred iframes a chance to load.
function initDeferredIframes(): void {
// TODO-barret; This method uses `window.Shiny`. Could be replaced with `fullShinyObj_.shinyapp?.isConnected()`,
// but that would not use `window.Shiny`. Is it a problem???
if (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny.shinyapp ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Do not want to define `window.Shiny` as a type to discourage usage of `window.Shiny`;
// Can not expect error when combining with window available Shiny definition
!window.Shiny.shinyapp.isConnected()
) {
// If somehow we accidentally call this before the server connection is
// established, just ignore the call. At the time of this writing it
// doesn't happen, but it's easy to imagine a later refactoring putting
// us in this situation and it'd be hard to notice with either manual
// testing or automated tests, because the only effect is on HTTP request
// timing. (Update: Actually Aron saw this being called without even
// window.Shiny being defined, but it was hard to repro.)
return;
}
$(".shiny-frame-deferred").each(function (i, el) {
const $el = $(el);
$el.removeClass("shiny-frame-deferred");
// @ts-expect-error; If it is undefined, set using the undefined value
$el.attr("src", $el.attr("data-deferred-src"));
$el.attr("data-deferred-src", null);
});
}
export { initShiny };

View File

@@ -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;
}

View File

@@ -109,6 +109,9 @@ function addCustomMessageHandler(type: string, handler: Handler): void {
//// End message handler variables
/**
* The ShinyApp class handles the communication with the Shiny Server.
*/
class ShinyApp {
$socket: ShinyWebSocket | null = null;
@@ -611,7 +614,7 @@ class ShinyApp {
const nsPrefix = el.attr("data-ns-prefix") as string;
const nsScope = this._narrowScope(scope, nsPrefix);
const show = condFunc(nsScope);
const show = Boolean(condFunc(nsScope));
const showing = el.css("display") !== "none";
if (show !== showing) {
@@ -1610,4 +1613,4 @@ class ShinyApp {
}
export { ShinyApp, addCustomMessageHandler };
export type { Handler, ErrorsMessageValue };
export type { Handler, ErrorsMessageValue, ShinyWebSocket };

View File

@@ -148,7 +148,7 @@ function pixelRatio(): number {
//
// When the function is executed, it will evaluate that expression using
// "with" on the argument value, and return the result.
function scopeExprToFunc(expr: string): (scope: unknown) => boolean {
function scopeExprToFunc(expr: string): (scope: unknown) => unknown {
/*jshint evil: true */
const exprEscaped = expr
.replace(/[\\"']/g, "\\$&")
@@ -159,7 +159,7 @@ function scopeExprToFunc(expr: string): (scope: unknown) => boolean {
// \b has a special meaning; need [\b] to match backspace char.
.replace(/[\b]/g, "\\b");
let func: () => boolean;
let func: () => unknown;
try {
// @ts-expect-error; Do not know how to type this _dangerous_ situation
@@ -178,7 +178,7 @@ function scopeExprToFunc(expr: string): (scope: unknown) => boolean {
throw e;
}
return function (scope: unknown): boolean {
return function (scope: unknown): unknown {
return func.call(scope);
};
}

View File

@@ -0,0 +1,46 @@
// A shim for Promise.withResolvers. Once browser support is widespread, we can
// remove this.
export function promiseWithResolvers<T>(): {
promise: Promise<T>;
resolve: (value: PromiseLike<T> | T) => void;
reject: (reason?: any) => void;
} {
let resolve: (value: PromiseLike<T> | T) => void;
let reject: (reason?: any) => void;
const promise = new Promise(
(res: (value: PromiseLike<T> | T) => void, rej: (reason?: any) => void) => {
resolve = res;
reject = rej;
}
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return { promise, resolve: resolve!, reject: reject! };
}
export interface InitStatusPromise<T> extends Promise<T> {
promise: Promise<T>;
resolve(x: T): void;
resolved(): boolean;
}
export function createInitStatus<T>(): InitStatusPromise<T> {
const { promise, resolve } = promiseWithResolvers<T>();
// eslint-disable-next-line @typescript-eslint/naming-convention
let _resolved = false;
return {
promise,
resolve(x: T) {
_resolved = true;
resolve(x);
},
then: promise.then.bind(promise),
catch: promise.catch.bind(promise),
finally: promise.finally.bind(promise),
[Symbol.toStringTag]: "InitStatus",
resolved() {
return _resolved;
},
};
}

View File

@@ -1,12 +0,0 @@
import type { Shiny } from "../shiny";
function windowShiny(): Shiny {
// Use `any` type as we know what we are doing is _dangerous_
// Immediately init shiny on the window
if (!(window as any)["Shiny"]) {
(window as any)["Shiny"] = {};
}
return (window as any)["Shiny"];
}
export { windowShiny };

View File

@@ -1,8 +1,6 @@
import type { Shiny as RStudioShiny } from "../src/shiny/index";
import type { ShinyClass } from "../src/shiny/index";
declare global {
const Shiny: RStudioShiny;
interface Window {
Shiny: RStudioShiny;
Shiny: ShinyClass;
}
type Shiny = RStudioShiny;
}

View File

@@ -1,9 +1,8 @@
import { BindingRegistry } from "../registry";
import { InputBinding } from "./inputBinding";
import { FileInputBinding } from "./fileinput";
type InitInputBindings = {
declare function initInputBindings(): {
inputBindings: BindingRegistry<InputBinding>;
fileInputBinding: FileInputBinding;
};
declare function initInputBindings(): InitInputBindings;
export { initInputBindings, InputBinding };

View File

@@ -48,5 +48,5 @@ type Coordmap = {
mouseCoordinateSender: (inputId: string, clip?: boolean, nullOutside?: boolean) => (e: JQuery.MouseDownEvent | JQuery.MouseMoveEvent | null) => void;
};
declare function initCoordmap($el: JQuery<HTMLElement>, coordmap_: CoordmapInit): Coordmap;
export { findOrigin, initCoordmap };
export type { Coordmap, CoordmapInit };
export { initCoordmap, findOrigin };

View File

@@ -1 +1 @@
export {};
export { Shiny, type ShinyClass } from "./initialize";

View File

@@ -1,2 +1,4 @@
import { ShinyClass } from "../shiny";
declare let Shiny: ShinyClass;
declare function init(): void;
export { init };
export { init, Shiny, type ShinyClass };

View File

@@ -1,22 +1,21 @@
import { InputBinding, OutputBinding } from "../bindings";
import { initInputBindings } from "../bindings/input";
import { initOutputBindings } from "../bindings/output";
import type { BindingRegistry } from "../bindings/registry";
import { resetBrush } from "../imageutils/resetBrush";
import { $escape, compareVersion } from "../utils";
import { type InitStatusPromise } from "../utils/promise";
import type { shinyBindAll, shinyForgetLastInputValue, shinyInitializeInputs, shinySetInputValue, shinyUnbindAll } from "./initedMethods";
import { removeModal, showModal } from "./modal";
import { removeNotification, showNotification } from "./notifications";
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
import { renderContent, renderContentAsync, renderDependencies, renderDependenciesAsync, renderHtml, renderHtmlAsync } from "./render";
import type { Handler, ShinyApp } from "./shinyapp";
import { addCustomMessageHandler } from "./shinyapp";
interface Shiny {
import { addCustomMessageHandler, ShinyApp, type Handler } from "./shinyapp";
declare class ShinyClass {
version: string;
$escape: typeof $escape;
compareVersion: typeof compareVersion;
inputBindings: ReturnType<typeof initInputBindings>["inputBindings"];
inputBindings: BindingRegistry<InputBinding>;
InputBinding: typeof InputBinding;
outputBindings: ReturnType<typeof initOutputBindings>["outputBindings"];
outputBindings: BindingRegistry<OutputBinding>;
OutputBinding: typeof OutputBinding;
resetBrush: typeof resetBrush;
notifications: {
@@ -27,7 +26,6 @@ interface Shiny {
show: typeof showModal;
remove: typeof removeModal;
};
createSocket?: () => WebSocket;
showReconnectDialog: typeof showReconnectDialog;
hideReconnectDialog: typeof hideReconnectDialog;
renderDependenciesAsync: typeof renderDependenciesAsync;
@@ -36,9 +34,10 @@ interface Shiny {
renderContent: typeof renderContent;
renderHtmlAsync: typeof renderHtmlAsync;
renderHtml: typeof renderHtml;
user: string;
progressHandlers?: ShinyApp["progressHandlers"];
addCustomMessageHandler: typeof addCustomMessageHandler;
createSocket?: () => WebSocket;
user?: string;
progressHandlers?: ShinyApp["progressHandlers"];
shinyapp?: ShinyApp;
setInputValue?: typeof shinySetInputValue;
onInputChange?: typeof shinySetInputValue;
@@ -46,16 +45,16 @@ interface Shiny {
bindAll?: typeof shinyBindAll;
unbindAll?: typeof shinyUnbindAll;
initializeInputs?: typeof shinyInitializeInputs;
initializedPromise: InitStatusPromise<void>;
oncustommessage?: Handler;
constructor();
/**
* Method to check if Shiny is running in development mode. By packaging as a
* method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__`
* variable in the global scope.
* @returns `true` if Shiny is running in development mode, `false` otherwise.
*/
inDevMode: () => boolean;
inDevMode(): boolean;
initialize(): Promise<void>;
}
declare let windowShiny: Shiny;
declare function setShiny(windowShiny_: Shiny): void;
export { windowShiny, setShiny };
export type { Shiny };
export { ShinyClass };

View File

@@ -1,3 +0,0 @@
import type { Shiny } from ".";
declare function initShiny(windowShiny: Shiny): Promise<void>;
export { initShiny };

View File

@@ -1,10 +1,10 @@
import type { Shiny } from ".";
import type { ShinyClass } from ".";
import type { FileInputBinding } from "../bindings/input/fileinput";
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { EventPriority } from "../inputPolicies";
import type { BindScope } from "./bind";
import type { Handler, ShinyApp } from "./shinyapp";
declare function setShinyObj(shiny: Shiny): void;
declare function setShinyObj(shiny: ShinyClass): void;
declare function shinySetInputValue(name: string, value: unknown, opts?: {
priority?: EventPriority;
}): void;

View File

@@ -19,6 +19,9 @@ type InputValues = {
};
type MessageValue = Parameters<WebSocket["send"]>[0];
declare function addCustomMessageHandler(type: string, handler: Handler): void;
/**
* The ShinyApp class handles the communication with the Shiny Server.
*/
declare class ShinyApp {
$socket: ShinyWebSocket | null;
taskQueue: AsyncQueue<() => Promise<void> | void>;
@@ -104,4 +107,4 @@ declare class ShinyApp {
}): string;
}
export { ShinyApp, addCustomMessageHandler };
export type { Handler, ErrorsMessageValue };
export type { Handler, ErrorsMessageValue, ShinyWebSocket };

View File

@@ -10,7 +10,7 @@ declare function parseDate(dateString: string): Date;
declare function formatDateUTC(x: Date): string;
declare function makeResizeFilter(el: HTMLElement, func: (width: HTMLElement["offsetWidth"], height: HTMLElement["offsetHeight"]) => void): () => void;
declare function pixelRatio(): number;
declare function scopeExprToFunc(expr: string): (scope: unknown) => boolean;
declare function scopeExprToFunc(expr: string): (scope: unknown) => unknown;
declare function asArray<T>(value: T | T[] | null | undefined): T[];
declare function mergeSort<Item>(list: Item[], sortfunc: (a: Item, b: Item) => boolean | number): Item[];
declare function $escape(val: undefined): undefined;

11
srcts/types/src/utils/promise.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
export declare function promiseWithResolvers<T>(): {
promise: Promise<T>;
resolve: (value: PromiseLike<T> | T) => void;
reject: (reason?: any) => void;
};
export interface InitStatusPromise<T> extends Promise<T> {
promise: Promise<T>;
resolve(x: T): void;
resolved(): boolean;
}
export declare function createInitStatus<T>(): InitStatusPromise<T>;

View File

@@ -1,3 +0,0 @@
import type { Shiny } from "../shiny";
declare function windowShiny(): Shiny;
export { windowShiny };

View File

@@ -11,18 +11,18 @@ test_that("HTML has correct attributes", {
})
test_that("Github extensions are on by default", {
html <- markdown("a ~paragraph~ with a link: https://example.com")
html <- markdown("a ~~paragraph~~ with a link: https://example.com")
expect_equal(html, HTML("<p>a <del>paragraph</del> with a link: <a href=\"https://example.com\">https://example.com</a></p>\n"))
})
test_that("Github extensions can be disabled", {
html <- markdown("a ~paragraph~", extensions = FALSE)
expect_equal(html, HTML("<p>a ~paragraph~</p>\n"))
html <- markdown("a ~~paragraph~~", extensions = FALSE)
expect_equal(html, HTML("<p>a ~~paragraph~~</p>\n"))
})
test_that("Additional options are respected", {
html <- markdown("a ~paragraph~", extensions = FALSE, sourcepos = TRUE)
expect_equal(html, HTML("<p data-sourcepos=\"1:1-1:13\">a ~paragraph~</p>\n"))
html <- markdown("a ~~paragraph~~", extensions = FALSE, sourcepos = TRUE)
expect_equal(html, HTML("<p data-sourcepos=\"1:1-1:15\">a ~~paragraph~~</p>\n"))
})
test_that("Multiline markdown works properly", {

View File

@@ -1232,162 +1232,85 @@ test_that("event handling helpers take correct dependencies", {
})
test_that("debounce/throttle work properly (with priming)", {
do_priming <- TRUE
# Some of the CRAN test machines are heavily loaded and so the timing for
# these tests isn't reliable. https://github.com/rstudio/shiny/pull/2789
skip_on_cran()
for (do_priming in c(TRUE, FALSE)) {
label <- if (do_priming) "with priming" else "without priming"
test_that(sprintf("debounce/throttle work properly (%s)", label), {
# Some of the CRAN test machines are heavily loaded and so the timing for
# these tests isn't reliable. https://github.com/rstudio/shiny/pull/2789
skip_on_cran()
# The changing of rv$a will be the (chatty) source of reactivity.
rv <- reactiveValues(a = 0)
# The changing of rv$a will be the (chatty) source of reactivity.
rv <- reactiveValues(a = 0)
# This observer will be what changes rv$a.
src <- observe({
invalidateLater(100)
rv$a <- isolate(rv$a) + 1
})
on.exit(src$destroy(), add = TRUE)
# This observer will be what changes rv$a.
src <- observe({
invalidateLater(300)
rv$a <- isolate(rv$a) + 1
})
on.exit(src$destroy(), add = TRUE)
# Make a debounced reactive to test.
dr <- debounce(reactive(rv$a), 500)
# Make a debounced reactive to test.
dr <- debounce(reactive(rv$a), 500)
# Make a throttled reactive to test.
tr <- throttle(reactive(rv$a), 500)
# Make a throttled reactive to test.
tr <- throttle(reactive(rv$a), 500)
# Keep track of how often dr/tr are fired
dr_fired <- 0
dr_monitor <- observeEvent(dr(), {
dr_fired <<- dr_fired + 1
})
on.exit(dr_monitor$destroy(), add = TRUE)
# Keep track of how often dr/tr are fired
dr_fired <- 0
dr_monitor <- observeEvent(dr(), {
dr_fired <<- dr_fired + 1
})
on.exit(dr_monitor$destroy(), add = TRUE)
tr_fired <- 0
tr_monitor <- observeEvent(tr(), {
tr_fired <<- tr_fired + 1
})
on.exit(tr_monitor$destroy(), add = TRUE)
tr_fired <- 0
tr_monitor <- observeEvent(tr(), {
tr_fired <<- tr_fired + 1
})
on.exit(tr_monitor$destroy(), add = TRUE)
# Starting values are both 0. Earlier I found that the tests behaved
# differently if I accessed the values of dr/tr before the first call to
# flushReact(). That bug was fixed, but to ensure that similar bugs don't
# appear undetected, we run this test with and without do_priming.
if (do_priming) {
# Starting values are both 0. Earlier I found that the tests behaved
# differently if I accessed the values of dr/tr before the first call to
# flushReact(). That bug was fixed, but to ensure that similar bugs don't
# appear undetected, we run this test with and without do_priming.
if (do_priming) {
expect_identical(isolate(dr()), 0)
expect_identical(isolate(tr()), 0)
}
# Pump timer and reactives for about 1.3 seconds
stopAt <- Sys.time() + 1.3
while (Sys.time() < stopAt) {
timerCallbacks$executeElapsed()
flushReact()
Sys.sleep(0.001)
}
# dr() should not have had time to fire, other than the initial run, since
# there haven't been long enough gaps between invalidations.
expect_identical(dr_fired, 1)
# The value of dr() should not have updated either.
expect_identical(isolate(dr()), 0)
expect_identical(isolate(tr()), 0)
}
# Pump timer and reactives for about 1.3 seconds
stopAt <- Sys.time() + 1.3
while (Sys.time() < stopAt) {
timerCallbacks$executeElapsed()
flushReact()
Sys.sleep(0.001)
}
# tr() however, has had time to fire multiple times and update its value.
expect_identical(tr_fired, 3)
expect_identical(isolate(tr()), 4)
# dr() should not have had time to fire, other than the initial run, since
# there haven't been long enough gaps between invalidations.
expect_identical(dr_fired, 1)
# The value of dr() should not have updated either.
expect_identical(isolate(dr()), 0)
# Now let some time pass without any more updates.
src$destroy() # No more updates
stopAt <- Sys.time() + 1
while (Sys.time() < stopAt) {
timerCallbacks$executeElapsed()
flushReact()
Sys.sleep(0.001)
}
# tr() however, has had time to fire multiple times and update its value.
expect_identical(tr_fired, 3)
expect_identical(isolate(tr()), 10)
# Now let some time pass without any more updates.
src$destroy() # No more updates
stopAt <- Sys.time() + 1
while (Sys.time() < stopAt) {
timerCallbacks$executeElapsed()
flushReact()
Sys.sleep(0.001)
}
# dr should've fired, and we should have converged on the right answer.
expect_identical(dr_fired, 2)
isolate(expect_identical(rv$a, dr()))
expect_identical(tr_fired, 4)
isolate(expect_identical(rv$a, tr()))
})
# Identical to test block above, but with do_priming set to FALSE.
test_that("debounce/throttle work properly (without priming)", {
do_priming <- FALSE
# Some of the CRAN test machines are heavily loaded and so the timing for
# these tests isn't reliable. https://github.com/rstudio/shiny/pull/2789
skip_on_cran()
# The changing of rv$a will be the (chatty) source of reactivity.
rv <- reactiveValues(a = 0)
# This observer will be what changes rv$a.
src <- observe({
invalidateLater(100)
rv$a <- isolate(rv$a) + 1
# dr should've fired, and we should have converged on the right answer.
expect_identical(dr_fired, 2)
isolate(expect_identical(rv$a, dr()))
expect_identical(tr_fired, 4)
isolate(expect_identical(rv$a, tr()))
})
on.exit(src$destroy(), add = TRUE)
# Make a debounced reactive to test.
dr <- debounce(reactive(rv$a), 500)
# Make a throttled reactive to test.
tr <- throttle(reactive(rv$a), 500)
# Keep track of how often dr/tr are fired
dr_fired <- 0
dr_monitor <- observeEvent(dr(), {
dr_fired <<- dr_fired + 1
})
on.exit(dr_monitor$destroy(), add = TRUE)
tr_fired <- 0
tr_monitor <- observeEvent(tr(), {
tr_fired <<- tr_fired + 1
})
on.exit(tr_monitor$destroy(), add = TRUE)
# Starting values are both 0. Earlier I found that the tests behaved
# differently if I accessed the values of dr/tr before the first call to
# flushReact(). That bug was fixed, but to ensure that similar bugs don't
# appear undetected, we run this test with and without do_priming.
if (do_priming) {
expect_identical(isolate(dr()), 0)
expect_identical(isolate(tr()), 0)
}
# Pump timer and reactives for about 1.3 seconds
stopAt <- Sys.time() + 1.3
while (Sys.time() < stopAt) {
timerCallbacks$executeElapsed()
flushReact()
Sys.sleep(0.001)
}
# dr() should not have had time to fire, other than the initial run, since
# there haven't been long enough gaps between invalidations.
expect_identical(dr_fired, 1)
# The value of dr() should not have updated either.
expect_identical(isolate(dr()), 0)
# tr() however, has had time to fire multiple times and update its value.
expect_identical(tr_fired, 3)
expect_identical(isolate(tr()), 10)
# Now let some time pass without any more updates.
src$destroy() # No more updates
stopAt <- Sys.time() + 1
while (Sys.time() < stopAt) {
timerCallbacks$executeElapsed()
flushReact()
Sys.sleep(0.001)
}
# dr should've fired, and we should have converged on the right answer.
expect_identical(dr_fired, 2)
isolate(expect_identical(rv$a, dr()))
expect_identical(tr_fired, 4)
isolate(expect_identical(rv$a, tr()))
})
}
test_that("reactive domain works across async handlers", {
obj <- new.env()

View File

@@ -3,7 +3,7 @@ new file mode 100644
index 00000000..ba052f8b
--- /dev/null
+++ b/inst/www/shared/ionrangeslider/scss/shiny.scss
@@ -0,0 +1,201 @@
@@ -0,0 +1,206 @@
+/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
+© Posit, PBC, 2023
+© RStudio, Inc, 2014
@@ -149,6 +149,11 @@ index 00000000..ba052f8b
+ border-radius: $handle_width;
+ z-index: 2;
+
+ &.type_last {
+ // Ensure last-used handle is on top if it overlaps with another handle
+ z-index: 3;
+ }
+
+ &.state_hover,
+ &:hover {
+ background: $handle_color_hover;