Compare commits

...

95 Commits

Author SHA1 Message Date
Garrick Aden-Buie
10c2398990 Add jsdoc strings to shinyEvents.ts 2023-05-01 09:54:09 -04:00
Garrick Aden-Buie
064da3a883 yarn build 2023-04-27 10:05:33 -04:00
Garrick Aden-Buie
eddb0d9f22 Add more context in comments 2023-04-27 10:05:33 -04:00
Garrick Aden-Buie
4648423c54 Use more descriptive names for events, not evt 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
0adeb4bd36 DRY properties and type definitions
* Use getters to expose data from `this.event` as if they were top-level
* Refer to the interfaces directly rather than repeating type definitions
2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
ce4961f5ef EventBase.triggerOn() now accepts jquery html elements 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
4f50796a3d import $ from jquery 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
08ced6badb Use @babel/plugin-transform-typescript
It was already used by preset-typescript, but we need to use the
plugin so that the transpilation happens at the right time, in
particular because we are using `declare` within classes.
2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
a66ef6de01 Implement EventMessage for shiny:message 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
b5c6561dc9 Implement EventError for shiny:error events 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
2740be168a Implement EventValue for shiny:value 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
922d16d387 Implement EventUpdateInput for shiny:updateinput 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
651b768310 Implement one lower-level event class EventBase and use event rather than evt
It turns out the props on EventCommon aren't used everywhere
2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
63427a7178 Go back to using EvtFn() to extend jquery event handler 2023-04-27 10:03:58 -04:00
Garrick Aden-Buie
54fa8570a8 Use EventInputChanged in InputEventDecorator 2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
60f074daac Make the EventCommon class work more like an Event 2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
e2d19b9392 Use EventInputChanged() to emit file upload input event
We now do this in place rather than as a separate function,
i.e. we replace `triggerFileInputChanged()` with our improved
abstraction.
2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
0e2d040c54 Implement EventCommon and EventInputChanged classes
These classes create the custom Shiny events and help us extend JQuery's event handlers for these event types.
2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
8d12ae7351 Remove value, add el to ShinyEventInputChanged
Also mark `priority` as optional since it was optionally provided in practice
2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
90c052341f Allow any-typed arguments in exported functions 2023-04-27 10:03:57 -04:00
Garrick Aden-Buie
01fd8fac64 Remove el from ShinyEventCommon, value is any 2023-04-27 10:03:57 -04:00
Winston Chang
62bb21d5b6 Merge pull request #3804 from rstudio/docs/ex-download-button 2023-04-14 15:48:19 -05:00
Garrick Aden-Buie
4f85268d44 More complete downloadButton() example 2023-04-13 09:04:40 -04:00
Carson Sievert
611e517bb8 Rename actionQueue to taskQueue, add more context to the NEWS item (#3801) 2023-03-31 14:38:28 -05:00
Winston Chang
4d05a568c1 Remove unneeded packages from package.json 2023-03-06 17:01:43 -06:00
Winston Chang
1330325519 Rebuild docs 2023-03-01 21:38:10 -06:00
Winston Chang
92d850efa6 Rebuild shiny.js 2023-03-01 21:26:59 -06:00
Winston Chang
7bf56125eb Update @types/node 2023-03-01 21:26:59 -06:00
Winston Chang
69f861cc8a Rebuild yarn.lock 2023-03-01 21:20:56 -06:00
Winston Chang
a94be7b128 Fix brush resetting behavior. Closes #3785 2023-03-01 20:58:26 -06:00
Winston Chang
703766fb2e Merge pull request #3782 from rstudio/plot-interact-init 2023-02-24 16:59:42 -06:00
Winston Chang
8e73749e21 Bump fastmap dependency to 1.1.1 2023-02-24 10:30:07 -06:00
wch
dc8ffa115b Sync package version (GitHub Actions) 2023-02-23 20:32:23 +00:00
Winston Chang
a0385da0d7 Rebuild shiny.js 2023-02-23 14:18:21 -06:00
Winston Chang
a6b7dee4cd Send initial values for plot interaction 2023-02-23 14:14:01 -06:00
Winston Chang
f9ff5c2637 Bump version to 1.7.4.9002 2023-01-25 11:19:40 -06:00
Winston Chang
6a1fbc57f4 Clarify comments 2023-01-25 11:18:20 -06:00
Winston Chang
38337a926f Ensure that reactiveValues keys and values are sorted (#3774) 2023-01-25 11:10:05 -06:00
Winston Chang
bf6b87886c Merge pull request #3775 from rstudio/map-loadtime 2023-01-24 13:55:37 -06:00
Winston Chang
33e6b0a305 Add on_load function for registering expressions to run on load 2023-01-23 17:25:26 -06:00
Winston Chang
cb5eac052f Initialize Map objects at load time instead of build time 2023-01-23 16:26:44 -06:00
Winston Chang
39fee3782f Merge pull request #3772 from rstudio/fix-slider-stoppropagation 2023-01-23 10:59:47 -06:00
Winston Chang
654f30a312 Udpate NEWS 2023-01-20 17:04:21 -06:00
Winston Chang
a763da2b94 Fix stopPropagation error in ion.rangeSlider 2023-01-20 17:00:12 -06:00
Jon Calder
0c177d30dc Fix two typos in insertUI() docs (#3712)
* Fix two typos in insertUI() docs

* document()

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

* Update R/modal.R

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

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

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

* Update NEWS.md

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

* sync package version (GitHub Actions)

* yarn build (GitHub Actions)

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

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

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

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

This reverts commit 4ab4cb0e46.

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

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

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

* Sync package version (GitHub Actions)

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

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

* Clean up news

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* Remote remotes (htmltools is now in CRAN)

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

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2022-12-15 11:12:19 -06:00
Winston Chang
0fc1be52eb Render deps before modal or notification element is created 2022-12-13 17:21:59 -06:00
Carson Sievert
f12334e839 Properly check for NaN values upon resizing a brushable image. (#3754)
Regression introduced by https://github.com/rstudio/shiny/pull/3644/files#diff-9aad79e444091956075dc1e1dc5ab9202b5e998f5d441e69f040319b6c00d100L228-L230

JS error discovered by 104-plot-interaction-select (with showcase mode)
2022-12-08 14:32:27 -06:00
Winston Chang
ffb6736f11 Don't add "use strict" to external libraries (#3746)
* Add missing var in loop

* `yarn build` (GitHub Actions)

Co-authored-by: wch <wch@users.noreply.github.com>
2022-12-06 14:17:36 -06:00
Winston Chang
f084d3a34f Merge pull request #3747 from rstudio/eslint-newline-after-var
Remove eslint `newline-after-var rule`
2022-12-06 13:07:43 -06:00
Winston Chang
0fe7cad876 Remove eslint newline-after-var rule 2022-12-05 16:34:23 -06:00
Carson Sievert
ecff638920 Don't supply width/height to the device if they aren't defined (#3740)
* Close #1409: don't supply width/height to the device if they aren't defined

* Update news

* Update unit tests to reflect that plotPNG()/startPNG() now handles NULL dimensions

* Add a note about NULL dimensions on plotPNG() help page

* Update news
2022-12-02 20:27:07 -06:00
Winston Chang
db2ad780c0 Don't ignore errors when loading or executing a script 2022-12-01 17:16:59 -06:00
Winston Chang
5cd848bd28 Await running each action in actionQueue 2022-12-01 17:01:00 -06:00
Carson Sievert
ed6022e3f2 Have renderPlot() error early if height/width of a plot aren't yet defined (#3739)
* Close #3704. Close #3735. Close #1409. Throw informative error in renderPlot() early if height/width of a plot aren't yet defined

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

* Add unit tests

* Use consistent filename; add intentional failure (to get artifact uploads)

* Make output id argument name more unique

* Update news

* plotPNG() test isn't worth it

* Don't try to provide a suggestion on how to fix the issue (it's no worse than what we currently have, and we probably should be defaulting to an 'arbitrary' size anyway

* update news

* minimize diff

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2022-11-22 17:23:19 -06:00
Winston Chang
a063540407 Build shiny.js 2022-11-01 21:30:29 -05:00
Winston Chang
aa932532f3 Add sync and async versions of renderContent, renderHtml, renderDependencies 2022-11-01 21:30:15 -05:00
Winston Chang
8160f8c726 Add .d.ts files 2022-10-31 16:51:13 -05:00
Winston Chang
af900d1037 Use actionQueue 2022-10-31 16:51:13 -05:00
Winston Chang
49320e6edd Make HtmlOutputBinding.renderValue an async function 2022-10-31 16:51:13 -05:00
Winston Chang
4308887296 Fix types for message.multiple 2022-10-31 16:51:13 -05:00
Winston Chang
dffd8bc7fd Commit .d.ts files 2022-10-31 16:51:13 -05:00
Winston Chang
554f835293 Make sure not to send input values during dispatchMessage 2022-10-31 16:51:13 -05:00
Winston Chang
50e7b6768d Use async queue to handle incoming messages 2022-10-31 16:51:13 -05:00
Winston Chang
db222af7e0 Make sure dynamic scripts run in order 2022-10-31 16:51:13 -05:00
Winston Chang
5b688707b7 Add await for renderContent() calls 2022-10-31 16:51:13 -05:00
Winston Chang
8dfd8f5b33 Convert renderDependency() to async 2022-10-31 16:51:13 -05:00
Carson Sievert
20cc8e26b5 Use getBoundingClientRect() over offsetHeight/offsetWidth to get more precise sizing (#3720)
* Use getBoundingClientRect() over offsetHeight/offsetWidth to get more precise sizing

* Update news
2022-10-28 15:55:24 -05:00
Carson Sievert
e48e9c6904 Add fill arguments to plotOutput(), imageOutput(), and uiOutput() (#3715)
* Add fill arguments to plotOutput(), imageOutput(), and uiOutput()

* Update news

* Code review

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

* `yarn build` (GitHub Actions)

* Sync package version (GitHub Actions)

* Update news

* Update to use bindFillRole()

Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2022-10-26 11:52:26 -05:00
Joe Cheng
87c673f283 Bump version to v1.7.3.9000 2022-10-25 18:01:14 -07:00
Joe Cheng
dfaefa8905 Merge tag 'v1.7.3' 2022-10-25 17:59:18 -07:00
Joe Cheng
cd4f406234 Squelch R CMD check message
"Package has help file(s) containing install/render-stage \Sexpr{} expressions but no prebuilt PDF manual."
2022-10-24 18:37:15 -07:00
Joe Cheng
190b542613 Use v1.7.3 instead
Something in our yarn build toolchain doesn't like version numbers
with 4 segments
2022-10-24 16:54:54 -07:00
Joe Cheng
73e48ab5f4 Remove Remotes, add NEWS item 2022-10-24 16:22:42 -07:00
Barret Schloerke
62a95b9ce2 Reverting selectize logic change from #3644 (#3716) 2022-10-24 12:18:00 -04:00
Barret Schloerke
999eb1de3c Add fontawesome remote 2022-10-21 15:47:39 -04:00
Barret Schloerke
55985740de Skip template tests even if shinytest2 is available. Install shinytest2 from CRAN
We will bring these tests back after this release.
We will move shinytest2 to suggests after this release
2022-10-21 15:30:00 -04:00
Barret Schloerke
e82b71da65 Update template code to work with latest shinytest2 2022-10-21 14:54:05 -04:00
Joe Cheng
9ce1e6c549 Fix unit test to be compatible with fontawesome 0.4.0 2022-10-21 11:05:04 -07:00
Winston Chang
cda59da698 Remove types-jquery.patch (#3710)
Co-authored-by: wch <wch@users.noreply.github.com>
Co-authored-by: Barret Schloerke <schloerke@gmail.com>
2022-10-05 10:50:19 -04:00
Winston Chang
51da80d381 Merge pull request #3709 from rstudio/blob 2022-10-03 12:36:29 -05:00
wch
412606c594 yarn build (GitHub Actions) 2022-10-03 17:26:14 +00:00
Winston Chang
da2df5ac58 Use correct type for messages 2022-10-03 12:20:59 -05:00
131 changed files with 15421 additions and 6984 deletions

View File

@@ -27,6 +27,7 @@ rules:
- off
"@typescript-eslint/explicit-module-boundary-types":
- error
- allowArgumentsExplicitlyTypedAsAny: true
default-case:
- error
@@ -44,9 +45,6 @@ rules:
semi:
- error
- always
newline-after-var:
- error
- always
dot-location:
- error
- property

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.7.2.9000
Version: 1.7.4.9002
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com", comment = c(ORCID = "0000-0002-1576-2126")),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
@@ -78,8 +78,8 @@ Imports:
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
fontawesome (>= 0.2.1),
htmltools (>= 0.5.2),
fontawesome (>= 0.4.0),
htmltools (>= 0.5.4),
R6 (>= 2.0),
sourcetools,
later (>= 1.0.0),
@@ -87,7 +87,7 @@ Imports:
tools,
crayon,
rlang (>= 0.4.10),
fastmap (>= 1.1.0),
fastmap (>= 1.1.1),
withr,
commonmark (>= 1.7),
glue (>= 1.3.2),
@@ -202,10 +202,10 @@ Collate:
'version_selectize.R'
'version_strftime.R'
'viewer.R'
RoxygenNote: 7.2.1
RoxygenNote: 7.2.3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RdMacros: lifecycle
Config/testthat/edition: 3
Config/Needs/check:
rstudio/shinytest2
shinytest2

223
NEWS.md
View File

@@ -1,5 +1,4 @@
shiny 1.7.2.9000
================
# shiny 1.7.4.9002
## Full changelog
@@ -7,15 +6,43 @@ shiny 1.7.2.9000
### New features and improvements
* Internal: Added clearer and strict TypeScript type definitions (#3644)
* Closed #789: Dynamic UI is now rendered asynchronously, thanks in part to the newly exported `Shiny.renderDependenciesAsync()`, `Shiny.renderHtmlAsync()`, and `Shiny.renderContentAsync()`. Importantly, this means `<script>` tags are now loaded asynchronously (the old way used `XMLHttpRequest`, which is synchronous). In addition, `Shiny` now manages a queue of async tasks (exposed via `Shiny.shinyapp.taskQueue`) so that order of execution is preserved. (#3666)
* For `reactiveValues()` objects, whenever the `$names()` or `$values()` methods are called, the keys are now returned in the order that they were inserted. (#3774)
* `Map` objects are now initialized at load time instead of build time. This avoids potential problems that could arise from storing `fastmap` objects into the built Shiny package. (#3775)
### Bug fixes
* Fixed #3771: Sometimes the error `ion.rangeSlider.min.js: i.stopPropagation is not a function` would appear in the JavaScript console. (#3772)
# shiny 1.7.4
## Full changelog
### Breaking changes
* Closed #3719: Output container sizes, which are available via [`session$clientData` and `getCurrentOutputInfo()`](https://shiny.rstudio.com/articles/client-data.html), no longer round to the nearest pixel (i.e., they are now more exact, possibly fractional values). (#3720)
* Closed #3704, #3735, and #3740: `renderPlot()` no longer generates an error (or segfault) when it executes before the output is visible. Instead, it'll now use the graphics device's default size for it's initial size. Relatedly, `plotPNG()` now ignores `NULL` values for `width`/`height` (and uses the device's default `width`/`height` instead). (#3739)
### New features and improvements
* `plotOutput()`, `imageOutput()`, and `uiOutput()` gain a `fill` argument. If `TRUE` (the default for `plotOutput()`), the output container is allowed to grow/shrink to fit a fill container (created via `htmltools::bindFillRole()`) with an opinionated height. This means `plotOutput()` will grow/shrink by default [inside of `bslib::card_body_fill()`](https://rstudio.github.io/bslib/articles/cards.html#responsive-sizing), but `imageOutput()` and `uiOutput()` will have to opt-in to similar behavior with `fill = TRUE`. (#3715)
* Closed #3687: Updated jQuery-UI to v1.13.2. (#3697)
* Internal: Added clearer and strict TypeScript type definitions (#3644)
shiny 1.7.2
===========
# shiny 1.7.3
### Bug fixes
* Shiny 1.7.0 changed the `icon(lib="fontawesome")` implementation from a bundled copy of fontawesome, to the {fontawesome} package. This led to issue #3688, where icons that were previously working, were now breaking. That's because {fontawesome} 0.3.0 and earlier did not have support for icon names used in Font Awesome 5 and earlier, only the newest icon names used in Font Awesome 6. Now, {fontawesome} 0.4.0 has restored support for those older icon names, and Shiny 1.7.2.1 has updated its {fontawesome} requirement to >=0.4.0.
# shiny 1.7.2
## Full changelog
@@ -66,8 +93,7 @@ shiny 1.7.2
* HTML dependencies that are sent to dynamic UI now have better type checking, and no longer require a `dep.src.href` field. (#3537)
shiny 1.7.1
===========
# shiny 1.7.1
## Bug Fixes
@@ -76,8 +102,7 @@ shiny 1.7.1
* Re-arranged conditions for testthat 1.0.0 compatibility. (#3512)
shiny 1.7.0
===========
# shiny 1.7.0
## Full changelog
@@ -134,8 +159,7 @@ shiny 1.7.0
* Updated to jQuery 3.6.0. (#3311)
shiny 1.6.0
===========
# shiny 1.6.0
This release focuses on improvements in three main areas:
@@ -244,8 +268,7 @@ This release focuses on improvements in three main areas:
* Removed es5-shim library, which was internally used within `selectInput()` for ECMAScript 5 compatibility. (#2993)
shiny 1.5.0
===========
# shiny 1.5.0
## Full changelog
@@ -298,20 +321,17 @@ shiny 1.5.0
* Updated from Font-Awesome 5.3.1 to 5.13.0, which includes icons related to COVID-19. For upgrade notes, see https://github.com/FortAwesome/Font-Awesome/blob/master/UPGRADING.md. (#2891)
shiny 1.4.0.2
===========
# shiny 1.4.0.2
Minor patch release: fixed some timing-dependent tests failed intermittently on CRAN build machines.
shiny 1.4.0.1
===========
# shiny 1.4.0.1
Minor patch release to account for changes to the grid package that will be upcoming in the R 4.0 release (#2776).
shiny 1.4.0
===========
# shiny 1.4.0
## Full changelog
@@ -374,8 +394,7 @@ shiny 1.4.0
* Fixed #2329, #1817: These bugs were reported as fixed in Shiny 1.3.0 but were not actually fixed because some JavaScript changes were accidentally not included in the release. The fix resolves issues that occur when `withProgressBar()` or bookmarking are combined with the [networkD3](https://christophergandrud.github.io/networkD3/) package's Sankey plot.
shiny 1.3.2
===========
# shiny 1.3.2
### Bug fixes
@@ -384,8 +403,7 @@ shiny 1.3.2
* Fixed #2280: Shiny applications that used a www/index.html file did not serve up the index file. (#2382)
shiny 1.3.1
===========
# shiny 1.3.1
## Full changelog
@@ -394,8 +412,7 @@ shiny 1.3.1
* Fixed a performance issue introduced in v1.3.0 when using large nested lists within Shiny. (#2377)
shiny 1.3.0
===========
# shiny 1.3.0
## Full changelog
@@ -426,8 +443,7 @@ shiny 1.3.0
* Fixed #2247: `renderCachedPlot` now supports using promises for either `expr` or `cacheKeyExpr`. (Shiny v1.2.0 supported async `expr`, but only if `cacheKeyExpr` was async as well; now you can use any combination of sync/async for `expr` and `cacheKeyExpr`.) #2261
shiny 1.2.0
===========
# shiny 1.2.0
This release features plot caching, an important new tool for improving performance and scalability. Using `renderCachedPlot` in place of `renderPlot` can greatly improve responsiveness for apps that show the same plot many times (for example, a dashboard or report where all users view the same data). Shiny gives you a fair amount of control in where the cache is stored and how cached plots are invalidated, so be sure to read [this article](https://shiny.rstudio.com/articles/plot-caching.html) to get the most out of this feature.
@@ -492,8 +508,7 @@ This release features plot caching, an important new tool for improving performa
* Addressed #1864 by changing `optgroup` documentation to use `list` instead of `c`. (#2084)
shiny 1.1.0
===========
# shiny 1.1.0
This is a significant release for Shiny, with a major new feature that was nearly a year in the making: support for asynchronous operations! Until now, R's single-threaded nature meant that performing long-running calculations or tasks from Shiny would bring your app to a halt for other users of that process. This release of Shiny deeply integrates the [promises](https://rstudio.github.io/promises/) package to allow you to execute some tasks asynchronously, including as part of reactive expressions and outputs. See the [promises](https://rstudio.github.io/promises/) documentation to learn more.
@@ -566,8 +581,7 @@ This is a significant release for Shiny, with a major new feature that was nearl
In some rare cases, interrupting an application (by pressing Ctrl-C or Esc) may result in the message `Error in execCallbacks(timeoutSecs) : c++ exception (unknown reason)`. Although this message sounds alarming, it is harmless, and will go away in a future version of the later package (more information [here](https://github.com/r-lib/later/issues/55)).
shiny 1.0.5
===========
# shiny 1.0.5
## Full changelog
@@ -580,8 +594,7 @@ shiny 1.0.5
* Fixed #1824: HTTP HEAD requests on static files caused the application to stop. (#1825)
shiny 1.0.4
===========
# shiny 1.0.4
There are three headlining features in this release of Shiny. It is now possible to add and remove tabs from a `tabPanel`; there is a new function, `onStop()`, which registers callbacks that execute when an application exits; and `fileInput`s now can have files dragged and dropped on them. In addition to these features, this release has a number of minor features and bug fixes. See the full changelog below for more details.
@@ -642,8 +655,7 @@ There are three headlining features in this release of Shiny. It is now possible
* Fixed #1474: A `browser()` call in an observer could cause an error in the RStudio IDE on Windows. (#1802)
shiny 1.0.3
================
# shiny 1.0.3
This is a hotfix release of Shiny. With previous versions of Shiny, when running an application on the newly-released version of R, 3.4.0, it would print a message: `Warning in body(fun) : argument is not a function`. This has no effect on the application, but because the message could be alarming to users, we are releasing a new version of Shiny that fixes this issue.
@@ -656,8 +668,7 @@ This is a hotfix release of Shiny. With previous versions of Shiny, when running
* Fixed #1676: On R 3.4.0, running a Shiny application gave a warning: `Warning in body(fun) : argument is not a function`. (#1677)
shiny 1.0.2
================
# shiny 1.0.2
This is a hotfix release of Shiny. The primary reason for this release is because the web host for MathJax JavaScript library is scheduled to be shut down in the next few weeks. After it is shut down, Shiny applications that use MathJax will no longer be able to load the MathJax library if they are run with Shiny 1.0.1 and below. (If you don't know whether your application uses MathJax, it probably does not.) For more information about why the MathJax CDN is shutting down, see https://www.mathjax.org/cdn-shutting-down/.
@@ -676,8 +687,7 @@ This is a hotfix release of Shiny. The primary reason for this release is becaus
* Fixed #1653: wrong code example in documentation. (#1658)
shiny 1.0.1
================
# shiny 1.0.1
This is a maintenance release of Shiny, mostly aimed at fixing bugs and introducing minor features. The most notable additions in this version of Shiny are the introduction of the `reactiveVal()` function (it's like `reactiveValues()`, but it only stores a single value), and that the choices of `radioButtons()` and `checkboxGroupInput()` can now contain HTML content instead of just plain text.
@@ -747,8 +757,7 @@ in shiny apps. For more info, see the documentation (`?updateQueryString` and `?
* Closed #1500: Updated ion.rangeSlider to 2.1.6. (#1540)
shiny 1.0.0
===========
# shiny 1.0.0
Shiny has reached a milestone: version 1.0.0! In the last year, we've added two major features that we considered essential for a 1.0.0 release: bookmarking, and support for testing Shiny applications. As usual, this version of Shiny also includes many minor features and bug fixes.
@@ -813,8 +822,7 @@ Now there's an official way to slow down reactive values and expressions that in
* Updated to Font Awesome 4.7.0.
shiny 0.14.2
============
# shiny 0.14.2
This is a maintenance release of Shiny, with some bug fixes and minor new features.
@@ -842,8 +850,7 @@ This is a maintenance release of Shiny, with some bug fixes and minor new featur
* Fixed a bug where, in versions of R before 3.2, Shiny applications could crash due to a bug in R's implementation of `list2env()`. (#1446)
shiny 0.14.1
============
# shiny 0.14.1
This is a maintenance release of Shiny, with some bug fixes and minor new features.
@@ -873,8 +880,7 @@ This is a maintenance release of Shiny, with some bug fixes and minor new featur
* Updated to jQuery UI 1.12.1. Previously, Shiny included a build of 1.11.4 which was missing the datepicker component due to a conflict with the bootstrap-datepicker used by Shiny's `dateInput()` and `dateRangeInput()`. (#1374)
shiny 0.14
==========
# shiny 0.14
A new Shiny release is upon us! There are many new exciting features, bug fixes, and library updates. We'll just highlight the most important changes here, but you can browse through the full changelog below for details. This will likely be the last release before shiny 1.0, so get out your party hats!
@@ -1073,14 +1079,12 @@ There are many more minor features, small improvements, and bug fixes than we ca
* Updated to jQuery 1.12.4.
shiny 0.13.2
============
# shiny 0.13.2
* Updated documentation for `htmlTemplate`.
shiny 0.13.1
============
# shiny 0.13.1
* `flexCol` did not work on RStudio for Windows or Linux.
@@ -1089,8 +1093,7 @@ shiny 0.13.1
* BREAKING CHANGE: The long-deprecated ability to pass functions (rather than expressions) to reactive() and observe() has finally been removed.
shiny 0.13.0
============
# shiny 0.13.0
* Fixed #962: plot interactions did not work with the development version of ggplot2 (after ggplot2 1.0.1).
@@ -1141,8 +1144,7 @@ shiny 0.13.0
* Added support for the new htmltools 0.3 feature `htmlTemplate`. It's now possible to use regular HTML markup to design your UI, but still use R expressions to define inputs, outputs, and HTML widgets.
shiny 0.12.2
============
# shiny 0.12.2
* GitHub changed URLs for gists from .tar.gz to .zip, so `runGist` was updated to work with the new URLs.
@@ -1165,16 +1167,14 @@ shiny 0.12.2
* Shiny now correctly handles HTTP HEAD requests. (#876)
shiny 0.12.1
============
# shiny 0.12.1
* Fixed an issue where unbindAll() causes subsequent bindAll() to be ignored for previously bound outputs. (#856)
* Undeprecate `dataTableOutput` and `renderDataTable`, which had been deprecated in favor of the new DT package. The DT package is a bit too new and has a slightly different API, we were too hasty in deprecating the existing Shiny functions.
shiny 0.12.0
============
# shiny 0.12.0
In addition to the changes listed below (in the *Full Changelog* section), there is an infrastructure change that could affect existing Shiny apps.
@@ -1230,8 +1230,7 @@ Shiny 0.12.0 deprecated Shiny's dataTableOutput and renderDataTable functions an
* renderDataTable() and dataTableOutput() have been deprecated in shiny and will be removed in future versions of shiny. Please use the DT package instead: http://rstudio.github.io/DT/ (#807)
shiny 0.11.1
============
# shiny 0.11.1
* Major client-side performance improvements for pages that have many conditionalPanels, tabPanels, and plotOutputs. (#693, #717, #723)
@@ -1258,8 +1257,7 @@ shiny 0.11.1
* downloadHandler content callback functions are now invoked with a temp file name that has the same extension as the final filename that will be used by the download. This is to deal with the fact that some file writing functions in R will auto-append the extension for their file type (pdf, zip).
shiny 0.11
==========
# shiny 0.11
Shiny 0.11 switches away from the Bootstrap 2 web framework to the next version, Bootstrap 3. This is in part because Bootstrap 2 is no longer being developed, and in part because it allows us to tap into the ecosystem of Bootstrap 3 themes.
@@ -1337,20 +1335,17 @@ Along with the release of Shiny 0.11, we've packaged up some Bootstrap 3 themes
* Password input fields can now be used, with `passwordInput()`. (#672)
shiny 0.10.2.2
==============
# shiny 0.10.2.2
* Remove use of `rstudio::viewer` in a code example, for R CMD check.
shiny 0.10.2.1
==============
# shiny 0.10.2.1
* Changed some examples to use \donttest instead of \dontrun.
shiny 0.10.2
============
# shiny 0.10.2
* The minimal version of R required for the shiny package is 3.0.0 now.
@@ -1383,8 +1378,7 @@ shiny 0.10.2
* Added `position` parameter to `navbarPage`.
shiny 0.10.1
============
# shiny 0.10.1
* Added Unicode support for Windows. Shiny apps running on Windows must use the UTF-8 encoding for ui.R and server.R (also the optional global.R) if they contain non-ASCII characters. See this article for details and examples: http://shiny.rstudio.com/gallery/unicode-characters.html (#516)
@@ -1397,8 +1391,7 @@ shiny 0.10.1
* Added support for option groups in the select/selectize inputs. When the `choices` argument for `selectInput()`/`selectizeInput()` is a list of sub-lists and any sub-list is of length greater than 1, the HTML tag `<optgroup>` will be used. See an example at http://shiny.rstudio.com/gallery/option-groups-for-selectize-input.html (#542)
shiny 0.10.0
============
# shiny 0.10.0
* BREAKING CHANGE: By default, observers now terminate themselves if they were created during a session and that session ends. See ?domains for more details.
@@ -1435,14 +1428,12 @@ shiny 0.10.0
* `runGitHub()` can also take a value of the form "username/repo" in its first argument, e.g. both runGitHub("shiny_example", "rstudio") and runGitHub("rstudio/shiny_example") are valid ways to run the GitHub repo.
shiny 0.9.1
===========
# shiny 0.9.1
* Fixed warning 'Error in Context$new : could not find function "loadMethod"' that was happening to dependent packages on "R CMD check".
shiny 0.9.0
===========
# shiny 0.9.0
* BREAKING CHANGE: Added a `host` parameter to runApp() and runExample(), which defaults to the shiny.host option if it is non-NULL, or "127.0.0.1" otherwise. This means that by default, Shiny applications can only be accessed on the same machine from which they are served. To allow other clients to connect, as in previous versions of Shiny, use "0.0.0.0" (or the IP address of one of your network interfaces, if you care to be explicit about it).
@@ -1515,8 +1506,7 @@ shiny 0.9.0
* Dots are now legal characters for inputId/outputId. (Thanks, Kevin Lindquist. #358)
shiny 0.8.0
===========
# shiny 0.8.0
* Debug hooks are registered on all user-provided functions and (reactive) expressions (e.g., in renderPlot()), which makes it possible to set breakpoints in these functions using the latest version of the RStudio IDE, and the RStudio visual debugging tools can be used to debug Shiny apps. Internally, the registration is done via installExprFunction(), which is a new function introduced in this version to replace exprToFunction() so that the registration can be automatically done.
@@ -1535,8 +1525,7 @@ shiny 0.8.0
* The minimal required version for the httpuv package was increased to 1.2 (on CRAN now).
shiny 0.7.0
===========
# shiny 0.7.0
* Stopped sending websocket subprotocol. This fixes a compatibility issue with Google Chrome 30.
@@ -1565,8 +1554,7 @@ shiny 0.7.0
* Add shiny.sharedSecret option, to require the HTTP header Shiny-Shared-Secret to be set to the given value.
shiny 0.6.0
===========
# shiny 0.6.0
* `tabsetPanel()` can be directed to start with a specific tab selected.
@@ -1597,8 +1585,7 @@ shiny 0.6.0
* Shiny apps can be run without a server.r and ui.r file.
shiny 0.5.0
===========
# shiny 0.5.0
* Switch from websockets package for handling websocket connections to httpuv.
@@ -1615,16 +1602,14 @@ shiny 0.5.0
* Fix bug #55, where `renderTable()` would throw error with an empty data frame.
shiny 0.4.1
===========
# shiny 0.4.1
* Fix bug where width and height weren't passed along properly from `reactivePlot` to `renderPlot`.
* Fix bug where infinite recursion would happen when `reactivePlot` was passed a function for width or height.
shiny 0.4.0
===========
# shiny 0.4.0
* Added suspend/resume capability to observers.
@@ -1639,8 +1624,7 @@ shiny 0.4.0
* Fixed a bug where empty values in a numericInput were sent to the R process as 0. They are now sent as NA.
shiny 0.3.1
===========
# shiny 0.3.1
* Fix issue #91: bug where downloading files did not work.
@@ -1649,8 +1633,7 @@ shiny 0.3.1
* Reactive functions now preserve the visible/invisible state of their returned values.
shiny 0.3.0
===========
# shiny 0.3.0
* Reactive functions are now evaluated lazily.
@@ -1675,52 +1658,44 @@ shiny 0.3.0
* Fix issue #64, where pressing Enter in a textbox would cause a form to submit.
shiny 0.2.4
===========
# shiny 0.2.4
* `runGist` has been updated to use the new download URLs from https://gist.github.com.
* Shiny now uses `CairoPNG()` for output, when the Cairo package is available. This provides better-looking output on Linux and Windows.
shiny 0.2.3
===========
# shiny 0.2.3
* Ignore request variables for routing purposes
shiny 0.2.2
===========
# shiny 0.2.2
* Fix CRAN warning (assigning to global environment)
shiny 0.2.1
===========
# shiny 0.2.1
* [BREAKING] Modify API of `downloadHandler`: The `content` function now takes a file path, not writable connection, as an argument. This makes it much easier to work with APIs that only write to file paths, not connections.
shiny 0.2.0
===========
# shiny 0.2.0
* Fix subtle name resolution bug--the usual symptom being S4 methods not being invoked correctly when called from inside of ui.R or server.R
shiny 0.1.14
===========
# shiny 0.1.14
* Fix slider animator, which broke in 0.1.10
shiny 0.1.13
===========
# shiny 0.1.13
* Fix temp file leak in reactivePlot
shiny 0.1.12
===========
# shiny 0.1.12
* Fix problems with runGist on Windows
@@ -1729,8 +1704,7 @@ shiny 0.1.12
* Add CSS hooks for app-wide busy indicators
shiny 0.1.11
===========
# shiny 0.1.11
* Fix input binding with IE8 on Shiny Server
@@ -1739,8 +1713,7 @@ shiny 0.1.11
* Allow dynamic sizing of reactivePlot (i.e. using a function instead of a fixed value)
shiny 0.1.10
===========
# shiny 0.1.10
* Support more MIME types when serving out of www
@@ -1753,8 +1726,7 @@ shiny 0.1.10
* Fix plot rendering with IE8 on Shiny Server
shiny 0.1.9
===========
# shiny 0.1.9
* Much less flicker when updating plots
@@ -1763,8 +1735,7 @@ shiny 0.1.9
* Add `includeText`, `includeHTML`, and `includeMarkdown` functions for putting text, HTML, and Markdown content from external files in the application's UI.
shiny 0.1.8
===========
# shiny 0.1.8
* Add `runGist` function for conveniently running a Shiny app that is published on gist.github.com.
@@ -1777,8 +1748,7 @@ shiny 0.1.8
* Add `bootstrapPage` function for creating new Bootstrap based layouts from scratch.
shiny 0.1.7
===========
# shiny 0.1.7
* Fix issue #26: Shiny.OutputBindings not correctly exported.
@@ -1787,8 +1757,7 @@ shiny 0.1.7
* Transcode JSON into UTF-8 (prevents non-ASCII reactivePrint values from causing errors on Windows).
shiny 0.1.6
===========
# shiny 0.1.6
* Import package dependencies, instead of attaching them (with the exception of websockets, which doesn't currently work unless attached).
@@ -1797,8 +1766,7 @@ shiny 0.1.6
* bindAll was not correctly sending initial values to the server; fixed.
shiny 0.1.5
===========
# shiny 0.1.5
* BREAKING CHANGE: JS APIs Shiny.bindInput and Shiny.bindOutput removed and replaced with Shiny.bindAll; Shiny.unbindInput and Shiny.unbindOutput removed and replaced with Shiny.unbindAll.
@@ -1813,8 +1781,7 @@ shiny 0.1.5
* htmlOutput (CSS class `shiny-html-output`) can contain inputs and outputs.
shiny 0.1.4
===========
# shiny 0.1.4
* Allow Bootstrap tabsets to act as reactive inputs; their value indicates which tab is active
@@ -1827,8 +1794,7 @@ shiny 0.1.4
* Add Shiny.bindInputs(scope), .unbindInputs(scope), .bindOutputs(scope), and .unbindOutputs(scope) JS API calls to allow dynamic binding/unbinding of HTML elements
shiny 0.1.3
===========
# shiny 0.1.3
* Introduce Shiny.inputBindings.register JS API and InputBinding class, for creating custom input controls
@@ -1841,7 +1807,6 @@ shiny 0.1.3
* Fix issue #10: Plots in tabsets not rendered
shiny 0.1.2
===========
# shiny 0.1.2
* Initial private beta release!

View File

@@ -25,6 +25,7 @@
#' `- tests
#' |- testthat.R
#' `- testthat
#' |- setup-shinytest2.R
#' |- test-examplemodule.R
#' |- test-server.R
#' |- test-shinytest2.R
@@ -46,6 +47,7 @@
#' `tests/testthat/` directory using the
#' [shinytest2](https://rstudio.github.io/shinytest2/reference/test_app.html)
#' package.
#' * `tests/testthat/setup-shinytest2.R` is setup file to source your `./R` folder into the testing environment.
#' * `tests/testthat/test-examplemodule.R` is a test for an application's module server function.
#' * `tests/testthat/test-server.R` is a test for the application's server code
#' * `tests/testthat/test-shinytest2.R` is a test that uses the
@@ -126,7 +128,7 @@ shinyAppTemplate <- function(path = NULL, examples = "default", dryrun = FALSE)
}
if ("tests" %in% examples) {
rlang::check_installed("shinytest2", "for {testthat} tests to work as expected")
rlang::check_installed("shinytest2", "for {testthat} tests to work as expected", version = "0.2.0")
}
# =======================================================

View File

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

View File

@@ -794,7 +794,7 @@ verbatimTextOutput <- function(outputId, placeholder = FALSE) {
#' @export
imageOutput <- function(outputId, width = "100%", height="400px",
click = NULL, dblclick = NULL, hover = NULL, brush = NULL,
inline = FALSE) {
inline = FALSE, fill = FALSE) {
style <- if (!inline) {
# Using `css()` here instead of paste/sprintf so that NULL values will
@@ -850,7 +850,8 @@ imageOutput <- function(outputId, width = "100%", height="400px",
}
container <- if (inline) span else div
do.call(container, args)
res <- do.call(container, args)
bindFillRole(res, item = fill)
}
#' Create an plot or image output element
@@ -918,6 +919,11 @@ imageOutput <- function(outputId, width = "100%", height="400px",
#' `imageOutput`/`plotOutput` calls may share the same `id`
#' value; brushing one image or plot will cause any other brushes with the
#' same `id` to disappear.
#' @param fill Whether or not the returned tag should be treated as a fill item,
#' meaning that its `height` is allowed to grow/shrink to fit a fill container
#' with an opinionated height (see [htmltools::bindFillRole()]) with an
#' opinionated height. Examples of fill containers include `bslib::card()` and
#' `bslib::card_body_fill()`.
#' @inheritParams textOutput
#' @note The arguments `clickId` and `hoverId` only work for R base graphics
#' (see the \pkg{\link[graphics:graphics-package]{graphics}} package). They do
@@ -1088,11 +1094,11 @@ imageOutput <- function(outputId, width = "100%", height="400px",
#' @export
plotOutput <- function(outputId, width = "100%", height="400px",
click = NULL, dblclick = NULL, hover = NULL, brush = NULL,
inline = FALSE) {
inline = FALSE, fill = !inline) {
# Result is the same as imageOutput, except for HTML class
res <- imageOutput(outputId, width, height, click, dblclick,
hover, brush, inline)
hover, brush, inline, fill)
res$attribs$class <- "shiny-plot-output"
res
@@ -1135,15 +1141,21 @@ dataTableOutput <- function(outputId) {
#' Create an HTML output element
#'
#' Render a reactive output variable as HTML within an application page. The
#' text will be included within an HTML `div` tag, and is presumed to
#' contain HTML content which should not be escaped.
#' text will be included within an HTML `div` tag, and is presumed to contain
#' HTML content which should not be escaped.
#'
#' `uiOutput` is intended to be used with `renderUI` on the server
#' side. It is currently just an alias for `htmlOutput`.
#' `uiOutput` is intended to be used with `renderUI` on the server side. It is
#' currently just an alias for `htmlOutput`.
#'
#' @param outputId output variable to read the value from
#' @param ... Other arguments to pass to the container tag function. This is
#' useful for providing additional classes for the tag.
#' @param fill If `TRUE`, the result of `container` is treated as _both_ a fill
#' item and container (see [htmltools::bindFillRole()]), which means both the
#' `container` as well as its immediate children (i.e., the result of
#' `renderUI()`) are allowed to grow/shrink to fit a fill container with an
#' opinionated height. Set `fill = "item"` or `fill = "container"` to treat
#' `container` as just a fill item or a fill container.
#' @inheritParams textOutput
#' @return An HTML output element that can be included in a panel
#' @examples
@@ -1155,12 +1167,16 @@ dataTableOutput <- function(outputId) {
#' )
#' @export
htmlOutput <- function(outputId, inline = FALSE,
container = if (inline) span else div, ...)
container = if (inline) span else div, fill = FALSE, ...)
{
if (any_unnamed(list(...))) {
warning("Unnamed elements in ... will be replaced with dynamic UI.")
}
container(id = outputId, class="shiny-html-output", ...)
res <- container(id = outputId, class = "shiny-html-output", ...)
bindFillRole(
res, item = isTRUE(fill) || isTRUE("item" == fill),
container = isTRUE(fill) || isTRUE("container" == fill)
)
}
#' @rdname htmlOutput
@@ -1184,19 +1200,25 @@ uiOutput <- htmlOutput
#' @examples
#' \dontrun{
#' ui <- fluidPage(
#' p("Choose a dataset to download."),
#' selectInput("dataset", "Dataset", choices = c("mtcars", "airquality")),
#' downloadButton("downloadData", "Download")
#' )
#'
#' server <- function(input, output) {
#' # Our dataset
#' data <- mtcars
#' # The requested dataset
#' data <- reactive({
#' get(input$dataset)
#' })
#'
#' output$downloadData <- downloadHandler(
#' filename = function() {
#' paste("data-", Sys.Date(), ".csv", sep="")
#' # Use the selected dataset as the suggested file name
#' paste0(input$dataset, ".csv")
#' },
#' content = function(file) {
#' write.csv(data, file)
#' # Write the dataset to the `file` that will be downloaded
#' write.csv(data(), file)
#' }
#' )
#' }

View File

@@ -1,6 +1,6 @@
#' Shiny Developer Mode
#'
#' @description \lifecycle{experimental}
#' @description `r lifecycle::badge("experimental")`
#'
#' Developer Mode enables a number of [options()] to make a developer's life
#' easier, like enabling non-minified JS and printing messages about
@@ -190,8 +190,10 @@ devmode_inform <- function(
#' @include map.R
registered_devmode_options <- Map$new()
registered_devmode_options <- NULL
on_load({
registered_devmode_options <- Map$new()
})
#' @describeIn devmode Registers a Shiny Developer Mode option with an updated
#' value and Developer message. This registration method allows package
@@ -340,21 +342,22 @@ get_devmode_option <- function(
}
on_load({
register_devmode_option(
"shiny.autoreload",
"Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
TRUE
)
register_devmode_option(
"shiny.autoreload",
"Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
TRUE
)
register_devmode_option(
"shiny.minified",
"Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
FALSE
)
register_devmode_option(
"shiny.minified",
"Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
FALSE
)
register_devmode_option(
"shiny.fullstacktrace",
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
TRUE
)
register_devmode_option(
"shiny.fullstacktrace",
"Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
TRUE
)
})

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,13 @@ startPNG <- function(filename, width, height, res, ...) {
grDevices::png
}
args <- rlang::list2(filename=filename, width=width, height=height, res=res, ...)
args <- list2(filename = filename, width = width, height = height, res = res, ...)
# It's possible for width/height to be NULL/numeric(0) (e.g., when using
# suspendWhenHidden=F w/ tabsetPanel(), see rstudio/shiny#1409), so when
# this happens let the device determine what the default size should be.
if (length(args$width) == 0) args$width <- NULL
if (length(args$height) == 0) args$height <- NULL
# Set a smarter default for the device's bg argument (based on thematic's global state).
# Note that, technically, this is really only needed for CairoPNG, since the other
@@ -64,6 +70,10 @@ startPNG <- function(filename, width, height, res, ...) {
#' * Otherwise, use [grDevices::png()]. In this case, Linux and Windows
#' may not antialias some point shapes, resulting in poor quality output.
#'
#' @details
#' A `NULL` value provided to `width` or `height` is ignored (i.e., the
#' default `width` or `height` of the graphics device is used).
#'
#' @param func A function that generates a plot.
#' @param filename The name of the output file. Defaults to a temp file with
#' extension `.png`.

View File

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

View File

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

View File

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

View File

@@ -194,8 +194,8 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
}
resizeSavedPlot <- function(name, session, result, width, height, alt, pixelratio, res, ...) {
if (result$img$width == width && result$img$height == height &&
result$pixelratio == pixelratio && result$res == res) {
if (isTRUE(result$img$width == width && result$img$height == height &&
result$pixelratio == pixelratio && result$res == res)) {
return(result)
}

View File

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

View File

@@ -1,7 +1,12 @@
#' @include server-input-handlers.R
appsByToken <- Map$new()
appsNeedingFlush <- Map$new()
appsByToken <- NULL
appsNeedingFlush <- NULL
on_load({
appsByToken <- Map$new()
appsNeedingFlush <- Map$new()
})
# Provide a character representation of the WS that can be used
# as a key in a Map.
@@ -29,7 +34,7 @@ registerClient <- function(client) {
#' Define Server Functionality
#'
#' @description \lifecycle{superseded}
#' @description `r lifecycle::badge("superseded")`
#'
#' @description Defines the server-side logic of the Shiny application. This generally
#' involves creating functions that map user inputs to various kinds of output.
@@ -122,7 +127,10 @@ decodeMessage <- function(data) {
return(mainMessage)
}
autoReloadCallbacks <- Callbacks$new()
autoReloadCallbacks <- NULL
on_load({
autoReloadCallbacks <- Callbacks$new()
})
createAppHandlers <- function(httpHandlers, serverFuncSource) {
appvars <- new.env()

View File

@@ -148,7 +148,7 @@ shinyDependencyCSS <- function(theme) {
#' Create a Shiny UI handler
#'
#' @description \lifecycle{superseded}
#' @description `r lifecycle::badge("superseded")`
#'
#' @description Historically this function was used in ui.R files to register a user
#' interface with Shiny. It is no longer required as of Shiny 0.10; simply

View File

@@ -1,4 +1,12 @@
{
"plugins": [
[
"@babel/plugin-transform-typescript",
{
"allowDeclareFields": true
}
]
],
"presets": [
"@babel/preset-typescript",
[
@@ -9,7 +17,7 @@
}
]
],
"ignore":[
"ignore": [
"node_modules/core-js"
]
}

View File

@@ -0,0 +1,2 @@
# Load application support files into testing environment
shinytest2::load_app_env()

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -42,7 +42,7 @@ Selectize.define("selectize-plugin-a11y", function (options) {
// random IDs are assigned when .selectize() is called, but we're
// doing it here to limit the scope of changes.
var kids = self.$dropdown_content[0].children;
for (i = 0; i < kids.length; i++) {
for (var i = 0; i < kids.length; i++) {
var attrs = kids[i].attributes;
if (!attrs.role) {
kids[i].setAttribute("role", "option");

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@ registered \code{devmode_default} value will be used.}
\code{TRUE} and the specified option is not set in \code{\link[=options]{options()}}.}
}
\description{
\lifecycle{experimental}
\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}}
Developer Mode enables a number of \code{\link[=options]{options()}} to make a developer's life
easier, like enabling non-minified JS and printing messages about

View File

@@ -36,19 +36,25 @@ function.
\examples{
\dontrun{
ui <- fluidPage(
p("Choose a dataset to download."),
selectInput("dataset", "Dataset", choices = c("mtcars", "airquality")),
downloadButton("downloadData", "Download")
)
server <- function(input, output) {
# Our dataset
data <- mtcars
# The requested dataset
data <- reactive({
get(input$dataset)
})
output$downloadData <- downloadHandler(
filename = function() {
paste("data-", Sys.Date(), ".csv", sep="")
# Use the selected dataset as the suggested file name
paste0(input$dataset, ".csv")
},
content = function(file) {
write.csv(data, file)
# Write the dataset to the `file` that will be downloaded
write.csv(data(), file)
}
)
}

View File

@@ -9,10 +9,17 @@ htmlOutput(
outputId,
inline = FALSE,
container = if (inline) span else div,
fill = FALSE,
...
)
uiOutput(outputId, inline = FALSE, container = if (inline) span else div, ...)
uiOutput(
outputId,
inline = FALSE,
container = if (inline) span else div,
fill = FALSE,
...
)
}
\arguments{
\item{outputId}{output variable to read the value from}
@@ -22,6 +29,13 @@ for the output}
\item{container}{a function to generate an HTML element to contain the text}
\item{fill}{If \code{TRUE}, the result of \code{container} is treated as \emph{both} a fill
item and container (see \code{\link[htmltools:bindFillRole]{htmltools::bindFillRole()}}), which means both the
\code{container} as well as its immediate children (i.e., the result of
\code{renderUI()}) are allowed to grow/shrink to fit a fill container with an
opinionated height. Set \code{fill = "item"} or \code{fill = "container"} to treat
\code{container} as just a fill item or a fill container.}
\item{...}{Other arguments to pass to the container tag function. This is
useful for providing additional classes for the tag.}
}
@@ -30,12 +44,12 @@ An HTML output element that can be included in a panel
}
\description{
Render a reactive output variable as HTML within an application page. The
text will be included within an HTML \code{div} tag, and is presumed to
contain HTML content which should not be escaped.
text will be included within an HTML \code{div} tag, and is presumed to contain
HTML content which should not be escaped.
}
\details{
\code{uiOutput} is intended to be used with \code{renderUI} on the server
side. It is currently just an alias for \code{htmlOutput}.
\code{uiOutput} is intended to be used with \code{renderUI} on the server side. It is
currently just an alias for \code{htmlOutput}.
}
\examples{
htmlOutput("summary")

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,8 @@ imageOutput(
dblclick = NULL,
hover = NULL,
brush = NULL,
inline = FALSE
inline = FALSE,
fill = FALSE
)
plotOutput(
@@ -24,7 +25,8 @@ plotOutput(
dblclick = NULL,
hover = NULL,
brush = NULL,
inline = FALSE
inline = FALSE,
fill = !inline
)
}
\arguments{
@@ -76,6 +78,12 @@ same \code{id} to disappear.}
\item{inline}{use an inline (\code{span()}) or block container (\code{div()})
for the output}
\item{fill}{Whether or not the returned tag should be treated as a fill item,
meaning that its \code{height} is allowed to grow/shrink to fit a fill container
with an opinionated height (see \code{\link[htmltools:bindFillRole]{htmltools::bindFillRole()}}) with an
opinionated height. Examples of fill containers include \code{bslib::card()} and
\code{bslib::card_body_fill()}.}
}
\value{
A plot or image output element that can be included in a panel.

View File

@@ -46,3 +46,7 @@ is not set to \code{FALSE}), then use \code{\link[Cairo:Cairo]{Cairo::CairoPNG()
may not antialias some point shapes, resulting in poor quality output.
}
}
\details{
A \code{NULL} value provided to \code{width} or \code{height} is ignored (i.e., the
default \code{width} or \code{height} of the graphics device is used).
}

View File

@@ -45,6 +45,7 @@ following files and directories is created:
`- tests
|- testthat.R
`- testthat
|- setup-shinytest2.R
|- test-examplemodule.R
|- test-server.R
|- test-shinytest2.R
@@ -67,6 +68,7 @@ choose to use or remove any of them. They can be executed by the
\verb{tests/testthat/} directory using the
\href{https://rstudio.github.io/shinytest2/reference/test_app.html}{shinytest2}
package.
\item \code{tests/testthat/setup-shinytest2.R} is setup file to source your \code{./R} folder into the testing environment.
\item \code{tests/testthat/test-examplemodule.R} is a test for an application's module server function.
\item \code{tests/testthat/test-server.R} is a test for the application's server code
\item \code{tests/testthat/test-shinytest2.R} is a test that uses the

View File

@@ -11,7 +11,7 @@ shinyServer(func)
for more information.}
}
\description{
\lifecycle{superseded}
\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}}
Defines the server-side logic of the Shiny application. This generally
involves creating functions that map user inputs to various kinds of output.

View File

@@ -13,7 +13,7 @@ shinyUI(ui)
The user interface definition, without modifications or side effects.
}
\description{
\lifecycle{superseded}
\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}}
Historically this function was used in ui.R files to register a user
interface with Shiny. It is no longer required as of Shiny 0.10; simply

View File

@@ -3,7 +3,7 @@
"homepage": "https://shiny.rstudio.com",
"repository": "github:rstudio/shiny",
"name": "@types/rstudio-shiny",
"version": "1.7.2-alpha.9000",
"version": "1.7.4-alpha.9002",
"license": "GPL-3.0-only",
"main": "",
"browser": "",
@@ -19,11 +19,12 @@
"yarn": ">= 1.22"
},
"dependencies": {
"@babel/plugin-transform-typescript": "^7.21.3",
"@types/bootstrap": "3.4.0",
"@types/bootstrap-datepicker": "0.0.14",
"@types/datatables.net": "^1.10.19",
"@types/ion-rangeslider": "2.3.0",
"@types/jquery": "patch:@types/jquery@3.5.5#./srcts/patch/types-jquery.patch",
"@types/jquery": "3.5.14",
"@types/selectize": "0.12.34"
},
"devDependencies": {
@@ -40,12 +41,11 @@
"@types/jest": "^26.0.23",
"@types/jqueryui": "1.12.16",
"@types/lodash": "^4.14.170",
"@types/node": "^15.6.1",
"@types/node": "^18.14.2",
"@types/showdown": "^1.9.3",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1",
"autoprefixer": "^10.2.6",
"bootstrap-datepicker": "1.9.0",
"browserslist": "^4.19.1",
"caniuse-lite": "^1.0.30001312",
"core-js": "^3.13.0",
@@ -59,9 +59,9 @@
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unicorn": "^43.0.2",
"ion-rangeslider": "2.3.1",
"fs-readdir-recursive": "^1.1.0",
"jest": "^26.6.3",
"jquery": "3.6.0",
"jquery": "^3.6.0",
"lodash": "^4.17.21",
"madge": "^4.0.2",
"node-gyp": "^8.1.0",
@@ -70,8 +70,6 @@
"prettier": "^2.7.1",
"readcontrol": "^1.0.0",
"replace": "^1.2.1",
"selectize": "0.12.4",
"strftime": "0.9.2",
"ts-jest": "^26",
"ts-node": "^10.9.1",
"type-coverage": "^2.22.0",

97
srcts/build/_jquery.ts Normal file
View File

@@ -0,0 +1,97 @@
/*
Make sure all `*.ts` files contain `"jquery"` import statements to properly scope `jquery`.
Prior behavior:
- Use a patch file to remove globally declared `$` variable
- PR: https://github.com/rstudio/shiny/pull/3296/commits/169318382d1d00927d0148a16fde4c96a291a602
Prior reasoning:
- Only allow for jQuery type definitions to exist if imported.
- This is problematic as it can lead to improperly scoped values of `$`
- Ex:
* If `a.ts` imports jquery, then `b.ts` can see the global definition of `$`
even though `b.ts` does not import jquery.
* If `$` is not scoped / encapsulated, then it is possible to have
inconsistent versions of jquery executing within the shiny bundle.
Related:
- Open Issue ('16): https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11187
- Closed stale PR ('19): https://github.com/DefinitelyTyped/DefinitelyTyped/pull/40295/files
- Unsolved Issue ('14): https://github.com/DefinitelyTyped/DefinitelyTyped/issues/1564
- Multiple `$` conflicts ('22): https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/60443
- Bandaid PR ('22): https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60444
Approach within this file:
* For every `*.ts` file:
* Do not use `jQuery` (use `$`!)
* If utilizing `$`
* Require `jquery` import statement
- Advantages:
- Does not require a yarn patch file or separate GitHub repo containing the patched type definitions.
- Disadvantages:
- Variable reassignment isn't caught `jq = $; jq(divs)`. (This should not happen, but it is possible.)
- Only tested when bundling is started, not on every file change.
- PR: https://github.com/rstudio/shiny/pull/3710
*/
import fs from "fs";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore; Type definitions are not found
import readdirRecursive from "fs-readdir-recursive";
import { isUndefined } from "lodash";
const verifyJqueryUsage = async function (filename: string): Promise<void> {
const contents = await fs.promises.readFile(filename, "utf8");
const lines = contents.toString().split("\n");
// Find if using `jQuery` in the file
const jqueryMatch = lines.find((line) => {
return line.includes("jQuery.") || line.includes("jQuery(");
});
if (!isUndefined(jqueryMatch)) {
throw (
`Using \`jQuery\` in file: ${filename}\n` +
`Match:\n${jqueryMatch}\n` +
"Please use `$` instead of `jQuery`\n" +
"See file ./srcts/build/_jquery.ts for more details"
);
}
// Find if using `$` in the file
const dollarMatch = lines.find((line) => {
return line.includes("$.") || line.includes("$(");
});
if (isUndefined(dollarMatch)) {
// No match found. Not using jquery
return;
}
// Using jquery, find that it is being imported
const importJquery = 'import $ from "jquery";';
const hasJqueryImport = lines.includes(importJquery);
if (!hasJqueryImport) {
// Not importing jquery, yell
throw (
`Using \`$\` in file: ${filename}\n` +
`Match:\n${dollarMatch}\n` +
`Please call \`${importJquery}\` at the top of the file.\n` +
"See file ./srcts/build/_jquery.ts for more details"
);
}
// Using jquery and importing it;
return;
};
const verifyJqueryImport = async function (dir = "."): Promise<void> {
const tsFiles = (readdirRecursive(dir) as string[])
.filter((path: string) => path.endsWith(".ts"))
.map((path) => dir + "/" + path);
// Run all checks in parallel
await Promise.all(tsFiles.map((file) => verifyJqueryUsage(file)));
return;
};
export { verifyJqueryImport };

View File

@@ -10,6 +10,12 @@ import globalsPlugin from "esbuild-plugin-globals";
const opts = {
bundle: false,
sourcemap: false,
// Oddly, esbuild seems to use the top-level tsconfig.json file even when just
// minifying JS to JS. Because that tsconfig file has "strict":true, esbuild
// ends up adding "use strict" to the top of each minified JS file, which can
// alter behavior. To avoid this, we have a separate tsconfig file with
// "alwaysStrict":false.
tsconfig: "srcts/build/external_libs_tsconfig.json",
};
readdir(outDir + "datepicker/js/locales/").then(async (localeFiles) => {

View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"target": "ES5",
"alwaysStrict": false
}
}

View File

@@ -6,6 +6,7 @@
import { banner, build, outDir, shinyDesc, babelPlugin } from "./_build";
import globalsPlugin from "esbuild-plugin-globals";
import type { BuildOptions } from "esbuild";
import { verifyJqueryImport } from "./_jquery";
const opts: BuildOptions = {
entryPoints: ["srcts/src/index.ts"],
@@ -26,12 +27,21 @@ const opts: BuildOptions = {
banner: banner,
};
build({
...opts,
outfile: outDir + "shiny.js",
});
build({
...opts,
outfile: outDir + "shiny.min.js",
minify: true,
});
// Make sure all ts files contain jquery import statements before building
verifyJqueryImport("srcts/src")
.then(() => {
Promise.all([
build({
...opts,
outfile: outDir + "shiny.js",
}),
build({
...opts,
outfile: outDir + "shiny.min.js",
minify: true,
}),
]);
})
.catch((err) => {
console.error("Error:\n" + err);
});

View File

@@ -1,7 +1,5 @@
# `yarn` Patch files
* `types-jquery.patch`
* Do not export `$` as a globally available variable. When developing TS code, I like to have full control over the variables. It is good to have a record of where everything comes from. Shiny can not use the latest jQuery loaded onto the page and needs to use a scoped `jQuery` variable. In the end, we can shim the `jquery` import to be `window.jQuery`
* `yarn_pnp.patch`
* This file is currently not used and is outdated.
* This provides a good game plan on how to use PnP with Yarn once esbuild can easily be integrated with Yarn PnP.

View File

@@ -1,21 +0,0 @@
diff --git a/misc.d.ts b/misc.d.ts
index 126d374477db459e1a251f5af548e88b46f43cdd..948cfb287b0bfa6057f3ccbcd013579aef07a29a 100644
--- a/misc.d.ts
+++ b/misc.d.ts
@@ -6618,7 +6618,7 @@ $( "#checkMetaKey" ).click(function( event ) {
}
declare const jQuery: JQueryStatic;
-declare const $: JQueryStatic;
+// declare const $: JQueryStatic;
type _Event = Event;
type _UIEvent = UIEvent;
@@ -6643,6 +6643,6 @@ interface SymbolConstructor {
readonly toStringTag: symbol;
}
-declare var Symbol: SymbolConstructor;
+// declare var Symbol: SymbolConstructor;
// #endregion

View File

@@ -71,7 +71,7 @@ class SelectInputBinding extends InputBinding {
} else {
const selectize = this._selectize(el);
selectize.setValue(value);
selectize?.setValue(value);
}
}
getState(el: SelectHTMLElement): {
@@ -107,10 +107,10 @@ class SelectInputBinding extends InputBinding {
// This will replace all the options
if (hasDefinedProperty(data, "options")) {
const selectize = this._selectize(el);
// Must destroy selectize before appending new options, otherwise
// selectize will restore the original select
if (selectize) selectize.destroy();
selectize?.destroy();
// Clear existing options and add each new one
$el.empty().append(data.options);
this._selectize(el);
@@ -176,9 +176,7 @@ class SelectInputBinding extends InputBinding {
callback(res);
if (!loaded) {
if (hasDefinedProperty(data, "value")) {
if (typeof data.value === "string") {
selectize.setValue(data.value);
}
selectize.setValue(data.value as any);
} else if (settings.maxItems === 1) {
// first item selected by default only for single-select
selectize.setValue(res[0].value);
@@ -221,15 +219,19 @@ class SelectInputBinding extends InputBinding {
initialize(el: SelectHTMLElement): void {
this._selectize(el);
}
protected _selectize(el: SelectHTMLElement, update = false): SelectizeInfo {
if (!$.fn.selectize) throw "selectize jquery is not defined";
protected _selectize(
el: SelectHTMLElement,
update = false
): SelectizeInfo | undefined {
// Apps like 008-html do not have the selectize js library
// Safe-guard against missing the selectize js library
if (!$.fn.selectize) return undefined;
const $el = $(el);
const config = $el
.parent()
.find('script[data-for="' + $escape(el.id) + '"]');
if (config.length === 0)
throw "No config found for selectize with id:" + $escape(el.id);
if (config.length === 0) return undefined;
let options: SelectizeOptions & {
labelField: "label";

View File

@@ -24,7 +24,7 @@ $(document).on(
function (e: Event) {
e;
const evt: FileDownloadEvent = jQuery.Event("shiny:filedownload");
const evt: FileDownloadEvent = $.Event("shiny:filedownload");
evt.name = this.id;
evt.href = this.href;

View File

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

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
import $ from "jquery";
import type { FileInputBinding } from "../bindings/input/fileinput";
import type { ShinyEventInputChanged } from "./shinyEvents";
function triggerFileInputChanged(
name: string,
value: unknown,
binding: FileInputBinding,
el: HTMLElement,
inputType: string,
onEl: typeof document
): ShinyEventInputChanged {
const evt = $.Event("shiny:inputchanged") as ShinyEventInputChanged;
evt.name = name;
evt.value = value;
evt.binding = binding;
evt.el = el;
evt.inputType = inputType;
$(onEl).trigger(evt);
return evt;
}
export { triggerFileInputChanged };

View File

@@ -54,3 +54,5 @@ declare global {
): this;
}
}
export type { EvtFn };

View File

@@ -2,35 +2,589 @@ import type { InputBinding } from "../bindings/input/inputBinding";
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { EventPriority } from "../inputPolicies/inputPolicy";
import type { ErrorsMessageValue } from "../shiny/shinyapp";
import type { EvtFn } from "./jQueryEvents";
import $ from "jquery";
/**
* Shiny Event Base
*
* This class implements a common interface for all Shiny events, and provides a
* layer of abstraction between the Shiny event and the underlying jQuery event
* object. We use a new class, rather than extending JQuery.Event, because
* JQuery.Event is an old function-style class. Each Event class has a
* corresponding ShinyEvent interface that describes the event object that is
* emitted. At the end of this file, we extend JQuery's `on()` method to
* associate the ShinyEvent interfaces with their corresponding event string.
*
* @class EventBase
* @typedef {EventBase}
*/
class EventBase {
/**
* The underlying jQuery event object wrapped by `EventBase`.
* @type {JQuery.Event}
*/
event: JQuery.Event;
/**
* Creates an instance of EventBase.
*
* @constructor
* @param {string} type - The event type.
*/
constructor(type: string) {
this.event = $.Event(type);
}
/**
* Trigger the event on an element or the document.
*
* @example
* // Instead of this...
* el.trigger(shinyEvent);
* // ...do this
* shinyEvent.triggerOn(el);
*
* @param {(HTMLElement | JQuery<HTMLElement> | typeof document | null)} el -
* The element to trigger the event on, or `null` to trigger on `document`.
*/
triggerOn(
el: HTMLElement | JQuery<HTMLElement> | typeof document | null
): void {
$(el || window.document).trigger(this.event);
}
/**
* Proxy for `event.preventDefault()`.
*
* @returns {boolean} `true` if the default action was prevented, `false`
* otherwise.
*/
isDefaultPrevented(): boolean {
return this.event.isDefaultPrevented();
}
}
/**
* A common interface for most Shiny events.
*
* @interface ShinyEventCommon
* @typedef {ShinyEventCommon}
* @extends {JQuery.Event}
*/
interface ShinyEventCommon extends JQuery.Event {
/**
* The event name.
* @type {string}
*/
name: string;
value: unknown;
el: HTMLElement | null;
/**
* Event value containing arbitrary event data.
* @type {*}
*/
value: any;
}
/**
* Create a common Shiny event.
*
* @class EventCommon
* @typedef {EventCommon}
* @extends {EventBase}
*/
class EventCommon extends EventBase {
/**
* The actual event object.
* @type {ShinyEventCommon}
*/
declare event: ShinyEventCommon;
/**
* Creates an instance of EventCommon.
*
* @constructor
* @param {ShinyEventCommon["type"]} type - The Shiny custom event type, e.g.
* `shiny:value`.
* @param {ShinyEventCommon["name"]} name - The event name.
* @param {ShinyEventCommon["value"]} value - The event value, or arbitrary
* data included with the event.
*/
constructor(
type: ShinyEventCommon["type"],
name: ShinyEventCommon["name"],
value: ShinyEventCommon["value"]
) {
super(type);
this.event.name = name;
this.event.value = value;
}
/**
* Get the event name.
* @readonly
* @type {ShinyEventCommon["name"]}
*/
get name(): ShinyEventCommon["name"] {
return this.event.name;
}
/**
* Get the event value.
* @readonly
* @type {ShinyEventCommon["value"]}
*/
get value(): ShinyEventCommon["value"] {
return this.event.value;
}
}
/**
* An interface for the `shiny:inputchanged` event.
*
* @interface ShinyEventInputChanged
* @typedef {ShinyEventInputChanged}
* @extends {ShinyEventCommon}
*/
interface ShinyEventInputChanged extends ShinyEventCommon {
value: unknown;
/**
* The input element whose value has changed.
* @type {(HTMLElement | null)}
*/
el: HTMLElement | null;
/**
* The input binding for the changed input.
* @type {(InputBinding | null)}
*/
binding: InputBinding | null;
/**
* The input type.
* @type {string}
*/
inputType: string;
priority: EventPriority;
/**
* The input event priority.
* @type {?EventPriority}
*/
priority?: EventPriority;
}
/**
* Create a custom `shiny:inputchanged` event as an instance of
* EventInputChanged.
*
* @class EventInputChanged
* @typedef {EventInputChanged}
* @extends {EventCommon}
*/
class EventInputChanged extends EventCommon {
/**
* The `ShinyEventInputChanged` event object.
* @type {ShinyEventInputChanged}
*/
declare event: ShinyEventInputChanged;
/**
* Creates an instance of EventInputChanged.
*
* @constructor
* @param {{
name: ShinyEventInputChanged["name"];
value: ShinyEventInputChanged["value"];
el: ShinyEventInputChanged["el"];
binding: ShinyEventInputChanged["binding"];
inputType: ShinyEventInputChanged["inputType"];
priority?: ShinyEventInputChanged["priority"];
}} {
name,
value,
el,
binding,
inputType,
priority,
}
*/
constructor({
name,
value,
el,
binding,
inputType,
priority,
}: {
name: ShinyEventInputChanged["name"];
value: ShinyEventInputChanged["value"];
el: ShinyEventInputChanged["el"];
binding: ShinyEventInputChanged["binding"];
inputType: ShinyEventInputChanged["inputType"];
priority?: ShinyEventInputChanged["priority"];
}) {
super("shiny:inputchanged", name, value);
this.event.el = el;
this.event.binding = binding;
this.event.inputType = inputType;
if (priority) {
this.event.priority = priority;
}
}
/**
* Get the input element whose value has changed.
* @readonly
* @type {ShinyEventInputChanged["el"]}
*/
get el(): ShinyEventInputChanged["el"] {
return this.event.el;
}
/**
* Get the input binding for the changed input.
* @readonly
* @type {ShinyEventInputChanged["binding"]}
*/
get binding(): ShinyEventInputChanged["binding"] {
return this.event.binding;
}
/**
* Get the input type.
* @readonly
* @type {ShinyEventInputChanged["inputType"]}
*/
get inputType(): ShinyEventInputChanged["inputType"] {
return this.event.inputType;
}
/**
* Get the input event priority.
* @readonly
* @type {ShinyEventInputChanged["priority"]}
*/
get priority(): ShinyEventInputChanged["priority"] {
return this.event.priority;
}
}
/**
* A interface for the custom `shiny:updateinput` event.
*
* @interface ShinyEventUpdateInput
* @typedef {ShinyEventUpdateInput}
* @extends {ShinyEventCommon}
*/
interface ShinyEventUpdateInput extends ShinyEventCommon {
message: unknown;
/**
* Arbitrary message data, typically sent from the server, to be processed by
* the `receiveMessage` method of the input binding.
* @type {?*}
*/
message?: any;
/**
* The input binding for the input.
* @type {InputBinding}
*/
binding: InputBinding;
}
/**
* Create a shiny custom `shiny:updateinput` event as an instance of
* EventUpdateInput. This event carries message data from the server, sent via
* `session$sendInputMessage()`, to the input binding's `receiveMessage` method.
*
* @class EventUpdateInput
* @typedef {EventUpdateInput}
* @extends {EventBase}
*/
class EventUpdateInput extends EventBase {
/**
* The `ShinyEventUpdateInput` event object.
* @type {ShinyEventUpdateInput}
*/
declare event: ShinyEventUpdateInput;
/**
* Creates an instance of EventUpdateInput.
*
* @constructor
* @param {{
message?: ShinyEventUpdateInput["message"];
binding: ShinyEventUpdateInput["binding"];
}} {
message,
binding,
}
*/
constructor({
message,
binding,
}: {
message?: ShinyEventUpdateInput["message"];
binding: ShinyEventUpdateInput["binding"];
}) {
super("shiny:updateinput");
if (message) {
this.event.message = message;
}
this.event.binding = binding;
}
/**
* Get the `shiny:updateinput` message data.
* @readonly
* @type {ShinyEventUpdateInput["message"]}
*/
get message(): ShinyEventUpdateInput["message"] {
return this.event.message;
}
/**
* Get the input binding for the input.
* @readonly
* @type {ShinyEventUpdateInput["binding"]}
*/
get binding(): ShinyEventUpdateInput["binding"] {
return this.event.binding;
}
}
/**
* A interface for the custom `shiny:value` event.
*
* @interface ShinyEventValue
* @typedef {ShinyEventValue}
* @extends {ShinyEventCommon}
*/
interface ShinyEventValue extends ShinyEventCommon {
value: unknown;
/**
* The output binding for the output that updated.
* @type {OutputBindingAdapter}
*/
binding: OutputBindingAdapter;
}
/**
* Create a shiny custom `shiny:value` event as an instance of EventValue. This
* event is triggered when an output's value changes.
*
* @class EventValue
* @typedef {EventValue}
* @extends {EventCommon}
*/
class EventValue extends EventCommon {
/**
* The `ShinyEventValue` event object.
* @type {ShinyEventValue}
*/
declare event: ShinyEventValue;
/**
* Creates an instance of EventValue.
*
* @constructor
* @param {{
name: ShinyEventValue["name"];
value: ShinyEventValue["value"];
binding: ShinyEventValue["binding"];
}} {
name,
value,
binding,
}
*/
constructor({
name,
value,
binding,
}: {
name: ShinyEventValue["name"];
value: ShinyEventValue["value"];
binding: ShinyEventValue["binding"];
}) {
super("shiny:value", name, value);
this.event.binding = binding;
}
/**
* Get the output binding for the output that updated.
* @readonly
* @type {ShinyEventValue["binding"]}
*/
get binding(): ShinyEventValue["binding"] {
return this.event.binding;
}
}
/**
* A interface for the custom `shiny:error` event.
*
* @interface ShinyEventError
* @typedef {ShinyEventError}
* @extends {ShinyEventCommon}
*/
interface ShinyEventError extends ShinyEventCommon {
/**
* The output binding for the output where the error occurred.
* @type {OutputBindingAdapter}
*/
binding: OutputBindingAdapter;
/**
* The error message.
* @type {ErrorsMessageValue}
*/
error: ErrorsMessageValue;
}
/**
* Create a shiny custom `shiny:error` event as an instance of EventError. This
* event is triggered when an error occurs while processing the reactive
* expression that produces the output.
*
* @class EventError
* @typedef {EventError}
* @extends {EventCommon}
*/
class EventError extends EventCommon {
/**
* The `ShinyEventError` event object.
* @type {ShinyEventError}
*/
declare event: ShinyEventError;
/**
* Creates an instance of EventError.
*
* @constructor
* @param {{
name: ShinyEventError["name"];
binding: ShinyEventError["binding"];
error: ShinyEventError["error"];
}} {
name,
binding,
error,
}
*/
constructor({
name,
binding,
error,
}: {
name: ShinyEventError["name"];
binding: ShinyEventError["binding"];
error: ShinyEventError["error"];
}) {
super("shiny:error", name, null);
this.event.binding = binding;
this.event.error = error;
}
/**
* Get the output binding for the output where the error occurred.
* @readonly
* @type {ShinyEventError["binding"]}
*/
get binding(): ShinyEventError["binding"] {
return this.event.binding;
}
/**
* Get the error message.
* @readonly
* @type {ShinyEventError["error"]}
*/
get error(): ShinyEventError["error"] {
return this.event.error;
}
}
/**
* A interface for the custom `shiny:message` event.
*
* @interface ShinyEventMessage
* @typedef {ShinyEventMessage}
* @extends {JQuery.Event}
*/
interface ShinyEventMessage extends JQuery.Event {
/**
* Arbitrary message data from the server. This data will ultimately be
* handled by the function provided to `Shiny.addCustomMessageHandler()` for
* the given message type.
*
* @type {{ [key: string]: unknown }}
*/
message: { [key: string]: unknown };
}
/**
* Create a shiny custom `shiny:message` event as an instance of EventMessage.
* This message is triggered when the server sends a custom message to the app
* via `session$sendCustomMessage()`.
*
* @class EventMessage
* @typedef {EventMessage}
* @extends {EventBase}
*/
class EventMessage extends EventBase {
/**
* The `ShinyEventMessage` event object.
* @type {ShinyEventMessage}
*/
declare event: ShinyEventMessage;
/**
* Creates an instance of EventMessage.
*
* @constructor
* @param {ShinyEventMessage["message"]} message
*/
constructor(message: ShinyEventMessage["message"]) {
super("shiny:message");
this.event.message = message;
}
/**
* Get the message data from the event.
* @readonly
* @type {ShinyEventMessage["message"]}
*/
get message(): ShinyEventMessage["message"] {
return this.event.message;
}
}
// Augment the JQuery interface ----------------------------------------------
// This allows extensions to use .on() in Typescript with Shiny's custom events.
// E.g. in {bslib}, we can use the following with complete type information:
//
// ```
// $(document).on("shiny:value", function(event: ShinyEventValue) { })
// ```
declare global {
interface JQuery {
on(
events: "shiny:inputchanged",
handler: EvtFn<ShinyEventInputChanged>
): this;
on(
events: "shiny:updateinput",
handler: EvtFn<ShinyEventUpdateInput>
): this;
on(events: "shiny:value", handler: EvtFn<ShinyEventValue>): this;
on(events: "shiny:error", handler: EvtFn<ShinyEventError>): this;
on(events: "shiny:message", handler: EvtFn<ShinyEventMessage>): this;
}
}
export {
EventCommon,
EventInputChanged,
EventUpdateInput,
EventValue,
EventError,
EventMessage,
};
export type {
ShinyEventInputChanged,
ShinyEventUpdateInput,

View File

@@ -1,8 +1,8 @@
import $ from "jquery";
import { triggerFileInputChanged } from "../events/inputChanged";
import { $escape } from "../utils";
import type { ShinyApp } from "../shiny/shinyapp";
import { getFileInputBinding } from "../shiny/initedMethods";
import { EventInputChanged } from "../events/shinyEvents";
type JobId = string;
type UploadUrl = string;
@@ -227,14 +227,14 @@ class FileUploader extends FileProcessor {
// Trigger shiny:inputchanged. Unlike a normal shiny:inputchanged event,
// it's not possible to modify the information before the values get
// sent to the server.
const evt = triggerFileInputChanged(
this.id,
fileInfo,
getFileInputBinding(),
this.el,
"shiny.fileupload",
document
);
const inputChangedEvent = new EventInputChanged({
name: this.id,
value: fileInfo,
el: this.el,
binding: getFileInputBinding(),
inputType: "shiny.fileupload",
});
inputChangedEvent.triggerOn(document);
this.makeRequest(
"uploadEnd",
@@ -245,7 +245,7 @@ class FileUploader extends FileProcessor {
this.$bar().text("Upload complete");
// Reset the file input's value to "". This allows the same file to be
// uploaded again. https://stackoverflow.com/a/22521275
$(evt.el as HTMLElement).val("");
$(inputChangedEvent.el as HTMLElement).val("");
},
(error) => {
this.onError(error);

View File

@@ -58,6 +58,7 @@ type BrushOpts = {
type Brush = {
reset: () => void;
hasOldBrush: () => boolean;
importOldBrush: () => void;
isInsideBrush: (offsetCss: Offset) => boolean;
isInResizeArea: (offsetCss: Offset) => boolean;
@@ -173,10 +174,15 @@ function createBrush(
if ($div) $div.remove();
}
function hasOldBrush(): boolean {
const oldDiv = $el.find("#" + el.id + "_brush");
return oldDiv.length > 0;
}
// If there's an existing brush div, use that div to set the new brush's
// settings, provided that the x, y, and panel variables have the same names,
// and there's a panel with matching panel variable values.
function importOldBrush() {
function importOldBrush(): void {
const oldDiv = $el.find("#" + el.id + "_brush");
if (oldDiv.length === 0) return;
@@ -220,11 +226,9 @@ function createBrush(
// div being resized.
function onResize() {
const boundsDataVal = boundsData();
// Check to see if we have valid boundsData
for (const val in Object.values(boundsDataVal)) {
if (isnan(val)) return;
}
// Check to see if we have valid boundsData
if (Object.values(boundsDataVal).some(isnan)) return;
boundsData(boundsDataVal);
updateDiv();
@@ -619,6 +623,7 @@ function createBrush(
return {
reset: reset,
hasOldBrush,
importOldBrush: importOldBrush,
isInsideBrush: isInsideBrush,
isInResizeArea: isInResizeArea,

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import $ from "jquery";
import type { InputPolicy, InputPolicyOpts } from "./inputPolicy";
import type { ShinyEventInputChanged } from "../events/shinyEvents";
import { EventInputChanged } from "../events/shinyEvents";
import { splitInputNameType } from "./splitInputNameType";
class InputEventDecorator implements InputPolicy {
@@ -11,31 +10,34 @@ class InputEventDecorator implements InputPolicy {
}
setInput(nameType: string, value: unknown, opts: InputPolicyOpts): void {
const evt = jQuery.Event("shiny:inputchanged") as ShinyEventInputChanged;
const input = splitInputNameType(nameType);
evt.name = input.name;
evt.inputType = input.inputType;
evt.value = value;
evt.binding = opts.binding || null;
evt.el = opts.el || null;
evt.priority = opts.priority;
const inputChangedEvent = new EventInputChanged({
name: input.name,
value,
el: opts.el || null,
binding: opts.binding || null,
inputType: input.inputType,
priority: opts.priority,
});
// The `shiny:inputchanged` JavaScript event now triggers on the related
// input element instead of `document`. Existing event listeners bound to
// `document` will still detect the event due to event bubbling. #2446
// If no `el` exists, use `document` instead. #3584
$(opts.el || window.document).trigger(evt);
inputChangedEvent.triggerOn(opts.el || window.document);
if (!evt.isDefaultPrevented()) {
let name = evt.name;
if (!inputChangedEvent.isDefaultPrevented()) {
let name = inputChangedEvent.name;
if (evt.inputType !== "") name += ":" + evt.inputType;
if (inputChangedEvent.inputType !== "")
name += ":" + inputChangedEvent.inputType;
// Most opts aren't passed along to lower levels in the input decorator
// stack.
this.target.setInput(name, evt.value, { priority: opts.priority });
this.target.setInput(name, inputChangedEvent.value, {
priority: opts.priority,
});
}
}
}

View File

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

View File

@@ -154,12 +154,12 @@ function initShiny(windowShiny: Shiny): void {
// in case it is auto-sizing
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
function () {
const id = getIdFromEl(this);
const id = getIdFromEl(this),
rect = this.getBoundingClientRect();
if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
initialValues[".clientdata_output_" + id + "_width"] = this.offsetWidth;
initialValues[".clientdata_output_" + id + "_height"] =
this.offsetHeight;
if (rect.width !== 0 || rect.height !== 0) {
initialValues[".clientdata_output_" + id + "_width"] = rect.width;
initialValues[".clientdata_output_" + id + "_height"] = rect.height;
}
}
);
@@ -275,17 +275,12 @@ function initShiny(windowShiny: Shiny): void {
function doSendImageSize() {
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
function () {
const id = getIdFromEl(this);
const id = getIdFromEl(this),
rect = this.getBoundingClientRect();
if (this.offsetWidth !== 0 || this.offsetHeight !== 0) {
inputs.setInput(
".clientdata_output_" + id + "_width",
this.offsetWidth
);
inputs.setInput(
".clientdata_output_" + id + "_height",
this.offsetHeight
);
if (rect.width !== 0 || rect.height !== 0) {
inputs.setInput(".clientdata_output_" + id + "_width", rect.width);
inputs.setInput(".clientdata_output_" + id + "_height", rect.height);
}
}
);

View File

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

View File

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

View File

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

View File

@@ -10,24 +10,25 @@ import {
import { isQt } from "../utils/browser";
import { showNotification, removeNotification } from "./notifications";
import { showModal, removeModal } from "./modal";
import { renderContent, renderHtml } from "./render";
import { renderContentAsync, renderHtmlAsync } from "./render";
import type { HtmlDep } from "./render";
import { hideReconnectDialog, showReconnectDialog } from "./reconnectDialog";
import { resetBrush } from "../imageutils/resetBrush";
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type {
ShinyEventError,
ShinyEventMessage,
ShinyEventValue,
ShinyEventUpdateInput,
import {
EventValue,
EventUpdateInput,
EventError,
EventMessage,
} from "../events/shinyEvents";
import type { InputBinding } from "../bindings";
import { indirectEval } from "../utils/eval";
import type { WherePosition } from "./singletons";
import type { UploadInitValue, UploadEndValue } from "../file/fileProcessor";
import { AsyncQueue } from "../utils/asyncQueue";
type ResponseValue = UploadEndValue | UploadInitValue;
type Handler = (message: any) => void;
type Handler = (message: any) => Promise<void> | void;
type ShinyWebSocket = WebSocket & {
allowReconnect?: boolean;
@@ -43,6 +44,8 @@ type OnSuccessRequest = (value: ResponseValue) => void;
type OnErrorRequest = (err: string) => void;
type InputValues = { [key: string]: unknown };
type MessageValue = Parameters<WebSocket["send"]>[0];
//// 2021/03 - TypeScript conversion note:
// These four variables were moved from being internally defined to being defined globally within the file.
// Before the TypeScript conversion, the values where attached to `window.Shiny.addCustomMessageHandler()`.
@@ -106,6 +109,13 @@ function addCustomMessageHandler(type: string, handler: Handler): void {
class ShinyApp {
$socket: ShinyWebSocket | null = null;
// An asynchronous queue of functions. This is sort of like an event loop for
// Shiny, to allow scheduling async callbacks so that they can run in order
// without overlapping. This is used for handling incoming messages from the
// server and scheduling outgoing messages to the server, and can be used for
// other things tasks as well.
taskQueue = new AsyncQueue<() => Promise<void> | void>();
config: {
workerId: string;
sessionId: string;
@@ -127,7 +137,7 @@ class ShinyApp {
// Conditional bindings (show/hide element based on expression)
$conditionals = {};
$pendingMessages: string[] = [];
$pendingMessages: MessageValue[] = [];
$activeRequests: {
[key: number]: { onSuccess: OnSuccessRequest; onError: OnErrorRequest };
} = {};
@@ -226,9 +236,11 @@ class ShinyApp {
socket.send(msg as string);
}
this.startActionQueueLoop();
};
socket.onmessage = (e) => {
this.dispatchMessage(e.data);
this.taskQueue.enqueue(async () => await this.dispatchMessage(e.data));
};
// Called when a successfully-opened websocket is closed, or when an
// attempt to open a connection fails.
@@ -251,6 +263,19 @@ class ShinyApp {
return socket;
}
async startActionQueueLoop(): Promise<void> {
// eslint-disable-next-line no-constant-condition
while (true) {
const action = await this.taskQueue.dequeue();
try {
await action();
} catch (e) {
console.error(e);
}
}
}
sendInput(values: InputValues): void {
const msg = JSON.stringify({
method: "update",
@@ -381,7 +406,7 @@ class ShinyApp {
onError: onError,
};
let msg = JSON.stringify({
let msg: Blob | string = JSON.stringify({
method: method,
args: args,
tag: requestId,
@@ -423,13 +448,13 @@ class ShinyApp {
const blob: Blob = new Blob(payload);
msg = blob as unknown as string;
msg = blob;
}
this.$sendMsg(msg);
}
$sendMsg(msg: string): void {
$sendMsg(msg: MessageValue): void {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!this.$socket!.readyState) {
this.$pendingMessages.push(msg);
@@ -446,37 +471,31 @@ class ShinyApp {
delete this.$values[name];
const binding = this.$bindings[name];
const evt: ShinyEventError = jQuery.Event("shiny:error");
evt.name = name;
evt.error = error;
evt.binding = binding;
$(binding ? binding.el : document).trigger(evt);
if (!evt.isDefaultPrevented() && binding && binding.onValueError) {
binding.onValueError(evt.error);
const errorEvent = new EventError({ name, error, binding });
errorEvent.triggerOn(binding?.el);
if (!errorEvent.isDefaultPrevented() && binding && binding.onValueError) {
binding.onValueError(errorEvent.error);
}
}
receiveOutput<T>(name: string, value: T): T | undefined {
async receiveOutput<T>(name: string, value: T): Promise<T | undefined> {
const binding = this.$bindings[name];
const evt: ShinyEventValue = jQuery.Event("shiny:value");
evt.name = name;
evt.value = value;
evt.binding = binding;
const valueEvent = new EventValue({ name, value, binding });
if (this.$values[name] === value) {
$(binding ? binding.el : document).trigger(evt);
valueEvent.triggerOn(binding?.el);
return undefined;
}
this.$values[name] = value;
delete this.$errors[name];
$(binding ? binding.el : document).trigger(evt);
valueEvent.triggerOn(binding?.el);
if (!evt.isDefaultPrevented() && binding) {
binding.onValueChange(evt.value);
if (!valueEvent.isDefaultPrevented() && binding) {
await binding.onValueChange(valueEvent.value);
}
return value;
@@ -596,8 +615,8 @@ class ShinyApp {
// // Added in shiny init method
// Shiny.addCustomMessageHandler = addCustomMessageHandler;
dispatchMessage(data: ArrayBufferLike | string): void {
let msgObj: ShinyEventMessage["message"] = {};
async dispatchMessage(data: ArrayBufferLike | string): Promise<void> {
let msgObj: EventMessage["message"] = {};
if (typeof data === "string") {
msgObj = JSON.parse(data);
@@ -618,15 +637,13 @@ class ShinyApp {
msgObj.custom[type] = data;
}
const evt: ShinyEventMessage = jQuery.Event("shiny:message");
evt.message = msgObj;
$(document).trigger(evt);
if (evt.isDefaultPrevented()) return;
const messageEvent = new EventMessage({ message: msgObj });
messageEvent.triggerOn(document);
if (messageEvent.isDefaultPrevented()) return;
// Send msgObj.foo and msgObj.bar to appropriate handlers
this._sendMessagesToHandlers(
evt.message,
await this._sendMessagesToHandlers(
messageEvent.message,
messageHandlers,
messageHandlerOrder
);
@@ -638,11 +655,11 @@ class ShinyApp {
// A function for sending messages to the appropriate handlers.
// - msgObj: the object containing messages, with format {msgObj.foo, msObj.bar
private _sendMessagesToHandlers(
private async _sendMessagesToHandlers(
msgObj: { [key: string]: unknown },
handlers: { [key: string]: Handler },
handlerOrder: string[]
): void {
): Promise<void> {
// Dispatch messages to handlers, if handler is present
for (let i = 0; i < handlerOrder.length; i++) {
const msgType = handlerOrder[i];
@@ -650,7 +667,7 @@ class ShinyApp {
if (hasOwnProperty(msgObj, msgType)) {
// Execute each handler with 'this' referring to the present value of
// 'this'
handlers[msgType].call(this, msgObj[msgType]);
await handlers[msgType].call(this, msgObj[msgType]);
}
}
}
@@ -660,7 +677,7 @@ class ShinyApp {
// * Use arrow functions to allow the Types to propagate.
// * However, `_sendMessagesToHandlers()` will adjust the `this` context to the same _`this`_.
addMessageHandler("values", (message: { [key: string]: any }) => {
addMessageHandler("values", async (message: { [key: string]: any }) => {
for (const name in this.$bindings) {
if (hasOwnProperty(this.$bindings, name))
this.$bindings[name].showProgress(false);
@@ -668,17 +685,14 @@ class ShinyApp {
for (const key in message) {
if (hasOwnProperty(message, key)) {
this.receiveOutput(key, message[key]);
await this.receiveOutput(key, message[key]);
}
}
});
addMessageHandler(
"errors",
function (
this: ShinyApp,
message: { [key: string]: ErrorsMessageValue }
) {
(message: { [key: string]: ErrorsMessageValue }) => {
for (const key in message) {
if (hasOwnProperty(message, key))
this.receiveError(key, message[key]);
@@ -698,14 +712,13 @@ class ShinyApp {
if ($obj.length > 0) {
if (!$obj.attr("aria-live")) $obj.attr("aria-live", "polite");
const el = $obj[0];
const evt: ShinyEventUpdateInput =
jQuery.Event("shiny:updateinput");
evt.message = message[i].message;
evt.binding = inputBinding;
$(el).trigger(evt);
if (!evt.isDefaultPrevented())
inputBinding.receiveMessage(el, evt.message);
const updateInputEvent = new EventUpdateInput({
message: message[i].message,
binding: inputBinding,
});
updateInputEvent.triggerOn(el);
if (!updateInputEvent.isDefaultPrevented())
inputBinding.receiveMessage(el, updateInputEvent.message);
}
}
}
@@ -724,13 +737,10 @@ class ShinyApp {
addMessageHandler(
"progress",
function (
this: ShinyApp,
message: { type: string; message: { id: string } }
) {
async (message: { type: string; message: { id: string } }) => {
if (message.type && message.message) {
// @ts-expect-error; Unknown values handled with followup if statement
const handler = this.progressHandlers[message.type];
const handler = await this.progressHandlers[message.type];
if (handler) handler.call(this, message.message);
}
@@ -739,13 +749,13 @@ class ShinyApp {
addMessageHandler(
"notification",
(
async (
message:
| { type: "remove"; message: string }
| { type: "show"; message: Parameters<typeof showNotification>[0] }
| { type: void }
) => {
if (message.type === "show") showNotification(message.message);
if (message.type === "show") await showNotification(message.message);
else if (message.type === "remove") removeNotification(message.message);
else throw "Unkown notification type: " + message.type;
}
@@ -753,13 +763,13 @@ class ShinyApp {
addMessageHandler(
"modal",
(
async (
message:
| { type: "remove"; message: string }
| { type: "show"; message: Parameters<typeof showModal>[0] }
| { type: void }
) => {
if (message.type === "show") showModal(message.message);
if (message.type === "show") await showModal(message.message);
// For 'remove', message content isn't used
else if (message.type === "remove") removeModal();
else throw "Unkown modal type: " + message.type;
@@ -859,12 +869,12 @@ class ShinyApp {
addMessageHandler(
"shiny-insert-ui",
(message: {
async (message: {
selector: string;
content: { html: string; deps: HtmlDep[] };
multiple: false | void;
multiple: boolean;
where: WherePosition;
}) => {
}): Promise<void> => {
const targets = $(message.selector);
if (targets.length === 0) {
@@ -876,19 +886,24 @@ class ShinyApp {
message.selector +
'") could not be found in the DOM.'
);
renderHtml(message.content.html, $([]).get(0), message.content.deps);
await renderHtmlAsync(
message.content.html,
$([]),
message.content.deps
);
} else {
targets.each(function (i, target) {
renderContent(target, message.content, message.where);
return message.multiple;
});
for (const target of targets) {
await renderContentAsync(target, message.content, message.where);
// If multiple is false, only render to the first target.
if (message.multiple === false) break;
}
}
}
);
addMessageHandler(
"shiny-remove-ui",
(message: { selector: string; multiple: false | void }) => {
(message: { selector: string; multiple: boolean }) => {
const els = $(message.selector);
els.each(function (i, el) {
@@ -896,8 +911,8 @@ class ShinyApp {
$(el).remove();
// If `multiple` is false, returning false terminates the function
// and no other elements are removed; if `multiple` is true,
// returning true continues removing all remaining elements.
return message.multiple;
// returning nothing continues removing all remaining elements.
return message.multiple === false ? false : undefined;
});
}
);
@@ -977,7 +992,7 @@ class ShinyApp {
addMessageHandler(
"shiny-insert-tab",
(message: {
async (message: {
inputId: string;
divTag: { html: string; deps: HtmlDep[] };
liTag: { html: string; deps: HtmlDep[] };
@@ -985,7 +1000,7 @@ class ShinyApp {
position: "after" | "before" | void;
select: boolean;
menuName: string;
}) => {
}): Promise<void> => {
const $parentTabset = getTabset(message.inputId);
let $tabset = $parentTabset;
const $tabContent = getTabContent($tabset);
@@ -1059,7 +1074,7 @@ class ShinyApp {
}
}
renderContent($liTag[0], {
await renderContentAsync($liTag[0], {
html: $liTag.html(),
deps: message.liTag.deps,
});
@@ -1094,13 +1109,13 @@ class ShinyApp {
// lower-level functions that renderContent uses. Like if we pre-process
// the value of message.divTag.html for singletons, we could do that, then
// render dependencies, then do $tabContent.append($divTag).
renderContent(
await renderContentAsync(
$tabContent[0],
{ html: "", deps: message.divTag.deps },
// @ts-expect-error; TODO-barret; There is no usage of beforeend
"beforeend"
);
$divTag.get().forEach((el) => {
for (const el of $divTag.get()) {
// Must not use jQuery for appending el to the doc, we don't want any
// scripts to run (since they will run when renderContent takes a crack).
$tabContent[0].appendChild(el);
@@ -1109,8 +1124,8 @@ class ShinyApp {
// and not the whole tag. That's fine in this case because we control the
// R code that generates this HTML, and we know that the element is not
// a script tag.
renderContent(el, el.innerHTML || el.textContent);
});
await renderContentAsync(el, el.innerHTML || el.textContent);
}
if (message.select) {
$liTag.find("a").tab("show");
@@ -1202,10 +1217,10 @@ class ShinyApp {
// value for the tabset gets updated (i.e. input$tabsetId
// should be null if there are no tabs).
const destTabValue = getFirstTab($tabset);
const evt: ShinyEventUpdateInput = jQuery.Event("shiny:updateinput");
evt.binding = inputBinding;
$tabset.trigger(evt);
const updateInputEvent = new EventUpdateInput({
binding: inputBinding,
});
updateInputEvent.triggerOn($tabset);
inputBinding.setValue($tabset[0], destTabValue);
}
}
@@ -1378,17 +1393,17 @@ class ShinyApp {
},
// Open a page-level progress bar
open: function (message: {
open: async function (message: {
style: "notification" | "old";
id: string;
}): void {
}): Promise<void> {
if (message.style === "notification") {
// For new-style (starting in Shiny 0.14) progress indicators that use
// the notification API.
// Progress bar starts hidden; will be made visible if a value is provided
// during updates.
showNotification({
await showNotification({
html:
`<div id="shiny-progress-${message.id}" class="shiny-progress-notification">` +
'<div class="progress active" style="display: none;"><div class="progress-bar"></div></div>' +

View File

@@ -66,7 +66,7 @@ function registerNames(s: string[] | string): void {
// Inserts new content into document head
function addToHead(head: string) {
if (head.length > 0) {
const tempDiv = $("<div>" + head + "</div>").get(0);
const tempDiv = $("<div>" + head + "</div>").get(0) as HTMLDivElement;
const $head = $("head");
while (tempDiv.hasChildNodes()) {

View File

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

View File

@@ -125,7 +125,8 @@ function makeResizeFilter(
let lastSize: LastSizeInterface = {};
return function () {
const size = { w: el.offsetWidth, h: el.offsetHeight };
const rect = el.getBoundingClientRect();
const size = { w: rect.width, h: rect.height };
if (size.w === 0 && size.h === 0) return;
if (size.w === lastSize.w && size.h === lastSize.h) return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,18 @@
/// <reference types="selectize" />
import { InputBinding } from "./inputBinding";
import type { NotUndefined } from "../../utils/extraTypes";
declare type SelectHTMLElement = HTMLSelectElement & {
type SelectHTMLElement = HTMLSelectElement & {
nonempty: boolean;
};
declare type SelectInputReceiveMessageData = {
type SelectInputReceiveMessageData = {
label: string;
options?: string;
config?: string;
url?: string;
value?: string;
};
declare type SelectizeOptions = Selectize.IOptions<string, unknown>;
declare type SelectizeInfo = Selectize.IApi<string, unknown> & {
type SelectizeOptions = Selectize.IOptions<string, unknown>;
type SelectizeInfo = Selectize.IApi<string, unknown> & {
settings: SelectizeOptions;
};
declare class SelectInputBinding extends InputBinding {
@@ -33,7 +33,7 @@ declare class SelectInputBinding extends InputBinding {
subscribe(el: SelectHTMLElement, callback: (x: boolean) => void): void;
unsubscribe(el: HTMLElement): void;
initialize(el: SelectHTMLElement): void;
protected _selectize(el: SelectHTMLElement, update?: boolean): SelectizeInfo;
protected _selectize(el: SelectHTMLElement, update?: boolean): SelectizeInfo | undefined;
}
export { SelectInputBinding };
export type { SelectInputReceiveMessageData };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,32 +3,447 @@ import type { InputBinding } from "../bindings/input/inputBinding";
import type { OutputBindingAdapter } from "../bindings/outputAdapter";
import type { EventPriority } from "../inputPolicies/inputPolicy";
import type { ErrorsMessageValue } from "../shiny/shinyapp";
import type { EvtFn } from "./jQueryEvents";
/**
* Shiny Event Base
*
* This class implements a common interface for all Shiny events, and provides a
* layer of abstraction between the Shiny event and the underlying jQuery event
* object. We use a new class, rather than extending JQuery.Event, because
* JQuery.Event is an old function-style class. Each Event class has a
* corresponding ShinyEvent interface that describes the event object that is
* emitted. At the end of this file, we extend JQuery's `on()` method to
* associate the ShinyEvent interfaces with their corresponding event string.
*
* @class EventBase
* @typedef {EventBase}
*/
declare class EventBase {
/**
* The underlying jQuery event object wrapped by `EventBase`.
* @type {JQuery.Event}
*/
event: JQuery.Event;
/**
* Creates an instance of EventBase.
*
* @constructor
* @param {string} type - The event type.
*/
constructor(type: string);
/**
* Trigger the event on an element or the document.
*
* @example
* // Instead of this...
* el.trigger(shinyEvent);
* // ...do this
* shinyEvent.triggerOn(el);
*
* @param {(HTMLElement | JQuery<HTMLElement> | typeof document | null)} el -
* The element to trigger the event on, or `null` to trigger on `document`.
*/
triggerOn(el: HTMLElement | JQuery<HTMLElement> | typeof document | null): void;
/**
* Proxy for `event.preventDefault()`.
*
* @returns {boolean} `true` if the default action was prevented, `false`
* otherwise.
*/
isDefaultPrevented(): boolean;
}
/**
* A common interface for most Shiny events.
*
* @interface ShinyEventCommon
* @typedef {ShinyEventCommon}
* @extends {JQuery.Event}
*/
interface ShinyEventCommon extends JQuery.Event {
/**
* The event name.
* @type {string}
*/
name: string;
value: unknown;
el: HTMLElement | null;
/**
* Event value containing arbitrary event data.
* @type {*}
*/
value: any;
}
/**
* Create a common Shiny event.
*
* @class EventCommon
* @typedef {EventCommon}
* @extends {EventBase}
*/
declare class EventCommon extends EventBase {
/**
* The actual event object.
* @type {ShinyEventCommon}
*/
event: ShinyEventCommon;
/**
* Creates an instance of EventCommon.
*
* @constructor
* @param {ShinyEventCommon["type"]} type - The Shiny custom event type, e.g.
* `shiny:value`.
* @param {ShinyEventCommon["name"]} name - The event name.
* @param {ShinyEventCommon["value"]} value - The event value, or arbitrary
* data included with the event.
*/
constructor(type: ShinyEventCommon["type"], name: ShinyEventCommon["name"], value: ShinyEventCommon["value"]);
/**
* Get the event name.
* @readonly
* @type {ShinyEventCommon["name"]}
*/
get name(): ShinyEventCommon["name"];
/**
* Get the event value.
* @readonly
* @type {ShinyEventCommon["value"]}
*/
get value(): ShinyEventCommon["value"];
}
/**
* An interface for the `shiny:inputchanged` event.
*
* @interface ShinyEventInputChanged
* @typedef {ShinyEventInputChanged}
* @extends {ShinyEventCommon}
*/
interface ShinyEventInputChanged extends ShinyEventCommon {
value: unknown;
/**
* The input element whose value has changed.
* @type {(HTMLElement | null)}
*/
el: HTMLElement | null;
/**
* The input binding for the changed input.
* @type {(InputBinding | null)}
*/
binding: InputBinding | null;
/**
* The input type.
* @type {string}
*/
inputType: string;
priority: EventPriority;
/**
* The input event priority.
* @type {?EventPriority}
*/
priority?: EventPriority;
}
/**
* Create a custom `shiny:inputchanged` event as an instance of
* EventInputChanged.
*
* @class EventInputChanged
* @typedef {EventInputChanged}
* @extends {EventCommon}
*/
declare class EventInputChanged extends EventCommon {
/**
* The `ShinyEventInputChanged` event object.
* @type {ShinyEventInputChanged}
*/
event: ShinyEventInputChanged;
/**
* Creates an instance of EventInputChanged.
*
* @constructor
* @param {{
name: ShinyEventInputChanged["name"];
value: ShinyEventInputChanged["value"];
el: ShinyEventInputChanged["el"];
binding: ShinyEventInputChanged["binding"];
inputType: ShinyEventInputChanged["inputType"];
priority?: ShinyEventInputChanged["priority"];
}} {
name,
value,
el,
binding,
inputType,
priority,
}
*/
constructor({ name, value, el, binding, inputType, priority, }: {
name: ShinyEventInputChanged["name"];
value: ShinyEventInputChanged["value"];
el: ShinyEventInputChanged["el"];
binding: ShinyEventInputChanged["binding"];
inputType: ShinyEventInputChanged["inputType"];
priority?: ShinyEventInputChanged["priority"];
});
/**
* Get the input element whose value has changed.
* @readonly
* @type {ShinyEventInputChanged["el"]}
*/
get el(): ShinyEventInputChanged["el"];
/**
* Get the input binding for the changed input.
* @readonly
* @type {ShinyEventInputChanged["binding"]}
*/
get binding(): ShinyEventInputChanged["binding"];
/**
* Get the input type.
* @readonly
* @type {ShinyEventInputChanged["inputType"]}
*/
get inputType(): ShinyEventInputChanged["inputType"];
/**
* Get the input event priority.
* @readonly
* @type {ShinyEventInputChanged["priority"]}
*/
get priority(): ShinyEventInputChanged["priority"];
}
/**
* A interface for the custom `shiny:updateinput` event.
*
* @interface ShinyEventUpdateInput
* @typedef {ShinyEventUpdateInput}
* @extends {ShinyEventCommon}
*/
interface ShinyEventUpdateInput extends ShinyEventCommon {
message: unknown;
/**
* Arbitrary message data, typically sent from the server, to be processed by
* the `receiveMessage` method of the input binding.
* @type {?*}
*/
message?: any;
/**
* The input binding for the input.
* @type {InputBinding}
*/
binding: InputBinding;
}
/**
* Create a shiny custom `shiny:updateinput` event as an instance of
* EventUpdateInput. This event carries message data from the server, sent via
* `session$sendInputMessage()`, to the input binding's `receiveMessage` method.
*
* @class EventUpdateInput
* @typedef {EventUpdateInput}
* @extends {EventBase}
*/
declare class EventUpdateInput extends EventBase {
/**
* The `ShinyEventUpdateInput` event object.
* @type {ShinyEventUpdateInput}
*/
event: ShinyEventUpdateInput;
/**
* Creates an instance of EventUpdateInput.
*
* @constructor
* @param {{
message?: ShinyEventUpdateInput["message"];
binding: ShinyEventUpdateInput["binding"];
}} {
message,
binding,
}
*/
constructor({ message, binding, }: {
message?: ShinyEventUpdateInput["message"];
binding: ShinyEventUpdateInput["binding"];
});
/**
* Get the `shiny:updateinput` message data.
* @readonly
* @type {ShinyEventUpdateInput["message"]}
*/
get message(): ShinyEventUpdateInput["message"];
/**
* Get the input binding for the input.
* @readonly
* @type {ShinyEventUpdateInput["binding"]}
*/
get binding(): ShinyEventUpdateInput["binding"];
}
/**
* A interface for the custom `shiny:value` event.
*
* @interface ShinyEventValue
* @typedef {ShinyEventValue}
* @extends {ShinyEventCommon}
*/
interface ShinyEventValue extends ShinyEventCommon {
value: unknown;
/**
* The output binding for the output that updated.
* @type {OutputBindingAdapter}
*/
binding: OutputBindingAdapter;
}
/**
* Create a shiny custom `shiny:value` event as an instance of EventValue. This
* event is triggered when an output's value changes.
*
* @class EventValue
* @typedef {EventValue}
* @extends {EventCommon}
*/
declare class EventValue extends EventCommon {
/**
* The `ShinyEventValue` event object.
* @type {ShinyEventValue}
*/
event: ShinyEventValue;
/**
* Creates an instance of EventValue.
*
* @constructor
* @param {{
name: ShinyEventValue["name"];
value: ShinyEventValue["value"];
binding: ShinyEventValue["binding"];
}} {
name,
value,
binding,
}
*/
constructor({ name, value, binding, }: {
name: ShinyEventValue["name"];
value: ShinyEventValue["value"];
binding: ShinyEventValue["binding"];
});
/**
* Get the output binding for the output that updated.
* @readonly
* @type {ShinyEventValue["binding"]}
*/
get binding(): ShinyEventValue["binding"];
}
/**
* A interface for the custom `shiny:error` event.
*
* @interface ShinyEventError
* @typedef {ShinyEventError}
* @extends {ShinyEventCommon}
*/
interface ShinyEventError extends ShinyEventCommon {
/**
* The output binding for the output where the error occurred.
* @type {OutputBindingAdapter}
*/
binding: OutputBindingAdapter;
/**
* The error message.
* @type {ErrorsMessageValue}
*/
error: ErrorsMessageValue;
}
/**
* Create a shiny custom `shiny:error` event as an instance of EventError. This
* event is triggered when an error occurs while processing the reactive
* expression that produces the output.
*
* @class EventError
* @typedef {EventError}
* @extends {EventCommon}
*/
declare class EventError extends EventCommon {
/**
* The `ShinyEventError` event object.
* @type {ShinyEventError}
*/
event: ShinyEventError;
/**
* Creates an instance of EventError.
*
* @constructor
* @param {{
name: ShinyEventError["name"];
binding: ShinyEventError["binding"];
error: ShinyEventError["error"];
}} {
name,
binding,
error,
}
*/
constructor({ name, binding, error, }: {
name: ShinyEventError["name"];
binding: ShinyEventError["binding"];
error: ShinyEventError["error"];
});
/**
* Get the output binding for the output where the error occurred.
* @readonly
* @type {ShinyEventError["binding"]}
*/
get binding(): ShinyEventError["binding"];
/**
* Get the error message.
* @readonly
* @type {ShinyEventError["error"]}
*/
get error(): ShinyEventError["error"];
}
/**
* A interface for the custom `shiny:message` event.
*
* @interface ShinyEventMessage
* @typedef {ShinyEventMessage}
* @extends {JQuery.Event}
*/
interface ShinyEventMessage extends JQuery.Event {
/**
* Arbitrary message data from the server. This data will ultimately be
* handled by the function provided to `Shiny.addCustomMessageHandler()` for
* the given message type.
*
* @type {{ [key: string]: unknown }}
*/
message: {
[key: string]: unknown;
};
}
/**
* Create a shiny custom `shiny:message` event as an instance of EventMessage.
* This message is triggered when the server sends a custom message to the app
* via `session$sendCustomMessage()`.
*
* @class EventMessage
* @typedef {EventMessage}
* @extends {EventBase}
*/
declare class EventMessage extends EventBase {
/**
* The `ShinyEventMessage` event object.
* @type {ShinyEventMessage}
*/
event: ShinyEventMessage;
/**
* Creates an instance of EventMessage.
*
* @constructor
* @param {ShinyEventMessage["message"]} message
*/
constructor(message: ShinyEventMessage["message"]);
/**
* Get the message data from the event.
* @readonly
* @type {ShinyEventMessage["message"]}
*/
get message(): ShinyEventMessage["message"];
}
declare global {
interface JQuery {
on(events: "shiny:inputchanged", handler: EvtFn<ShinyEventInputChanged>): this;
on(events: "shiny:updateinput", handler: EvtFn<ShinyEventUpdateInput>): this;
on(events: "shiny:value", handler: EvtFn<ShinyEventValue>): this;
on(events: "shiny:error", handler: EvtFn<ShinyEventError>): this;
on(events: "shiny:message", handler: EvtFn<ShinyEventMessage>): this;
}
}
export { EventCommon, EventInputChanged, EventUpdateInput, EventValue, EventError, EventMessage, };
export type { ShinyEventInputChanged, ShinyEventUpdateInput, ShinyEventValue, ShinyEventError, ShinyEventMessage, };

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More